Skip to content

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=7 packs 7 MPEG-TS packets (188 bytes each, 1316 bytes total) per UDP payload — the standard packing IPTV/QAM receiving hardware expects, and it matches udpsink's buffer-size=1316.
  • send-duplicates=false on the udpsink avoids 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 souphttpsrc does internally; if the HLS source disappears entirely (e.g. OBS stops streaming), the container will need Compose's restart: unless-stopped to recover once the source comes back.
  • There's no health signal from the coax bridge back into Redis today — unlike dive_detector's state:obs_ok, nothing on the dashboard reflects whether the coax feed is actually reaching the ship's TVs.