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

@ -128,3 +128,31 @@ func (h HWAccel) FFmpegVideoCodec(target string) string {
return "libx264"
}
}
// H264LevelForHeight returns the lowest H.264 profile level capable of encoding
// a stream at the given output pixel height (assumes ~16:9, ≤30 fps). The
// previous code used a fixed "4.0" which silently rejects anything above 1080p
// — libx264 logs "frame MB size > level limit" and emits a corrupt stream.
// Returning a tighter level on smaller outputs keeps player compatibility on
// older devices where the encoder can't auto-pick.
func H264LevelForHeight(height int) string {
switch {
case height <= 0:
// Unknown source — pick a level that covers up to 4K so we never
// re-introduce the silent-failure mode that motivated this helper.
return "5.1"
case height <= 480:
return "3.0"
case height <= 720:
return "3.1"
case height <= 1080:
return "4.0"
case height <= 1440:
return "5.0"
case height <= 2160:
return "5.1"
default:
// 4K @ 60 fps and 8K all fall under 6.x.
return "6.0"
}
}