diff --git a/cmd/mxl-pattern/main.go b/cmd/mxl-pattern/main.go index e2785d1..65f6a14 100644 --- a/cmd/mxl-pattern/main.go +++ b/cmd/mxl-pattern/main.go @@ -355,7 +355,7 @@ func buildVideoConfig(args appArgs) (*video.Config, error) { ) } - definition, err = flowdef.ParseV210Video(data) + definition, err = flowdef.ParseVideo(data) if err != nil { return nil, fmt.Errorf( "parse video flow definition %q: %w", diff --git a/internal/flowdef/flowdef.go b/internal/flowdef/flowdef.go index 0e0b108..a5cbdd3 100644 --- a/internal/flowdef/flowdef.go +++ b/internal/flowdef/flowdef.go @@ -12,6 +12,7 @@ const ( FormatVideo = "urn:x-nmos:format:video" FormatAudio = "urn:x-nmos:format:audio" MediaTypeV210 = "video/v210" + MediaTypeV210A = "video/v210a" MediaTypeFloat32 = "audio/float32" InterlaceProgressive = "progressive" @@ -75,7 +76,12 @@ type VideoComponent struct { BitDepth uint `json:"bit_depth"` } -func NewV210Video(id string, width, height uint, rate Rational) (Video, error) { +func newVideo( + id string, + width, height uint, + rate Rational, + mediaType string, +) (Video, error) { definition := Video{ Common: Common{ Description: "go-mxl-pattern-gen generated video", @@ -86,7 +92,7 @@ func NewV210Video(id string, width, height uint, rate Rational) (Video, error) { Format: FormatVideo, Label: "go-mxl-pattern-gen generated video", Parents: []string{}, - MediaType: MediaTypeV210, + MediaType: mediaType, }, GrainRate: rate, FrameWidth: width, @@ -105,7 +111,15 @@ func NewV210Video(id string, width, height uint, rate Rational) (Video, error) { return definition, nil } -func ParseV210Video(data []byte) (Video, error) { +func NewV210Video(id string, width, height uint, rate Rational) (Video, error) { + return newVideo(id, width, height, rate, MediaTypeV210) +} + +func NewV210AVideo(id string, width, height uint, rate Rational) (Video, error) { + return newVideo(id, width, height, rate, MediaTypeV210A) +} + +func ParseVideo(data []byte) (Video, error) { var definition Video if err := json.Unmarshal(data, &definition); err != nil { return Video{}, fmt.Errorf("decode video flow definition: %w", err) @@ -123,8 +137,10 @@ func (v Video) Validate() error { if v.Format != FormatVideo { return fmt.Errorf("format must be %q, got %q", FormatVideo, v.Format) } - if v.MediaType != MediaTypeV210 { - return fmt.Errorf("media_type must be %q, got %q", MediaTypeV210, v.MediaType) + switch v.MediaType { + case MediaTypeV210, MediaTypeV210A: + default: + return fmt.Errorf("media_type must be %q or %q, got %q", MediaTypeV210, MediaTypeV210A, v.MediaType) } if v.InterlaceMode != InterlaceProgressive { return fmt.Errorf("interlace_mode must be %q, got %q", InterlaceProgressive, v.InterlaceMode) diff --git a/internal/flowdef/flowdef_test.go b/internal/flowdef/flowdef_test.go index dbf8fe7..74f0ee4 100644 --- a/internal/flowdef/flowdef_test.go +++ b/internal/flowdef/flowdef_test.go @@ -28,6 +28,29 @@ func TestNewV210Video(t *testing.T) { } } +func TestNewV210AVideo(t *testing.T) { + definition, err := NewV210AVideo(testVideoID, 1280, 720, Rational{Numerator: 50, Denominator: 1}) + if err != nil { + t.Fatalf("NewV210AVideo: %v", err) + } + if definition.MediaType != MediaTypeV210A { + t.Fatalf("media type = %q, want %q", definition.MediaType, MediaTypeV210A) + } + wantComponents := []VideoComponent{ + {Name: "Y", Width: 1280, Height: 720, BitDepth: 10}, + {Name: "Cb", Width: 640, Height: 720, BitDepth: 10}, + {Name: "Cr", Width: 640, Height: 720, BitDepth: 10}, + } + if len(definition.Components) != len(wantComponents) { + t.Fatalf("component count = %d, want %d", len(definition.Components), len(wantComponents)) + } + for i, want := range wantComponents { + if definition.Components[i] != want { + t.Fatalf("component %d = %+v, want %+v", i, definition.Components[i], want) + } + } +} + func TestNewV210VideoRejectsOddWidth(t *testing.T) { _, err := NewV210Video(testVideoID, 1919, 1080, Rational{Numerator: 25, Denominator: 1}) if err == nil || !strings.Contains(err.Error(), "even") { @@ -35,7 +58,7 @@ func TestNewV210VideoRejectsOddWidth(t *testing.T) { } } -func TestParseV210Video(t *testing.T) { +func TestParseVideo(t *testing.T) { want, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 25, Denominator: 1}) if err != nil { t.Fatalf("NewV210Video: %v", err) @@ -45,28 +68,58 @@ func TestParseV210Video(t *testing.T) { t.Fatalf("json.Marshal: %v", err) } - got, err := ParseV210Video(data) + got, err := ParseVideo(data) if err != nil { - t.Fatalf("ParseV210Video: %v", err) + t.Fatalf("ParseVideo: %v", err) } if got.ID != want.ID || got.FrameWidth != want.FrameWidth || got.GrainRate != want.GrainRate { t.Fatalf("parsed definition = %+v, want %+v", got, want) } } -func TestParseV210VideoRejectsAudio(t *testing.T) { +func TestParseVideoAcceptsV210A(t *testing.T) { + want, err := NewV210AVideo(testVideoID, 1920, 1080, Rational{Numerator: 25, Denominator: 1}) + if err != nil { + t.Fatalf("NewV210AVideo: %v", err) + } + data, err := json.Marshal(want) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + + got, err := ParseVideo(data) + if err != nil { + t.Fatalf("ParseVideo: %v", err) + } + if got.MediaType != MediaTypeV210A { + t.Fatalf("media type = %q, want %q", got.MediaType, MediaTypeV210A) + } +} + +func TestParseVideoRejectsAudio(t *testing.T) { data := []byte(`{ "id":"5fbec3b1-1b0f-417d-9059-8b94a47197ed", "format":"urn:x-nmos:format:audio", "media_type":"audio/float32" }`) - _, err := ParseV210Video(data) + _, err := ParseVideo(data) if err == nil || !strings.Contains(err.Error(), "format must be") { t.Fatalf("error = %v, want video format error", err) } } +func TestVideoRejectsUnknownMediaType(t *testing.T) { + definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 25, Denominator: 1}) + if err != nil { + t.Fatalf("NewV210Video: %v", err) + } + definition.MediaType = "video/unknown" + if err := definition.Validate(); err == nil || !strings.Contains(err.Error(), "media_type") { + t.Fatalf("error = %v, want media_type error", err) + } +} + func TestNewFloat32Audio(t *testing.T) { definition, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 1}) if err != nil {