feat(hls): resume-aware first spawn + capped-CRF/CQ rate control

- HLSSessionConfig.StartSec (sync StreamSession.startSec): el primer
  ffmpeg arranca ya seekeado en el punto de resume (-ss +
  -output_ts_offset + -start_number, misma maquinaria que el
  seek-restart) en vez de encodear desde seg-0 para morir en el
  seek-restart inmediato del player (doble spawn, resume lento).
  readyMax se pre-siembra al índice de arranque; el ready-watcher
  compara ReadyCount() > WriterStartIdx() para no marcar "ready" antes
  del primer segmento real. startSec >= duración → arranque desde 0
  (resume obsoleto de un fichero reemplazado).
- Rate control: capped constant-quality donde el encoder lo hace bien —
  libx264 -crf 23, NVENC -cq 23 -b:v 0 — con el mismo -maxrate de
  siempre y -bufsize 2x (antes 1x estrangulaba picos). Escenas fáciles
  emiten muchos menos bits (menos stalls vía funnel/LTE); el peor caso
  no cambia. QSV/VideoToolbox/VAAPI conservan el triple de bitrate fijo
  probado (sus knobs de calidad tienen gotchas de vendor).
- Limpieza: wrapper buildHLSFFmpegArgs y guard startIdx<0 muertos.
This commit is contained in:
Deivid Soto 2026-06-10 00:21:15 +02:00
parent f7ca282ca0
commit 9b97aedfe4
5 changed files with 259 additions and 16 deletions

View file

@ -790,6 +790,7 @@ func runDaemonStart() error {
Quality: sess.Quality,
AudioIndex: sess.AudioIndex,
BurnSubtitleIndex: sess.BurnSubtitleIndex,
StartSec: sess.StartSec,
Transcode: tcRuntime,
Cache: hlsCache,
// 2c: refresh the debrid link if it expires mid-transcode; the
@ -925,6 +926,7 @@ func runDaemonStart() error {
Quality: sess.Quality,
AudioIndex: sess.AudioIndex,
BurnSubtitleIndex: sess.BurnSubtitleIndex,
StartSec: sess.StartSec,
Transcode: tcRuntime,
Cache: hlsCache,
}, hlsCtx, hlsCancel)
@ -1449,8 +1451,13 @@ func watchSessionReady(ctx context.Context, client *agent.Client, hsess *engine.
if hsess.IsClosed() {
return
}
// Phase 1: cache HIT or seg-0 ready → flip the "Preparando…" UI now.
if !readyPosted && (hsess.FromCache() || hsess.ReadyCount() >= 1) {
// Phase 1: cache HIT or first segment ready → flip the "Preparando…"
// UI now. Compare against WriterStartIdx, not `>= 1`: a resume
// session (StartSec) pre-seeds readyMax to the start index, so
// ReadyCount() is ≥ 1 before ffmpeg has written a single byte —
// `>= 1` would fire "ready" instantly and freeze the player waiting
// on a segment that doesn't exist yet.
if !readyPosted && (hsess.FromCache() || hsess.ReadyCount() > hsess.WriterStartIdx()) {
postReady(nil)
readyPosted = true
// Cache replay has no live encode → no telemetry to report, done.