From 9eb3e44153837fb2691fa0375378ee12729b912d Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 10 Jun 2026 23:31:58 +0200 Subject: [PATCH] fix(stream): el modo copy ignora StartSec (offset EVENT rompe iOS nativo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un playlist EVENT cuyas entradas empiezan en 0 mientras los fragmentos llevan tfdt desplazado (-ss + -output_ts_offset) es exactamente la forma que el parser HLS nativo de iOS no traga: resume a 368s → error del player y bucle de re-bootstrap de sesión en iPhone (observado 2026-06-10). Copy produce siempre desde 0 con PTS absolutos reales: adelanta a la reproducción a velocidad de I/O, así que el punto de resume aparece en la timeline creciente en segundos y el seek de startPosition del player aterriza con normalidad. Test de resume actualizado: el playlist debe cubrir la timeline completa. --- internal/engine/hls.go | 21 ++++++++++----------- internal/engine/hls_copy_smoke_test.go | 9 +++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/engine/hls.go b/internal/engine/hls.go index 8f90c15..cf9c506 100644 --- a/internal/engine/hls.go +++ b/internal/engine/hls.go @@ -205,7 +205,8 @@ type HLSSessionConfig struct { // file is remuxed at I/O speed, minutes at worst on a weak NAS); // - no HLS cache (re-generating costs no encode — caching would only // burn disk); - // - StartSec is passed straight to `-ss` (keyframe-snapped by ffmpeg). + // - StartSec is ignored: copy produces from 0 (outruns playback at I/O + // speed); an offset EVENT playlist breaks iOS's native HLS parser. // See docs/plans/hls-copy-remux-replacement.md (web repo). VideoCopy bool } @@ -1902,13 +1903,14 @@ func renderVideoPlaylist(durationSec float64, segCount int) string { func buildHLSCopyArgs(cfg HLSSessionConfig, probe *StreamProbe, tmpDir string) []string { args := []string{"-y", "-hide_banner", "-loglevel", "warning", "-stats"} - // Resume: input-side seek snaps to the keyframe at/before StartSec (demux - // seek — instant). -output_ts_offset keeps the fragments' tfdt on the - // absolute timeline so the player's clock matches the real position. - if cfg.StartSec > 0 && cfg.StartSec < probe.DurationSec { - ss := strconv.FormatFloat(cfg.StartSec, 'f', 3, 64) - args = append(args, "-ss", ss) - } + // StartSec is INTENTIONALLY ignored in copy mode: an EVENT playlist whose + // entries start at position 0 while the fragments carry an offset tfdt + // (-ss + -output_ts_offset) is exactly the shape iOS's native HLS parser + // chokes on (observed 2026-06-10: resume at 368s → player error + session + // re-bootstrap loop on iPhone). Copy always produces from 0 with true + // absolute PTS — it outruns playback at I/O speed, so the resume point + // appears in the growing timeline within seconds and the player's own + // startPosition seek lands normally. if cfg.SourceURL != "" { args = append(args, @@ -1919,9 +1921,6 @@ func buildHLSCopyArgs(cfg HLSSessionConfig, probe *StreamProbe, tmpDir string) [ ) } args = append(args, "-i", cfg.sourceRef()) - if cfg.StartSec > 0 && cfg.StartSec < probe.DurationSec { - args = append(args, "-output_ts_offset", strconv.FormatFloat(cfg.StartSec, 'f', 3, 64)) - } // Map video + selected audio (same clamping rules as the encode path). args = append(args, "-map", "0:v:0") diff --git a/internal/engine/hls_copy_smoke_test.go b/internal/engine/hls_copy_smoke_test.go index 4ab5b3b..203dcb3 100644 --- a/internal/engine/hls_copy_smoke_test.go +++ b/internal/engine/hls_copy_smoke_test.go @@ -205,8 +205,9 @@ func TestHLSCopy_ResumeStartSec(t *testing.T) { []string{"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p"}, []string{"-c:a", "aac", "-b:a", "128k"}, 12) _, pl := runCopySession(t, rt, src, 6) - // Resume covers roughly the back half (keyframe-snapped, so allow the - // full GOP of slack: 60 frames @30fps = 2s). + // StartSec must be IGNORED in copy mode: the playlist covers the FULL + // timeline from 0 (an offset EVENT playlist breaks iOS's native parser; + // the player seeks to the resume point itself). Sum ≈ full 12s. var sum float64 for _, line := range strings.Split(pl, "\n") { if strings.HasPrefix(line, "#EXTINF:") { @@ -215,8 +216,8 @@ func TestHLSCopy_ResumeStartSec(t *testing.T) { sum += d } } - if sum < 4 || sum > 9 { - t.Errorf("resume EXTINF sum = %.2fs, want ≈6s (12s source, -ss 6, ±GOP)", sum) + if sum < 10.5 || sum > 13.5 { + t.Errorf("copy EXTINF sum = %.2fs, want ≈12s (StartSec ignored, full timeline)", sum) } }