feat(webrtc): add -rtp-host flag + TrueNAS Docker deploy
- core/webrtc: NewSourceOn(streamID, host, port) allows binding the RTP UDP socket on something other than 127.0.0.1, required when the PoC runs in a container and must accept RTP from LAN publishers. NewSource(streamID, port) stays as a convenience wrapper on 127.0.0.1 for existing tests and tight local tests. - cmd/webrtc-poc: new -rtp-host flag (default 127.0.0.1 for safety). - deploy/docker/Dockerfile: two-stage build, scratch runtime, ~14 MB. - deploy/truenas/docker-compose.yml: host-networked stack template driven by a .env file. Host networking is required for WebRTC ICE to work without NAT rewriting per-candidate. - deploy/truenas/README.md: operator runbook with port picking, bring-up, verification curls, and security notes.
This commit is contained in:
parent
413d0f24b6
commit
9e3f031f95
5 changed files with 160 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
streamID = flag.String("stream", "test", "stream id to serve")
|
streamID = flag.String("stream", "test", "stream id to serve")
|
||||||
|
rtpHost = flag.String("rtp-host", "127.0.0.1", "bind address for RTP UDP socket (use 0.0.0.0 for LAN publishers)")
|
||||||
rtpPort = flag.Int("rtp-port", 10000, "UDP port to receive RTP on")
|
rtpPort = flag.Int("rtp-port", 10000, "UDP port to receive RTP on")
|
||||||
listen = flag.String("listen", ":8787", "WHEP HTTP listen address")
|
listen = flag.String("listen", ":8787", "WHEP HTTP listen address")
|
||||||
publicIP = flag.String("public-ip", "", "server public IP for NAT1To1 (optional)")
|
publicIP = flag.String("public-ip", "", "server public IP for NAT1To1 (optional)")
|
||||||
|
|
@ -27,7 +28,7 @@ func main() {
|
||||||
cfg.WHEPListen = *listen
|
cfg.WHEPListen = *listen
|
||||||
cfg.PublicIP = *publicIP
|
cfg.PublicIP = *publicIP
|
||||||
|
|
||||||
src, err := webrtc.NewSource(*streamID, *rtpPort)
|
src, err := webrtc.NewSourceOn(*streamID, *rtpHost, *rtpPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("NewSource: %v", err)
|
log.Fatalf("NewSource: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,25 @@ type Source struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSource binds a UDP socket on 127.0.0.1:port. Pass port=0 to let the OS
|
// NewSource binds a UDP socket on 127.0.0.1:port. Pass port=0 to let the OS
|
||||||
// assign an ephemeral port (useful for tests).
|
// assign an ephemeral port (useful for tests). Equivalent to
|
||||||
|
// NewSourceOn(streamID, "127.0.0.1", port).
|
||||||
func NewSource(streamID string, port int) (*Source, error) {
|
func NewSource(streamID string, port int) (*Source, error) {
|
||||||
addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}
|
return NewSourceOn(streamID, "127.0.0.1", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSourceOn binds a UDP socket on host:port. Use "0.0.0.0" to accept
|
||||||
|
// RTP from any LAN publisher — required when running in a container
|
||||||
|
// with host networking that needs to receive from other hosts. Empty
|
||||||
|
// host is treated as 127.0.0.1 for backward compatibility.
|
||||||
|
func NewSourceOn(streamID, host string, port int) (*Source, error) {
|
||||||
|
if host == "" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("webrtc: invalid host %q", host)
|
||||||
|
}
|
||||||
|
addr := &net.UDPAddr{IP: ip, Port: port}
|
||||||
conn, err := net.ListenUDP("udp4", addr)
|
conn, err := net.ListenUDP("udp4", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("webrtc: listen udp: %w", err)
|
return nil, fmt.Errorf("webrtc: listen udp: %w", err)
|
||||||
|
|
|
||||||
34
deploy/docker/Dockerfile
Normal file
34
deploy/docker/Dockerfile
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Dockerfile for the Dragon Fork WebRTC PoC (M1).
|
||||||
|
#
|
||||||
|
# Two-stage:
|
||||||
|
# 1. builder: compile a static linux/amd64 binary inside the repo
|
||||||
|
# 2. runtime: minimal scratch image with the binary only
|
||||||
|
#
|
||||||
|
# The PoC has no outbound HTTPS needs and no dynamic libraries, so
|
||||||
|
# `scratch` is safe. Image size ~14 MB.
|
||||||
|
#
|
||||||
|
# The binary's flags (-stream, -rtp-port, -listen, -public-ip) are
|
||||||
|
# passed via `command:` in docker-compose (or `docker run ...`).
|
||||||
|
|
||||||
|
# ---- builder ----
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Static, stripped, no CGO — no shared libs needed in runtime stage.
|
||||||
|
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||||
|
RUN go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o /out/webrtc-poc \
|
||||||
|
./cmd/webrtc-poc
|
||||||
|
|
||||||
|
# ---- runtime ----
|
||||||
|
FROM scratch AS runtime
|
||||||
|
|
||||||
|
COPY --from=builder /out/webrtc-poc /webrtc-poc
|
||||||
|
|
||||||
|
# Defaults — override via `command:` or `docker run ...`.
|
||||||
|
EXPOSE 8787/tcp
|
||||||
|
EXPOSE 10000/udp
|
||||||
|
|
||||||
|
ENTRYPOINT ["/webrtc-poc"]
|
||||||
70
deploy/truenas/README.md
Normal file
70
deploy/truenas/README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# TrueNAS deploy — WebRTC PoC (M1)
|
||||||
|
|
||||||
|
Host-networked Docker stack that runs `cmd/webrtc-poc` on TrueNAS for
|
||||||
|
manual end-to-end testing. Not wired into the Core binary.
|
||||||
|
|
||||||
|
## Prereqs
|
||||||
|
|
||||||
|
- Docker on the TrueNAS host (TrueNAS SCALE includes it)
|
||||||
|
- LAN or public IP that clients can reach
|
||||||
|
- One free TCP port (WHEP) and one free UDP port (RTP ingest)
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
```
|
||||||
|
# On TrueNAS:
|
||||||
|
sudo mkdir -p /mnt/NVME/Docker/dragonfork-webrtc-poc
|
||||||
|
cd /mnt/NVME/Docker/dragonfork-webrtc-poc
|
||||||
|
|
||||||
|
# Copy the repo's deploy/truenas/docker-compose.yml in here, and the
|
||||||
|
# whole repo (or just cmd/ + core/ + go.mod + vendor/) somewhere the
|
||||||
|
# Dockerfile build context can see. Simplest: clone the repo adjacent
|
||||||
|
# and symlink docker-compose.yml, or point `context:` at the clone.
|
||||||
|
|
||||||
|
cat > .env <<EOF
|
||||||
|
WHEP_PORT=45121
|
||||||
|
RTP_PORT=49248
|
||||||
|
STREAM_ID=test
|
||||||
|
PUBLIC_IP=10.0.0.25
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
listening for RTP on 127.0.0.1:49248 # or 0.0.0.0:49248 on real deploy
|
||||||
|
WHEP listening on :45121 — POST /whep/test to subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify from another host on the LAN
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -i -X GET http://10.0.0.25:45121/whep/test # → 405 (POST only)
|
||||||
|
curl -i -X POST http://10.0.0.25:45121/whep/nope # → 404 (stream not found)
|
||||||
|
```
|
||||||
|
|
||||||
|
For a real end-to-end check, point the repo's `test/publish.sh` at
|
||||||
|
`10.0.0.25 49248` and the `whep-client` at `http://10.0.0.25:45121/whep/test`.
|
||||||
|
|
||||||
|
## Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security notes
|
||||||
|
|
||||||
|
- WHEP is served plain HTTP. Put nginx-proxy-manager or Caddy in front
|
||||||
|
for TLS — but note that WHEP itself is fine over HTTPS; the real
|
||||||
|
media is DTLS-SRTP-encrypted regardless.
|
||||||
|
- No auth in M1. Anyone who can reach the port can subscribe.
|
||||||
|
M3 adds a token check.
|
||||||
|
- The binary runs as PID 1 in `scratch` — no shell, no package
|
||||||
|
manager, no privilege escalation path. Exit codes only.
|
||||||
36
deploy/truenas/docker-compose.yml
Normal file
36
deploy/truenas/docker-compose.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Dragon Fork WebRTC PoC — TrueNAS deployment template.
|
||||||
|
#
|
||||||
|
# Host networking is required: WebRTC ICE needs each container-visible
|
||||||
|
# UDP socket to be reachable from the peer using the LAN (or public)
|
||||||
|
# IP advertised in SDP. Bridge + port mapping breaks ICE because
|
||||||
|
# remote candidates encode the peer-visible host:port.
|
||||||
|
#
|
||||||
|
# Copy this file to /mnt/NVME/Docker/dragonfork-webrtc-poc/
|
||||||
|
# alongside a .env like:
|
||||||
|
#
|
||||||
|
# WHEP_PORT=45121 # TCP, the WHEP HTTP listener
|
||||||
|
# RTP_PORT=49248 # UDP, publisher's RTP ingest port
|
||||||
|
# STREAM_ID=test
|
||||||
|
# PUBLIC_IP=10.0.0.25 # LAN IP; rewrites ICE host candidates via NAT1To1.
|
||||||
|
# Set to your public IP when exposing externally.
|
||||||
|
#
|
||||||
|
# Then:
|
||||||
|
# docker compose up -d --build
|
||||||
|
|
||||||
|
services:
|
||||||
|
webrtc-poc:
|
||||||
|
build:
|
||||||
|
context: ../.. # repo root (where go.mod lives)
|
||||||
|
dockerfile: deploy/docker/Dockerfile
|
||||||
|
container_name: dragonfork-webrtc-poc
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
command:
|
||||||
|
- -stream=${STREAM_ID:-test}
|
||||||
|
- -rtp-host=${RTP_HOST:-0.0.0.0}
|
||||||
|
- -rtp-port=${RTP_PORT:?set RTP_PORT}
|
||||||
|
- -listen=:${WHEP_PORT:?set WHEP_PORT}
|
||||||
|
- -public-ip=${PUBLIC_IP:-}
|
||||||
|
# No ports: host networking exposes whatever the process binds.
|
||||||
|
# No healthcheck: scratch image has no shell. Compose uses exit
|
||||||
|
# code only; the binary exits non-zero if it can't bind.
|
||||||
Loading…
Reference in a new issue