mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
guh
This commit is contained in:
@@ -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.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -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`.
|
||||
- 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
|
||||
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Executable
+40
@@ -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);
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user