From adc8aa67265f8af1b8c45501bda08cc21f7c8fbe Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 13 Aug 2026 22:35:01 -0400 Subject: [PATCH 1/3] feature: cameras --- .vitepress/config.mts | 4 ++++ advanced/web-push.md | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 advanced/web-push.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index df8cf41..4437209 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -352,6 +352,10 @@ export default defineConfig({ text: "Faceit Integration", link: "/faceit-integration", }, + { + text: "Web Push Notifications", + link: "/web-push", + }, { text: "HashiCorp Vault", link: "/vault", diff --git a/advanced/web-push.md b/advanced/web-push.md new file mode 100644 index 0000000..54ef3d3 --- /dev/null +++ b/advanced/web-push.md @@ -0,0 +1,51 @@ +# Web Push Notifications + +Web Push lets 5stack notify players on their phone or desktop **when the app isn't open** — a match going live, an invite, a scrim request, a new message. + +It is enabled out of the box and needs no configuration. Players turn it on per device under **Settings → Notification Preferences**, where they can also mute individual categories. Application settings → **Integrations → Web Push** shows whether it's active and how many devices are subscribed. + +## No service, no keys to buy + +A common assumption is that push notifications need a paid provider like Firebase, OneSignal or Pusher. **They don't.** + +Every browser vendor runs its own push service, free, with no account and no API key: + +| Browser | Push service | +| --- | --- | +| Chrome, Edge, Brave | Google's push endpoints | +| Firefox | Mozilla autopush | +| Safari (macOS 16+, iOS 16.4+) | Apple Push Notification service | + +When a player enables notifications, their browser hands 5stack a subscription URL pointing at its own vendor's service. 5stack sends notifications to that URL directly. + +The one piece of configuration is a **VAPID keypair**, which identifies your panel to those services. It is self-signed and registered with nobody, so 5stack generates it for you on install. + +::: tip Why people think it costs money +The old **GCM** protocol did require a Firebase project and a server key. It was replaced by the **VAPID** standard, which needs neither. If a guide tells you to paste a Firebase key, it's describing the deprecated flow. +::: + +## Requirements and limitations + +These are constraints of the Web Push standard and the browsers, not of 5stack. + +**HTTPS is required.** Service workers only run on a secure origin. A panel served over plain HTTP cannot deliver push notifications at all. + +**iOS requires the app be installed to the home screen.** On iPhone and iPad, Safari has no notification support in a normal tab. Players must open 5stack in Safari, tap Share → *Add to Home Screen*, and enable notifications from the installed app. This needs iOS 16.4 or newer. The settings page shows this hint automatically on iOS. + +**The permission prompt is one-shot.** A browser only asks once. If a player dismisses or blocks it, 5stack cannot ask again — they have to re-enable notifications in their browser or OS settings. + +**Delivery is best-effort.** There are no delivery or read receipts. Push services may delay, batch, or drop notifications for a device that has been offline for a long time — the spec makes no guarantees. The in-app alert bell is the reliable record; push is the nudge. + +**Subscriptions expire on their own.** Clearing site data, uninstalling the PWA, or a long period of inactivity ends a subscription. 5stack deletes those rows automatically, so the subscriber count reflects reality. + +**Payloads are small** — roughly 4KB, so bodies are trimmed to a short preview. Tapping through opens the relevant page in the app. + +**Content passes through the vendor's infrastructure** encrypted, but avoid putting anything in a notification title or body you wouldn't want on a lock screen. + +## Troubleshooting + +**A player sees "Push notifications aren't supported in this browser".** They're on iOS in a Safari tab (see above), on an old browser, or the panel is being served over HTTP. + +**The toggle does nothing and no prompt appears.** Notifications are blocked for the site at the browser or OS level. It has to be re-enabled there first; the page will say so when it detects this. + +**Notifications stopped for everyone at once.** The signing keys were rotated — every device has to re-enable notifications. Only rotate if the private key has leaked. From d757b3ddfd8e49e1889dc733317608234c17e334 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Fri, 14 Aug 2026 00:03:21 -0400 Subject: [PATCH 2/3] wip --- .vitepress/config.mts | 2 + advanced/web-push.md | 12 +-- features/admin/index.md | 3 +- features/admin/integrations.md | 7 ++ features/index.md | 14 +++- features/live-streaming.md | 10 +++ features/matches.md | 8 ++ features/player-cameras.md | 130 +++++++++++++++++++++++++++++++++ features/social.md | 30 ++++++++ features/voice-chat.md | 72 ++++++++++++++++++ index.md | 2 + 11 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 features/player-cameras.md create mode 100644 features/voice-chat.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 4437209..d71c3b1 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -93,6 +93,7 @@ export default defineConfig({ { text: "Matches", link: "/matches" }, { text: "Map Veto Format", link: "/map-veto" }, { text: "Scrim Finder", link: "/scrim-finder" }, + { text: "Player Cameras", link: "/player-cameras" }, ], }, { @@ -131,6 +132,7 @@ export default defineConfig({ base: "/features", items: [ { text: "Teams, Lobbies & Chat", link: "/social" }, + { text: "Voice Chat", link: "/voice-chat" }, { text: "Steam Presence", link: "/steam-presence" }, { text: "Moderation & Sanctions", link: "/moderation" }, { text: "News", link: "/news" }, diff --git a/advanced/web-push.md b/advanced/web-push.md index 54ef3d3..25668d1 100644 --- a/advanced/web-push.md +++ b/advanced/web-push.md @@ -2,7 +2,7 @@ Web Push lets 5stack notify players on their phone or desktop **when the app isn't open** — a match going live, an invite, a scrim request, a new message. -It is enabled out of the box and needs no configuration. Players turn it on per device under **Settings → Notification Preferences**, where they can also mute individual categories. Application settings → **Integrations → Web Push** shows whether it's active and how many devices are subscribed. +It is enabled out of the box and needs no configuration. Players turn it on per device under **Settings → Notification Preferences**, where they can also mute individual categories and set **quiet hours** — a window in which push stays silent while the notifications still land in the in-app alert bell. Application settings → **Integrations → Web Push** shows whether it's active and how many devices are subscribed. ## No service, no keys to buy @@ -10,10 +10,10 @@ A common assumption is that push notifications need a paid provider like Firebas Every browser vendor runs its own push service, free, with no account and no API key: -| Browser | Push service | -| --- | --- | -| Chrome, Edge, Brave | Google's push endpoints | -| Firefox | Mozilla autopush | +| Browser | Push service | +| ----------------------------- | ------------------------------- | +| Chrome, Edge, Brave | Google's push endpoints | +| Firefox | Mozilla autopush | | Safari (macOS 16+, iOS 16.4+) | Apple Push Notification service | When a player enables notifications, their browser hands 5stack a subscription URL pointing at its own vendor's service. 5stack sends notifications to that URL directly. @@ -30,7 +30,7 @@ These are constraints of the Web Push standard and the browsers, not of 5stack. **HTTPS is required.** Service workers only run on a secure origin. A panel served over plain HTTP cannot deliver push notifications at all. -**iOS requires the app be installed to the home screen.** On iPhone and iPad, Safari has no notification support in a normal tab. Players must open 5stack in Safari, tap Share → *Add to Home Screen*, and enable notifications from the installed app. This needs iOS 16.4 or newer. The settings page shows this hint automatically on iOS. +**iOS requires the app be installed to the home screen.** On iPhone and iPad, Safari has no notification support in a normal tab. Players must open 5stack in Safari, tap Share → _Add to Home Screen_, and enable notifications from the installed app. This needs iOS 16.4 or newer. The settings page shows this hint automatically on iOS. **The permission prompt is one-shot.** A browser only asks once. If a player dismisses or blocks it, 5stack cannot ask again — they have to re-enable notifications in their browser or OS settings. diff --git a/features/admin/index.md b/features/admin/index.md index f52c372..6295db8 100644 --- a/features/admin/index.md +++ b/features/admin/index.md @@ -43,4 +43,5 @@ Roles are assigned from the [player directory](/features/admin/roles-and-players performance, backups, and the render queue. The admin controls for [demo settings](/features/demos), [highlights](/features/highlights), -and [streaming](/features/live-streaming) are covered on those feature pages. +[streaming](/features/live-streaming), [player cameras](/features/player-cameras), +and [voice chat](/features/voice-chat) are covered on those feature pages. diff --git a/features/admin/integrations.md b/features/admin/integrations.md index 076d7c0..cadec07 100644 --- a/features/admin/integrations.md +++ b/features/admin/integrations.md @@ -32,3 +32,10 @@ link their own Steam match history from their personal settings. ## Chat Set the chat **message TTL**, how long chat messages are retained (seconds). + +## Web Push + +Whether push notifications are active, how many devices are subscribed, and where +the signing keys came from, plus a **regenerate keys** action for the case where a +private key leaks (it unsubscribes every device). Nothing to sign up for and no +third-party provider, see [Web Push Notifications](/advanced/web-push). diff --git a/features/index.md b/features/index.md index 9251661..354c14e 100644 --- a/features/index.md +++ b/features/index.md @@ -19,6 +19,9 @@ Pages linked below go deeper on the bigger features. match rules (knife, overtime, pauses, best-of, coaches, and more). - **[Scrim Finder](/features/scrim-finder)**: advertise when your team plays, set who you'll play, and negotiate scrims without a Discord thread. +- **[Player Cameras](/features/player-cameras)**: require a webcam from every + player, watch the feeds from the panel, auto-pause when one drops, and put the + spectated player's camera on the broadcast. - **Custom Map Pools**: curated pools for every format (1v1, 2v2, 5v5), including workshop maps. @@ -65,8 +68,10 @@ Pages linked below go deeper on the bigger features. ## Community -- **[Teams, Lobbies & Chat](/features/social)**: teams, friends, parties, and - smart, context-aware chat that bridges the web app and in-server. +- **[Teams, Lobbies & Chat](/features/social)**: teams, friends, parties, direct + messages, and smart, context-aware chat that bridges the web app and in-server. +- **[Voice Chat](/features/voice-chat)**: party and team voice on 5Stack's own + relay, with device pickers, a mic check, and voice-activity transmission. - **[Steam Presence](/features/steam-presence)**: see what friends are playing, in 5Stack and outside it. - **[Moderation & Sanctions](/features/moderation)**: bans, mutes, gags, and @@ -93,6 +98,9 @@ Pages linked below go deeper on the bigger features. - **Installable app (PWA)**: add 5Stack to your home screen or desktop for an app-like experience, with offline-aware caching. +- **[Push notifications](/advanced/web-push)**: per-category, per-device alerts + that reach players when the app isn't open, with no third-party push service to + sign up for. - **[17 languages](/features/languages)**: the interface *and* the in-game plugin are fully localized, with automatic detection of your browser language. - **Embeds & popouts**: drop a live, auto-cycling tournament bracket onto a @@ -126,7 +134,7 @@ Pages linked below go deeper on the bigger features. Several features are optional and can be switched on or off instance-wide from **Settings → Application**: matchmaking, leagues, seasons, events, news, the -Scrim Finder, plugins, and linked accounts. +Scrim Finder, voice chat, plugins, and linked accounts. ::: tip Rendering features need a GPU Highlights, in-browser demo playback, and live streaming are all rendered on a diff --git a/features/live-streaming.md b/features/live-streaming.md index 5cf3662..0ace678 100644 --- a/features/live-streaming.md +++ b/features/live-streaming.md @@ -23,6 +23,9 @@ The Stream Deck is the broadcaster's control panel: - **Spectator grid**: CT/T player slots fed by live game state; click a slot to spectate that player. - **Autodirector** toggle for automatic spectator camera work. +- **Player webcams inside the spectator slots**, when the match requires them, so + the box you read to pick a target is the box you click. See + [Player Cameras](/features/player-cameras). - GPU availability and active stream count so you know your capacity. ## Scoreboard overlay @@ -34,6 +37,13 @@ every map in the series. It has a compact mode for small windows. Streams play over **WebRTC (WHEP)** for ultra-low latency, and you can **pop a match out** into its own chromeless window to drag onto a second monitor. +## Face cams + +If a match [requires player cameras](/features/player-cameras), the camera of the +player currently being spectated is composited onto the stream through the +existing overlay, no second capture, no extra encoder, and nothing for the +broadcaster to wire up. + ## Picture-in-picture Start watching, then keep browsing. A live stream **detaches into a floating, diff --git a/features/matches.md b/features/matches.md index ce13fd3..19c5b3b 100644 --- a/features/matches.md +++ b/features/matches.md @@ -51,6 +51,14 @@ Tune the match to the occasion: **knife round, overtime** (rounds and bomb timer ready-up, default player models, and more. Game-mode presets apply sensible defaults in one click. +## Player cameras + +A match can **require a webcam** from every player: they connect one from their +PC or by scanning a QR code with a phone, the server details stay hidden until +they do, and the match pauses automatically if a feed drops. Officials watch the +feeds from the panel, and the spectated player's camera can appear on the +broadcast. See [Player Cameras](/features/player-cameras). + ## Watching live While a match is live you can follow the score and action in real time, jump in diff --git a/features/player-cameras.md b/features/player-cameras.md new file mode 100644 index 0000000..63b3b70 --- /dev/null +++ b/features/player-cameras.md @@ -0,0 +1,130 @@ +# Player Cameras + +Matches can require every player to publish a **webcam** for the duration of the +game. Officials watch the feeds from the panel, and the camera of whoever is +being spectated can appear on the [broadcast](/features/live-streaming) — the +face-cam grid you see on a LAN stream, without the LAN. + +## Turning it on + +Cameras are a **per-match option**, set when the match is created or edited: + +- **Require Webcam**: players must connect a camera before the server details are + shown, and the match pauses automatically if a feed drops. +- **Teammates Can Watch**: let players see the cameras of their _own_ team. The + opposing team is never included. + +Instance-wide defaults for both live under +**Settings → Application → Cameras & Microphones → Match Defaults**. They only +decide what a newly created match starts with; an organizer can still change +either one per match, and existing matches are unaffected. + +## Connecting a camera + +When a match requires a webcam, a panel appears on the match page and on the +check-in button in a [draft lobby](/features/draft-lobbies). A player has two +ways in: + +- **Use this computer**: opens the camera page in a popup on the machine they're + already on. +- **Scan the QR code**: publishes from a phone instead, which is how most players + will do it — the phone becomes the camera and the PC stays untouched. + +The camera page previews before it publishes. The feed starts local-only, so a +player can pick a device, frame themselves, and only then connect — one +permission prompt, no dead air: + +- **Pick a camera** when the device has more than one; phones and tablets also + get a **flip** button for front/back. +- **Reframe** the shot: scroll or pinch to zoom, drag to re-centre. The crop is + applied to the **published** feed, not just the local preview, and is + remembered per camera. +- **Hide the preview** mid-match to give the resources back. The camera stays on + and stays published. +- Permission denied, no camera, or a camera already in use each explain + themselves in place, with a retry. + +The one rule that matters: **keep the page open for the rest of the match.** +Closing it stops the feed, which is the same as unplugging the camera. + +The setup panel can be dismissed to get at the rest of the match page, but the +requirement doesn't go with it: a banner stays pinned to the top of the page, and +clicking it brings the panel back. Check-in stays blocked until a camera is live. + +## Who can watch + +Watching is scoped on the server, not hidden in the browser — a feed a viewer +isn't entitled to is never sent to them at all. + +| Viewer | Sees | +| ----------------------------------------------- | ----------------------------------------------------------- | +| Organizer or admin **not** playing in the match | Both lineups | +| A player in the match | Their own lineup, and only if **Teammates Can Watch** is on | +| The opposing team | Nothing, ever | + +A match organizer who is also playing is a competitor first: a live view of the +other side is exactly the advantage cameras exist to prevent. Site +administrators keep full access so the feature can be exercised end to end. + +## Watching from the panel + +- **Watch Cameras** on the match page opens the grid, and doubles as the summary + — it carries a live count like _7 of 10 live_. +- Each tile shows the player the way the rest of the app does, with a + **connected / stalled / not connected** state. A quiet grid means everything is + fine; only a problem tile draws the eye. +- **Listen to a player** from any tile without starting a call. +- **Video call** a player from their tile. Their camera page shows you in a small + picture-in-picture, so you can talk to them face to face — an actual check that + a person is there, which no automated test of a video feed can give you. +- **Camera dots** on the lineup rows give organizers per-player state at a glance + without opening the grid. +- On the [Stream Deck](/features/live-streaming#stream-deck), player cameras + render **inside the observer-target slots**, so the box you read to pick a + spectate target is the box you click. Feeds start muted. + +## Enforcement + +5Stack keeps checking, rather than checking once at the door: + +- Feeds are polled continuously and classified **live**, **stalled**, or + **down** from whether video is actually arriving. +- A drop gets a **30-second grace period**, long enough to ride out a WiFi blip + or a reconnect. +- Past that, the game server is told to **pause the match** with a named reason. + The pause blocks `.resume` until the camera is back, and the scoreboard shows + who the match is waiting on. Players can ask the server for their own status + with `.cam`. +- Resume is **announced, never silent**, and an admin can override. +- If the relay itself is unreachable, nothing changes — the check **fails open**, + so an infrastructure problem never pauses a match. + +## On the broadcast + +While a player is being spectated, their camera can be composited onto the live +stream through the existing overlay — no extra capture, no second encoder, and +nothing for a broadcaster to set up. + +## Requirements & limits + +**HTTPS is required.** Browsers only grant camera access on a secure origin. A +panel served over plain HTTP cannot publish cameras at all. + +**Feeds are relayed, not recorded.** 5Stack passes camera video through its own +WebRTC relay and stores none of it. A camera that appears on a broadcast is, of +course, part of whatever that broadcast records. + +**Cameras are deliberately small.** A published feed is capped at 360p and 24fps +(portrait on a phone), roughly 350 kbps, so publishing from mobile data is +realistic and a full ten-player lineup costs your server a few Mbps rather than +ten HD streams. + +**A camera proves a feed is flowing, not that a person is being watched.** A +virtual camera looping a video passes every automated check there is. That is +what the video-call button is for — treat cameras as a deterrent and a broadcast +feature, and use a call when you need certainty. + +::: tip Cameras use the same relay as voice and live streaming +No third-party video service is involved, and there is no per-player cost. See +[Voice Chat](/features/voice-chat). +::: diff --git a/features/social.md b/features/social.md index 940bd75..f5ba6d0 100644 --- a/features/social.md +++ b/features/social.md @@ -37,6 +37,29 @@ who's currently in the room. 5Stack chat bridges the **web app and in-server**, so conversations stay connected whether players are in the browser or in the game. +Every conversation you're in, match, team, lobby, tournament, or organizers, +becomes a **tab** in the chat rail, with an unread badge that counts from the +moment you sign in rather than from the moment you open the panel. + +## Direct messages + +Message a friend one-to-one from their **player page** or straight from your +friends list. A direct message opens as its own chat tab, showing the person's +avatar instead of a channel icon. + +Direct messages are limited to **accepted friends** in both directions, so +nobody can be cold-messaged, and an incoming message opens its tab without +yanking you out of the conversation you're reading. They're also kept far longer +than room chatter, 30 days rather than the shared chat TTL, because a +conversation you can't scroll back through isn't much of a conversation. + +## Voice chat + +Talk to your party in a lobby, or to your team on the match page, with +[voice chat](/features/voice-chat) built into the panel: device pickers, a mic +check you can run before anyone hears you, voice-activity or open-mic +transmission, and noise suppression. + ## Connect Discord Link your Discord account to 5Stack to unlock the bot, create matches, manage @@ -50,3 +73,10 @@ lobbies, get match updates, and have your roles synced, without leaving Discord. Chat and match sounds are context-aware: they automatically quiet down when you're **in a game** or the tab is **hidden**, so you're never spammed mid-round. You can pick your own notification sounds and set the volume. + +**Settings → Notification Preferences** decides what reaches you and where. +Categories, matches, chat, invites, tournaments, scrims, leagues, teams, events, +seasons, account, and news, can be muted individually on the in-app alert bell, +and turned on per device as [web push notifications](/advanced/web-push) that +arrive even when 5Stack isn't open. **Quiet hours** keep push silent during a +window you set; those notifications still land in the alert bell. diff --git a/features/voice-chat.md b/features/voice-chat.md new file mode 100644 index 0000000..29fe63e --- /dev/null +++ b/features/voice-chat.md @@ -0,0 +1,72 @@ +# Voice Chat + +Talk to the people you're about to queue with, or the team you're playing with, +without leaving the panel. Voice runs on 5Stack's own relay — no Discord server, +no third-party service, no per-user cost. + +## Where you can talk + +- **Lobby voice**: the voice row in a matchmaking lobby or party. Everyone who + has accepted the lobby invite is in the channel. +- **Team voice**: on the match page and in a [draft lobby](/features/draft-lobbies), + scoped to your **lineup**. A channel is one team's — it can never span both + sides, and membership is enforced on the server rather than in the browser. + +You're in **one channel at a time**. Joining a second asks first, then moves you. + +## Controls + +Join, mute, and leave sit in the panel itself, with a status dot, a +speaking indicator, and a count of how many people are talking out of how many +are connected. Muting keeps your connection up and simply stops sending audio, +so nobody sees you drop and rejoin. + +## Voice settings + +The **gear sits on every place you can join a channel** — the party hub, the +match panel, a draft room — and opens the same settings each time. There's one +set of audio settings, not one per surface: pick your devices once and they +apply to whatever channel you're in next, remembered across sessions. + +They also open **before** you join, so you can get your setup right while nobody +can hear you. Your microphone is only opened while the dialog is up, and +released again when you close it unless a call is actually running. + +What's in there: + +- **Input and output device** pickers, remembered across sessions. Unplug a + remembered device and 5Stack falls back to the system default instead of + leaving you silent. Changing device mid-call swaps it in place — nobody's + audio drops. +- **Mic Check**: hear your own microphone played back, _after_ the gate, so you + hear exactly what everyone else hears — including being cut off when your + sensitivity is too high. Use headphones for this one. +- **Input level meter** with your threshold drawn on it, and a **test tone** for + the output device. +- **Input mode**: **Voice Activity** with an adjustable sensitivity, or + **Always Transmit**. +- **Noise suppression**, applied to the live track, so toggling it doesn't + interrupt the call. + +## Admin controls + +Under **Settings → Application → Cameras & Microphones**: + +- **Lobby Voice Chat**: turn voice on or off instance-wide. It's on by default. + +Voice shares the same WebRTC relay as [player cameras](/features/player-cameras) +and [live streaming](/features/live-streaming), so there's nothing extra to +install or pay for. + +## Requirements + +**HTTPS is required.** Browsers only expose a microphone on a secure origin — on +plain HTTP the join button is disabled and says so, rather than failing when +clicked. Blocked permission, a missing microphone, and a microphone already in +use each report themselves by name instead of as a generic failure. + +::: tip Discord still works +Voice chat is an alternative to hopping into a Discord call, not a replacement +for the [Discord bot](/advanced/discord-bot/) — per-tournament voice channels, +match creation, and role sync are unaffected. +::: diff --git a/index.md b/index.md index ee91331..af46032 100644 --- a/index.md +++ b/index.md @@ -93,6 +93,7 @@ features: - [Competitive Seasons](/features/seasons) Elo resets and season-scoped leaderboards - [Map Veto](/features/map-veto) deterministic pick/ban for any pool and best-of - [Custom Map Pools](/features/admin/matchmaking) per format, workshop maps included +- [Player Cameras](/features/player-cameras) required webcams, watched live, on the stream
@@ -110,6 +111,7 @@ features: **Community & operations** - [Teams, Lobbies & Chat](/features/social) chat that bridges web and in-server +- [Voice Chat](/features/voice-chat) party and team voice, on your own relay - [Moderation & Sanctions](/features/moderation) bans, mutes, gags, VAC detection - [News](/features/news) first-party announcements from inside the panel - [Monitoring](/features/admin/system) CPU, memory, logs, and database health From 09eeb623c29026ef2e4d5708792004de112a9b11 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Fri, 14 Aug 2026 13:27:36 -0400 Subject: [PATCH 3/3] wip --- .vitepress/config.mts | 4 + advanced/turn-server.md | 163 ++++++++++++++++++++++++++++++++++++++++ features/voice-chat.md | 37 +++++++++ 3 files changed, 204 insertions(+) create mode 100644 advanced/turn-server.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index d71c3b1..36be891 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -354,6 +354,10 @@ export default defineConfig({ text: "Faceit Integration", link: "/faceit-integration", }, + { + text: "TURN Relay", + link: "/turn-server", + }, { text: "Web Push Notifications", link: "/web-push", diff --git a/advanced/turn-server.md b/advanced/turn-server.md new file mode 100644 index 0000000..9d2207e --- /dev/null +++ b/advanced/turn-server.md @@ -0,0 +1,163 @@ +# TURN Relay + +Optional. Most installs never need one. + +[Voice chat](/features/voice-chat), [video calls](/features/voice-chat#video-calls) +and [player cameras](/features/player-cameras) all use WebRTC, which normally +connects a player straight to 5Stack's media server. When a player's network +won't allow that, a TURN relay gives them a way through — at the cost of every +one of their packets passing through your server. + +## Do you need one? + +Probably not. Try it without first — nothing is disabled, and you can add a +relay later without touching anything else. + +Add one if players report that voice or video **connects for some people and +never for others**, particularly: + +- **Symmetric or carrier-grade NAT.** Common on mobile networks and some ISPs. +- **Networks that block UDP outright.** Corporate and university networks. + +Both are the minority. If calls work on a phone over mobile data, you almost +certainly don't need a relay. + +::: tip Check before you build +The clearest signal is a player whose connection **never** establishes while +everyone else in the same channel is fine. A call that connects and then sounds +bad is a bandwidth or CPU problem, not a NAT one — a relay won't help. +::: + +## What it costs + +The relay forwards media rather than just helping peers find each other, so +**every relayed call uses your server's bandwidth in both directions**. It also +holds one UDP port per relayed session, and runs on the host network. + +That is the whole reason it is opt-in: 5Stack won't deploy a relay you didn't +ask for. + +## Enabling it + +### 1. DNS + +Add an `A` record for the node running 5Stack: + +``` +turn.example.com → +``` + +::: danger Must not be proxied +If you use Cloudflare, **turn the proxy off** (grey cloud, not orange) for this +record. + +TURN is not HTTP, so a CDN proxy cannot carry it — and worse, coturn resolves +this name at startup to work out which address to advertise to clients. A +proxied record answers with the CDN's address, so the relay would hand out +candidates pointing at the CDN. Every one of them is unreachable, and the +lookup *succeeded*, so nothing looks wrong in the logs. + +If you must keep the record proxied, set `TURN_PUBLIC_IP` (below) to the node's +real address and coturn will skip the lookup. +::: + +### 2. Firewall + +Open on that node: + +| Port | Protocol | What for | +| --- | --- | --- | +| `3478` | UDP | TURN | +| `3478` | TCP | TURN, for networks that block UDP | +| `50000-50200` | UDP | The relay range — one port per active relayed session | + +The relay range is deliberately narrow so this stays a short firewall rule. It +also caps how many calls can be relayed at once, which is a limit worth having. +Widen it in `TURN_MIN_PORT` / `TURN_MAX_PORT` if you actually run out. + +### 3. Configure + +Set the domain in `overlays/coturn/coturn.env`: + +```bash +TURN_DOMAIN=turn.example.com +``` + +Then run the updater: + +```bash +./update.sh +``` + +That's it. The shared secret is generated for you on first run, and the API +starts handing out relay credentials automatically. + +## Settings + +All in `overlays/coturn/coturn.env`: + +| Setting | Default | What it does | +| --- | --- | --- | +| `TURN_DOMAIN` | *(blank)* | The relay's hostname. **Blank means no relay is deployed at all.** | +| `TURN_PUBLIC_IP` | *(blank)* | Resolved from `TURN_DOMAIN` when blank. Set it by hand if that record is proxied. | +| `TURN_REALM` | `TURN_DOMAIN` | What coturn calls itself when challenging a client. Rarely worth changing. | +| `TURN_MIN_PORT` | `50000` | Start of the relay range. | +| `TURN_MAX_PORT` | `50200` | End of the relay range. | + +The shared secret lives separately, in +`overlays/local-secrets/coturn-secrets.env`, and is generated on first run. + +## How credentials work + +Nothing long-lived is ever handed to a browser. The API mints a username +carrying its own expiry and an HMAC of that username, signed with a secret the +relay verifies against. Both lapse on their own, so a leaked pair stops working +without anyone having to revoke it, and there are no TURN user accounts to +manage. + +The relay and the API read the **same secret** from the same place, so there is +nothing to keep in sync. + +## Turning it off + +Blank out `TURN_DOMAIN` and re-run `./update.sh`. The relay stops being +deployed, and calls fall back to connecting directly. + +An already-running relay pod isn't deleted for you — remove it with +`kubectl delete deployment coturn -n 5stack` if you want it gone. Left alone it +notices the blank domain and idles without binding any ports. + +## What does *not* use the relay + +Deliberately, and it matters: + +- **Regional matchmaking latency probes.** These time a connection to each + region to work out which one you're closest to. Routed through a relay, every + region would report the latency of the hop to the relay instead — quietly + breaking region selection with nothing to point at. +- **Game streams, demo playback and highlight clips.** These connect directly + without trouble, and relaying them would push every viewer's bitrate through + your server. + +Only calls and camera feeds ever use it. + +## Troubleshooting + +Check what the relay decided at startup: + +```bash +kubectl logs -n 5stack deployment/coturn | head +``` + +| Log line | Meaning | +| --- | --- | +| `TURN_DOMAIN is not set; no relay requested, idling` | The overlay is deployed but unconfigured — usually a leftover from before you blanked the domain. | +| `advertising for ` | Working as intended behind NAT. | +| `host is publicly addressable at ` | Working as intended with a public IP. | +| ` did not resolve; no external-ip set` | DNS isn't there yet. Candidates will be wrong until it is. | +| `TURN_SECRET is empty; refusing to start an open relay` | The secret didn't generate. Re-run `./update.sh`. | + +To confirm the API is handing out credentials, sign in and open +`https://api.example.com/voice/ice-servers`. With no relay configured you'll see +a single STUN entry; with one, a `turn:` entry alongside it carrying a `username` +and `credential`. diff --git a/features/voice-chat.md b/features/voice-chat.md index 29fe63e..71d799f 100644 --- a/features/voice-chat.md +++ b/features/voice-chat.md @@ -48,11 +48,40 @@ What's in there: - **Noise suppression**, applied to the live track, so toggling it doesn't interrupt the call. +## Video calls + +Any voice channel can also carry a camera. It is always **opt-in and off by +default** — joining a channel never turns your camera on, and there is a +separate control to do it deliberately. + +Your camera is a second, independent stream from your microphone, which is what +makes turning it on or off free: the audio carrying the conversation is never +renegotiated, so nobody hears a drop-out when someone appears or disappears. + +- **You only see other people's cameras once yours is on.** Opening the call and + sitting there does not show you the room. +- **Tiles show everyone in the channel**, with an avatar for anyone who has not + turned a camera on, so the grid doesn't reflow as people join. +- **Pop the video out** into a floating always-on-top window, and keep it in + view while you use the rest of the panel or watch a stream. Popping in and out + doesn't interrupt anything — the call keeps running throughout. (The floating + window needs a Chromium-based browser; elsewhere the grid stays in the panel.) + +Capture is capped well below what a webcam can produce. A call is watched in +tiles a few hundred pixels wide, so anything more is frames per second spent on +detail nobody looks at — on a machine that is also running a game. + ## Admin controls Under **Settings → Application → Cameras & Microphones**: - **Lobby Voice Chat**: turn voice on or off instance-wide. It's on by default. +- **Video Calls**: allow cameras in voice channels, on by default wherever + voice is. This only decides whether the control is *offered* — turning a + camera on is always the player's own deliberate act. Split into **party + lobbies** and **matches and draft rooms**, so you can drop the match one if + your players run tight on CPU: a tile grid decodes alongside the live + stream. Voice shares the same WebRTC relay as [player cameras](/features/player-cameras) and [live streaming](/features/live-streaming), so there's nothing extra to @@ -65,6 +94,14 @@ plain HTTP the join button is disabled and says so, rather than failing when clicked. Blocked permission, a missing microphone, and a microphone already in use each report themselves by name instead of as a generic failure. +Cameras additionally need permission for the camera itself, asked for only when +you first turn one on. + +**A [TURN relay](/advanced/turn-server) is optional.** Calls connect directly for +the large majority of players. Add one only if you see connections that never +establish for particular people — usually mobile networks or networks that block +UDP. + ::: tip Discord still works Voice chat is an alternative to hopping into a Discord call, not a replacement for the [Discord bot](/advanced/discord-bot/) — per-tournament voice channels,