mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21: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.
|
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.
|
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`.
|
- 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
@@ -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",
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
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) => {
|
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) {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user