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

@ -1,5 +1,10 @@
package engine
import (
"math"
"strconv"
)
// TranscodeRuntime carries the resolved ffmpeg/ffprobe paths + tunables so
// each session can decide whether to passthrough or pipe through ffmpeg.
type TranscodeRuntime struct {
@ -48,6 +53,35 @@ func resolveQualityCap(label string) qualityCap {
}
}
// doubleBitrate returns an ffmpeg bitrate string with twice the value of the
// input ("6000k" → "12000k", "1.5M" → "3M", "5M" → "10M"). Used to size
// `-bufsize` at the standard 2× of `-maxrate` for capped-CRF/CQ rate control.
// An unparseable string falls back to the input unchanged (1× bufsize — the
// pre-CRF behaviour, safe just suboptimal). The doubled CPB stays far below
// every H.264 level's limit for the (level, maxrate) pairs this package emits
// (worst case: 1080p level 4.1 → 12000k bufsize vs 62500k allowed).
func doubleBitrate(b string) string {
if b == "" {
return b
}
num := b
suffix := ""
switch b[len(b)-1] {
case 'k', 'K', 'm', 'M':
num = b[:len(b)-1]
suffix = string(b[len(b)-1])
}
v, err := strconv.ParseFloat(num, 64)
if err != nil || v <= 0 {
return b
}
d := v * 2
if d == math.Trunc(d) {
return strconv.FormatFloat(d, 'f', 0, 64) + suffix
}
return strconv.FormatFloat(d, 'f', -1, 64) + suffix
}
// capForHeight returns the bitrate-cap pair appropriate for an effective
// output height. Used after clamping outputHeight to the source's resolution:
// asking ffmpeg for "2160p" bitrate (25 Mbps) on a 1080p source overshoots