diff --git a/README.md b/README.md index c407d7a3..ac949730 100644 --- a/README.md +++ b/README.md @@ -88,3 +88,8 @@ authHTTPExclude: 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 Pi’s 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 socket’s 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 rover’s `ready` state and byte counters so you can instantly spot bridge issues. diff --git a/dist/roverd b/dist/roverd index 838b6f96..44a67747 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/mediamtx_server_integration.md b/mediamtx_server_integration.md index b41f28c2..f0781f1e 100644 --- a/mediamtx_server_integration.md +++ b/mediamtx_server_integration.md @@ -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/` 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/` and sets `source: whep://: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/?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/` 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 diff --git a/server/package.json b/server/package.json index 4a3e2017..697310b1 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "start": "node index.js", - "dev": "nodemon index.js" + "dev": "nodemon index.js", + "check:media": "node scripts/checkMedia.js" }, "dependencies": { "bcrypt": "^6.0.0", diff --git a/server/public/spectate/spectator.js b/server/public/spectate/spectator.js index 1b540675..add8941e 100644 --- a/server/public/spectate/spectator.js +++ b/server/public/spectate/spectator.js @@ -82,6 +82,7 @@ registerModule('spectator/main', (require, exports) => { videoPlayer .startStream(playerId, { url: resp.url, + token: resp.token, mount: wrap, onStatus: (state, detail) => { if (state === 'playing') { diff --git a/server/public/src/services/roverUI.js b/server/public/src/services/roverUI.js index cdffc984..a30dfaa9 100644 --- a/server/public/src/services/roverUI.js +++ b/server/public/src/services/roverUI.js @@ -152,10 +152,11 @@ registerModule('services/roverUI', (require, exports) => { videoPlayer.stopStream('driver'); return; } - const { url } = resp; + const { url, token } = resp; videoPlayer .startStream('driver', { url, + token, mount: videoContainer, videoEl: driverVideo, onStatus: (state, detail) => { diff --git a/server/public/src/services/videoPlayer.js b/server/public/src/services/videoPlayer.js index 91bc186c..80c45232 100644 --- a/server/public/src/services/videoPlayer.js +++ b/server/public/src/services/videoPlayer.js @@ -70,8 +70,7 @@ registerModule('services/videoPlayer', (require, exports) => { }; this.attachAutoPlayLoop(); - const credential = token || extractSessionToken(url); - if (!credential) { + if (!token) { throw new Error('Missing video session token'); } const offer = await this.pc.createOffer(); @@ -80,7 +79,7 @@ registerModule('services/videoPlayer', (require, exports) => { method: 'POST', headers: { 'Content-Type': 'application/sdp', - Authorization: `Basic ${btoa(`${credential}:${credential}`)}`, + Authorization: `Basic ${btoa(`${token}:${token}`)}`, }, 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 } = {}) { stopStream(id); const player = new Player(id, { mount, videoEl, onStatus }); diff --git a/server/scripts/checkMedia.js b/server/scripts/checkMedia.js new file mode 100755 index 00000000..d710c744 --- /dev/null +++ b/server/scripts/checkMedia.js @@ -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); +}); diff --git a/server/src/services/videoAuthService.js b/server/src/services/videoAuthService.js index 4c88363f..89cf551f 100644 --- a/server/src/services/videoAuthService.js +++ b/server/src/services/videoAuthService.js @@ -23,8 +23,7 @@ function canView(socket) { app.post('/mediamtx/auth', (req, res) => { const body = req.body || {}; const path = (body.path || '').replace(/^\//, ''); - const params = new URLSearchParams(body.query || ''); - const sessionId = params.get('session'); + const sessionId = body.user; const roverId = path; if (!sessionId || !roverId) { diff --git a/server/src/services/videoSocketService.js b/server/src/services/videoSocketService.js index 8e3472dd..378d9354 100644 --- a/server/src/services/videoSocketService.js +++ b/server/src/services/videoSocketService.js @@ -37,8 +37,8 @@ io.on('connection', (socket) => { throw new Error('Not authorized for video'); } const sessionId = videoSessions.createSession(socket, roverId); - const url = `${mediaConfig.whepBaseUrl.replace(/\/$/, '')}/${roverId}?session=${sessionId}`; - cb({ url }); + const url = `${mediaConfig.whepBaseUrl.replace(/\/$/, '')}/${roverId}`; + cb({ url, token: sessionId }); } catch (err) { logger.warn('video request failed: %s', err.message); cb({ error: err.message });