Live view over WebRTC (openpanel L1, server and browser)

A device reporting a `live` port streams its camera to the browser's own
RTCPeerConnection. The flows socket carries the signalling as text, so it
crosses the portal: `live` messages on a person's socket, `signal` on a
device's (start, sdp, candidate, bye, busy). The server routes by session,
holds a device to two viewers, ends a session when either socket closes or
no answer comes within 10 s, and gives each socket an outbox of its own
that ends a session rather than drop a message. TURN credentials are
minted per session in coturn's REST scheme (STUN_URLS, TURN_URLS,
TURN_SECRET), the viewer's with the device's offer; GET /devices/{id}/ice
hands them out too. The tunnel keeps a stream's early frames instead of
dropping them past eight and forgets them when the stream or link goes.

A media tile on a device's camera, and the Devices tab's preview, play
live when tapped, pause frames meanwhile, and fall back to frames with a
short note on busy, failure or no picture within 10 s. coturn is an
optional compose service behind the `turn` profile.
This commit is contained in:
Melvin Strobl
2026-10-17 08:37:39 +02:00
parent efd883d092
commit 6a98dcd23f
20 changed files with 1535 additions and 24 deletions
+51
View File
@@ -226,6 +226,57 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# ── TURN relay for live view (optional) ────────────────────────────
# coturn, for a camera's live view between a viewer and a device that
# cannot reach each other directly. It serves STUN too. Behind the `turn`
# profile, so a plain `make up` never starts it. On the host's network,
# because a relay port range through Docker's port mapping is a proxy
# process per port. Credentials are minted by the backend from the same
# TURN_SECRET (coturn's REST scheme), so nothing is stored here. The secret
# goes in through a config file rather than the command line, which `ps`
# shows. See DEPLOY.md for ports, DNS and certificates.
coturn:
image: coturn/coturn:4.6.3
container_name: fluksio-coturn
profiles: ["turn"]
restart: always
network_mode: host
security_opt:
- no-new-privileges:true
environment:
# Not `?`: compose reads every service, so a stack without the
# profile would refuse to start. Checked when coturn starts instead.
- TURN_SECRET=${TURN_SECRET:-}
- TURN_REALM=turn.${DOMAIN?Variable not set}
- TURN_MIN_PORT=${TURN_MIN_PORT:-49160}
- TURN_MAX_PORT=${TURN_MAX_PORT:-49200}
volumes:
# fullchain.pem and privkey.pem for turn.${DOMAIN}. Without them coturn
# runs without TLS on 5349.
- ${TURN_CERT_DIR:-./turn-certs}:/certs:ro
entrypoint: ["sh", "-c"]
command:
- |
[ -n "$$TURN_SECRET" ] || { echo "TURN_SECRET is empty" >&2; exit 1; }
cat > /tmp/turnserver.conf <<EOF
static-auth-secret=$$TURN_SECRET
EOF
exec turnserver -c /tmp/turnserver.conf --log-file=stdout --no-cli \
--listening-port=3478 --tls-listening-port=5349 \
--min-port=$$TURN_MIN_PORT --max-port=$$TURN_MAX_PORT \
--use-auth-secret --realm=$$TURN_REALM --fingerprint \
--cert=/certs/fullchain.pem --pkey=/certs/privkey.pem \
--no-multicast-peers \
--denied-peer-ip=0.0.0.0-0.255.255.255 \
--denied-peer-ip=10.0.0.0-10.255.255.255 \
--denied-peer-ip=100.64.0.0-100.127.255.255 \
--denied-peer-ip=127.0.0.0-127.255.255.255 \
--denied-peer-ip=169.254.0.0-169.254.255.255 \
--denied-peer-ip=172.16.0.0-172.31.255.255 \
--denied-peer-ip=192.168.0.0-192.168.255.255 \
--denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff \
--denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
# ── Site analytics (optional) ──────────────────────────────────────
# Self-hosted, cookieless Umami on analytics.${DOMAIN}. Gated behind the
# `analytics` profile, so a plain `make up` never starts it; bring it up with