From 440325296c900a5bc0b780fff75665eec9f11e12 Mon Sep 17 00:00:00 2001 From: legop3 Date: Fri, 14 Nov 2025 02:29:11 -0500 Subject: [PATCH] guh --- README.md | 5 +++ dist/roverd | Bin 6815928 -> 6815928 bytes mediamtx_server_integration.md | 2 +- server/package.json | 3 +- server/public/spectate/spectator.js | 1 + server/public/src/services/roverUI.js | 3 +- server/public/src/services/videoPlayer.js | 14 ++------ server/scripts/checkMedia.js | 40 ++++++++++++++++++++++ server/src/services/videoAuthService.js | 3 +- server/src/services/videoSocketService.js | 4 +-- 10 files changed, 56 insertions(+), 19 deletions(-) create mode 100755 server/scripts/checkMedia.js 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 838b6f966bcca5903871ad0e3dcf3fa09fbbcc70..44a67747cc111823e89f954c674e1ce60d6f0983 100755 GIT binary patch delta 581 zcmbWxyHXPY0EOX|MM1pesEBw$;}t=XY?96HMo~dyf*}|p1cMjcglrseCNj~{p+buj zTZe+y#@b6ER8+o!!!uZDFNMx{p26bl{^DPM{oY)cPj5qgPbOc*G9$&seEr$HT`#>@ zn#nyM9DSLcTWAzhPa9+NwM@av7VO;0=TMio7@wVX>$!B=ibe+}N{w=DY1n8r45L#_ zNsmt%-&($Z%(uU54s~xX*c%^yZF~t<-hkLfv=AoIN^B=~5N$*|v6JW^b`iUYJ;Yuj zM5y0K>?aNooy0++i#SAd6NiZ-#8KiH(L)?3P7q<@Byoy3O`IXl66c6s;ylqu^b;3| zi^KqNi5MgzghfP&7!fCIB0)HWOL#<*xJ+CjhKQ@gHR3ukOpFjWh?`%L+WgULyKcqt zecQ1r5zh}Qf#tZamxw3qpzL|kYRvK7YBjR@=Xa{7%}H8T(sr8Rt(4r7v}9ydZcA3i zBq!r?NAfZu1-UDeG9^WsmU~i?8M!YH`_a8uJza;g%P*s(gGK>w6j6Q;od&sxa0l&a|hR z&AHhZ1EUkuGp_IEie9r_@y+~jD_MN^Db_Vu4_n#su+%KumDy}LXoc-y&WIX@5toRJ z6iUYDr5~2R9RHGf9&LPmpPY8TfB5zW#0Fv`5h0?)CSo(uL2Mzm5}m|0Vmq;e*h$0) z^}C4O#2z9}>?OL0eZ+p^0CA8wL>wl%i6g{OB0=;J$B5&^3F0Jiis&Uy6MaNKafUcc zoFmQ?1B6K=35&3a6yXpqktRH1kjM}hh>OG};xaKrTp_L!*NE#xb{V;~pS^)u3+j%W ztk%+jX}O;1r2Mo$8F-#;rEJ@(1?j43Ig5XO=ej%GOv=fa?m}WcC&Mx#H)K?9$}PDq zc`3*pxhwaiDEDPdN>Y}JJdkmjkcaX}9?KJXD$iCD>;A9K|5{ztYX8D~q$+{bq%M;y J^O4Z|`wtD|zpwxR 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 });