From f00c4edc5c863edbe037a4d3593ef0fcc4430d14 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Mon, 13 Apr 2026 13:15:00 +0100 Subject: [PATCH] feat(streaming): add local TURN relay support --- WebApp/.env.example | 5 +++++ WebApp/src/lib/app/controller.js | 30 ++++++++++++++++++++++++++++- docker-compose.yml | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 WebApp/.env.example diff --git a/WebApp/.env.example b/WebApp/.env.example new file mode 100644 index 0000000..4442d13 --- /dev/null +++ b/WebApp/.env.example @@ -0,0 +1,5 @@ +VITE_BACKEND_URL=http://localhost:3000 +VITE_STUN_URLS=stun:stun.l.google.com:19302 +VITE_TURN_URLS=turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp +VITE_TURN_USERNAME=securecam +VITE_TURN_CREDENTIAL=securecamturn diff --git a/WebApp/src/lib/app/controller.js b/WebApp/src/lib/app/controller.js index e2530ac..77ce1f6 100644 --- a/WebApp/src/lib/app/controller.js +++ b/WebApp/src/lib/app/controller.js @@ -76,8 +76,36 @@ const SOCKET_HEARTBEAT_INTERVAL_MS = 10_000; const MAX_STREAM_DIAGNOSTIC_SESSIONS = 12; const MAX_STREAM_DIAGNOSTIC_ENTRIES = 24; +const parseRtcUrls = (value = '') => + value + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + +const buildIceServers = () => { + const stunUrls = parseRtcUrls(import.meta.env.VITE_STUN_URLS ?? 'stun:stun.l.google.com:19302'); + const turnUrls = parseRtcUrls(import.meta.env.VITE_TURN_URLS ?? ''); + const turnUsername = (import.meta.env.VITE_TURN_USERNAME ?? '').trim(); + const turnCredential = (import.meta.env.VITE_TURN_CREDENTIAL ?? '').trim(); + const iceServers = []; + + if (stunUrls.length > 0) { + iceServers.push({ urls: stunUrls }); + } + + if (turnUrls.length > 0) { + iceServers.push({ + urls: turnUrls, + username: turnUsername, + credential: turnCredential + }); + } + + return iceServers.length > 0 ? iceServers : [{ urls: 'stun:stun.l.google.com:19302' }]; +}; + const rtcConfig = { - iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + iceServers: buildIceServers() }; let initialized = false; diff --git a/docker-compose.yml b/docker-compose.yml index 266f455..520e388 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,27 @@ services: + coturn: + image: coturn/coturn:4.6.3 + # Local development default. If camera/client are on other devices, replace 127.0.0.1 + # in TURN_URLS/VITE_TURN_URLS and --external-ip with the host machine's LAN IP. + command: + - -n + - --log-file=stdout + - --realm=securecam.local + - --fingerprint + - --lt-cred-mech + - --user=securecam:securecamturn + - --listening-port=3478 + - --listening-ip=0.0.0.0 + - --relay-ip=0.0.0.0 + - --external-ip=127.0.0.1 + - --min-port=49160 + - --max-port=49200 + ports: + - "3478:3478" + - "3478:3478/udp" + - "49160-49200:49160-49200/udp" + restart: unless-stopped + postgres: image: postgres:16-alpine environment: @@ -54,6 +77,9 @@ services: MEDIA_MAX_SUBSCRIBERS_PER_ROOM: 12 ADMIN_USERNAME: admin ADMIN_PASSWORD: strong-password + TURN_URLS: turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp + TURN_USERNAME: securecam + TURN_CREDENTIAL: securecamturn ports: - "3000:3000" volumes: @@ -65,6 +91,8 @@ services: condition: service_healthy minio: condition: service_started + coturn: + condition: service_started restart: unless-stopped webapp: @@ -75,6 +103,10 @@ services: environment: BACKEND_URL: http://backend:3000 VITE_BACKEND_URL: http://localhost:3000 + VITE_STUN_URLS: stun:stun.l.google.com:19302 + VITE_TURN_URLS: turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp + VITE_TURN_USERNAME: securecam + VITE_TURN_CREDENTIAL: securecamturn ports: - "5173:5173" volumes: @@ -82,6 +114,7 @@ services: - webapp_node_modules:/app/node_modules depends_on: - backend + - coturn restart: unless-stopped volumes: