From cb5321cd8b16d6e4c1a4ae882752bc218827c563 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Tue, 1 Sep 2026 22:16:36 +0300 Subject: [PATCH] CLI playlist support --- cmd/mxl-player/main.go | 44 +++++++++++++++++++ cmd/mxl-player/playlist_runtime.go | 10 +++++ cmd/mxl-player/playlist_runtime_test.go | 56 +++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index 8f78947..c064608 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -53,6 +53,7 @@ type appArgs struct { ListGPU bool SyncRequested bool MaxAttempts int + PlaylistPath string } func printCliHelp(fs *pflag.FlagSet) { @@ -129,6 +130,12 @@ func main() { 0, "Maximum connection attempts per playback lifecycle; 0 retries indefinitely", ) + flagSet.StringVar( + &args.PlaylistPath, + "playlist", + "", + "Load playlist from a JSON file", + ) flagSet.BoolVarP(&args.IsFullscreen, "fullscreen", "f", false, "Run app in fullscreen mode") flagSet.Uint32VarP(&args.GpuId, "gpu-id", "g", 0, "GPU id [TODO]") flagSet.Uint32VarP(&args.PlaybackId, "playback-id", "p", 0, "Playback audio device id") @@ -159,6 +166,16 @@ func main() { fmt.Fprintln(os.Stderr, "invalid retry configuration:", err) os.Exit(2) } + configuredPlaylist := playback.Playlist{} + hasPlaylist := args.PlaylistPath != "" + if hasPlaylist { + playlist, err := loadPlaylistFile(args.PlaylistPath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + configuredPlaylist = playlist + } if args.VideoDomain == "" { args.VideoDomain = args.Domain } @@ -415,6 +432,28 @@ func main() { ) }() + var playlistRuntime *playerPlaylist + var playlistDone chan error + if hasPlaylist { + playlistRuntime, err = newPlayerPlaylist( + configuredPlaylist, + retryPolicy, + player, + ) + if err != nil { + panic(err) + } + playlistDone = make(chan error, 1) + go func() { + playlistDone <- playlistRuntime.Run(ctx) + }() + + if shouldAutoStartPlaylist(args, configuredPlaylist) && + !playlistRuntime.Select(0) { + log.Print("playlist command queue is full") + } + } + running := true resized := false fullscreen := args.IsFullscreen @@ -774,6 +813,11 @@ func main() { if err := <-playbackDone; err != nil && !errors.Is(err, context.Canceled) { log.Printf("playback controller: %v", err) } + if playlistDone != nil { + if err := <-playlistDone; err != nil && !errors.Is(err, context.Canceled) { + log.Printf("playlist runtime: %v", err) + } + } if err := player.Close(); err != nil { log.Printf("close playback: %v", err) } diff --git a/cmd/mxl-player/playlist_runtime.go b/cmd/mxl-player/playlist_runtime.go index 3d88c71..52208c5 100644 --- a/cmd/mxl-player/playlist_runtime.go +++ b/cmd/mxl-player/playlist_runtime.go @@ -125,3 +125,13 @@ func playlistRuntimeError(err error) error { } return err } + +func shouldAutoStartPlaylist( + args appArgs, + playlist playback.Playlist, +) bool { + return args.PlaylistPath != "" && + args.VideoFlowId == "" && + args.AudioFlowId == "" && + len(playlist.Entries) > 0 +} diff --git a/cmd/mxl-player/playlist_runtime_test.go b/cmd/mxl-player/playlist_runtime_test.go index 74e013a..5c93180 100644 --- a/cmd/mxl-player/playlist_runtime_test.go +++ b/cmd/mxl-player/playlist_runtime_test.go @@ -242,6 +242,62 @@ func TestPlayerPlaylistTimedEntryAdvances(t *testing.T) { } } +func TestShouldAutoStartPlaylist(t *testing.T) { + playlist := playback.Playlist{Entries: []playback.PlaylistEntry{ + {Video: playback.PlaylistFeed{Domain: "domain", UUID: "video"}}, + }} + tests := []struct { + name string + args appArgs + playlist playback.Playlist + want bool + }{ + { + name: "playlist only", + args: appArgs{PlaylistPath: "playlist.json"}, + playlist: playlist, + want: true, + }, + { + name: "direct video", + args: appArgs{PlaylistPath: "playlist.json", VideoFlowId: "video"}, + playlist: playlist, + }, + { + name: "direct audio", + args: appArgs{PlaylistPath: "playlist.json", AudioFlowId: "audio"}, + playlist: playlist, + }, + { + name: "both direct feeds", + args: appArgs{ + PlaylistPath: "playlist.json", + VideoFlowId: "video", + AudioFlowId: "audio", + }, + playlist: playlist, + }, + { + name: "empty playlist", + args: appArgs{PlaylistPath: "playlist.json"}, + playlist: playback.Playlist{}, + }, + { + name: "no playlist flag", + args: appArgs{}, + playlist: playlist, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := shouldAutoStartPlaylist(test.args, test.playlist); got != test.want { + t.Fatalf("shouldAutoStartPlaylist() = %v, want %v", got, test.want) + } + }) + } +} + func newPlaylistTestPlayer(t *testing.T) *playerPlayback { t.Helper() return &playerPlayback{