diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index ae20965..63ca4b5 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + mxladapter "mxl-player/internal/adapter/mxl" "mxl-player/internal/imgui" "mxl-player/internal/playback" "mxl-player/internal/renderer" @@ -144,7 +145,8 @@ func main() { if !args.ListAudio && !args.ListGPU { checkMXLargs(args) } - // end of cli args parse + // path selection + useVideoSlot := args.VideoFlowId != "" && args.AudioFlowId == "" runtime.LockOSThread() if err := sdl.Load(); err != nil { @@ -297,12 +299,7 @@ func main() { fmt.Printf("sync: video %dx%d audio %dch batch=%d\n", syncSrc.Width(), syncSrc.Height(), aChans, audioBatch) case args.VideoFlowId != "": - videoSrc, err = source.Open(args.Domain, args.VideoFlowId) - if err != nil { - log.Fatalf("source: %v", err) - } - fmt.Printf("video: %dx%d stride=%d\n", videoSrc.Width(), videoSrc.Height(), videoSrc.Stride()) - + // VideoSlot owns opening and closing the video reader. default: audioSrc, err = source.OpenAudio(args.Domain, args.AudioFlowId) if err != nil { @@ -400,6 +397,39 @@ func main() { control := make(chan reconnectParams, 1) videoBridge := playback.NewVideoBridge() + videoWorker, err := playback.NewVideoWorker( + mxladapter.VideoFactory{}, + videoBridge, + retryPolicy, + mxladapter.ShouldRetry, + func(status playback.Status) { + if status.Err != nil { + log.Printf( + "video: state=%v attempt=%d failed=%d: %v", + status.State, + status.Attempt, + status.FailedAttempts, + status.Err, + ) + return + } + log.Printf( + "video: state=%v attempt=%d failed=%d", + status.State, + status.Attempt, + status.FailedAttempts, + ) + }, + ) + if err != nil { + panic(err) + } + videoSlot, err := playback.NewVideoSlot(videoWorker) + if err != nil { + panic(err) + } + videoCommands := make(chan playback.FeedConfig, 1) + reopen := func(params reconnectParams) error { // Close current sources if syncSrc != nil { @@ -452,6 +482,23 @@ func main() { } doReconnect := func() { + if useVideoSlot { + config := playback.FeedConfig{ + Domain: domainStr, + UUID: videoStr, + Active: videoStr != "", + } + // GUI is the only sender. Replace an older pending command + select { + case <-videoCommands: + default: + } + select { + case videoCommands <- config: + default: + } + return + } select { case <-control: default: @@ -459,7 +506,26 @@ func main() { control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr} } + playbackDone := make(chan struct{}) go func() { + defer close(playbackDone) + + if useVideoSlot { + err := videoSlot.Run( + ctx, + playback.FeedConfig{ + Domain: args.Domain, + UUID: args.VideoFlowId, + Active: true, + }, + videoCommands, + ) + if err != nil && !errors.Is(err, context.Canceled) { + log.Printf("video slot: %v", err) + } + return + } + // Audio-only mode: independent loop. if audioSrc != nil && syncSrc == nil && videoSrc == nil { for { @@ -809,4 +875,7 @@ func main() { time.Sleep(10 * time.Millisecond) } } + + cancel() + <-playbackDone }