push! PUSH!!

This commit is contained in:
legop3
2025-11-14 03:45:51 -05:00
parent 440325296c
commit 026ce2efac
10 changed files with 12 additions and 162 deletions
+3 -3
View File
@@ -56,7 +56,7 @@ cd ~/MultiRoombaRover
sudo ./pi/install_roverd.sh --mediamtx
```
Then point each rover's `/etc/roverd.yaml` at `ws://<server>:8080/rover`, enable the sensor stream from the UI, and drive with WASD. `roverd` automatically advertises its local mediaMTX WHEP endpoint (derived from the Pis DHCP address), so no video URL configuration is required unless you want to override the defaults.
Then point each rover's `/etc/roverd.yaml` at `ws://<server>:8080/rover`, set `name` to the rovers ID, and edit `/etc/mediamtx/mediamtx.yml` so `whipPublishURLs` contains `http://192.168.0.86:8889/whip/<name>`. The Pi continuously pushes WebRTC media to the control server; drivers/spectators always watch through the server-side mediaMTX, so no browser ever talks to the Pi directly.
Use the “Restart Camera” button if you enable media management so roverd can bounce the mediamtx service remotely.
Heads-up: the BRC pulser now uses libgpiod; make sure the `roverd` service account is in the `gpio` group (or otherwise allowed to access `/dev/gpiochip*`) and set `brc.gpioChip` if your hardware exposes a different chip name.
@@ -87,9 +87,9 @@ 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 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` (`http://192.168.0.86:8889/whep`), and `media.mediamtxApiUrl` (usually `http://127.0.0.1:9997`). The Node server hits that API for health checks and token hooks while mediaMTX fans WHIP publishers back out as WHEP viewers. 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.
- To see what mediaMTX is ingesting from the Pis, 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 publish issues.
Vendored
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -92,9 +92,9 @@ If you set `media.manage: true` in `/etc/roverd.yaml`, make sure the `roverd` se
sudo systemctl enable --now mediamtx.service
```
The sample config uses the Raspberry Pi camera module as the source and enables the local mediaMTX HTTP API so `roverd` can health-check the service. Edit `/etc/mediamtx/mediamtx.yml` to point its WHIP client at your central media server (see `mediamtx_server_integration.md` for details).
The sample config uses the Raspberry Pi camera module as the source, enables the local mediaMTX HTTP API so `roverd` can health-check the service, and includes a placeholder WHIP target pointing at the control server (`http://192.168.0.86:8889/whip/ROVER_ID`). Replace `ROVER_ID` with the value of `name` from `/etc/roverd.yaml` so every rover pushes to a unique publish path.
Expose the mediaMTX HTTP API locally (default `http://127.0.0.1:9997`) and set `media.healthUrl` so `roverd` can monitor the pipeline; `media.service` should match the systemd unit name (default `mediamtx.service`).
Finally, edit `/etc/mediamtx/mediamtx.yml` so the Pi publishes the camera feed to your central control server via WHIP (for example by pointing it at `https://control.example.com/whip/<roverName>`). The Pi never serves viewers directly—drivers and spectators always watch through the control server.
Once the WHIP URL points at the server, the Pi continuously publishes to `192.168.0.86`; the server-side mediaMTX fans the stream out to every driver/spectator via WHEP—no Pi ever serves viewers directly.
## Server + UI
-20
View File
@@ -1,20 +0,0 @@
# mediaMTX Integration
Each rover runs mediaMTX locally to capture the Pi camera, and the control server hosts a central mediaMTX instance that fans video out to drivers and spectators. When a rover connects, the Node server asks the central mediaMTX to _pull_ the rovers local WHEP stream and expose it under the rovers name.
## Pi (publisher)
- mediaMTX samples the Pi camera (`paths.rovercam.source: rpiCamera`) and exposes the HTTP API on `http://127.0.0.1:9997`.
- `roverd` automatically detects the Pis own WHEP endpoint (e.g. `http://roomba-alpha.local:8889/whep/rovercam`) and reports a `bridgeWhepUrl` (converted to `whep://.../whep`) to the server. No manual per-rover video URL configuration is required.
- The `media.manage` flag keeps the local service alive via `systemctl` and hits the API for health checks (`media.healthUrl`, defaults to `http://127.0.0.1:9997/v3/paths/list`).
## Control server (viewer)
- 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. 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
- When a user requests video the browser asks the Node server for a session token, and if the current mode/role allows it, the server returns the WHEP URL plus the session id.
- Spectator mode and lockdown are enforced purely on the Node server: no direct Pi URLs are ever exposed, and once a user has a session it remains valid until they disconnect or lockdown revokes it.
+3
View File
@@ -18,3 +18,6 @@ paths:
rpiCameraExposure: long
rpiCameraHFlip: false
rpiCameraVFlip: false
# Replace ROVER_ID with the rover's configured name before deploy.
whipPublishURLs:
- http://192.168.0.86:8889/whip/ROVER_ID
-31
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
@@ -60,7 +59,6 @@ type MediaConfig struct {
WhepPort int `yaml:"whepPort" json:"-"`
WhepPath string `yaml:"whepPath" json:"-"`
LegacyPublish string `yaml:"publishUrl,omitempty" json:"-"`
BridgeWhepURL string `yaml:"-" json:"bridgeWhepUrl,omitempty"`
Manage bool `yaml:"manage"`
Service string `yaml:"service"`
HealthURL string `yaml:"healthUrl"`
@@ -143,38 +141,9 @@ func LoadConfig(path string) (*Config, error) {
scheme := "http"
cfg.Media.WhepURL = fmt.Sprintf("%s://%s:%d%s", scheme, ip, effectivePort(cfg.Media.WhepPort), path)
}
if bridge, err := buildBridgeURL(cfg.Media.WhepURL); err == nil {
cfg.Media.BridgeWhepURL = bridge
}
return &cfg, nil
}
func buildBridgeURL(src string) (string, error) {
if src == "" {
return "", errors.New("empty whep url")
}
parsed, err := url.Parse(src)
if err != nil {
return "", err
}
if parsed.Host == "" {
return "", errors.New("missing host")
}
proto := "whep"
if parsed.Scheme == "https" {
proto = "wheps"
}
path := strings.Trim(parsed.Path, "/")
if strings.HasPrefix(path, "whep/") {
path = strings.TrimPrefix(path, "whep/")
}
path = strings.Trim(path, "/")
if path == "" {
path = "rovercam"
}
return fmt.Sprintf("%s://%s/%s/whep", proto, parsed.Host, path), nil
}
func ensureLeadingSlash(path string) string {
if !strings.HasPrefix(path, "/") {
return "/" + path
+1 -1
View File
@@ -8,5 +8,5 @@ admins:
discord_id: "0987654321"
lockdown: true
media:
whepBaseUrl: "http://control-server.local:8889/whep"
whepBaseUrl: "http://192.168.0.86:8889/whep"
mediamtxApiUrl: "http://127.0.0.1:9997"
-1
View File
@@ -14,7 +14,6 @@ require('./src/services/roverManager');
require('./src/services/commandService');
require('./src/services/roverConnectionService');
require('./src/services/assignmentService');
require('./src/services/mediaBridgeService');
require('./src/services/videoSessions');
require('./src/services/videoAuthService');
require('./src/services/videoSocketService');
-104
View File
@@ -1,104 +0,0 @@
const { loadConfig } = require('../helpers/configLoader');
const logger = require('../globals/logger').child('mediaBridge');
const { managerEvents, rovers } = require('./roverManager');
const mediaConfig = loadConfig().media || {};
const apiBase = (mediaConfig.mediamtxApiUrl || '').replace(/\/$/, '');
if (!apiBase) {
logger.info('media bridge disabled (media.mediamtxApiUrl not set)');
return;
}
const activeSources = new Map(); // roverId -> source
managerEvents.on('rover', (evt) => {
if (evt.action === 'upsert' && evt.record) {
syncRover(evt.record).catch((err) => {
logger.error('failed to sync rover %s: %s', evt.roverId, err.message);
});
} else if (evt.action === 'removed') {
removePath(evt.roverId).catch((err) => {
logger.error('failed to remove rover %s path: %s', evt.roverId, err.message);
});
}
});
// guh
// Initialize existing rovers (in case service loads after they connect)
for (const record of rovers.values()) {
syncRover(record).catch((err) => {
logger.error('failed to sync rover %s on init: %s', record.id, err.message);
});
}
async function syncRover(record) {
if (!record?.meta?.media) {
await removePath(record?.id);
return;
}
const source = record.meta.media.bridgeWhepUrl;
if (!source) {
logger.warn(
'rover %s missing bridgeWhepUrl; video bridge disabled for this rover',
record.id
);
await removePath(record.id);
return;
}
if (activeSources.get(record.id) === source) {
return;
}
await upsertPath(record.id, source);
activeSources.set(record.id, source);
logger.info('bridge path ready for %s -> %s', record.id, source);
}
async function upsertPath(roverId, source) {
const body = {
source,
sourceOnDemand: false,
};
try {
await callApi('POST', `/v3/config/paths/replace/${encodeURIComponent(roverId)}`, body);
} catch (err) {
if (err.status === 404) {
await callApi('POST', `/v3/config/paths/add/${encodeURIComponent(roverId)}`, body);
} else {
throw err;
}
}
}
async function removePath(roverId) {
if (!roverId || !activeSources.has(roverId)) {
activeSources.delete(roverId);
return;
}
activeSources.delete(roverId);
try {
await callApi('POST', `/v3/config/paths/delete/${encodeURIComponent(roverId)}`, {});
logger.info('bridge path removed for %s', roverId);
} catch (err) {
if (err.status !== 404) {
throw err;
}
}
}
async function callApi(method, path, body) {
const url = `${apiBase}${path}`;
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
const err = new Error(`HTTP ${res.status} ${text}`);
err.status = res.status;
throw err;
}
return res.headers.get('content-type')?.includes('application/json') ? res.json() : null;
}
@@ -33,6 +33,9 @@ io.on('connection', (socket) => {
if (!roverId) {
throw new Error('roverId required');
}
if (!roverManager.rovers.has(roverId)) {
throw new Error('Rover offline');
}
if (!canView(socket, roverId)) {
throw new Error('Not authorized for video');
}