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.
This commit is contained in:
parent
4314c06c5c
commit
66ac79664b
6 changed files with 583 additions and 51 deletions
|
|
@ -64,6 +64,26 @@ func NewTranscoder(ctx context.Context, filePath string, opts TranscodeOpts) (*T
|
|||
return t, nil
|
||||
}
|
||||
|
||||
// startTranscoderToFile spawns ffmpeg with a pre-built argv where the last
|
||||
// argument is an output file path (instead of pipe:1). Used by streamSource
|
||||
// when we want random-access reads against a growing temp file rather than
|
||||
// sequential pipe consumption.
|
||||
func startTranscoderToFile(ctx context.Context, ffmpegPath string, args []string, t *Transcoder) (*Transcoder, error) {
|
||||
if ffmpegPath == "" {
|
||||
return nil, fmt.Errorf("transcoder: empty ffmpeg path")
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
||||
if t == nil {
|
||||
t = &Transcoder{}
|
||||
}
|
||||
t.cmd = cmd
|
||||
cmd.Stderr = &errWriter{t: t}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("transcoder: start ffmpeg: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (t *Transcoder) Read(p []byte) (int, error) { return t.out.Read(p) }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue