Commit Graph
4 Commits
Author SHA1 Message Date
Claude 60b471d540 feat(streaming): serve live streams the pre-fanout way when fanout is off
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
2026-09-25 10:07:47 +00:00
Divarion_D 19c8865501 chore(dead-code): remove SegmentReader orphaned by E2
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.
2026-08-20 18:39:09 +03:00
Divarion_D 4902e953a0 fix(streaming): fill the on-demand prebuffer before the first TS burst
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.
2026-08-09 00:04:04 +03:00
Divarion_D 1c9d837761 fix(streaming): make HLS prebuffer duration-based, not segment-count
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.
2026-08-07 22:36:44 +03:00