Coax Video Bridge module
Containers: coax_bridge/entrypoint.sh (image built from
docker/coax_bridge.dockerfile), deployed once per ship as
rc-coax-bridge and dp-coax-bridge in compose.yaml
The coax bridge is the last piece of the pipeline that lives in this repo. Everywhere else in Dive Detector, video is something OBS pushes out over RTMP/SRT to the live-stream platform (see Architecture). The coax bridge runs in the opposite direction: it pulls the already-published HLS stream back down, repackages it as raw MPEG transport stream over UDP, and hands it off to physical broadcast hardware on the ship that puts the feed onto the vessel's internal coax distribution system — the RF network that feeds cabin TVs. Everything downstream of the UDP socket (the transcoder and the RF modulator) is hardware this repo doesn't manage or configure; it only has to show up at a well-known IP and port with a UDP stream that hardware expects.
flowchart LR
OBS["OBS Studio\n(per ship)"]
PLAT["Live Stream Platform\n(HLS origin:\nteledistributor.shore.mbari.org)"]
CB["coax_bridge container\n(gstreamer pipeline)"]
XCODE["Transcoder\n(ship hardware)"]
MOD["RF Modulator\n(ship hardware)"]
COAX["Ship coax distribution\n→ cabin TVs"]
OBS -->|"RTMP/SRT"| PLAT
PLAT -->|"HLS (.m3u8 + segments)"| CB
CB -->|"MPEG-TS over UDP"| XCODE
XCODE --> MOD
MOD --> COAX
GStreamer pipeline
entrypoint.sh runs a single gst-launch-1.0 pipeline that never
touches disk — it's a live re-mux from HLS to UDP:
souphttpsrc (HLS_URL)
→ hlsdemux # pulls the .m3u8 playlist + .ts segments over HTTP
→ tsdemux # unpacks the transport stream carried in each segment
→ h264parse # normalizes the raw H.264 elementary stream
→ mpegtsmux alignment=${ALIGNMENT}
→ udpsink host=${UDP_HOST} port=${UDP_PORT}
mpegtsmux alignment=7packs 7 MPEG-TS packets (188 bytes each, 1316 bytes total) per UDP payload — the standard packing IPTV/QAM receiving hardware expects, and it matchesudpsink'sbuffer-size=1316.send-duplicates=falseon theudpsinkavoids re-sending the last buffer on an empty queue, which would otherwise show up as stutter/duplicate frames downstream.- The container does no transcoding of its own — it re-wraps whatever H.264 the streaming platform already produced. Any bitrate/format conversion needed for the modulator happens in the downstream transcoder hardware, not here.
Environment variables
| Variable | Default | Description |
|---|---|---|
HLS_URL |
— (required) | HLS playlist URL to pull, e.g. http://teledistributor.shore.mbari.org:5080/LiveLink/streams/rvrachelcarson.m3u8 |
UDP_HOST |
— (required) | IP address of the downstream transcoder/modulator hardware |
UDP_PORT |
2345 |
UDP destination port |
ALIGNMENT |
7 |
MPEG-TS packets per UDP payload passed to mpegtsmux |
entrypoint.sh fails fast (set -e plus a ${VAR:?...} guard) if
HLS_URL or UDP_HOST isn't set, so a misconfigured container exits
immediately instead of running with a broken destination.
Deployment
Two containers run side by side, one per ship, both built from the same image and pointed at different HLS sources and destination ports on the same modulator host:
| Container | Ship | HLS source | UDP destination |
|---|---|---|---|
rc-coax-bridge |
R/V Rachel Carson | .../streams/rvrachelcarson.m3u8 |
134.89.5.12:2346 |
dp-coax-bridge |
R/V David Packard | .../streams/rvdavidpackard.m3u8 |
134.89.5.12:2345 |
Both run with network_mode: host rather than joining redisnet —
the coax bridge doesn't talk to Redis or any other Dive Detector
service, and host networking lets the UDP stream reach the modulator
hardware's address directly without being NAT'd behind Docker's bridge
network.
Dockerfile
FROM restreamio/gstreamer:2026-05-28T07-39-13Z-prod
COPY coax_bridge/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
The image is just the restreamio/gstreamer base (which bundles the
GStreamer plugins the pipeline needs — souphttpsrc, hlsdemux,
tsdemux, h264parse, mpegtsmux, udpsink) plus the entrypoint
script. There's no application code to build.
Operational notes
- Because the bridge re-pulls the HLS output rather than tapping OBS directly, it depends on the streaming platform's HLS segmenting being healthy — an HLS outage or elevated latency shows up as stutter/dropout in the coax feed, not just the web dashboard.
- The pipeline has no reconnect/backoff logic beyond what
souphttpsrcdoes internally; if the HLS source disappears entirely (e.g. OBS stops streaming), the container will need Compose'srestart: unless-stoppedto recover once the source comes back. - There's no health signal from the coax bridge back into Redis today
— unlike
dive_detector'sstate:obs_ok, nothing on the dashboard reflects whether the coax feed is actually reaching the ship's TVs.