From fa787b5ef1157e1fcac69bf16763ebd5f515b38b Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Thu, 17 Sep 2026 23:16:59 +0300 Subject: [PATCH] video backend CLI flag --- cmd/mxl-pattern/main.go | 37 +++++++++++++++++--- cmd/mxl-pattern/main_test.go | 65 ++++++++++++++++++++++++++++++++++++ internal/video/config.go | 9 +++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/cmd/mxl-pattern/main.go b/cmd/mxl-pattern/main.go index e073784..0f9d8cb 100644 --- a/cmd/mxl-pattern/main.go +++ b/cmd/mxl-pattern/main.go @@ -43,11 +43,12 @@ type appArgs struct { overlayY int overlayPos string - videoWidth uint - videoHeight uint - videoFPS string - videoUUID string - noVideo bool + videoWidth uint + videoHeight uint + videoFPS string + videoUUID string + noVideo bool + videoBackend string audioChannels uint8 audioSamplingFreq string @@ -136,6 +137,9 @@ func validateVideoArgs(args *appArgs) error { if args.noVideo { return nil } + if _, err := parseVideoBackend(args.videoBackend); err != nil { + return err + } if !video.HasPattern(args.pattern) { return fmt.Errorf("unknown video pattern %q (use --list-patterns to see available patterns)", args.pattern) } @@ -284,6 +288,7 @@ func addFlags(fs *pflag.FlagSet, args *appArgs) { fs.StringVar(&args.videoFPS, "fps", "25", "Video pattern FPS") fs.StringVar(&args.videoUUID, "video-id", "", "Video UUID. Will be created, if not provided") fs.BoolVar(&args.noVideo, "no-video", false, "Disable video generation; audio must be enabled") + fs.StringVar(&args.videoBackend, "backend", string(video.BackendAuto), "Video generator backend: auto, gpu or cpu") // Audio pattern flags fs.Uint8VarP(&args.audioChannels, "channel", "c", 0, "Amount of audio channels. Each channel: num * 1kHz") fs.StringVarP(&args.audioSamplingFreq, "freq", "f", "48", "Sampling frequency of test audio feed in kHz") @@ -319,6 +324,22 @@ func parseArgs(argv []string, stdout, stderr io.Writer) (parseResult, error) { return parseResult{args: args, shouldRun: true}, nil } +func parseVideoBackend(value string) (video.Backend, error) { + if value == "" { + return video.BackendAuto, nil + } + backend := video.Backend(value) + switch backend { + case video.BackendAuto, video.BackendGPU, video.BackendCPU: + return backend, nil + default: + return "", fmt.Errorf( + "unsupported video backend %q (supported: auto, gpu, cpu)", + value, + ) + } +} + func buildVideoConfig(args appArgs) (*video.Config, error) { if args.noVideo { return nil, nil @@ -370,6 +391,11 @@ func buildVideoConfig(args appArgs) (*video.Config, error) { return nil, fmt.Errorf("unknown video pattern %q", args.pattern) } + backend, err := parseVideoBackend(args.videoBackend) + if err != nil { + return nil, err + } + return &video.Config{ Definition: definition, Pattern: args.pattern, @@ -379,6 +405,7 @@ func buildVideoConfig(args appArgs) (*video.Config, error) { Y: args.overlayY, Position: args.overlayPos, }, + Backend: backend, }, nil } diff --git a/cmd/mxl-pattern/main_test.go b/cmd/mxl-pattern/main_test.go index 16686b5..60b7453 100644 --- a/cmd/mxl-pattern/main_test.go +++ b/cmd/mxl-pattern/main_test.go @@ -9,6 +9,7 @@ import ( "mxl-pattern-generator/internal/audio" "mxl-pattern-generator/internal/flowdef" + "mxl-pattern-generator/internal/video" ) func TestParseArgsHelpStopsBeforeValidation(t *testing.T) { @@ -112,6 +113,46 @@ func TestValidateVideoArgsSkipsDisabledVideo(t *testing.T) { } } +func TestParseVideoBackend(t *testing.T) { + tests := []struct { + name string + value string + want video.Backend + wantErr bool + }{ + {name: "zero value defaults to auto", want: video.BackendAuto}, + {name: "auto", value: "auto", want: video.BackendAuto}, + {name: "gpu", value: "gpu", want: video.BackendGPU}, + {name: "cpu", value: "cpu", want: video.BackendCPU}, + {name: "unknown", value: "other", wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := parseVideoBackend(tc.value) + if tc.wantErr { + if err == nil || !strings.Contains(err.Error(), "unsupported video backend") { + t.Fatalf("error = %v, want unsupported backend error", err) + } + return + } + if err != nil { + t.Fatalf("parseVideoBackend: %v", err) + } + if got != tc.want { + t.Fatalf("backend = %q, want %q", got, tc.want) + } + }) + } +} + +func TestValidateVideoArgsRejectsUnknownBackend(t *testing.T) { + args := appArgs{videoBackend: "other"} + if err := validateVideoArgs(&args); err == nil || !strings.Contains(err.Error(), "unsupported video backend") { + t.Fatalf("error = %v, want unsupported backend error", err) + } +} + func TestValidateMediaSelection(t *testing.T) { tests := []struct { name string @@ -187,6 +228,30 @@ func TestBuildVideoConfigFromArgs(t *testing.T) { if cfg.Rate().Num != 30000 || cfg.Rate().Den != 1001 { t.Fatalf("rate = %d/%d, want 30000/1001", cfg.Rate().Num, cfg.Rate().Den) } + if cfg.Backend != video.BackendAuto { + t.Fatalf("backend = %q, want %q", cfg.Backend, video.BackendAuto) + } +} + +func TestBuildVideoConfigBackends(t *testing.T) { + for _, backend := range []video.Backend{video.BackendGPU, video.BackendCPU} { + t.Run(string(backend), func(t *testing.T) { + cfg, err := buildVideoConfig(appArgs{ + videoUUID: "5fbec3b1-1b0f-417d-9059-8b94a47197ed", + videoWidth: 1920, + videoHeight: 1080, + videoFPS: "25", + pattern: "ebu75", + videoBackend: string(backend), + }) + if err != nil { + t.Fatalf("buildVideoConfig: %v", err) + } + if cfg.Backend != backend { + t.Fatalf("backend = %q, want %q", cfg.Backend, backend) + } + }) + } } func TestBuildVideoConfigDisabled(t *testing.T) { diff --git a/internal/video/config.go b/internal/video/config.go index 44e8900..d044d20 100644 --- a/internal/video/config.go +++ b/internal/video/config.go @@ -6,10 +6,19 @@ import ( "github.com/qvest-digital/go-mxl/mxl" ) +type Backend string + +const ( + BackendAuto Backend = "auto" + BackendGPU Backend = "gpu" + BackendCPU Backend = "cpu" +) + type Config struct { Definition flowdef.Video Pattern string Overlay OverlayConfig + Backend Backend } type OverlayConfig struct {