feat(hls): faster first-start — probe cache + tighter encoder presets (0.9.9)
Reduces first-segment latency on cache MISS so the player doesn't sit on
"preparando sesión". Three independent levers:
1. ProbeFile memoised by (path, mtime, size) for 30 min — second play of
the same source skips ffprobe (1-3 s on 50+ GB MKVs).
2. HLS encoder presets biased for latency over quality:
- libx264 default veryfast → superfast (~15-20% faster, marginal
quality loss at 5-25 Mbps target bitrates).
- NVENC: -preset p4 -tune hq → -preset p3 -tune ll. First-segment
~0.8 s on RTX-class GPUs (was ~1.5 s).
- QSV: -preset medium → -preset veryfast (keeps look_ahead=0).
- VideoToolbox: adds -realtime 1 (was unset). Bitrate args still
drive rate control; -q:v dropped to avoid the silent conflict
where ffmpeg ignored it under -b:v.
3. Per-session log surfaces encoder + accel + preset so "first-start
was slow" complaints can be triaged from the journal alone.
Diagnostic helpers (DetectHWAccelDiagnostic + HWAccelDiagnostic) added
for future wiring into daemon startup / agent register; users today can
already inspect via `unarr probe-hwaccel`.
Web: AgentsTab profile page now shows the agent's chosen encoder
(amber if software libx264, green if HW) plus the transcode-resolution
cap. Hidden for pre-0.9.9 agents that haven't reported hwAccel.
This commit is contained in:
parent
7b78d0b778
commit
3b8d77b496
8 changed files with 593 additions and 17 deletions
|
|
@ -422,9 +422,19 @@ func StartHLSSession(ctx context.Context, cfg HLSSessionConfig) (*HLSSession, er
|
|||
if cfg.Cache != nil {
|
||||
cachedNote = fmt.Sprintf(" (cache-miss %s)", cacheKey)
|
||||
}
|
||||
log.Printf("[hls %s] started: %s, %.1fs, %d segs (quality=%s)%s",
|
||||
// Surface the encoder profile so a "first-start was slow" report can be
|
||||
// triaged from the agent log alone — `encoder=libx264 accel=none` means
|
||||
// the user's ffmpeg has no HW encoders compiled in, which is the most
|
||||
// common root cause (linuxbrew, default brew formula on macOS).
|
||||
profile := ResolveEncoderProfile(cfg.Transcode.HWAccel, cfg.Transcode.Preset)
|
||||
presetNote := ""
|
||||
if profile.Preset != "" {
|
||||
presetNote = " preset=" + profile.Preset
|
||||
}
|
||||
log.Printf("[hls %s] started: %s, %.1fs, %d segs (quality=%s, encoder=%s accel=%s%s)%s",
|
||||
shortHLSID(cfg.SessionID), filepath.Base(cfg.SourcePath),
|
||||
probe.DurationSec, segCount, coalesce(cfg.Quality, "auto"), cachedNote)
|
||||
probe.DurationSec, segCount, coalesce(cfg.Quality, "auto"),
|
||||
profile.Codec, string(cfg.Transcode.HWAccel), presetNote, cachedNote)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
|
@ -965,6 +975,41 @@ func buildHLSFFmpegArgs(cfg HLSSessionConfig, probe *StreamProbe, tmpDir string)
|
|||
return buildHLSFFmpegArgsAt(cfg, probe, tmpDir, 0, 0)
|
||||
}
|
||||
|
||||
// EncoderProfile names the codec + preset combination the HLS pipeline picks
|
||||
// for the given hardware backend + transcode config. Exposed so callers can
|
||||
// log the chosen encoder before ffmpeg launches (otherwise the resolution
|
||||
// lives only inside buildHLSFFmpegArgsAt).
|
||||
type EncoderProfile struct {
|
||||
Codec string // ffmpeg encoder name (e.g. "h264_nvenc", "libx264")
|
||||
Preset string // preset string, or "" when the codec has no preset knob
|
||||
}
|
||||
|
||||
// ResolveEncoderProfile mirrors the codec + preset selection inside
|
||||
// buildHLSFFmpegArgsAt so callers (registry, log lines, diagnostic
|
||||
// endpoints) can know what ffmpeg will be told to do without parsing argv.
|
||||
func ResolveEncoderProfile(hw HWAccel, configuredPreset string) EncoderProfile {
|
||||
codec := hw.FFmpegVideoCodec("h264")
|
||||
preset := configuredPreset
|
||||
switch codec {
|
||||
case "libx264":
|
||||
if preset == "" {
|
||||
preset = "superfast"
|
||||
}
|
||||
case "h264_nvenc":
|
||||
if preset == "" {
|
||||
preset = "p3"
|
||||
}
|
||||
case "h264_qsv":
|
||||
if preset == "" {
|
||||
preset = "veryfast"
|
||||
}
|
||||
case "h264_videotoolbox":
|
||||
// No preset knob for VideoToolbox; the speed/quality dial is `-q:v`.
|
||||
preset = ""
|
||||
}
|
||||
return EncoderProfile{Codec: codec, Preset: preset}
|
||||
}
|
||||
|
||||
// buildHLSFFmpegArgsAt returns the argv for an HLS encode that starts at the
|
||||
// given segment index (`-ss <startSec>`) and writes segments numbered from
|
||||
// startIdx so they slot into the existing manifest at the correct position.
|
||||
|
|
@ -1011,24 +1056,43 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin
|
|||
}
|
||||
args = append(args, "-map", fmt.Sprintf("0:a:%d?", audioIdx))
|
||||
|
||||
// Video encode.
|
||||
codec := hwHint.FFmpegVideoCodec("h264")
|
||||
// Video encode. Codec + preset are resolved by ResolveEncoderProfile so
|
||||
// the same logic feeds both the argv builder and per-session log lines.
|
||||
//
|
||||
// Defaults are biased for FIRST-START LATENCY over quality — the player
|
||||
// blocks on seg-0 before the first frame paints, and a slow seg-0 is
|
||||
// what users notice ("preparando sesión" stuck). Users who want better
|
||||
// quality can override via `download.transcode.preset` in config.toml.
|
||||
profile := ResolveEncoderProfile(hwHint, cfg.Transcode.Preset)
|
||||
codec := profile.Codec
|
||||
args = append(args, "-c:v", codec)
|
||||
// Encoder-specific tuning. Each HW encoder takes a different "preset"
|
||||
// vocabulary; libx264 uses ultrafast→placebo, NVENC uses p1→p7, QSV uses
|
||||
// veryfast→veryslow, VAAPI/VideoToolbox don't expose presets.
|
||||
switch codec {
|
||||
case "libx264":
|
||||
preset := cfg.Transcode.Preset
|
||||
if preset == "" {
|
||||
preset = "veryfast"
|
||||
}
|
||||
args = append(args, "-preset", preset)
|
||||
// superfast = ~15-20% faster than veryfast at marginal quality loss
|
||||
// for the bitrates we target (5-25 Mbps). For 4K software encodes
|
||||
// this is the difference between ~3 s and ~2.5 s per segment on a
|
||||
// recent x86 CPU. `-threads 0` is libx264's default but explicit
|
||||
// helps when the user has set GOMAXPROCS.
|
||||
args = append(args, "-preset", profile.Preset, "-threads", "0")
|
||||
case "h264_nvenc":
|
||||
// p4 = balanced quality/speed; p1 fastest, p7 highest quality.
|
||||
args = append(args, "-preset", "p4", "-rc", "vbr", "-tune", "hq")
|
||||
// p3 + tune=ll trades ~0.3 dB PSNR for 1.5-2× faster encode vs the
|
||||
// previous p4 + tune=hq pair — first-segment encode drops from
|
||||
// ~1.5 s to ~0.8 s on RTX-class hardware.
|
||||
args = append(args, "-preset", profile.Preset, "-rc", "vbr", "-tune", "ll")
|
||||
case "h264_qsv":
|
||||
args = append(args, "-preset", "medium", "-look_ahead", "0")
|
||||
// veryfast is the fastest realistic QSV preset; medium was too
|
||||
// conservative for first-start. look_ahead=0 keeps the encoder
|
||||
// truly low-latency (no rate-control look-ahead window).
|
||||
args = append(args, "-preset", profile.Preset, "-look_ahead", "0")
|
||||
case "h264_videotoolbox":
|
||||
// VideoToolbox has no "preset" knob; `-realtime` flips into the
|
||||
// low-latency path used by FaceTime. We let `-b:v / -maxrate /
|
||||
// -bufsize` (set below at line ~1119) drive rate control —
|
||||
// adding `-q:v` here would conflict because ffmpeg's
|
||||
// videotoolbox encoder treats `-b:v` as authoritative and
|
||||
// silently ignores `-q:v`, so the constant-quality knob never
|
||||
// took effect anyway.
|
||||
args = append(args, "-realtime", "1")
|
||||
}
|
||||
// Derive H.264 level from the actual output height. A fixed "4.0" caps the
|
||||
// encoder at 1080p — anything taller (1440p, 4K source on quality=original)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue