- 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.
106 lines
3.6 KiB
Go
106 lines
3.6 KiB
Go
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 {
|
||
FFmpegPath string
|
||
FFprobePath string
|
||
HWAccel HWAccel
|
||
Preset string
|
||
VideoBitrate string
|
||
AudioBitrate string
|
||
MaxHeight int
|
||
// Disabled forces passthrough for every file even when codecs are not
|
||
// browser-friendly. Useful when the user explicitly turns transcoding
|
||
// off in config.
|
||
Disabled bool
|
||
// TonemapHDR enables HDR→SDR tonemapping of HDR sources during transcode.
|
||
// Set only when the ffmpeg build has zscale (FFmpegSupportsZscale); without
|
||
// it the tonemap filter would error and break playback, so it stays off.
|
||
TonemapHDR bool
|
||
// HasLibplacebo: the ffmpeg build has the libplacebo filter (GPU HDR tonemap).
|
||
// Preferred over the zscale chain for HDR sources — one GPU pass, higher
|
||
// quality, and present where zscale is missing.
|
||
HasLibplacebo bool
|
||
}
|
||
|
||
// qualityCap maps a session's Quality label to a (MaxHeight, VideoBitrate)
|
||
// pair. An empty label or "original" returns zero-values, signalling "no
|
||
// override" to the caller.
|
||
type qualityCap struct {
|
||
MaxHeight int
|
||
VideoBitrate string // ffmpeg -b:v string, e.g. "3500k"
|
||
}
|
||
|
||
func resolveQualityCap(label string) qualityCap {
|
||
switch label {
|
||
case "2160p":
|
||
return qualityCap{MaxHeight: 2160, VideoBitrate: "25000k"}
|
||
case "1080p":
|
||
return qualityCap{MaxHeight: 1080, VideoBitrate: "6000k"}
|
||
case "720p":
|
||
return qualityCap{MaxHeight: 720, VideoBitrate: "3500k"}
|
||
case "480p":
|
||
return qualityCap{MaxHeight: 480, VideoBitrate: "1500k"}
|
||
default:
|
||
// "original", "auto", "" → defer to config.
|
||
return 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
|
||
// the H.264 level we derived from the EFFECTIVE height (4.0, max 20 Mbps) and
|
||
// makes libx264 refuse with "VBV bitrate > level limit". This helper picks
|
||
// the bitrate that matches the level libx264 will actually accept.
|
||
func capForHeight(height int) qualityCap {
|
||
switch {
|
||
case height <= 0:
|
||
return qualityCap{}
|
||
case height <= 480:
|
||
return qualityCap{MaxHeight: 480, VideoBitrate: "1500k"}
|
||
case height <= 720:
|
||
return qualityCap{MaxHeight: 720, VideoBitrate: "3500k"}
|
||
case height <= 1080:
|
||
return qualityCap{MaxHeight: 1080, VideoBitrate: "6000k"}
|
||
case height <= 1440:
|
||
return qualityCap{MaxHeight: 1440, VideoBitrate: "12000k"}
|
||
default:
|
||
return qualityCap{MaxHeight: 2160, VideoBitrate: "25000k"}
|
||
}
|
||
}
|