diff --git a/README.md b/README.md index 31bb0221..77632bf2 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ cd ~/MultiRoombaRover sudo ./pi/install_roverd.sh --mediamtx ``` -Then point each rover's `/etc/roverd.yaml` at `ws://:8080/rover`, set `name` to the rover’s ID, and make sure the Pi’s mediaMTX is reachable at `http://:8889/whep/rovercam` (the default). Each rover advertises that WHEP URL in its `hello` frame, and the server’s media bridge automatically creates a matching pull path so mediaMTX on 192.168.0.86 fans the stream out to drivers/spectators—browsers never talk to the Pi directly. +Then point each rover's `/etc/roverd.yaml` at `ws://:8080/rover`, set `name` to the rover’s ID, and make sure the Pi’s mediaMTX is reachable at `http://:8889/rovercam/whep` (the default). Each rover advertises that WHEP URL in its `hello` frame, and the server’s media bridge automatically creates a matching pull path so mediaMTX on 192.168.0.86 fans the stream out to drivers/spectators—browsers never talk 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. diff --git a/dist/roverd b/dist/roverd index ab1d26ea..0f6723d1 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/docs/pi-deployment.md b/docs/pi-deployment.md index 2057e84f..4b34461c 100644 --- a/docs/pi-deployment.md +++ b/docs/pi-deployment.md @@ -55,7 +55,7 @@ Flags: | `--mediamtx` | download/install mediaMTX plus the provided config + unit | | `--mediamtx-version X.Y.Z` | override the mediaMTX release tag (default `1.15.3`) | -If the script installs the sample config, it will remind you to edit `/etc/roverd.yaml` before manually restarting the service: set `name`, `serverUrl`, serial device, BRC pin, battery thresholds, and optionally override `media.whepUrl`. When left blank, roverd automatically uses the Pi’s primary IPv4 plus `:8889/whep/rovercam`. +If the script installs the sample config, it will remind you to edit `/etc/roverd.yaml` before manually restarting the service: set `name`, `serverUrl`, serial device, BRC pin, battery thresholds, and optionally override `media.whepUrl`. When left blank, roverd automatically uses the Pi’s primary IPv4 plus `:8889/rovercam/whep`. ## Manual installation @@ -65,7 +65,7 @@ If the script installs the sample config, it will remind you to edit `/etc/rover sudo install -o roverd -g roverd -m 0755 dist/roverd /usr/local/bin/roverd sudo install -o roverd -g roverd -m 0640 pi/roverd/roverd.sample.yaml /etc/roverd.yaml ``` - Adjust `/etc/roverd.yaml` for each rover: `name`, `serverUrl` (e.g. `ws://control-server:8080/rover`), serial port path, battery thresholds, GPIO pin for BRC, and (if needed) the media `whepUrl` override. Otherwise, roverd fills in `http://:8889/whep/rovercam` based on the DHCP-assigned address. + Adjust `/etc/roverd.yaml` for each rover: `name`, `serverUrl` (e.g. `ws://control-server:8080/rover`), serial port path, battery thresholds, GPIO pin for BRC, and (if needed) the media `whepUrl` override. Otherwise, roverd fills in `http://:8889/rovercam/whep` based on the DHCP-assigned address. 2. Install the systemd unit: ```bash @@ -92,7 +92,7 @@ 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, enables the local mediaMTX HTTP API so `roverd` can health-check the service, and serves WebRTC playback at `http://:8889/whep/rovercam`. Keep that URL reachable from the control server—`roverd` advertises it automatically and the central media bridge pulls each rover’s feed over WHEP. +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 serves WebRTC playback at `http://:8889/rovercam/whep`. Keep that URL reachable from the control server—`roverd` advertises it automatically and the central media bridge pulls each rover’s feed over WHEP. 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`). With that in place, the Pi only talks to the server over the trusted LAN while the server-side mediaMTX distributes video to every driver and spectator. diff --git a/pi/roverd/config.go b/pi/roverd/config.go index f1bcd782..04b2f6d3 100644 --- a/pi/roverd/config.go +++ b/pi/roverd/config.go @@ -94,7 +94,7 @@ func LoadConfig(path string) (*Config, error) { }, Media: MediaConfig{ WhepPort: 8889, - WhepPath: "/whep/rovercam", + WhepPath: "/rovercam/whep", HealthInterval: Duration{Duration: 30 * time.Second}, }, } @@ -135,7 +135,7 @@ func LoadConfig(path string) (*Config, error) { } path := cfg.Media.WhepPath if path == "" { - path = "/whep/rovercam" + path = "/rovercam/whep" } path = ensureLeadingSlash(path) scheme := "http" diff --git a/server/src/services/mediaBridgeService.js b/server/src/services/mediaBridgeService.js index ee33466e..a2ef74cc 100644 --- a/server/src/services/mediaBridgeService.js +++ b/server/src/services/mediaBridgeService.js @@ -93,23 +93,50 @@ function normalizeSource(raw) { if (!raw) return null; const trimmed = String(raw).trim(); if (!trimmed) return null; - if (/^wheps?:\/\//i.test(trimmed)) { - return trimmed; - } try { const parsed = new URL(trimmed); - const protocol = parsed.protocol === 'https:' ? 'wheps' : 'whep'; - let pathname = parsed.pathname || '/'; - if (!pathname.startsWith('/')) { - pathname = `/${pathname}`; + let protocol = parsed.protocol; + if (!protocol || protocol === 'http:') { + protocol = 'whep:'; + } else if (protocol === 'https:') { + protocol = 'wheps:'; + } else if (protocol !== 'whep:' && protocol !== 'wheps:') { + throw new Error('unsupported protocol'); } - return `${protocol}://${parsed.host}${pathname}${parsed.search || ''}`; + const host = parsed.host; + if (!host) { + throw new Error('missing host'); + } + let pathname = parsed.pathname || '/'; + pathname = normalizeWhepPath(pathname); + return `${protocol.slice(0, -1)}://${host}${pathname}${parsed.search || ''}`; } catch (err) { logger.warn('invalid WHEP URL provided by rover (%s): %s', raw, err.message); return null; } } +function normalizeWhepPath(pathname) { + let result = pathname || '/'; + if (!result.startsWith('/')) { + result = `/${result}`; + } + result = result.replace(/\/+/g, '/'); + result = result.replace(/\/+$/, ''); + if (result.startsWith('/whep/')) { + result = `/${result.slice(6)}`; + } else if (result === '/whep') { + result = '/rovercam'; + } + if (!result || result === '/') { + result = '/rovercam'; + } + if (!result.endsWith('/whep')) { + result = `${result}/whep`; + } + return result; +} + async function callApi(method, path, body) { const url = `${apiBase}${path}`; const res = await fetch(url, {