diff --git a/README.md b/README.md index 40778de1..c407d7a3 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 (or leave blank to auto-derive) `media.whepUrl` for the Pi’s mediaMTX instance (defaults to `http://:8889/whep/rovercam`), enable the sensor stream from the UI, and drive with WASD. +Then point each rover's `/etc/roverd.yaml` at `ws://:8080/rover`, enable the sensor stream from the UI, and drive with WASD. `roverd` automatically advertises its local mediaMTX WHEP endpoint (derived from the Pi’s DHCP address), so no video URL configuration is required unless you want to override the defaults. 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/dummy1.yml b/dist/dummy1.yml index 02517ab4..5b70188b 100644 --- a/dist/dummy1.yml +++ b/dist/dummy1.yml @@ -14,7 +14,6 @@ battery: urgent: 1650 maxWheelSpeed: 350 media: - whepUrl: http://dummy1.local:8889/whep/rovercam manage: false service: mediamtx.service healthUrl: http://127.0.0.1:9997/v3/paths/list diff --git a/dist/dummy2.yml b/dist/dummy2.yml index 0d66a627..7de4a341 100644 --- a/dist/dummy2.yml +++ b/dist/dummy2.yml @@ -14,7 +14,6 @@ battery: urgent: 1650 maxWheelSpeed: 350 media: - whepUrl: http://dummy2.local:8889/whep/rovercam manage: false service: mediamtx.service healthUrl: http://127.0.0.1:9997/v3/paths/list diff --git a/dist/roverd b/dist/roverd index 46275343..0a594ed8 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/mediamtx_server_integration.md b/mediamtx_server_integration.md index 28f79a09..b41f28c2 100644 --- a/mediamtx_server_integration.md +++ b/mediamtx_server_integration.md @@ -5,7 +5,7 @@ Each rover runs mediaMTX locally to capture the Pi camera, and the control serve ## Pi (publisher) - mediaMTX samples the Pi camera (`paths.rovercam.source: rpiCamera`) and exposes the HTTP API on `http://127.0.0.1:9997`. -- `/etc/roverd.yaml` contains `media.whepUrl`, pointing at the Pi’s own WHEP endpoint (e.g. `http://roomba-alpha.local:8889/whep/rovercam`). This is what the central server will pull. +- `roverd` automatically detects the Pi’s 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) diff --git a/pi/roverd/config.go b/pi/roverd/config.go index f1bcd782..c1684bd8 100644 --- a/pi/roverd/config.go +++ b/pi/roverd/config.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net" + "net/url" "os" "strings" "time" @@ -59,6 +60,7 @@ 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"` @@ -141,9 +143,40 @@ 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 := parsed.Path + if path == "" || path == "/" { + path = "/" + } + if !strings.HasSuffix(path, "/whep") { + if !strings.HasSuffix(path, "/") { + path += "/" + } + path += "whep" + } + return fmt.Sprintf("%s://%s%s", proto, parsed.Host, path), nil +} + func ensureLeadingSlash(path string) string { if !strings.HasPrefix(path, "/") { return "/" + path diff --git a/pi/roverd/roverd.sample.yaml b/pi/roverd/roverd.sample.yaml index 5a217b07..ed1deddd 100644 --- a/pi/roverd/roverd.sample.yaml +++ b/pi/roverd/roverd.sample.yaml @@ -15,7 +15,6 @@ battery: urgent: 1650 maxWheelSpeed: 350 media: - whepUrl: http://roomba-alpha.local:8889/whep/rovercam manage: false service: mediamtx.service healthUrl: http://127.0.0.1:9997/v3/paths/list diff --git a/pi/roverd/roverd.yaml b/pi/roverd/roverd.yaml index 5a217b07..ed1deddd 100644 --- a/pi/roverd/roverd.yaml +++ b/pi/roverd/roverd.yaml @@ -15,7 +15,6 @@ battery: urgent: 1650 maxWheelSpeed: 350 media: - whepUrl: http://roomba-alpha.local:8889/whep/rovercam manage: false service: mediamtx.service healthUrl: http://127.0.0.1:9997/v3/paths/list diff --git a/server/src/services/mediaBridgeService.js b/server/src/services/mediaBridgeService.js index 79d303b8..175fdcd7 100644 --- a/server/src/services/mediaBridgeService.js +++ b/server/src/services/mediaBridgeService.js @@ -36,8 +36,12 @@ async function syncRover(record) { await removePath(record?.id); return; } - const source = normalizeSource(record.meta.media.whepUrl); + 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; } @@ -81,27 +85,6 @@ async function removePath(roverId) { } } -function normalizeSource(raw) { - if (!raw) return null; - const trimmed = raw.trim(); - if (!trimmed) return null; - try { - const parsed = new URL(/^[a-z]+:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`); - const protocol = parsed.protocol === 'https:' ? 'wheps' : 'whep'; - let path = parsed.pathname || '/'; - if (!path.endsWith('/whep')) { - if (!path.endsWith('/')) { - path += '/'; - } - path += 'whep'; - } - return `${protocol}://${parsed.host}${path}`; - } catch (err) { - logger.warn('invalid WHEP URL: %s (%s)', raw, err.message); - return null; - } -} - async function callApi(method, path, body) { const url = `${apiBase}${path}`; const res = await fetch(url, {