MSF / CMSF catalog validator

Paste a catalog JSON document (or upload a file) to validate it against the matching CUE schema. Validation dispatches on the catalog's version field.

Source on GitHub · Specifications: MSF · CMSF

Examples

Click Load into editor to drop a sample into the box above, then press Validate. Browse the full set in the testdata directory on GitHub.

Time-aligned audio/video (LOC) A minimal compliant MSF catalog with one video and one audio LOC track.
{
  "version": "draft-01",
  "generatedAt": 1780586526447,
  "tracks": [
    {
      "name": "video",
      "namespace": "msf/clear",
      "packaging": "loc",
      "isLive": true,
      "targetLatency": 1500,
      "role": "video",
      "renderGroup": 1,
      "codec": "av01.0.08M.10.0.110.09",
      "width": 1920,
      "height": 1080,
      "framerate": 30,
      "bitrate": 1500000
    },
    {
      "name": "audio",
      "namespace": "msf/clear",
      "packaging": "loc",
      "isLive": true,
      "targetLatency": 1500,
      "role": "audio",
      "renderGroup": 1,
      "codec": "opus",
      "samplerate": 48000,
      "channelConfig": "2",
      "bitrate": 32000
    }
  ]
}
Non-compliant example Shows how violations are reported (missing samplerate, invalid packaging).
{
  "version": "draft-01",
  "tracks": [
    {
      "name": "audio",
      "packaging": "loc",
      "isLive": true,
      "role": "audio",
      "codec": "opus",
      "channelConfig": "2",
      "bitrate": 32000
    },
    {
      "name": "video",
      "packaging": "mp4",
      "isLive": true,
      "role": "video",
      "codec": "avc1.640028",
      "bitrate": 1500000
    }
  ]
}

CUE schema

Show the draft-01 schema
// Package draft01 holds the CUE schema for MSF / CMSF catalogs that declare
// a "version" of "draft-01" (draft-ietf-moq-msf-01 and draft-ietf-moq-cmsf-01).
//
// CMSF is a superset of MSF: it adds the "cmaf" packaging value, the
// maxGrpSapStartingType / maxObjSapStartingType track fields, a root-level
// contentProtections array and the track-level contentProtectionRefIDs field.
// A single schema therefore covers both formats; whether a catalog is "MSF"
// or "CMSF" follows from the packaging values and DRM fields it actually uses.
//
// The schema also accepts the "locmaf" packaging value and the locmafVersion
// track field from [LOCMAF].
//
// References:
//   [MSF]    draft-ietf-moq-msf-01    - MOQT Streaming Format
//   [CMSF]   draft-ietf-moq-cmsf-01   - CMAF-based MOQT Streaming Format
//   [LOCMAF] draft-einarsson-moq-locmaf - Low Overhead CMAF for Media over QUIC
//
// Design notes
//   - Structs are *open* (`...`): the spec allows producers to add custom,
//     namespaced fields (e.g. "com.example-tier") and requires parsers to
//     ignore unknown fields. Typo detection for near-miss field names is done
//     in Go, not here, so that custom fields are never rejected.
//   - Conditional requirements (an audio track MUST carry samplerate, a media
//     track MUST carry codec/bitrate/isLive, ...) are driven by the hidden
//     `_pkg` and `_role` fields. These mirror `packaging`/`role` but fall back
//     to a sentinel when the source field is absent, so the `if` guards always
//     see a concrete value instead of failing as "non-concrete".
//   - Reference integrity is enforced here too: initRef must resolve to an
//     initDataList id and each contentProtectionRefIDs entry to a
//     contentProtections refID (see #refCheck).
//   - A few checks that CUE expresses poorly (mutually-exclusive fields, unique
//     track names, duplicate ids) are implemented as Go semantic checks instead.
package draft01

import "list"

