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.
Follow-ups from /critico review on commits eb2548f + 40e7977. No
functional change.
- engine/hls.go restartFromSegment now reads `s.exited` under
`readyMu`. The field is documented as readyMu-protected (see field
declaration) and writers in waitFFmpeg / pollSegments hold the lock
consistently; the previous direct read produced a `go test -race`
warning under concurrent restart paths.
- engine/hls.go renderMasterPlaylist drops the `defaultIdx := -1`
branch that was unreachable (no rendition was ever flagged DEFAULT
or AUTOSELECT). Output is unchanged; the source is just shorter.
- engine/hls.go subtitle "(forzados)" suffix → "(forced)". Daemon
convention is English; the web client localises if needed.
- engine/hls.go hlsStderrCapture now also caps single-write payloads
larger than maxStderrBuf (was only capping the cumulative buffer).
- engine/hls.go waitFFmpeg restart-window reset drops the redundant
`!IsZero` guard — a zero time is far enough in the past that the
`> restartWindow` branch covers it.
Reliability hardening pass for the HLS daemon. None of these change the
public API, all reduce the chances of an end-user seeing a broken
session in production.
- engine/hls.go waitFFmpeg now supervises ffmpeg: on a non-graceful
exit while the session is still in use, restart from the last good
segment up to 3 times within a 60 s window. Beyond that we give up
and log the file as broken — better than a perpetually black player
with no error.
- engine/hls.go CleanupHLSOrphanDirs() removes tmpdirs older than 1 h
at startup; cmd/daemon.go calls it before streamSrv.Listen so a
daemon crash + restart doesn't leak gigabytes of segment files.
- engine/hls.go StartHLSSession wraps ffprobe in a 15 s timeout. A
hung probe on a slow remote fs would otherwise block the goroutine
forever and the player would stay on "Preparando sesion".
- engine/hls.go hlsStderrCapture buffer is capped at 64 KiB; a
misbehaving ffmpeg that emits megabytes without newlines used to
grow daemon memory unbounded.
Follow-ups on the daemon HLS pipeline (0fc0e1c):
- engine/hls.go HLSSession.Register now closes every other active
session in the registry. Modeled as "one viewer == one transcode" so
repeated quality switches or page reloads don't leave orphan ffmpegs
saturating the CPU until the idle sweeper reaps them 30 min later.
- engine/hls.go restartFromSegment kills + respawns ffmpeg with
-ss / -output_ts_offset / -start_number when the browser asks for a
segment far ahead of the writer head. Segments already on disk stay
cached. Without this, a user dragging the scrubber to minute 30 of a
fresh stream blocks until the encoder reaches minute 30 in real time.
- engine/hls.go subtitle disambiguation: never set DEFAULT=YES on any
rendition (anime forced "signs only" tracks were autoselected and
rendered nothing during opening dialogue, looking broken). Names get
numeric suffixes when language is duplicated; FORCED tracks get a
"(forzados)" suffix.
- engine/hls.go ProbeInfo() exposes codec / audio / subtitle metadata
to the new GET /hls/<id>/probe.json endpoint for the player's info
badge + bandwidth logic.
- engine/hls.go scale chain fix: chains a trunc(iw/2)*2 scale after
the height cap so libx264 stops rejecting odd widths (853x480 etc.).
- engine/hls.go HW encoder tuning: NVENC -preset p4 -rc vbr -tune hq,
QSV -preset medium.
- engine/stream_server.go routes /hls/<id>/probe.json to the session.
- cmd/daemon.go runs an idle sweeper goroutine every 5 min, reaping
sessions whose last segment fetch was >30 min ago.
Introduces an HLS-over-HTTP path as Plan B for in-browser streaming. The
WebRTC + MSE pipeline keeps working untouched; the new path is selected
when the backend sets transport="hls" on a streaming session.
Daemon scope:
- engine/hls.go: HLSSession + HLSSessionRegistry. Spawns ffmpeg with
-f hls -hls_segment_type fmp4 + force_key_frames aligned with 4 s
segments. Pre-renders master + media playlists from the probe duration
so the browser knows the total timeline before any segment exists,
fixing seek/duration/pause/multi-track issues seen with the live fMP4
pipe.
- engine/probe.go: enumerate every audio + subtitle track instead of
collapsing to a single default audio track.
- engine/stream_server.go: route /hls/<id>/{master.m3u8,video/...,
subs/...} to the matching session. Emit a synthesised single-VTT
subtitle playlist per text track; bitmap subs (PGS/DVB) skip silently.
- cmd/daemon.go: branch on WebRTCSession.Transport == "hls" to register
an HLS session instead of running the legacy DataChannel pump.
- agent/types.go: WebRTCSession.Transport + AudioIndex fields.
Backend + web sides land in a follow-up commit.