Staff-only web app for tdarts.hu tournaments: create rooms with join codes, stream phones as WebRTC publishers, monitor from an admin dashboard, and open clean viewer pages for OBS Browser Source.
- Next.js 16 (App Router) + TypeScript + Tailwind v4 + shadcn/ui (Base UI)
- Custom Node entry
server.ts: Next HTTP handler + Socket.IO signaling on the same port - Rooms + camera presence: Redis if
REDIS_URLis set; otherwise an in-memory store in the Node process (no extra infra; see below). - WebRTC mesh: one
RTCPeerConnectionper viewer (admin tile or/view/[cameraSessionId])
-
Copy env and edit secrets:
cp .env.example .env.local
-
Redis (optional): leave
REDIS_URLunset to use the built-in in-memory store (good for one machine and quick tournaments). SetREDIS_URLwhen you need data to survive app restarts or multiple app instances. -
Install and run (uses
server.ts, notnext devalone):npm install npm run dev
-
Open
http://localhost:3000/login, sign in withSESSION_PASSWORD. -
In Dashboard, create a room and Copy join URL for phones.
-
On each phone: Camera page → enter code (or use URL query
?code=) → Start (preview can use Black / Dim; outgoing video stays full quality). -
For OBS: open Open viewer (OBS) from the dashboard, or
/view/<cameraSessionId>in a Browser Source (toggle HUD if you want a chromeless capture).
SESSION_PASSWORD(required): Shared staff password used at login.SESSION_SECRET(required in production): At least 16 characters; signs the httpOnly session JWT.REDIS_URL(optional): Exampleredis://localhost:6379. If unset, rooms and camera presence are stored in memory in the Node process (lost on restart; use a single app instance).NEXT_PUBLIC_APP_URL(recommended): Public site URL (https://…) for join links when behind a reverse proxy.ICE_SERVERS_JSON(optional): JSON array ofRTCIceServerobjects. If omitted, a public Google STUN server is used.
Without TURN, some phone networks will not connect across NAT; use coturn (or a hosted TURN) for production venues. See comments in docker-compose.yml.
export SESSION_PASSWORD='your-password'
export SESSION_SECRET='at-least-16-characters-here'
docker compose up --buildThen open http://localhost:3000.
- Terminate TLS at your reverse proxy; set
NEXT_PUBLIC_APP_URLto the publichttps://origin so join links are correct. - Set
Securecookies: app usessecure: trueon cookies whenNODE_ENV=production— your proxy must sendX-Forwarded-Proto: https(standard for Caddy/Traefik/nginx). - Mesh limits: each admin preview and each OBS viewer adds a separate decode path; keep concurrent previews modest, or rely on dedicated
/view/...pages for full quality. - Redis vs memory: with no
REDIS_URL, you avoid deploying Redis; tradeoff is no persistence across process restarts and no shared state across multiple Node replicas. Add Redis when you outgrow that.
On every push to main, .github/workflows/docker-publish.yml builds a multi-arch image (linux/amd64, linux/arm64) and pushes to:
ghcr.io/<github_owner>/<repo_name>:latest and :sha-<commit>
Use a lowercase repository path (GitHub enforces this for packages).
src/app/— routes:login,admin,camera,view/[cameraSessionId],api/*src/server/io.ts— Socket.IO signaling (v1:*events) + room/camera presence (Redis or in-memory)src/hooks/—useSignalingSocket,usePublisher,useSubscribersrc/lib/— auth, redis, rooms, media helpers
v1:admin:join— admin receivesv1:admin:snapshotandv1:rosterupdates.v1:camera:join{ joinCode, cameraSessionId, label }— registers camera (requires activeMediaStreamfrom client before join).v1:viewer:subscribe{ cameraSessionId, viewerPeerId }— triggersv1:control:subscriber-addto the camera.v1:webrtc:offer/answer/candidate— relayed between matching camera and viewer sockets.v1:control:mute-audio{ cameraSessionId, muted }— admin forces camera mic track off (and presence flag in the store).
Private / internal for tdarts.hu — adjust as needed.