feat(stream): direct-play passthrough for browser-native files

Hueco #3 / 3a (CLI side). StreamSession gains PlayMethod; when the web
sends "direct", the daemon serves the raw file over /stream (HTTP Range,
no ffmpeg) instead of transcoding to HLS — zero CPU, instant seek. Runs
before the ffmpeg-availability check so direct-play works even with
transcode disabled. Legacy/empty PlayMethod keeps the HLS path, so an old
web that never sends "direct" is unaffected.
This commit is contained in:
Deivid Soto 2026-05-31 10:32:34 +02:00
parent 3592b9f95a
commit c8d7c4bba5
2 changed files with 31 additions and 0 deletions

View file

@ -410,6 +410,13 @@ type StreamSession struct {
// AudioIndex selects the source audio track (-map 0:a:N). -1 means // AudioIndex selects the source audio track (-map 0:a:N). -1 means
// "use the default/first track". // "use the default/first track".
AudioIndex int `json:"audioIndex,omitempty"` AudioIndex int `json:"audioIndex,omitempty"`
// PlayMethod is how the daemon should serve this session:
// "" — default (HLS transcode); also what legacy servers send.
// "direct" — the source is already browser-native (the web decided this
// from library scan metadata + an agent-version gate). Serve
// the raw file over /stream (HTTP Range, no ffmpeg) instead of
// transcoding to HLS. See hueco #3 phase 3a in the roadmap.
PlayMethod string `json:"playMethod,omitempty"`
} }
// SyncResponse is returned by the server with all pending actions for the CLI. // SyncResponse is returned by the server with all pending actions for the CLI.

View file

@ -589,6 +589,30 @@ func runDaemonStart() error {
filePath = found filePath = found
} }
// Direct-play (hueco #3 / 3a): the web decided this source is already
// browser-native (mp4 h264/aac 8-bit SDR) from library scan metadata,
// gated on agent version. Serve the raw file over /stream (HTTP Range,
// no ffmpeg) instead of transcoding to HLS — zero CPU, instant seek.
// Runs BEFORE the ffmpeg-availability check on purpose: direct-play
// needs no ffmpeg, so it must work even when transcode is disabled.
if sess.PlayMethod == "direct" {
streamSrv.SetFile(engine.NewDiskFileProvider(filePath), sess.TaskID)
// cancel just clears the served file so daemon shutdown / drain
// stops exposing it on /stream. There's no ffmpeg child to kill.
playerSessionRegistry.add(sess.SessionID, func() { streamSrv.ClearFile() })
log.Printf("[stream %s] direct-play: %s", agent.ShortID(sess.SessionID), filepath.Base(filePath))
// File is on disk → ready immediately. Tell the web so the player
// attaches <video src> without burning its HEAD-probe retry budget.
go func() {
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := agentClient.MarkSessionReady(rctx, sess.SessionID); err != nil {
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sess.SessionID), err)
}
}()
return
}
tcRuntime := buildTranscodeRuntime(ctx, cfg) tcRuntime := buildTranscodeRuntime(ctx, cfg)
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" { if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
log.Printf("[hls %s] rejected: ffmpeg/ffprobe unavailable", agent.ShortID(sess.SessionID)) log.Printf("[hls %s] rejected: ffmpeg/ffprobe unavailable", agent.ShortID(sess.SessionID))