Streaming Subsystem¶
The streaming subsystem handles live, VOD, and timeshift delivery. It is the hot path (~10K-100K req/min, <50ms p99) and uses a separate lightweight bootstrap to avoid loading the full admin stack.
Request Flow¶
client request
|
nginx rewrite (/auth/{token} -> /stream/live.php?token={token})
|
StreamingRequestBootstrap::init()
|
StreamingBootstrap::bootstrap()
|
LegacyInitializer::initStreaming()
|
endpoint logic (live.php / vod.php / timeshift.php)
|
ShutdownHandler::handle()
nginx rewrites all streaming URLs to PHP entry points under Public/stream/:
| URL pattern | Entry point | Purpose |
|---|---|---|
/auth/{token} |
live.php |
Live stream delivery |
/vauth/{token} |
vod.php |
Video-on-demand delivery |
/tsauth/{token} |
timeshift.php |
Archive/timeshift playback |
/hls/{token} |
segment.php |
HLS segment delivery |
/key/{token} |
key.php |
AES-128 encryption key |
/subauth/{token} |
subtitle.php |
Subtitle delivery |
Directory Layout¶
src/Streaming/
├── StreamingBootstrap.php
├── AsyncFileOperations.php
├── Auth/
│ ├── StreamAuth.php
│ └── StreamAuthMiddleware.php
├── Balancer/
│ └── ProxySelector.php
├── Codec/
│ ├── FFmpegCommand.php
│ ├── FfmpegPaths.php
│ └── FFprobeRunner.php
├── Delivery/
│ ├── HLSGenerator.php
│ ├── OffAirHandler.php
│ └── StreamRedirector.php
├── Fanout/
│ └── FanoutClient.php
├── Health/
│ └── ProcessChecker.php
├── Lifecycle/
│ └── ShutdownHandler.php
└── Protection/
└── ConnectionLimiter.php
src/Public/stream/
├── index.php # Entry router for the stream endpoints
├── auth.php # Token validation gateway
├── live.php # Live streaming delivery
├── vod.php # VOD delivery
├── timeshift.php # Archive/timeshift playback
├── segment.php # HLS segment delivery
├── key.php # Encryption key delivery
├── subtitle.php # Subtitle delivery
├── thumb.php # Thumbnail delivery
├── probe.php # Stream probe / off-air status
└── rtmp.php # RTMP publishing endpoint
Bootstrap Pipeline¶
1. StreamingRequestBootstrap::init()¶
File: src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php
Actions in order:
- Load error codes, handler, paths, config, binaries.
- Flood protection (HTTP only): check for
FLOOD_TMP_PATH . 'block_' . $rIP. - Load settings from file cache (
CACHE_TMP_PATH . 'settings'). - Host verification (HTTP only): validate against
allowed_domains. - Initialize logger.
- Fail-closed gate: return 404 if settings missing (except
/status). - Call
StreamingBootstrap::bootstrap().
2. StreamingBootstrap::bootstrap()¶
File: src/Streaming/StreamingBootstrap.php
Classifies the endpoint:
- Probe endpoints:
probe,player_api(light load) - Default endpoints:
live,thumb,subtitle,timeshift,vod,status - Privileged endpoints:
rtmp,portal
Loads AsyncFileOperations.php and DatabaseHandler.php, stores settings in $GLOBALS['rSettings'] and access data in $GLOBALS['rAccess'], then calls LegacyInitializer::initStreaming().
Returns the $db database instance (used by legacy entry points).
3. LegacyInitializer::initStreaming()¶
File: src/Core/Init/LegacyInitializer.php
Populates global variables from cache:
$GLOBALS['rSettings'],$GLOBALS['rServers'],$GLOBALS['rBouquets']$GLOBALS['rBlockedUA'],$GLOBALS['rBlockedISP'],$GLOBALS['rBlockedIPs']$GLOBALS['rAllowedIPs'],$GLOBALS['rProxies'],$GLOBALS['rSegmentSettings']$GLOBALS['rFFMPEG_CPU'],$GLOBALS['rFFMPEG_GPU'],$GLOBALS['rFFPROBE']
Connects to database/Redis based on $rSettings['redis_handler'].
Important: The streaming path reads exclusively from file cache. It does not query the database for settings or user lookups during normal operation.
Token Authentication¶
File: src/Streaming/Auth/StreamAuthMiddleware.php
Token contents:
| Field | Description |
|---|---|
username |
Line username |
password |
Line password |
stream_id |
Target stream ID |
expires |
Token expiration timestamp |
channel_info |
Stream metadata (on_demand, proxy, pid) |
user_info |
User permissions (max_connections, is_restreamer) |
country_code |
GeoIP country code |
video_codec |
Requested video codec |
Validation:
- Decrypt token using
live_streaming_pass. - Check expiration:
$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']. - Return parsed token data or trigger error.
Response headers are set via StreamAuthMiddleware::sendStreamHeaders():
Access-Control-Allow-Origin: *
X-XSS-Protection: 0
X-Content-Type-Options: nosniff
Alt-Svc: h3-29, h3-T051, h3-Q050 (HTTP/3 hints)
Stream Delivery¶
Live (live.php)¶
Main delivery endpoint (~650 lines):
- Decrypt token via
StreamAuthMiddleware::decryptToken(). - Resolve server/proxy:
StreamAuth::checkAccess()+ProxySelector::availableProxy(). - Enforce connection limits:
StreamAuth::validateConnections(). - Create connection record:
ConnectionTracker::createConnection(). - Hand delivery to the
xc_fanoutdaemon (see below): PHP emits anX-Accel-Redirectand exits the byte path — nginx streams the bytes. - TS:
X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>&prebuffer=N(nginx rewrites to the daemon's/live/<id>). - HLS: the playlist points at tokenized segments;
segment.phpserves live segments only through the daemon (/xc_fanout_hls/<id>_<seq>), else404. - On exit:
ShutdownHandler::handle()→ close connection record.
VOD (vod.php)¶
Same auth flow as live. Reads from VOD_PATH instead of STREAMS_PATH.
Timeshift (timeshift.php)¶
Serves archived segments (timeshift / catch-up) from the archive path.
Daemon delivery — xc_fanout¶
Live client delivery (TS and HLS) is daemon-only: PHP authorizes the viewer and then leaves the byte path entirely, so a viewer no longer pins a PHP-FPM worker for the life of the stream.
- Fan-out.
xc_fanout(a bundled Go daemon) pulls each source once and fans it out to every viewer over a unix socket, with an in-RAM HLS segmenter. PHP is out of the per-viewer byte path: the worker-per-viewer chase-read serving loop and theHLSGenerator::generateHLS()client-serving path are no longer used for live delivery (generateHLS()is retained in the class but has no callers).AsyncFileOperations::awaitFileExists()is not removed — it is still used for stream-startup waits and the VOD/timeshift byte path (see the Performance table). - Two sockets. A client socket (nginx-facing) serves
/live/<id>and/hls/...; a PHP-only control socket registers sources (PUT /streams/<id>//ingest/<id>), answers off-air status (GET /streams/<id>,GET /probe/<id>) and exposes telemetry. - Telemetry / reconciliation.
fanout_syncpollsGET /rates(per-uuid KB/s →lines_divergence) andGET /connections(reconcileslines_liverows, since PHP cannot see a disconnect underX-Accel). - Off-air. If the daemon reports no data (
has_data=false/ stale), PHP shows a "not on air" page instead of letting the viewer hang. - On-disk HLS retained only for timeshift / thumbnails /
.analyse/MonitorCommand— not for client delivery.
Send-message overlay¶
The admin "Send Message" action burns a text banner onto one viewer's video.
PHP posts it to the daemon control socket
(FanoutClient::sendSignal → POST /signal/<uuid>), and the daemon applies an
ffmpeg drawtext overlay to that viewer's next HLS segment (or a short ~5s TS
window), one-shot, best-effort — a signal never breaks playback. The daemon must
be launched with an ffmpeg that actually has the drawtext filter, so the
service launcher picks a drawtext-capable build.
Connection Management¶
ConnectionTracker¶
Manages live connection state. Backend is selected by $rSettings['redis_handler']:
Redis (preferred for scale):
- Connections stored in sorted sets:
LINE#{identity}— connections for userSTREAM#{stream_id}— connections for streamSERVER#{server_id}— connections on server
MySQL (fallback):
- Table:
lines_livewith fields:activity_id,user_id,stream_id,server_id,uuid,pid,hls_end
Key methods:
ConnectionTracker::createConnection($data)
ConnectionTracker::updateConnection($connection, $changes, 'open'|'close')
ConnectionTracker::getConnection($uuid)
ConnectionTracker::getLineConnections($user_id)
ConnectionTracker::getCapacity()
ConnectionLimiter¶
File: src/Streaming/Protection/ConnectionLimiter.php
Enforces per-user connection limits when max_connections is exceeded:
| Priority | Criteria | Action |
|---|---|---|
| 2 | Same IP + same User-Agent | Kill first |
| 1 | Same IP (any UA) | Kill next |
| 0 | Any connection | Kill as fallback |
Settings:
disallow_2nd_ip_con— enforce single IP per userip_subnet_match— match by /24 subnet instead of exact IPrestrict_same_ip— return error on IP mismatch instead of killing
ShutdownHandler¶
File: src/Streaming/Lifecycle/ShutdownHandler.php
Registered via register_shutdown_function(). On PHP process exit:
- Close connection record in
lines_liveor Redis. - Delete tmp files at
CONS_TMP_PATH . $uuid. - Remove on-demand stream from queue if applicable.
Load Balancing¶
Server Selection (StreamAuth::checkAccess)¶
File: src/Streaming/Auth/StreamAuth.php
Algorithm:
- Get available servers:
server_online == true,server_type == 0,online_clients < total_clients. - Sort by capacity (ascending) — least loaded first.
- Apply GeoIP routing (if
enable_geoip == 1): - Exact country match → select immediately.
geoip_type == 'strict'→ exclude non-matching.- Otherwise → assign priority weight.
- Apply ISP routing (if
enable_isp == 1): same logic as GeoIP. - Return server with lowest capacity from highest-priority group.
Proxy Selection (ProxySelector::availableProxy)¶
File: src/Streaming/Balancer/ProxySelector.php
Same algorithm as StreamAuth::checkAccess() but applied to proxy server list.
Rate Limiting and Flood Protection¶
Three layers:
1. nginx (connection level)¶
20 requests/second per IP with 8-request burst. 30-minute sliding window.
2. StreamingRequestBootstrap (IP block)¶
File-based IP blocking. Block files are created by upstream flood detection logic.
3. ConnectionLimiter (per-user)¶
Enforced after token validation. Limits concurrent streams per user based on max_connections.
HLS Encryption¶
Client HLS is served by the xc_fanout daemon (see Daemon delivery), so encryption happens daemon-side:
StreamProcesswrites the stream's AES-128 key/IV tocontent/streams/<id>_.key/_.iv.- At ingest registration (
FanoutClient::registerIngest), whenencrypt_hlsis on, the key/IV are handed to the daemon, which encrypts the HLS segments it serves and emits a matching#EXT-X-KEY. HLSGenerator::tokenizeDaemonPlaylist()rewrites the daemon playlist's segment URLs into per-segment auth'd/hls/<token>links thatsegment.phpproxies from the daemon.- The AES key is delivered to players by
key.php(src/Public/stream/key.php) using the same token mechanism.
The legacy
HLSGenerator::generateHLS()(which built and encrypted an on-disk HLS playlist for PHP to serve) is retained in the class but is no longer on the client path after the daemon cutover.
Performance¶
Key design decisions for throughput and latency:
| Feature | Mechanism |
|---|---|
| Stream-online wait | AsyncFileOperations::awaitFileExists() waits for _.pid/_.monitor/first segment as a stream comes up (and in the VOD/timeshift byte path). Live client delivery is daemon-served — not chase-read by PHP. |
| Zero-CPU sleep | time_nanosleep() via AsyncFileOperations::efficientSleep() |
| nginx buffering | 128 x 32KB buffers per request |
| Connection pooling | Redis (preferred) or persistent MySQL |
| Cache-only reads | Settings and user data read from file cache, no DB queries |
| Early exit (VOD/timeshift) | Those byte loops poll connection_status() to stop when the client disconnects. Live has no per-viewer PHP byte loop (daemon-served). |
| Settings refresh | Every 5 minutes (300s) to catch config changes without restart |
File System Paths¶
STREAMS_PATH = /home/xc_vm/content/streams/
VOD_PATH = /home/xc_vm/content/vod/
ARCHIVE_PATH = /home/xc_vm/content/archive/
VIDEO_PATH = /home/xc_vm/content/video/
CONS_TMP_PATH = /home/xc_vm/tmp/opened_cons/
CACHE_TMP_PATH = /home/xc_vm/tmp/cache/
FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/
SIGNALS_TMP_PATH = /home/xc_vm/tmp/signals/
SIGNALS_PATH = /home/xc_vm/signals/
Diagnostics & Tooling¶
The standalone stream-integrity tool (tools/stream-check/stream_queue_check.py) now lives on its own page — see Streaming Diagnostics & Tooling.
Design rationale (ADRs)¶
Why live delivery moved off tmpfs and out of the PHP byte path — the decisions behind the current
xc_fanout architecture — is recorded in the Architecture Decision Records (repo-internal notes,
not part of the published site):
- ADR 0001 — Tmpfs-free streaming — PHP out of the byte path, native fan-out, in-RAM HLS.
- ADR 0002 —
xc_fanoutdaemon — the native live fan-out daemon. - ADR 0003 — Full daemon cutover — retiring the legacy byte path for live.
Related files¶
| File | Purpose |
|---|---|
src/Streaming/StreamingBootstrap.php |
core streaming bootstrap |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
HTTP-level init |
src/Streaming/Auth/StreamAuth.php |
server selection and connection validation |
src/Streaming/Auth/StreamAuthMiddleware.php |
token decryption and response headers |
src/Streaming/Balancer/ProxySelector.php |
proxy server selection |
src/Streaming/Protection/ConnectionLimiter.php |
per-user connection limits |
src/Streaming/Delivery/HLSGenerator.php |
M3U8 playlist generation |
src/Streaming/Delivery/StreamRedirector.php |
stream availability and server routing |
src/Streaming/AsyncFileOperations.php |
non-blocking filesystem utilities |
src/Streaming/Lifecycle/ShutdownHandler.php |
connection cleanup on exit |
src/Domain/Stream/ConnectionTracker.php |
connection state in Redis/MySQL |
src/Core/Init/LegacyInitializer.php |
global variable setup for streaming |
tools/stream-check/stream_queue_check.py |
queue-integrity monitor + live buffer dashboard |