// #Catalog is an independent (complete) catalog object. Delta updates are
// validated against #DeltaCatalog; the caller chooses based on the presence of
// the deltaUpdate field.
#Catalog: {
	// Section 5.1.1 - MSF version. Required.
	version!: string

	// Section 5.1.2 - wallclock generation time, milliseconds since the Unix
	// epoch. SHOULD NOT be present when the content is not live.
	generatedAt?: int & >=0

	// Section 5.1.3 - a catalog-level "broadcast complete" flag. It MUST NOT be
	// present when false, so the only legal value is true.
	isComplete?: true

	// Section 5.1.4 - the track array. Required for an independent catalog.
	// Each track is additionally constrained by #refCheck so that its
	// references resolve to entries that actually exist in this catalog.
	tracks!: [...(#Track & #refCheck)]

	// Section 5.1.5 - publish tracks (logs, metrics, QoE). Same shape as tracks.
	publishTracks?: [...(#Track & #refCheck)]

	// Section 5.1.7 - initialization data list. Per the spec it SHOULD appear
	// after tracks; ordering is checked in Go. Defaults to an empty list so the
	// reference checks below always have a concrete set to test against.
	initDataList: [...#InitData] | *[]

	// CMSF Section 4.1.1 - root-level DRM descriptions.
	contentProtections: [...#ContentProtection] | *[]

	... // allow custom root-level fields

	// The ids/refIDs declared in this catalog, used for reference resolution.
	let _initIDs = [for d in initDataList {d.id}]
	let _cpIDs = [for c in contentProtections {c.refID}]

	// Reference domains. A sentinel that cannot appear in real data keeps each
	// domain non-empty when nothing is declared, so an unresolved reference
	// still fails *at the reference field* instead of as a bare empty-set error.
	let _unresolved = "<unresolved-reference>"
	let _initDomain = list.Concat([_initIDs, [if len(_initIDs) == 0 {_unresolved}]])
	let _cpDomain = list.Concat([_cpIDs, [if len(_cpIDs) == 0 {_unresolved}]])

	// #refCheck constrains a track's reference fields against the ids declared
	// above. It is unified onto every track:
	//   - initRef (Section 5.2.13) must equal one of the initDataList ids.
	//   - each contentProtectionRefIDs entry (CMSF Section 4.1.2) must equal one
	//     of the contentProtections refIDs.
	#refCheck: {
		initRef?: or(_initDomain)
		contentProtectionRefIDs?: [...or(_cpDomain)]
		...
	}
}

// #DeltaCatalog is a delta (partial) update. Per Section 5.3 it MUST carry a
// deltaUpdate array with at least one operation and MUST NOT carry tracks or
// version.
// A delta update MUST carry a deltaUpdate array with at least one operation
// and MUST NOT carry tracks or version (Section 5.3). The "MUST NOT" half is a
// Go semantic check so the report can explain it clearly.
#DeltaCatalog: {
	generatedAt?: int & >=0
	deltaUpdate!: [#DeltaOp, ...#DeltaOp]
	...
}

#DeltaOp: {
	op!: "add" | "remove" | "clone"

	if op == "add" {
		tracks!: [...#Track]
	}
	if op == "remove" {
		// A remove entry carries only name (and optionally namespace).
		tracks!: [...#RemoveTrack]
	}
	if op == "clone" {
		tracks!: [...#CloneTrack]
	}
	...
}

#RemoveTrack: close({
	name!:      string
	namespace?: string
})

// A cloned track inherits everything from its parent, so the only hard
// requirements are parentName and the new name.
#CloneTrack: #TrackBase & {
	parentName!:      string
	parentNamespace?: string
	name!:            string
}

// -----------------------------------------------------------------------------
// Tracks
// -----------------------------------------------------------------------------

// Section 5.2.4 - allowed packaging values. CMSF adds "cmaf"; "locmaf" is the
// Low Overhead CMAF packaging defined in [LOCMAF] (draft-einarsson-moq-locmaf,
// "Low Overhead CMAF for Media over QUIC").
#Packaging: "loc" | "cmaf" | "locmaf" | "mediatimeline" | "eventtimeline" | "moqlog" | "moqmetrics"

// #Track is a declared track in an independent catalog or an "add" delta op.
// packaging is required here (Section 5.2.4).
#Track: #TrackBase & {
	packaging!: #Packaging
}

// #TrackBase carries every track field and all conditional rules, but leaves
// packaging optional so it can be reused for cloned tracks (which inherit it).
#TrackBase: {
	// Section 5.2.3 - track name. Required and, per namespace, unique
	// (uniqueness is enforced in Go).
	name!: string

	// Section 5.2.2 - namespace. Optional; inherits the catalog namespace.
	namespace?: string

	// Section 5.2.4 - packaging. Optional at this level; #Track makes it
	// required for declared tracks.
	packaging?: #Packaging

	// LOCMAF wire-format version, a track-level field defined by [LOCMAF]
	// (draft-einarsson-moq-locmaf). Required for "locmaf" packaging and, for
	// now, fixed to "0.2" (the only version this validator knows); see the
	// conditional rule below.
	locmafVersion?: string

	// Section 5.2.6 - track role. Optional; known roles are documented below
	// but custom roles are permitted, so the type stays an open string.
	//   audiodescription | video | audio | mediatimeline | eventtimeline |
	//   caption | subtitle | signlanguage | log | metrics | <custom>
	role?: string

	// Section 5.2.5 - event timeline type. Constrained below: required for
	// eventtimeline packaging and forbidden otherwise.
	eventType?: string

	// Section 5.2.7 - whether new objects will be added to this track.
	isLive?: bool

	// Section 5.2.8 - target latency in milliseconds. Mutually exclusive with
	// buffers (checked in Go).
	targetLatency?: int & >=0

	// Section 5.2.9 - target buffer object.
	buffers?: #Buffers

	// Section 5.2.10 - human-readable label.
	label?: string

	// Section 5.2.11 / 5.2.12 - render and alternate group identifiers.
	renderGroup?: int & >=0
	altGroup?:    int & >=0

	// Section 5.2.13 - reference into initDataList (resolved by #refCheck).
	initRef?: string

	// Section 5.2.14 - track names this track depends on.
	depends?: [...string]

	// Section 5.2.16 / 5.2.17 - SVC layer identifiers.
	temporalId?: int & >=0
	spatialId?:  int & >=0

	// Section 5.2.18 - codec string. Required for media tracks (below).
	codec?: string

	// Section 5.2.19 - mime type. The field table spells it "mimeType"; the
	// lower-case "mimetype" used by some early examples is not accepted (it is
	// flagged as a probable typo by the Go field lint).
	mimeType?: string

	// Section 5.2.20 - frame rate (may be fractional, e.g. 29.97).
	framerate?: number & >0

	// Section 5.2.21 - timescale.
	timescale?: int & >0

	// Section 5.2.22 / 5.2.23 - bitrates in bits per second.
	bitrate?:    int & >0
	avgBitrate?: int & >0

	// Section 5.2.24 / 5.2.25 - durations in milliseconds.
	maxGopDuration?:   int & >=0
	maxGroupDuration?: int & >=0

	// Section 5.2.26 / 5.2.27 - encoded dimensions.
	width?:  int & >0
	height?: int & >0

	// Section 5.2.28 / 5.2.29 - audio properties. Required for audio (below).
	samplerate?:    int & >0
	channelConfig?: string

	// Section 5.2.30 / 5.2.31 - intended display dimensions.
	displayWidth?:  int & >0
	displayHeight?: int & >0

	// Section 5.2.32 - BCP-47 language tag.
	lang?: string

	// Section 5.2.35 - track duration in milliseconds (VOD only).
	trackDuration?: int & >=0

	// Section 5.2.36 / 5.2.37 - publish-track connection details.
	connectionURI?: string
	token?:         string

	// Section 5.2.38-5.2.41 - MoQ Secure Objects encryption.
	encryptionScheme?: string
	cipherSuite?:      string
	keyId?:            string
	trackBaseKey?:     string

	// Section 5.2.42 - authorization requirement (scheme -> config object).
	authInfo?: {...}

	// Section 5.2.44 - accessibility descriptors.
	accessibility?: [...#Accessibility]

	// CMSF Section 3.5.2 - max SAP starting types (0..3).
	maxGrpSapStartingType?: int & >=0 & <=3
	maxObjSapStartingType?: int & >=0 & <=3

	// CMSF Section 4.1.2 - references into contentProtections (resolved by #refCheck).
	contentProtectionRefIDs?: [...string]

	... // allow custom, namespaced track fields

	// --- hidden discriminators -------------------------------------------------
	// Mirror packaging/role but fall back to a sentinel when the source field
	// is absent, so the `if` guards below always evaluate against a concrete
	// value. The default marker (*) is on the real field: when it carries a
	// concrete value that value wins; only when it is absent does the sentinel
	// apply.
	_pkg:  *packaging | "__unset__"
	_role: *role | "__unset__"

	// --- conditional requirements ----------------------------------------------

	// LOC/CMAF/LOCMAF media tracks MUST carry isLive and codec. bitrate is
	// required only for audio and video tracks (Section 5.2.22), which is
	// enforced by the role-based rules below, so it is not required here (a
	// subtitle/caption track legitimately has no bitrate).
	// (Sections 5.2.7, 5.2.18.)
	if _pkg == "loc" || _pkg == "cmaf" || _pkg == "locmaf" {
		isLive!: bool
		codec!:  string
	}

	// LOCMAF tracks MUST carry locmafVersion; only "0.2" is supported for now.
	// ([LOCMAF] draft-einarsson-moq-locmaf.)
	if _pkg == "locmaf" {
		locmafVersion!: "0.2"
	}

	// Audio tracks MUST carry codec, samplerate, channelConfig and bitrate.
	// (Sections 5.2.18, 5.2.28, 5.2.29, 5.2.22.)
	if _role == "audio" {
		codec!:         string
		samplerate!:    int & >0
		channelConfig!: string
		bitrate!:       int & >0
	}

	// Video tracks MUST carry codec and bitrate. (Sections 5.2.18, 5.2.22.)
	if _role == "video" {
		codec!:   string
		bitrate!: int & >0
	}

	// eventType is required for eventtimeline packaging (Section 5.2.5). The
	// converse rule ("eventType MUST NOT appear on other packagings") is
	// enforced as a Go semantic check so the report can carry a clear message.
	if _pkg == "eventtimeline" {
		eventType!: string
	}
}

#Buffers: {
	target?: int & >=0
	min?:    int & >=0
	max?:    int & >=0
	... // unknown keys MUST be ignored
}

#Accessibility: {
	scheme!: string
	value!:  string
	...
}

// -----------------------------------------------------------------------------
// Initialization data
// -----------------------------------------------------------------------------

#InitData: {
	// Section 5.1.7 - unique reference id, type and payload.
	id!:   string
	type!: "inline"
	data!: string
	...
}

// -----------------------------------------------------------------------------
// Content protection (CMSF Section 4.1)
// -----------------------------------------------------------------------------

#UUID: =~"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"

#ContentProtection: {
	refID!: string
	defaultKID!: [...#UUID]
	scheme!:    "cenc" | "cbcs"
	drmSystem!: #DRMSystem
	...
}

#DRMSystem: {
	systemID!:   #UUID
	laURL?:      #URLObject
	certURL?:    #URLObject
	authURL?:    #URLObject
	pssh?:       string
	robustness?: string
	...
}

#URLObject: {
	url!:  string
	type?: string
	...
}