With fanout switched off (FanoutMode::legacyDelivery(), which is also true
when the licence denies fanout), live delivery goes back to the paths that
ADR 0003 Phase E removed. Each is restored from git history:
- HLS (E3): live.php serves HLSGenerator::generateHLS() over the on-disk
playlist. segment.php serves on-disk LIVE segments again through
X-Accel /xc_hls/, AES-128-encrypting on first read, and burns a pending
send-message signal with SignalSender. generateHLS now mints tokens with
Encryption::mintToken (so secure_stream_tokens applies), rewrites one
segment line at a time (a str_replace of 1_1.ts also hit 11_1.ts), and
tokenizes an fMP4 init map.
- TS (E2): live.php chase-reads the on-disk segments again (SegmentReader
and its test restored). $rSegmentSettings, which the old loop read but
never defined, is now set there. A signalled segment is no longer sent
twice.
- Proxy streams (E1): StreamProcess::startProxy() starts ProxyCommand
(XC_VMProxy[<id>]), and live.php relays its datagrams from the viewer's
unix socket. Fixed while restoring:
- the read loop ended after its first read (a stray break);
- it spun on an idle source;
- it never exited when the last viewer left;
- it passed the proxy argument's whole row to ffmpeg;
- it ran ffmpeg with no input when no source opened.
Runtime state goes through StreamStateWriter.
With fanout on, nothing changes: a daemon that is merely down still gives
not-on-air until the keepalive restarts it, and segment.php still 404s
non-daemon live segments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
E2 deleted live.php's non-proxy chase-read — the only user of SegmentReader
(playlist segment extraction). Remove the now-dead class, its unit test, and the
doc references. SignalSender (still used by segment.php) and CacheReader (used
widely) are kept.
For continuous-TS delivery (/play/<token>/ts) the client's whole playback buffer
is the initial prebuffer burst — the feed loop then runs in real time, so the
buffer never grows past it. On a cold on-demand start the server began serving
the instant the playlist first appeared, when only the 1-2 fast-start (2s)
segments existed, so selectSegments could only return ~2s and the client was
stranded at ~2s of buffer regardless of client_prebuffer.
Wait until the playlist actually holds client_prebuffer seconds (bounded by
on_demand_wait_time, bailing if the stream stops) before the first read. Clients
only — restreamer chains keep their low-latency behaviour; warm streams already
satisfy the condition and don't wait. Adds SegmentReader::playlistBufferedSeconds()
(sum of #EXTINF) as the gate.
Validated live on stream 567: at cold start OLD served 1 seg / 2.0s; NEW served
5 seg / 10.0s after a 0.2s wait (fast-start pre-generates segments, so the added
startup latency is negligible).
Verified: php -l, phpunit (SegmentReaderTest 10/10), make gates.
An on-demand stream emits short (2s) fast-start segments before it ramps up to
seg_time. getPlaylistSegments picked intval(client_prebuffer / seg_time)
segments -- with the default 10s prebuffer / 10s nominal seg_time that is 1
segment, i.e. only ~2s of actual buffer at cold start, which players surfaced as
"caches 2 seconds". Persistent streams (segments already ~10s) were unaffected:
1 segment = 10s.
Select the newest segments whose #EXTINF durations sum to at least the requested
prebuffer seconds instead, so client_prebuffer means real seconds regardless of
segment length. Fast-start (hls_init_time 2) is kept. Falls back to the old
nominal count when the playlist has no #EXTINF lines; the -1 (all) and 0 (index)
modes are unchanged. Split into a pure selectSegments()/parseSegmentDurations()
so the selection is unit tested.
Verified: phpstan level 5 green, phpunit 405/405, make gates green.