package playback import ( "context" "fmt" "io" "net/http" "os" "os/exec" "strconv" "sync" ) var ( resolvedFFmpegPath string ffmpegOnce sync.Once ) // ffmpegBinary returns the path to the ffmpeg binary. // Resolved once at first call, then cached for the process lifetime. func ffmpegBinary() string { ffmpegOnce.Do(func() { const jellyfinPath = "/usr/lib/jellyfin-ffmpeg/ffmpeg" if _, err := exec.LookPath(jellyfinPath); err == nil { resolvedFFmpegPath = jellyfinPath return } resolvedFFmpegPath = "ffmpeg" }) return resolvedFFmpegPath } // RemuxSession represents a running ffmpeg remux process that copies // codecs to a new container format without re-encoding. type RemuxSession struct { cmd *exec.Cmd cancel context.CancelFunc outputPipe io.ReadCloser } // buildRemuxArgs constructs the ffmpeg argument list for a remux operation. // The args perform codec copy (-c copy) into the target container format, // using fragmented output for streaming (frag_keyframe+empty_moov+default_base_moof) and // pipe:1 for stdout output. // When transcodeAudio is true, video is copied but audio is transcoded to // stereo AAC (handles cases like DTS/TrueHD that browsers cannot decode). func buildRemuxArgs(filePath, outputFormat string, seekSeconds float64, transcodeAudio bool, audioTrackIndex int) []string { args := []string{ "-nostdin", "-hide_banner", "-loglevel", "error", // Cap the input probe to speed up startup on large MKV containers // (defaults scan up to 5 seconds / several MB). Mirrors the transcode // pipeline, which has run these caps in production without issue. "-fflags", "+genpts+fastseek", "-analyzeduration", "3000000", "-probesize", "5000000", } // Add seek if requested (before input for fast seek). if seekSeconds > 0 { args = append(args, "-ss", strconv.FormatFloat(seekSeconds, 'f', 3, 64)) if transcodeAudio { // In copy mode (-c:v copy) video must start at a keyframe, but // accurate_seek (the default) trims re-encoded audio to the exact // seek point. This mismatch causes A/V desync equal to the gap // between the keyframe and the seek point. Disabling accurate seek // keeps both streams aligned at the keyframe boundary. args = append(args, "-noaccurate_seek") } } args = append(args, "-i", filePath) // Strip metadata/chapters and skip subtitle/data streams — the remux // output is fed to an HTML