feat(transcode): dynamic H.264 level + HW probe + capability reporting

Three related fixes around 4K-source transcoding that left the web
player stuck on "preparing session" with no useful diagnostics:

1. Dynamic -level:v derived from output height (hls.go, transcoder.go).
   The previous fixed "4.0" silently rejected anything taller than 1080p
   inside libx264 — "frame MB size > level limit", "DPB size > level
   limit" — and emitted unplayable segments. Helper H264LevelForHeight()
   now picks 4.0 / 5.0 / 5.1 / 6.0 from the actual encode height.

2. New `unarr probe-hwaccel` diagnostic command. Lists the HW encoders
   compiled into ffmpeg, the device files / drivers present, and the
   backend the daemon would actually pick today. Surfaces the canonical
   gotcha: a host with an RTX 3090 + nvidia-smi but a Homebrew ffmpeg
   built without --enable-nvenc still falls back to libx264 software.

3. Register payload now includes hwAccel + maxTranscodeHeight so the web
   side can suggest a smaller alternate quality before the user even
   tries to play a 4K source on a software-only host. Software-only =
   1080p cap, any HW backend = 2160p cap.
This commit is contained in:
Deivid Soto 2026-05-08 15:57:02 +02:00
parent 01941ed2e4
commit 209ea38ecf
9 changed files with 297 additions and 30 deletions

View file

@ -845,9 +845,21 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin
case "h264_qsv":
args = append(args, "-preset", "medium", "-look_ahead", "0")
}
args = append(args, "-profile:v", "main", "-level:v", "4.0")
// 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)
// fails libx264 with "frame MB size > level limit" and emits unplayable
// segments. The output height matches qcap.MaxHeight when the source is
// downscaled, otherwise probe.Height (already populated by ffprobe).
qcap := resolveQualityCap(cfg.Quality)
outputHeight := qcap.MaxHeight
if outputHeight == 0 {
outputHeight = cfg.Transcode.MaxHeight
}
if outputHeight == 0 || (probe.Height > 0 && probe.Height < outputHeight) {
outputHeight = probe.Height
}
args = append(args, "-profile:v", "main", "-level:v", H264LevelForHeight(outputHeight))
bitrate := qcap.VideoBitrate
if bitrate == "" {
bitrate = cfg.Transcode.VideoBitrate