This commit is contained in:
legop3
2025-11-14 02:29:11 -05:00
parent 588c7aa571
commit 440325296c
10 changed files with 56 additions and 19 deletions
+5
View File
@@ -88,3 +88,8 @@ authHTTPExclude:
Then restart `mediamtx.service` so WHIP pushes from the Pis stop getting rejected. Then restart `mediamtx.service` so WHIP pushes from the Pis stop getting rejected.
Once finished, update `server/config.yaml` with your admin passwords, `media.whepBaseUrl` (public playback URL), and the new `media.mediamtxApiUrl` (usually `http://127.0.0.1:9997`). The Node server uses that API to create per-rover pull paths so the central mediaMTX automatically connects to each Pis WHEP feed as rovers come and go. Restart `multirover.service` whenever you edit the config. To pull updates later, just `git pull`, re-run `npm install --production` inside `server/`, and restart the service—no need to rerun the installer. Once finished, update `server/config.yaml` with your admin passwords, `media.whepBaseUrl` (public playback URL), and the new `media.mediamtxApiUrl` (usually `http://127.0.0.1:9997`). The Node server uses that API to create per-rover pull paths so the central mediaMTX automatically connects to each Pis WHEP feed as rovers come and go. Restart `multirover.service` whenever you edit the config. To pull updates later, just `git pull`, re-run `npm install --production` inside `server/`, and restart the service—no need to rerun the installer.
### Video handshake + diagnostics
- Every `video:request` returns `{ url, token }`. The browser posts the SDP offer to `url` and includes `Authorization: Basic base64(token:token)`. mediaMTX forwards the `token` to `/mediamtx/auth`, which checks the sockets permissions and either returns 200 or 401—no query parameters are involved anymore.
- To see what mediaMTX is pulling, run `npm run check:media` (or `node scripts/checkMedia.js`). It hits `/v3/paths/list` and prints each rovers `ready` state and byte counters so you can instantly spot bridge issues.
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -12,7 +12,7 @@ Each rover runs mediaMTX locally to capture the Pi camera, and the control serve
- The central mediaMTX instance serves WHEP playback at `/whep/<roverId>` and exposes its control API on `http://127.0.0.1:9997`. - The central mediaMTX instance serves WHEP playback at `/whep/<roverId>` and exposes its control API on `http://127.0.0.1:9997`.
- When a rover connects, the Node server calls `POST /v3/config/paths/replace/<roverId>` and sets `source: whep://<pi-ip>:8889/rovercam/whep`. When the rover disconnects, the path is removed. - When a rover connects, the Node server calls `POST /v3/config/paths/replace/<roverId>` and sets `source: whep://<pi-ip>:8889/rovercam/whep`. When the rover disconnects, the path is removed.
- Viewers still use `video:request` to obtain a session token. mediaMTX calls back into `/mediamtx/auth` before letting a client access `/whep/<roverId>?session=...`, and the Node server enforces lockdown/role rules there. - Viewers still use `video:request` to obtain a session token. They POST their SDP offer to `/whep/<roverId>` with `Authorization: Basic base64(token:token)`. mediaMTX forwards the `token` to `/mediamtx/auth` (in the `user` field) before allowing the stream to start, and the Node server enforces lockdown/role rules there.
## Driver / spectator UIs ## Driver / spectator UIs
+2 -1
View File
@@ -4,7 +4,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"dev": "nodemon index.js" "dev": "nodemon index.js",
"check:media": "node scripts/checkMedia.js"
}, },
"dependencies": { "dependencies": {
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
+1
View File
@@ -82,6 +82,7 @@ registerModule('spectator/main', (require, exports) => {
videoPlayer videoPlayer
.startStream(playerId, { .startStream(playerId, {
url: resp.url, url: resp.url,
token: resp.token,
mount: wrap, mount: wrap,
onStatus: (state, detail) => { onStatus: (state, detail) => {
if (state === 'playing') { if (state === 'playing') {
+2 -1
View File
@@ -152,10 +152,11 @@ registerModule('services/roverUI', (require, exports) => {
videoPlayer.stopStream('driver'); videoPlayer.stopStream('driver');
return; return;
} }
const { url } = resp; const { url, token } = resp;
videoPlayer videoPlayer
.startStream('driver', { .startStream('driver', {
url, url,
token,
mount: videoContainer, mount: videoContainer,
videoEl: driverVideo, videoEl: driverVideo,
onStatus: (state, detail) => { onStatus: (state, detail) => {
+2 -12
View File
@@ -70,8 +70,7 @@ registerModule('services/videoPlayer', (require, exports) => {
}; };
this.attachAutoPlayLoop(); this.attachAutoPlayLoop();
const credential = token || extractSessionToken(url); if (!token) {
if (!credential) {
throw new Error('Missing video session token'); throw new Error('Missing video session token');
} }
const offer = await this.pc.createOffer(); const offer = await this.pc.createOffer();
@@ -80,7 +79,7 @@ registerModule('services/videoPlayer', (require, exports) => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/sdp', 'Content-Type': 'application/sdp',
Authorization: `Basic ${btoa(`${credential}:${credential}`)}`, Authorization: `Basic ${btoa(`${token}:${token}`)}`,
}, },
body: offer.sdp, body: offer.sdp,
}); });
@@ -120,15 +119,6 @@ registerModule('services/videoPlayer', (require, exports) => {
} }
} }
function extractSessionToken(url) {
try {
const parsed = new URL(url, window.location.origin);
return parsed.searchParams.get('session');
} catch (err) {
return null;
}
}
async function startStream(id, { url, token, mount, videoEl, onStatus } = {}) { async function startStream(id, { url, token, mount, videoEl, onStatus } = {}) {
stopStream(id); stopStream(id);
const player = new Player(id, { mount, videoEl, onStatus }); const player = new Player(id, { mount, videoEl, onStatus });
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
const http = require('http');
const api = process.env.MEDIAMTX_API || 'http://127.0.0.1:9997';
function fetchJSON(path) {
return new Promise((resolve, reject) => {
const req = http.request(api + path, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
});
req.on('error', reject);
req.end();
});
}
async function main() {
const list = await fetchJSON('/v3/paths/list');
if (!list.items || !list.items.length) {
console.log('No active paths');
return;
}
list.items.forEach((item) => {
console.log(
`${item.name.padEnd(12)} ready=${item.ready} tracks=${item.tracks.join(',') || 'none'} bytes=${item.bytesReceived}`
);
});
}
main().catch((err) => {
console.error('check-media failed:', err.message);
process.exit(1);
});
+1 -2
View File
@@ -23,8 +23,7 @@ function canView(socket) {
app.post('/mediamtx/auth', (req, res) => { app.post('/mediamtx/auth', (req, res) => {
const body = req.body || {}; const body = req.body || {};
const path = (body.path || '').replace(/^\//, ''); const path = (body.path || '').replace(/^\//, '');
const params = new URLSearchParams(body.query || ''); const sessionId = body.user;
const sessionId = params.get('session');
const roverId = path; const roverId = path;
if (!sessionId || !roverId) { if (!sessionId || !roverId) {
+2 -2
View File
@@ -37,8 +37,8 @@ io.on('connection', (socket) => {
throw new Error('Not authorized for video'); throw new Error('Not authorized for video');
} }
const sessionId = videoSessions.createSession(socket, roverId); const sessionId = videoSessions.createSession(socket, roverId);
const url = `${mediaConfig.whepBaseUrl.replace(/\/$/, '')}/${roverId}?session=${sessionId}`; const url = `${mediaConfig.whepBaseUrl.replace(/\/$/, '')}/${roverId}`;
cb({ url }); cb({ url, token: sessionId });
} catch (err) { } catch (err) {
logger.warn('video request failed: %s', err.message); logger.warn('video request failed: %s', err.message);
cb({ error: err.message }); cb({ error: err.message });