224 lines
6.4 KiB
Go
224 lines
6.4 KiB
Go
// 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%2 != 0 {
|
|
return fmt.Errorf("frame_width must be greater than zero and even for 4:2:2 video, 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
|
|
}
|