// Package flowdef models and validates MXL flow-definition JSON. package flowdef import ( "encoding/json" "fmt" "github.com/google/uuid" ) const ( FormatVideo = "urn:x-nmos:format:video" FormatAudio = "urn:x-nmos:format:audio" MediaTypeV210 = "video/v210" MediaTypeFloat32 = "audio/float32" InterlaceProgressive = "progressive" ColorSpaceBT709 = "BT709" ) type Common struct { Description string `json:"description"` ID string `json:"id"` Tags map[string][]string `json:"tags"` Format string `json:"format"` Label string `json:"label"` Parents []string `json:"parents"` MediaType string `json:"media_type"` } type Rational struct { Numerator uint `json:"numerator"` Denominator uint `json:"denominator"` } func (r *Rational) UnmarshalJSON(data []byte) error { var value struct { Numerator uint `json:"numerator"` Denominator *uint `json:"denominator"` } if err := json.Unmarshal(data, &value); err != nil { return err } r.Numerator = value.Numerator r.Denominator = 1 if value.Denominator != nil { r.Denominator = *value.Denominator } return nil } type Video struct { Common GrainRate Rational `json:"grain_rate"` FrameWidth uint `json:"frame_width"` FrameHeight uint `json:"frame_height"` InterlaceMode string `json:"interlace_mode"` ColorSpace string `json:"colorspace"` Components []VideoComponent `json:"components"` } type Audio struct { Common SampleRate Rational `json:"sample_rate"` ChannelCount uint `json:"channel_count"` BitDepth uint `json:"bit_depth"` } type VideoComponent struct { Name string `json:"name"` Width uint `json:"width"` Height uint `json:"height"` BitDepth uint `json:"bit_depth"` } func NewV210Video(id string, width, height uint, rate Rational) (Video, error) { definition := Video{ Common: Common{ Description: "go-mxl-pattern-gen generated video", ID: id, Tags: map[string][]string{ "urn:x-nmos:tag:grouphint/v1.0": {"go-mxl-pattern-gen:Video"}, }, Format: FormatVideo, Label: "go-mxl-pattern-gen generated video", Parents: []string{}, MediaType: MediaTypeV210, }, GrainRate: rate, FrameWidth: width, FrameHeight: height, InterlaceMode: InterlaceProgressive, ColorSpace: ColorSpaceBT709, Components: []VideoComponent{ {Name: "Y", Width: width, Height: height, BitDepth: 10}, {Name: "Cb", Width: width / 2, Height: height, BitDepth: 10}, {Name: "Cr", Width: width / 2, Height: height, BitDepth: 10}, }, } if err := definition.Validate(); err != nil { return Video{}, err } return definition, nil } func ParseV210Video(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) } if err := definition.Validate(); err != nil { return Video{}, fmt.Errorf("invalid video flow definition: %w", err) } return definition, nil } func (v Video) Validate() error { if err := uuid.Validate(v.ID); err != nil { return fmt.Errorf("invalid id %q: %w", v.ID, err) } 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) } if v.InterlaceMode != InterlaceProgressive { return fmt.Errorf("interlace_mode must be %q, got %q", InterlaceProgressive, v.InterlaceMode) } if v.ColorSpace != ColorSpaceBT709 { return fmt.Errorf("colorspace must be %q, got %q", ColorSpaceBT709, v.ColorSpace) } if v.FrameWidth == 0 || v.FrameWidth%6 != 0 { return fmt.Errorf("frame_width must be greater than zero and divisible by 6, got %d", v.FrameWidth) } if v.FrameHeight == 0 { return fmt.Errorf("frame_height must be greater than zero") } if v.GrainRate.Numerator == 0 || v.GrainRate.Denominator == 0 { return fmt.Errorf("grain_rate numerator and denominator must be greater than zero, got %d/%d", v.GrainRate.Numerator, v.GrainRate.Denominator) } want := []VideoComponent{ {Name: "Y", Width: v.FrameWidth, Height: v.FrameHeight, BitDepth: 10}, {Name: "Cb", Width: v.FrameWidth / 2, Height: v.FrameHeight, BitDepth: 10}, {Name: "Cr", Width: v.FrameWidth / 2, Height: v.FrameHeight, BitDepth: 10}, } if len(v.Components) != len(want) { return fmt.Errorf("v210 requires %d components, got %d", len(want), len(v.Components)) } for i := range want { if v.Components[i] != want[i] { return fmt.Errorf("component %d must be %+v, got %+v", i, want[i], v.Components[i]) } } return nil } func NewFloat32Audio( id string, channels uint, rate Rational, ) (Audio, error) { definition := Audio{ Common: Common{ Description: "go-mxl-pattern-gen generated audio", ID: id, Tags: map[string][]string{ "urn:x-nmos:tag:grouphint/v1.0": { "go-mxl-pattern-gen:Audio", }, }, Format: FormatAudio, Label: "go-mxl-pattern-gen generated audio", Parents: []string{}, MediaType: MediaTypeFloat32, }, SampleRate: rate, ChannelCount: channels, BitDepth: 32, } if err := definition.Validate(); err != nil { return Audio{}, err } return definition, nil } func (a Audio) Validate() error { if err := uuid.Validate(a.ID); err != nil { return fmt.Errorf("invalid id %q: %w", a.ID, err) } if a.Format != FormatAudio { return fmt.Errorf("format must be %q, got %q", FormatAudio, a.Format) } if a.MediaType != MediaTypeFloat32 { return fmt.Errorf("media_type must be %q, got %q", MediaTypeFloat32, a.MediaType) } if a.ChannelCount == 0 { return fmt.Errorf("channel_count must be greater than zero") } if a.SampleRate.Numerator == 0 || a.SampleRate.Denominator == 0 { return fmt.Errorf("sample_rate numerator and denominator must be greater than zero, got %d/%d", a.SampleRate.Numerator, a.SampleRate.Denominator) } if a.BitDepth != 32 { return fmt.Errorf("bit_depth must be 32, got %d", a.BitDepth) } return nil } func ParseFloat32Audio(data []byte) (Audio, error) { var definition Audio if err := json.Unmarshal(data, &definition); err != nil { return Audio{}, fmt.Errorf("decode audio flow definition: %w", err) } if err := definition.Validate(); err != nil { return Audio{}, fmt.Errorf("invalid audio flow definition: %w", err) } return definition, nil }