VP Live & VP Audio Spaces Live Streaming Architecture NexGate / QBIT SPARK | Version 1.0 SRS · HLS · LiveKit · VP Live Video · VP Audio Radio · VP Audio Spaces Table of Contents Overview VP Live vs VP Audio — Key Differences How Live Streaming Works VP Live — Video Streaming VP Audio Radio — One Broadcaster Many Listeners VP Audio Spaces — Multi Speaker Rooms Live Chat — Ejabberd MUC Stream Key System File Thunder Integration — VOD After Stream Codecs & EA Network Strategy Docker Deployment Database Schema Scale Path 1. Overview VP Live and VP Audio Spaces live under VP Feed — the social pillar of NexGate. They are not separate products. They are the live expression layer of the social platform — where creators, merchants, and communities connect with their audiences in real time. VP Feed ┌───────────────────────────────────────────────────┐ │ │ │ Social Posts Stories Reels Live │ │ │ │ ┌─────────────────┐ │ │ │ VP Live │ │ │ │ Video Stream │ │ │ ├─────────────────┤ │ │ │ VP Audio │ │ │ │ Radio │ │ │ ├─────────────────┤ │ │ │ VP Audio │ │ │ │ Spaces │ │ │ └─────────────────┘ │ └───────────────────────────────────────────────────┘ All three modes share the same infrastructure foundation: SRS for ingest and transcoding, Cloudflare CDN for delivery, Ejabberd MUC for live chat, File Thunder for VOD processing, and Spring Boot for stream management and business logic. 2. VP Live vs VP Audio — Key Differences VP Live VP Audio Radio VP Audio Spaces (Video) (Radio/Podcast) (Twitter Spaces) ────────────────────────────────────────────────────────────────────── Broadcasters 1 1 Multiple (up to 30) Viewers Unlimited Unlimited Unlimited listeners Direction One way One way Multi-speaker Broadcaster RTMP RTMP audio WebRTC (LiveKit) transport (video+audio) (audio only) Listener HLS video HLS audio HLS audio transport (adaptive) (adaptive) (listeners) WebRTC (speakers) Latency 6-15 seconds 6-15 seconds Speakers: <200ms Listeners: 6-15s Bandwidth High Very low Low (speakers) broadcaster (2-4 Mbps) (128 kbps) Very low (listeners) Bandwidth Medium Very low Very low listener (300kbps-2Mbps) (32-128 kbps) (32-128 kbps) Works on 2G? ❌ No ✅ Yes ✅ Listeners yes Live chat Ejabberd MUC Ejabberd MUC Ejabberd MUC Raise hand ❌ ❌ ✅ VOD after ✅ File Thunder ✅ File Thunder ✅ File Thunder New infra SRS SRS SRS + LiveKit 3. How Live Streaming Works The Core Pattern — RTMP → HLS → CDN Broadcasting (sending): Broadcaster's phone records camera + mic App encodes: H.264 video + AAC audio App streams via RTMP protocol to SRS server One stream upload from broadcaster Processing (server): SRS receives RTMP stream FFmpeg transcodes to multiple quality variants Packages into HLS format (2-second chunks) Writes chunks to MinIO storage every 2 seconds Delivery (viewing): Cloudflare CDN pulls chunks from MinIO Caches chunks at edge nodes globally Viewers request HLS playlist → adaptive player picks quality 10,000 viewers = 10,000 CDN requests, NOT 10,000 SRS requests SRS barely notices the viewer count Why HLS and not WebRTC for viewers: WebRTC to viewers: broadcaster uploads N streams (one per viewer) HLS via CDN: broadcaster uploads 1 stream → CDN serves all At 10,000 viewers: WebRTC = impossible, HLS = trivial HLS — What It Actually Is HLS (HTTP Live Streaming) — Apple's open standard SRS generates: master.m3u8 → playlist of all quality variants 360p/playlist.m3u8 → playlist for 360p variant 360p/seg_000.ts → 2-second video chunk 360p/seg_001.ts → next 2-second chunk 720p/playlist.m3u8 720p/seg_000.ts ... master.m3u8 looks like: #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x360 360p/playlist.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720 720p/playlist.m3u8 Player (ExoPlayer / AVPlayer): Downloads master.m3u8 first Measures current network speed Picks 360p if on 3G → plays seg_000.ts → seg_001.ts → ... Switches to 720p if network improves → seamless All automatic — zero app code needed for quality switching 4. VP Live — Video Streaming Full Architecture [Broadcaster Phone] │ │ RTMP stream │ rtmp://stream.nexgate.com/live/{streamKey} │ H.264 video + AAC audio │ ~2-4 Mbps upload ▼ [SRS Media Server] │ ├── Validates stream key: │ POST /internal/stream/validate │ { streamKey: "abc123" } │ Spring Boot: ✅ allow or ❌ reject │ ├── Receives raw RTMP stream │ ├── FFmpeg transcoding (real-time): │ 1080p H.264 → 3 Mbps (WiFi viewers) │ 720p H.264 → 1.5 Mbps (4G viewers) │ 480p H.264 → 600 kbps (3G viewers) │ 360p H.264 → 300 kbps (2G viewers) │ ├── Package as HLS: │ Segment every 2 seconds │ live/{streamKey}/master.m3u8 │ live/{streamKey}/360p/seg_NNN.ts │ live/{streamKey}/720p/seg_NNN.ts │ └── Write to MinIO: nexgate-live bucket New segments every 2 seconds │ ▼ [Cloudflare CDN] │ Pulls from MinIO automatically │ Caches at edge (Nairobi edge closest to EA) │ Short TTL: 10 seconds (live content) │ ▼ [Viewers — ExoPlayer (Android) / AVPlayer (iOS)] Requests master.m3u8 Player picks quality based on network Downloads .ts segments every 2 seconds Seamless adaptive quality switching Stream Key Validation Flow Broadcaster taps "Go Live" in app │ ▼ POST /live/start Spring Boot: Generate unique stream key Store in DB: stream_key: "abc123" user_id: usr-kibuti status: PENDING created_at: now Return stream key to app │ App connects RTMP: rtmp://stream.nexgate.com/live/abc123 │ SRS receives connection │ ▼ POST /internal/stream/validate (SRS webhook) Spring Boot checks: Key exists? ✅ User account active? ✅ User has live permission? ✅ No other active stream for this user? ✅ → 200 OK → SRS allows stream → Update DB: status: LIVE, started_at: now → Notify followers via FCM: "Kibuti anastreamu sasa! Tazama live" → Create Ejabberd MUC room: live-abc123@conference.nexgate.com Broadcaster App — What Mobile Dev Implements Android library: rtmp-rtsp-stream-client-java iOS library: HaishinKit (Swift) Steps for broadcaster app: 1. GET /live/start → receive stream key 2. Initialize camera + microphone 3. Connect RTMP to stream.nexgate.com/live/{key} 4. Start streaming — library handles everything: H.264 encoding (hardware) AAC audio encoding RTMP packet framing Network reconnection on drop 5. Show: viewer count (from Redis via REST poll) live comments (from Ejabberd MUC via WS) duration timer 6. Tap End → POST /live/end → cleanup Adaptive upload bitrate: Library monitors upload speed Reduces video quality if upload struggles Broadcaster's bad network → lower quality for viewers Never drops stream if avoidable Viewer App — What Mobile Dev Implements Android: ExoPlayer (Google's official video player) iOS: AVPlayer (built into iOS, zero setup) Steps for viewer app: 1. GET /live/{streamId}/url Response: { masterUrl, viewerCount, startedAt } 2. Feed masterUrl to ExoPlayer/AVPlayer 3. Player handles everything automatically: Downloads master.m3u8 Picks quality based on network Downloads segments every 2s Switches quality up/down seamlessly 4. Join Ejabberd MUC room → show live comments 5. Player shows: loading → buffering → playing That is genuinely all the viewer needs to implement. HLS + ExoPlayer/AVPlayer is the easiest viewer experience to build in all of mobile development. 5. VP Audio Radio — One Broadcaster Many Listeners Why Audio Radio Matters for EA VP Live video: Broadcaster needs: 2-4 Mbps upload Viewer needs: 300kbps minimum Data cost viewer: ~900MB per hour at 360p Works on: 4G and strong 3G only VP Audio Radio: Broadcaster needs: 64-128 kbps upload Listener needs: 32 kbps minimum Data cost listener: ~15MB per hour at 32kbps Works on: 2G, Edge, any connection For a farmer in rural Tanzania with 2G: VP Live video → impossible, too expensive VP Audio Radio → accessible, affordable Use cases: Live podcast / commentary Religious broadcasts (huge in EA) Political discussions Community announcements Sports commentary Language learning sessions Business webinars (audio only) Architecture — Same SRS, Audio Only [Broadcaster Phone] │ │ RTMP audio only (no video track) │ AAC codec, 128 kbps │ rtmp://stream.nexgate.com/audio/{streamKey} ▼ [SRS Media Server] │ ├── Same validation flow as VP Live │ ├── FFmpeg transcoding (audio only): │ AAC 128 kbps → good network listeners │ AAC 64 kbps → 3G listeners │ AAC 32 kbps → 2G listeners │ ├── Package as HLS audio: │ audio/{streamKey}/master.m3u8 │ audio/{streamKey}/128k/seg_NNN.aac │ audio/{streamKey}/32k/seg_NNN.aac │ └── Write to MinIO: nexgate-live bucket │ ▼ [Cloudflare CDN] │ ▼ [Listeners — ExoPlayer / AVPlayer] HLS audio playlist Adaptive bitrate: 128k → 32k automatically Same player, same code — just no video surface Codec Choice — AAC Not Opus Why AAC for HLS audio radio (not Opus): Opus is better quality at low bitrates — true But HLS has a compatibility requirement: Apple mandates AAC for HLS audio AVPlayer on iOS does not support Opus in HLS Using Opus → iOS listeners cannot play AAC → works on every device, every OS Opus is used for: Voice calls (WebRTC — different transport) Voice notes (file-based, not streaming) AAC is used for: VP Live audio track (in video stream) VP Audio Radio (HLS streaming) VP Audio Spaces listener HLS output AAC at 32kbps for EA: Acceptable speech quality ~15MB per hour Works on any 2G connection Universal device support 6. VP Audio Spaces — Multi Speaker Rooms The Concept Not one broadcaster → many listeners Multiple people in a shared audio room Some speak, many listen Listeners can raise their hand to speak Host controls who gets the mic Like Twitter Spaces, Clubhouse, Discord Stage Channels Key insight: Speakers need LOW LATENCY (<200ms) to have a natural conversation HLS (6-15s delay) is too slow for speakers Listeners just need to HEAR clearly HLS delay is fine — they're not responding HLS scales to millions via CDN Solution: TWO transport layers in one room Speakers → WebRTC (LiveKit SFU) → <200ms Listeners → HLS via CDN → 6-15s delay → millions scale LiveKit SFU — What It Is SFU = Selective Forwarding Unit Traditional conference (MCU): Server mixes ALL audio into one stream Sends mixed stream to everyone High CPU (server does all mixing) Simple client LiveKit SFU approach: Each speaker sends audio once to LiveKit LiveKit forwards each speaker's stream to all other speakers Speakers' apps mix locally (device CPU) Much lower server CPU Lower latency Better quality (no mixing artifacts) For listeners: LiveKit outputs a mixed HLS stream Goes through SRS → Cloudflare CDN Listeners get one mixed audio stream Same HLS pattern as Audio Radio Who built LiveKit: The same team that built Twitter Spaces Then open sourced it Actively maintained, Docker ready Official Android + iOS SDKs available Full Architecture [Speaker A phone] ──WebRTC──▶┐ [Speaker B phone] ──WebRTC──▶│ [Speaker C phone] ──WebRTC──▶│ ▼ [LiveKit SFU] │ ┌──────────┼──────────────┐ │ │ │ WebRTC fwd HLS output Room events to speakers (mixed audio) to Spring Boot │ │ [Speakers [SRS receives hear each HLS from LiveKit] other live] │ ▼ [Cloudflare CDN] │ ▼ [Thousands of listeners via HLS audio player] ExoPlayer / AVPlayer (same as Audio Radio) Room events (raise hand, join, leave): LiveKit → Spring Boot via webhook Spring Boot → Ejabberd MUC → all participants Ejabberd MUC → Listeners also see events (who joined as speaker etc) Raise Hand Flow Listener wants to speak: │ taps "Raise Hand" 🖐 │ sends via Ejabberd WS to MUC room: │ { type: RAISE_HAND, roomId: "space-abc" } │ ▼ Spring Boot: Records raise hand request Notifies host via Ejabberd WS: { type: HAND_RAISED, userId, displayName } Host sees list of raised hands in UI │ Host taps "Allow to speak" on a listener: │ ▼ Spring Boot: Calls LiveKit API: Update participant permissions: canPublish: true ← now allowed to send audio Generate new LiveKit token for this user (speaker token, not listener token) Send token to user via Ejabberd WS: { type: SPEAKER_PROMOTED, livekitToken: "..." } │ Former listener's app: Receives promotion event Stops HLS player (was listening at 15s delay) Connects WebRTC to LiveKit with speaker token Starts sending audio Now hears speakers at <200ms latency Other speakers hear them immediately │ Host can also: Lower someone's hand (dismiss) Mute a specific speaker Remove speaker (back to listener) End the space entirely Speaker vs Listener — Connection Types ┌──────────────────────────────────────────────────────┐ │ Audio Space Room │ │ │ │ Speakers (up to ~20-30): │ │ Connected via WebRTC to LiveKit │ │ Send and receive audio streams │ │ Latency: <200ms (real conversation) │ │ Connection: persistent WebRTC │ │ │ │ Listeners (unlimited): │ │ Connected via HLS to Cloudflare CDN │ │ Receive mixed audio only │ │ Latency: 6-15 seconds (fine — just listening) │ │ Connection: HTTP requests every 2s │ │ Scale: millions — CDN handles it │ │ │ │ All participants: │ │ Connected to Ejabberd MUC room │ │ Text chat, reactions, raise hand events │ │ Room membership awareness │ └──────────────────────────────────────────────────────┘ LiveKit Token System Spring Boot manages all LiveKit tokens (LiveKit has official Java SDK) Host token: canPublish: true canSubscribe: true roomAdmin: true → full control, can speak, manage Speaker token: canPublish: true canSubscribe: true roomAdmin: false → can speak, cannot manage room Listener token: canPublish: false ← cannot send audio canSubscribe: true ← can hear speakers roomAdmin: false → receive only Token generation: GET /audio-spaces/{spaceId}/join Spring Boot checks: Is user the host? → host token Is user an approved speaker? → speaker token Otherwise → listener token (gets HLS URL instead) LiveKit Docker Config livekit: image: livekit/livekit-server:latest container_name: livekit restart: unless-stopped ports: - "7880:7880" # HTTP API (Spring Boot calls here) - "7881:7881" # WebRTC TCP - "7882:7882/udp" # WebRTC UDP (primary) - "50000-60000:50000-60000/udp" # ICE relay ports volumes: - ./livekit/livekit.yaml:/etc/livekit.yaml command: --config /etc/livekit.yaml # livekit.yaml port: 7880 rtc: tcp_port: 7881 udp_port: 7882 use_external_ip: true redis: address: redis:6379 # reuses existing Redis ✅ turn: enabled: true domain: turn.nexgate.com tls_port: 5349 credential: "${COTURN_SECRET}" # reuses existing Coturn ✅ room: max_participants: 10000 empty_timeout: 300 LiveKit reuses: Redis → already deployed ✅ Coturn → already deployed for calls ✅ No new infrastructure beyond LiveKit container itself 7. Live Chat — Ejabberd MUC All three live modes (VP Live, Audio Radio, Audio Spaces) use Ejabberd MUC rooms for real-time text interaction. Room Lifecycle Stream / space starts: │ Spring Boot → Ejabberd REST API: POST /api/create_room { name: "live-{streamId}", service: "conference.nexgate.com" } Room created: live-abc@conference.nexgate.com │ Broadcaster / host auto-joined as moderator │ Viewers / listeners join room as participants: App connects Ejabberd WS Sends MUC join stanza: │ Comments sent as MUC messages: Mzuri sana! 🔥 │ All room members receive instantly No delay — Ejabberd MUC is real-time │ Stream / space ends: Spring Boot → Ejabberd REST API: POST /api/destroy_room { name: "live-abc", service: "conference.nexgate.com" } Room destroyed, members disconnected Special Events in Live Chat Beyond text comments, the MUC room carries: Reactions (emoji bursts): { type: REACTION, emoji: "🔥", userId, displayName } Client renders floating emoji animation Gifts: { type: GIFT, giftId, giftName, amount, userId, displayName } Client renders gift animation Spring Boot processes payment separately Raise hand (Audio Spaces only): { type: RAISE_HAND, userId, displayName } Host sees in management panel Speaker promoted (Audio Spaces only): { type: SPEAKER_PROMOTED, userId, displayName } All participants see "Amina joined as speaker" Viewer count updates: Broadcast every 30 seconds from Spring Boot { type: VIEWER_COUNT, count: 12453 } Product card dropped by broadcaster: { type: PRODUCT_CARD, productId, name, price } Viewers tap → go to VP Shop product page Commerce during live ✅ Viewer / Listener Count Two sources of truth: 1. Ejabberd MUC occupant count: GET ejabberd REST /api/get_room_occupants_count { room: "live-abc", host: "conference.nexgate.com" } → exact WebSocket-connected count 2. Redis counter (includes HLS-only listeners): INCR live:{streamId}:viewers → on HLS playlist request DECR → on playlist stop / timeout More accurate for Audio Radio/Spaces where many listeners never connect WS Display count = Redis counter (higher, more accurate) Spring Boot broadcasts to MUC every 30 seconds 8. Stream Key System Stream Key Design Stream key = single-use authentication token Broadcaster uses it to connect RTMP to SRS SRS validates with Spring Boot before accepting stream Format: random 32-character alphanumeric string Example: nx_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4 Lifecycle: PENDING → generated, not yet used LIVE → broadcaster connected, stream active ENDED → stream finished normally EXPIRED → generated but never used (24h TTL) REVOKED → manually stopped by admin One active stream per user at a time Attempting second stream → rejected by Spring Boot validation SRS Webhooks to Spring Boot SRS fires these events to Spring Boot: on_publish → broadcaster connected RTMP Spring Boot: validate key, update status LIVE, notify followers FCM, create Ejabberd MUC room, create LiveKit room (if audio space) on_unpublish → broadcaster disconnected Spring Boot: update status ENDED, trigger File Thunder for VOD, destroy Ejabberd MUC room, log stream duration + peak viewers on_play → viewer started watching HLS Spring Boot: increment Redis viewer counter on_stop → viewer stopped watching Spring Boot: decrement Redis viewer counter 9. File Thunder Integration — VOD After Stream What Happens After Stream Ends Stream ends (broadcaster taps End / disconnects) │ SRS fires on_unpublish webhook │ Spring Boot: Update stream record: status ENDED Trigger File Thunder for VOD processing SRS has saved full recording as .mp4 │ ▼ Spring Boot → File Thunder: POST /api/v1/upload/request (HMAC signed) { ownerId: broadcasterId, domain: POSTS, context: LIVE_RECORDING, filename: "stream_{streamId}.mp4", mimeType: "video/mp4" } Returns: presigned MinIO PUT URL │ Spring Boot pulls recording from SRS Uploads to MinIO via presigned URL POST /api/v1/confirm { fileId } │ ▼ File Thunder VideoWheel processes: HLS transcoding (all quality variants) Thumbnail extraction (best frame detection) Watermark: "@{broadcasterUsername}" NO outro — live recordings are long NO shortClip — full stream only Store in nexgate-public bucket │ ▼ File Thunder fires webhook: media ready Spring Boot: Creates VOD post on broadcaster's profile "Watch replay" button appears Appears in VP Feed for followers Stream record linked to VOD fileId New File Thunder Contexts for Live Existing contexts (unchanged): SOCIAL_VIDEO regular video posts DM_ATTACHMENT files sent in DMs DIGITAL_PRODUCT digital goods in VP Shop ... New contexts added for live: LIVE_RECORDING full stream VOD VideoWheel — no outro, no shortClip always HLS, always long AUDIO_RECORDING audio space / radio recording AudioWheel processes outputs: .m4a (AAC) podcast episode on profile waveform extracted (like voice notes) nexgate-live MinIO Bucket Existing buckets: nexgate-raw temp uploads nexgate-public social content nexgate-private DMs and private files nexgate-digital VP Shop digital products New bucket: nexgate-live live stream segments only Why separate: SRS writes directly here (not via File Thunder) Short TTL segments — deleted after stream ends + VOD ready Different CDN caching rules (10s TTL vs 1 year for VOD) Different access pattern (SRS writes, CDN reads) Easy to monitor storage growth separately Lifecycle: Stream starts → SRS creates live/{streamKey}/ folder During stream → .ts segments written every 2 seconds Stream ends → Spring Boot schedules cleanup job VOD confirmed → delete nexgate-live/{streamKey}/ folder Total life: stream duration + ~1 hour buffer 10. Codecs & EA Network Strategy VP Live Video Codecs Broadcaster encoding (phone → SRS): Video: H.264 (hardware encoder — mandatory) Software H.264 too slow for real-time on phones H.264 hardware support: every phone since 2013 Audio: AAC 128kbps (RTMP standard) Container: RTMP (streaming protocol) SRS transcoding (server-side): Receives H.264 + AAC Transcodes to HLS quality ladder: Quality Video bitrate Audio Resolution EA target ───────────────────────────────────────────────────────── 1080p 3 Mbps 128k 1920×1080 WiFi only 720p 1.5 Mbps 128k 1280×720 4G 480p 600 kbps 64k 854×480 3G 360p 300 kbps 48k 640×360 2G minimum ───────────────────────────────────────────────────────── ExoPlayer/AVPlayer auto-selects based on network VP Audio Codecs Audio Radio (broadcaster → SRS): Codec: AAC 128kbps Container: RTMP audio only Audio Radio (SRS → HLS): 128kbps → WiFi/4G listeners 64kbps → 3G listeners 32kbps → 2G listeners (15MB/hour — affordable) Audio Spaces (speaker → LiveKit): Codec: Opus (WebRTC standard) Adaptive: 32-64kbps per speaker Echo cancellation: mandatory (multiple people) Noise suppression: mandatory (EA background noise) Audio Spaces (LiveKit → HLS for listeners): LiveKit mixes speaker streams Outputs mixed audio → SRS → HLS Same AAC ladder as Audio Radio Listeners hear all speakers in one stream Adaptive Streaming — EA Principle The player always knows the network speed because it measures how fast segments download Segment download faster than playback → upgrade quality Segment download slower than playback → downgrade quality For a viewer in Dodoma on shaky 3G: Opens stream → starts at 360p (safe default) Network good → player tries 480p Stays stable → tries 720p Network drops → immediately back to 360p No rebuffering if switch is fast enough Buffer strategy: Player buffers 3-4 segments ahead (6-8 seconds) Gives time to switch quality before buffer empties Viewer may notice brief quality dip — never a freeze NexGate player config recommendation: Min buffer: 6 seconds Max buffer: 30 seconds Quality switch: aggressive downgrade, conservative upgrade → Prioritize uninterrupted playback over quality → EA networks fluctuate — better to be at 360p than buffering 11. Docker Deployment Full docker-compose for Live Features # SRS Media Server srs: image: ossrs/srs:5 container_name: srs restart: unless-stopped ports: - "1935:1935" # RTMP ingest (broadcaster connects here) - "8080:8080" # HTTP API + HLS output - "1985:1985" # SRS management API volumes: - ./srs/srs.conf:/usr/local/srs/conf/srs.conf - ./srs/logs:/usr/local/srs/logs - ./srs/recordings:/usr/local/srs/objs/recordings depends_on: - chat-service networks: - nexgate-internal # LiveKit SFU (Audio Spaces) livekit: image: livekit/livekit-server:latest container_name: livekit restart: unless-stopped ports: - "7880:7880" - "7881:7881" - "7882:7882/udp" - "50000-60000:50000-60000/udp" volumes: - ./livekit/livekit.yaml:/etc/livekit.yaml command: --config /etc/livekit.yaml depends_on: - redis networks: - nexgate-internal SRS Config Highlights listen 1935; # RTMP port max_connections 1000; vhost __defaultVhost__ { # Validate stream key with Spring Boot http_hooks { enabled on; on_publish http://chat-service:8082/internal/stream/validate; on_unpublish http://chat-service:8082/internal/stream/ended; on_play http://chat-service:8082/internal/stream/viewer-join; on_stop http://chat-service:8082/internal/stream/viewer-leave; } # HLS output for viewers hls { enabled on; hls_path ./objs/nginx/html; hls_fragment 2; # 2 second chunks hls_window 10; # keep last 10 chunks in playlist } # FFmpeg transcoding to multiple qualities transcode { enabled on; ffmpeg /usr/bin/ffmpeg; engine 360p { enabled on; vcodec libx264; vbitrate 300; vfps 15; vwidth 640; vheight 360; acodec aac; abitrate 48; output rtmp://localhost:1935/live360p/{stream}; } engine 720p { enabled on; vcodec libx264; vbitrate 1500; vfps 30; vwidth 1280; vheight 720; acodec aac; abitrate 128; output rtmp://localhost:1935/live720p/{stream}; } } } Traefik — RTMP Does Not Go Through Traefik Important: RTMP is TCP port 1935 Traefik handles HTTP/HTTPS only RTMP port 1935 exposed directly on VPS What Traefik does handle: stream.nexgate.com → SRS port 8080 (HLS output) TLS termination for HLS delivery RTMP broadcaster connects: rtmp://stream.nexgate.com:1935/live/{key} No TLS on RTMP (RTMPS is complex, not needed for launch) HLS viewers connect via Cloudflare CDN: https://cdn.nexgate.com/live/{key}/master.m3u8 Cloudflare pulls from SRS port 8080 Traefik handles TLS for this path 12. Database Schema live_streams live_streams ───────────────────────────────────────────── stream_id UUID PK broadcaster_id UUID FK → users type ENUM VIDEO / AUDIO_RADIO / AUDIO_SPACE title TEXT description TEXT cover_file_id UUID File Thunder fileId (stream thumbnail) stream_key TEXT UNIQUE, used for RTMP auth status ENUM PENDING / LIVE / ENDED / EXPIRED / REVOKED started_at TIMESTAMPTZ ended_at TIMESTAMPTZ duration_seconds INT peak_viewers INT total_viewers INT muc_room_id TEXT Ejabberd MUC room name vod_file_id UUID File Thunder fileId after processing created_at TIMESTAMPTZ audio_spaces audio_spaces ───────────────────────────────────────────── space_id UUID PK stream_id UUID FK → live_streams livekit_room_id TEXT LiveKit room name host_id UUID FK → users title TEXT status ENUM SCHEDULED / LIVE / ENDED max_speakers INT default 30 started_at TIMESTAMPTZ ended_at TIMESTAMPTZ audio_space_participants audio_space_participants ───────────────────────────────────────────── space_id UUID FK → audio_spaces user_id UUID role ENUM HOST / SPEAKER / LISTENER joined_at TIMESTAMPTZ left_at TIMESTAMPTZ hand_raised_at TIMESTAMPTZ promoted_at TIMESTAMPTZ when promoted from listener to speaker promoted_by UUID host who approved stream_viewer_stats stream_viewer_stats ───────────────────────────────────────────── stat_id UUID PK stream_id UUID FK → live_streams timestamp TIMESTAMPTZ viewer_count INT quality_360p_pct DECIMAL % of viewers on 360p quality_720p_pct DECIMAL % of viewers on 720p avg_watch_seconds INT 13. Scale Path Current Architecture Limits Single SRS node (Hetzner CPX31 — €19/month): Concurrent streams: ~200 (with transcoding) Concurrent viewers: ~50,000 (before CDN helps) Bandwidth: 20TB/month included With Cloudflare CDN: Concurrent viewers: Unlimited (CDN absorbs it) SRS only serves cache misses 99%+ cache hit rate → SRS barely loaded LiveKit single node: Concurrent spaces: ~500 Speakers per space: up to 30 Listeners per space: Unlimited (HLS via CDN) This is enough for NexGate launch and strong early growth — tens of thousands of users Growth Stage — SRS Horizontal Scale When 200 concurrent streams is not enough: SRS Origin node: Receives RTMP from all broadcasters Passes stream to Transcode Farm Transcode Farm (2-3 nodes): Each node handles FFmpeg transcoding Horizontal — add nodes as streams grow CPU-bound work distributed SRS Edge nodes: Serve HLS to viewers Pull from Origin Multiple edges → load distributed ┌──────────────────────────────────────────┐ │ Broadcaster → SRS Origin │ │ │ │ │ Transcode Farm │ │ (3 nodes, FFmpeg) │ │ │ │ │ ┌──────────┴──────────┐ │ │ SRS Edge 1 SRS Edge 2 │ │ │ │ │ │ Cloudflare CDN ────────────┘ │ │ │ │ │ All viewers (millions) │ └──────────────────────────────────────────┘ WeChat EA Scale — Infrastructure Broadcaster latency problem: Current: Broadcaster in Dar → stream goes to Hetzner Germany 150-300ms upload latency Acceptable but not ideal At scale: SRS nodes in EA region Google Cloud Johannesburg OR AWS Cape Town Broadcaster → nearby SRS → low latency upload Better broadcaster experience Storage cost at scale: Current: MinIO on Hetzner At scale: Cloudflare R2 Zero egress cost (unlike AWS S3 which charges per GB) S3 compatible → zero code change to migrate At millions of viewer-hours: massive cost saving Transcoding cost: CPU-heavy work At scale: GPU-accelerated FFmpeg nodes NVIDIA hardware encoding 5-10x faster than CPU Lower cost per stream transcoded Summary VP Live and VP Audio Spaces are the live social layer of NexGate — living under VP Feed alongside regular posts, stories, and reels. All three modes (VP Live video, VP Audio Radio, VP Audio Spaces) share the same infrastructure foundation. SRS handles all RTMP ingest and transcoding. Cloudflare CDN distributes HLS to unlimited viewers and listeners. Ejabberd MUC powers live chat and room events for all modes. File Thunder processes every stream into a VOD after it ends. Spring Boot manages stream keys, webhooks, room lifecycle, and all business logic. VP Audio Spaces adds LiveKit SFU for the multi-speaker experience — speakers connect via WebRTC for real-time conversation while listeners receive the same HLS audio stream that Audio Radio uses, scaled to millions via Cloudflare CDN. The EA network strategy is woven into every decision: HLS adaptive streaming down to 32kbps means VP Audio Radio works on 2G in rural Tanzania. Video quality ladders from 1080p to 360p ensure VP Live is accessible on 3G. The entire viewer and listener experience requires only ExoPlayer or AVPlayer — the simplest possible mobile integration. For VOD, File Thunder's VideoWheel and new AudioWheel process every recording automatically after the stream ends — creating replay content with thumbnails, watermarks, and adaptive variants, stored in nexgate-public for CDN delivery. The live platform generates permanent content with zero extra work. NexGate VP Live & VP Audio Spaces — Architecture v1.0 QBIT SPARK | SRS · LiveKit · HLS · Ejabberd MUC · File Thunder VOD