unarr/internal/cmd/player_session_registry.go

97 lines
2.9 KiB
Go
Raw Normal View History

package cmd
import (
"context"
"sync"
feat(stream): real-time transcoding for non-browser-decodable codecs Source files in HEVC, AV1, AC3, DTS, EAC3, etc. now transcode through ffmpeg to fragmented MP4 (h264 + aac) on-the-fly when the browser would otherwise play silent black. Decision matrix lives in engine.DecideAction: passthrough → remux → audio-transcode → full video-transcode. Architecture — temp file + growing-size source: - engine.streamSource interface abstracts byte source. Two impls: * diskFileSource: passthrough when codecs are already browser-friendly. * transcodeSource: spawns ffmpeg writing to a /tmp/tc-stream-*.mp4 file. A ticker polls file size and wakes blocked ReadAt callers as ffmpeg produces output. Estimate of final size (bitrate × duration) is announced over the wire so the browser's scrubber has something to anchor on. - dataChannelPump now reads from streamSource instead of *os.File. HELLO carries Transcoding=true + an estimated total size; Seekable=true (we read random-access from the temp file even while writing). - Transcoder runtime resolved per session by buildTranscodeRuntime in cmd/daemon: ffmpeg/ffprobe path lookup + HWAccel auto-detection (NVENC/QSV/VAAPI/VideoToolbox). - New [downloads.transcode] TOML section: enabled (default true), hw_accel (auto), preset (veryfast), video_bitrate (5M), audio_bitrate (192k), max_height (optional downscale), max_concurrent (safety cap). Falls back to passthrough if ffprobe is missing, fails, or codecs are already browser-friendly. tmp file is cleaned up on session shutdown.
2026-05-07 09:26:05 +02:00
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/library/mediainfo"
)
// playerSessionRegistry tracks per-session cancel funcs for active in-browser
// HLS streaming sessions. Each session lives only as long as its ffmpeg
// process; the registry exists so duplicate sync responses don't double-spawn
// the same session and so daemon shutdown can drain.
var playerSessionRegistry = &playerSessionRegistryT{
cancels: make(map[string]context.CancelFunc),
}
type playerSessionRegistryT struct {
mu sync.Mutex
cancels map[string]context.CancelFunc
}
func (r *playerSessionRegistryT) has(sessionID string) bool {
r.mu.Lock()
defer r.mu.Unlock()
_, ok := r.cancels[sessionID]
return ok
}
func (r *playerSessionRegistryT) add(sessionID string, cancel context.CancelFunc) {
r.mu.Lock()
defer r.mu.Unlock()
r.cancels[sessionID] = cancel
}
func (r *playerSessionRegistryT) remove(sessionID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.cancels, sessionID)
}
// cancelAllPlayerSessions cancels every running session. Called on daemon
// shutdown so the ffmpeg children and SSE consumers exit cleanly.
func cancelAllPlayerSessions() {
playerSessionRegistry.mu.Lock()
cancels := make([]context.CancelFunc, 0, len(playerSessionRegistry.cancels))
for _, c := range playerSessionRegistry.cancels {
cancels = append(cancels, c)
}
playerSessionRegistry.cancels = make(map[string]context.CancelFunc)
playerSessionRegistry.mu.Unlock()
for _, c := range cancels {
c()
}
}
feat(stream): real-time transcoding for non-browser-decodable codecs Source files in HEVC, AV1, AC3, DTS, EAC3, etc. now transcode through ffmpeg to fragmented MP4 (h264 + aac) on-the-fly when the browser would otherwise play silent black. Decision matrix lives in engine.DecideAction: passthrough → remux → audio-transcode → full video-transcode. Architecture — temp file + growing-size source: - engine.streamSource interface abstracts byte source. Two impls: * diskFileSource: passthrough when codecs are already browser-friendly. * transcodeSource: spawns ffmpeg writing to a /tmp/tc-stream-*.mp4 file. A ticker polls file size and wakes blocked ReadAt callers as ffmpeg produces output. Estimate of final size (bitrate × duration) is announced over the wire so the browser's scrubber has something to anchor on. - dataChannelPump now reads from streamSource instead of *os.File. HELLO carries Transcoding=true + an estimated total size; Seekable=true (we read random-access from the temp file even while writing). - Transcoder runtime resolved per session by buildTranscodeRuntime in cmd/daemon: ffmpeg/ffprobe path lookup + HWAccel auto-detection (NVENC/QSV/VAAPI/VideoToolbox). - New [downloads.transcode] TOML section: enabled (default true), hw_accel (auto), preset (veryfast), video_bitrate (5M), audio_bitrate (192k), max_height (optional downscale), max_concurrent (safety cap). Falls back to passthrough if ffprobe is missing, fails, or codecs are already browser-friendly. tmp file is cleaned up on session shutdown.
2026-05-07 09:26:05 +02:00
// buildTranscodeRuntime resolves the ffmpeg/ffprobe binaries + config knobs
// for the HLS streaming pipeline. Failure to resolve a binary returns a
// runtime with empty paths so the caller can short-circuit instead of
// launching a transcoder that will immediately fail.
feat(stream): real-time transcoding for non-browser-decodable codecs Source files in HEVC, AV1, AC3, DTS, EAC3, etc. now transcode through ffmpeg to fragmented MP4 (h264 + aac) on-the-fly when the browser would otherwise play silent black. Decision matrix lives in engine.DecideAction: passthrough → remux → audio-transcode → full video-transcode. Architecture — temp file + growing-size source: - engine.streamSource interface abstracts byte source. Two impls: * diskFileSource: passthrough when codecs are already browser-friendly. * transcodeSource: spawns ffmpeg writing to a /tmp/tc-stream-*.mp4 file. A ticker polls file size and wakes blocked ReadAt callers as ffmpeg produces output. Estimate of final size (bitrate × duration) is announced over the wire so the browser's scrubber has something to anchor on. - dataChannelPump now reads from streamSource instead of *os.File. HELLO carries Transcoding=true + an estimated total size; Seekable=true (we read random-access from the temp file even while writing). - Transcoder runtime resolved per session by buildTranscodeRuntime in cmd/daemon: ffmpeg/ffprobe path lookup + HWAccel auto-detection (NVENC/QSV/VAAPI/VideoToolbox). - New [downloads.transcode] TOML section: enabled (default true), hw_accel (auto), preset (veryfast), video_bitrate (5M), audio_bitrate (192k), max_height (optional downscale), max_concurrent (safety cap). Falls back to passthrough if ffprobe is missing, fails, or codecs are already browser-friendly. tmp file is cleaned up on session shutdown.
2026-05-07 09:26:05 +02:00
func buildTranscodeRuntime(ctx context.Context, cfg config.Config) engine.TranscodeRuntime {
if !cfg.Download.Transcode.Enabled {
return engine.TranscodeRuntime{Disabled: true}
}
ffmpegPath, errF := mediainfo.ResolveFFmpeg(cfg.Library.FFmpegPath)
ffprobePath, errP := mediainfo.ResolveFFprobe(cfg.Library.FFprobePath)
if errF != nil || errP != nil {
return engine.TranscodeRuntime{Disabled: true}
}
hw := engine.HWAccelNone
switch cfg.Download.Transcode.HWAccel {
case "auto":
hw = engine.DetectHWAccel(ctx, ffmpegPath)
case "nvenc":
hw = engine.HWAccelNVENC
case "qsv":
hw = engine.HWAccelQSV
case "vaapi":
hw = engine.HWAccelVAAPI
case "videotoolbox":
hw = engine.HWAccelVideoToolbox
case "none", "":
hw = engine.HWAccelNone
}
return engine.TranscodeRuntime{
FFmpegPath: ffmpegPath,
FFprobePath: ffprobePath,
HWAccel: hw,
Preset: cfg.Download.Transcode.Preset,
VideoBitrate: cfg.Download.Transcode.VideoBitrate,
AudioBitrate: cfg.Download.Transcode.AudioBitrate,
MaxHeight: cfg.Download.Transcode.MaxHeight,
}
}