@norskvideo/moq-msf 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @moq/msf
2
+
3
+ Zod schemas and helpers for the [MSF (MOQT Streaming Format)](https://datatracker.ietf.org/doc/draft-ietf-moq-streaming-format/) catalog.
4
+
5
+ This package provides types for decoding the MSF catalog track delivered over `moq-lite`. It is consumed by `@moq/watch` when the player is configured with `catalogFormat: "msf"`.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm add @moq/msf
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import * as Msf from "@moq/msf";
17
+
18
+ // Fetch and decode a catalog from a moq-lite track
19
+ const catalog = await Msf.fetch(track);
20
+ if (catalog) {
21
+ for (const t of catalog.tracks) {
22
+ console.log(t.name, t.role, t.codec);
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## License
28
+
29
+ MIT OR Apache-2.0
package/catalog.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type * as Moq from "@norskvideo/moq-net";
2
+ import * as z from "zod/mini";
3
+ /** Zod schema for a track's wire packaging. Accepts known values or any future string. */
4
+ export declare const PackagingSchema: z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
5
+ cmaf: "cmaf";
6
+ eventtimeline: "eventtimeline";
7
+ legacy: "legacy";
8
+ loc: "loc";
9
+ mediatimeline: "mediatimeline";
10
+ }>, z.ZodMiniString<string>]>;
11
+ /** How a track's frames are packaged on the wire (e.g. "loc" or "cmaf"). */
12
+ export type Packaging = z.infer<typeof PackagingSchema>;
13
+ /** Zod schema for a track's role. Accepts known values or any future string. */
14
+ export declare const RoleSchema: z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
15
+ audio: "audio";
16
+ audiodescription: "audiodescription";
17
+ caption: "caption";
18
+ signlanguage: "signlanguage";
19
+ subtitle: "subtitle";
20
+ video: "video";
21
+ }>, z.ZodMiniString<string>]>;
22
+ /** The semantic role a track plays in the presentation (e.g. "video", "audio", "caption"). */
23
+ export type Role = z.infer<typeof RoleSchema>;
24
+ /** Zod schema describing a single track entry in an MSF catalog. */
25
+ export declare const TrackSchema: z.ZodMiniObject<{
26
+ name: z.ZodMiniString<string>;
27
+ packaging: z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
28
+ cmaf: "cmaf";
29
+ eventtimeline: "eventtimeline";
30
+ legacy: "legacy";
31
+ loc: "loc";
32
+ mediatimeline: "mediatimeline";
33
+ }>, z.ZodMiniString<string>]>;
34
+ isLive: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
35
+ role: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
36
+ audio: "audio";
37
+ audiodescription: "audiodescription";
38
+ caption: "caption";
39
+ signlanguage: "signlanguage";
40
+ subtitle: "subtitle";
41
+ video: "video";
42
+ }>, z.ZodMiniString<string>]>>;
43
+ codec: z.ZodMiniOptional<z.ZodMiniString<string>>;
44
+ width: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
45
+ height: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
46
+ framerate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
47
+ samplerate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
48
+ channelConfig: z.ZodMiniOptional<z.ZodMiniString<string>>;
49
+ bitrate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
50
+ initData: z.ZodMiniOptional<z.ZodMiniString<string>>;
51
+ renderGroup: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
52
+ altGroup: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
53
+ maxGrpSapStartingType: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
54
+ maxObjSapStartingType: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
55
+ jitter: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
56
+ }, z.core.$strip>;
57
+ /** A single track in an MSF catalog, including its codec and media properties. */
58
+ export type Track = z.infer<typeof TrackSchema>;
59
+ /** Zod schema for the top-level MSF catalog: a version-agnostic snapshot of tracks. */
60
+ export declare const CatalogSchema: z.ZodMiniObject<{
61
+ tracks: z.ZodMiniArray<z.ZodMiniObject<{
62
+ name: z.ZodMiniString<string>;
63
+ packaging: z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
64
+ cmaf: "cmaf";
65
+ eventtimeline: "eventtimeline";
66
+ legacy: "legacy";
67
+ loc: "loc";
68
+ mediatimeline: "mediatimeline";
69
+ }>, z.ZodMiniString<string>]>;
70
+ isLive: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
71
+ role: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniEnum<{
72
+ audio: "audio";
73
+ audiodescription: "audiodescription";
74
+ caption: "caption";
75
+ signlanguage: "signlanguage";
76
+ subtitle: "subtitle";
77
+ video: "video";
78
+ }>, z.ZodMiniString<string>]>>;
79
+ codec: z.ZodMiniOptional<z.ZodMiniString<string>>;
80
+ width: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
81
+ height: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
82
+ framerate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
83
+ samplerate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
84
+ channelConfig: z.ZodMiniOptional<z.ZodMiniString<string>>;
85
+ bitrate: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
86
+ initData: z.ZodMiniOptional<z.ZodMiniString<string>>;
87
+ renderGroup: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
88
+ altGroup: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
89
+ maxGrpSapStartingType: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
90
+ maxObjSapStartingType: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
91
+ jitter: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
92
+ }, z.core.$strip>>;
93
+ }, z.core.$strip>;
94
+ /** The MSF catalog: a snapshot of the available tracks. */
95
+ export type Catalog = z.infer<typeof CatalogSchema>;
96
+ /** The newest MSF draft version string this package emits on the wire. */
97
+ export declare const VERSION = "draft-01";
98
+ /** Serialize a catalog to its JSON wire representation (draft-01). */
99
+ export declare function encode(catalog: Catalog): Uint8Array;
100
+ /** Parse and validate a catalog from its JSON wire representation. Throws if invalid. */
101
+ export declare function decode(raw: Uint8Array): Catalog;
102
+ /** Read and decode the next catalog frame from a track, or undefined if the track ended. */
103
+ export declare function fetch(track: Moq.Track): Promise<Catalog | undefined>;
104
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,0FAA0F;AAC1F,eAAO,MAAM,eAAe;;;;;;6BAG1B,CAAC;AAEH,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,gFAAgF;AAChF,eAAO,MAAM,UAAU;;;;;;;6BAGrB,CAAC;AAEH,8FAA8F;AAC9F,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA+B9C,oEAAoE;AACpE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAuB,CAAC;AAEhD,kFAAkF;AAClF,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,uFAAuF;AACvF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAExB,CAAC;AAEH,2DAA2D;AAC3D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,0EAA0E;AAC1E,eAAO,MAAM,OAAO,aAAa,CAAC;AA0BlC,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAwBnD;AAED,yFAAyF;AACzF,wBAAgB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CA2B/C;AAED,4FAA4F;AAC5F,wBAAsB,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAI1E"}
package/catalog.js ADDED
@@ -0,0 +1,125 @@
1
+ /* @ts-self-types="./catalog.d.ts" */
2
+ import * as z from "zod/mini";
3
+ /** Zod schema for a track's wire packaging. Accepts known values or any future string. */
4
+ export const PackagingSchema = z.union([
5
+ z.enum(["loc", "cmaf", "legacy", "mediatimeline", "eventtimeline"]),
6
+ z.string(),
7
+ ]);
8
+ /** Zod schema for a track's role. Accepts known values or any future string. */
9
+ export const RoleSchema = z.union([
10
+ z.enum(["video", "audio", "audiodescription", "caption", "subtitle", "signlanguage"]),
11
+ z.string(),
12
+ ]);
13
+ // Shared track fields. This is the version-agnostic shape callers see: init data
14
+ // is exposed inline via `initData`, regardless of how it was carried on the wire.
15
+ const trackShape = {
16
+ name: z.string(),
17
+ packaging: PackagingSchema,
18
+ // draft-00 marks isLive required but omits it on mediatimeline/eventtimeline
19
+ // tracks, so accept its absence rather than reject the whole catalog.
20
+ isLive: z.optional(z.boolean()),
21
+ role: z.optional(RoleSchema),
22
+ codec: z.optional(z.string()),
23
+ width: z.optional(z.number()),
24
+ height: z.optional(z.number()),
25
+ framerate: z.optional(z.number()),
26
+ samplerate: z.optional(z.number()),
27
+ channelConfig: z.optional(z.string()),
28
+ bitrate: z.optional(z.number()),
29
+ /** Resolved base64 initialization data (draft-01's initRef indirection is resolved away). */
30
+ initData: z.optional(z.string()),
31
+ renderGroup: z.optional(z.number()),
32
+ altGroup: z.optional(z.number()),
33
+ maxGrpSapStartingType: z.optional(z.number()),
34
+ maxObjSapStartingType: z.optional(z.number()),
35
+ // Non-standard: maximum delay (ms) between consecutive frames on this track.
36
+ // The player's buffer must be at least this large to avoid underruns.
37
+ // Mirrors the `jitter` field in the hang catalog.
38
+ jitter: z.optional(z.number()),
39
+ };
40
+ /** Zod schema describing a single track entry in an MSF catalog. */
41
+ export const TrackSchema = z.object(trackShape);
42
+ /** Zod schema for the top-level MSF catalog: a version-agnostic snapshot of tracks. */
43
+ export const CatalogSchema = z.object({
44
+ tracks: z.array(TrackSchema),
45
+ });
46
+ /** The newest MSF draft version string this package emits on the wire. */
47
+ export const VERSION = "draft-01";
48
+ // --- Wire representation (internal) -----------------------------------------
49
+ //
50
+ // The wire format hides two things from callers: the catalog `version` (number
51
+ // in draft-00, "draft-XX" string in draft-01) and init data, which draft-01
52
+ // moved out of the track into a root `initDataList` referenced by `initRef`.
53
+ const InitDataSchema = z.object({
54
+ id: z.string(),
55
+ type: z.string(),
56
+ data: z.string(),
57
+ });
58
+ const WireTrackSchema = z.object({
59
+ ...trackShape,
60
+ initRef: z.optional(z.string()),
61
+ });
62
+ const WireCatalogSchema = z.object({
63
+ // draft-00 used the number 1; draft-01 uses a "draft-XX" string. Accept both.
64
+ version: z.union([z.literal(1), z.string()]),
65
+ tracks: z.optional(z.array(WireTrackSchema)),
66
+ initDataList: z.optional(z.array(InitDataSchema)),
67
+ });
68
+ /** Serialize a catalog to its JSON wire representation (draft-01). */
69
+ export function encode(catalog) {
70
+ // Hoist inline init payloads into a shared, deduplicated initDataList and
71
+ // reference each from its track via initRef (the draft-01 wire shape).
72
+ const initDataList = [];
73
+ const ids = new Map();
74
+ const tracks = catalog.tracks.map((track) => {
75
+ const { initData, ...rest } = track;
76
+ const wireTrack = { ...rest, isLive: rest.isLive ?? false };
77
+ if (initData === undefined)
78
+ return wireTrack;
79
+ let id = ids.get(initData);
80
+ if (id === undefined) {
81
+ id = `init${initDataList.length}`;
82
+ initDataList.push({ id, type: "inline", data: initData });
83
+ ids.set(initData, id);
84
+ }
85
+ return { ...wireTrack, initRef: id };
86
+ });
87
+ const wire = { version: VERSION, tracks };
88
+ if (initDataList.length > 0)
89
+ wire.initDataList = initDataList;
90
+ return new TextEncoder().encode(JSON.stringify(wire));
91
+ }
92
+ /** Parse and validate a catalog from its JSON wire representation. Throws if invalid. */
93
+ export function decode(raw) {
94
+ const str = new TextDecoder().decode(raw);
95
+ try {
96
+ const wire = WireCatalogSchema.parse(JSON.parse(str));
97
+ // id -> inline payload, built once so resolution is linear in the number
98
+ // of tracks rather than tracks x entries.
99
+ const inline = new Map((wire.initDataList ?? []).filter((e) => e.type === "inline").map((e) => [e.id, e.data]));
100
+ const tracks = (wire.tracks ?? []).map(({ initRef, ...track }) => {
101
+ track.isLive ??= false;
102
+ // Resolve draft-01 initRef into inline initData so callers never see
103
+ // the indirection. Inline initData (draft-00) is left untouched.
104
+ if (track.initData === undefined && initRef !== undefined) {
105
+ // Only inline entries are resolved here. A non-inline (e.g. `url`) initRef
106
+ // legitimately stays undefined; the consumer fetches that init out-of-band.
107
+ track.initData = inline.get(initRef);
108
+ }
109
+ return track;
110
+ });
111
+ return { tracks };
112
+ }
113
+ catch (error) {
114
+ console.warn("invalid MSF catalog", str);
115
+ throw error;
116
+ }
117
+ }
118
+ /** Read and decode the next catalog frame from a track, or undefined if the track ended. */
119
+ export async function fetch(track) {
120
+ const frame = await track.readFrame();
121
+ if (!frame)
122
+ return undefined;
123
+ return decode(frame);
124
+ }
125
+ //# sourceMappingURL=catalog.js.map
package/catalog.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as Moq from \"@moq/net\";\nimport * as z from \"zod/mini\";\n\n/** Zod schema for a track's wire packaging. Accepts known values or any future string. */\nexport const PackagingSchema = z.union([\n\tz.enum([\"loc\", \"cmaf\", \"legacy\", \"mediatimeline\", \"eventtimeline\"]),\n\tz.string(),\n]);\n\n/** How a track's frames are packaged on the wire (e.g. \"loc\" or \"cmaf\"). */\nexport type Packaging = z.infer<typeof PackagingSchema>;\n\n/** Zod schema for a track's role. Accepts known values or any future string. */\nexport const RoleSchema = z.union([\n\tz.enum([\"video\", \"audio\", \"audiodescription\", \"caption\", \"subtitle\", \"signlanguage\"]),\n\tz.string(),\n]);\n\n/** The semantic role a track plays in the presentation (e.g. \"video\", \"audio\", \"caption\"). */\nexport type Role = z.infer<typeof RoleSchema>;\n\n// Shared track fields. This is the version-agnostic shape callers see: init data\n// is exposed inline via `initData`, regardless of how it was carried on the wire.\nconst trackShape = {\n\tname: z.string(),\n\tpackaging: PackagingSchema,\n\t// draft-00 marks isLive required but omits it on mediatimeline/eventtimeline\n\t// tracks, so accept its absence rather than reject the whole catalog.\n\tisLive: z.optional(z.boolean()),\n\trole: z.optional(RoleSchema),\n\tcodec: z.optional(z.string()),\n\twidth: z.optional(z.number()),\n\theight: z.optional(z.number()),\n\tframerate: z.optional(z.number()),\n\tsamplerate: z.optional(z.number()),\n\tchannelConfig: z.optional(z.string()),\n\tbitrate: z.optional(z.number()),\n\t/** Resolved base64 initialization data (draft-01's initRef indirection is resolved away). */\n\tinitData: z.optional(z.string()),\n\trenderGroup: z.optional(z.number()),\n\taltGroup: z.optional(z.number()),\n\tmaxGrpSapStartingType: z.optional(z.number()),\n\tmaxObjSapStartingType: z.optional(z.number()),\n\n\t// Non-standard: maximum delay (ms) between consecutive frames on this track.\n\t// The player's buffer must be at least this large to avoid underruns.\n\t// Mirrors the `jitter` field in the hang catalog.\n\tjitter: z.optional(z.number()),\n};\n\n/** Zod schema describing a single track entry in an MSF catalog. */\nexport const TrackSchema = z.object(trackShape);\n\n/** A single track in an MSF catalog, including its codec and media properties. */\nexport type Track = z.infer<typeof TrackSchema>;\n\n/** Zod schema for the top-level MSF catalog: a version-agnostic snapshot of tracks. */\nexport const CatalogSchema = z.object({\n\ttracks: z.array(TrackSchema),\n});\n\n/** The MSF catalog: a snapshot of the available tracks. */\nexport type Catalog = z.infer<typeof CatalogSchema>;\n\n/** The newest MSF draft version string this package emits on the wire. */\nexport const VERSION = \"draft-01\";\n\n// --- Wire representation (internal) -----------------------------------------\n//\n// The wire format hides two things from callers: the catalog `version` (number\n// in draft-00, \"draft-XX\" string in draft-01) and init data, which draft-01\n// moved out of the track into a root `initDataList` referenced by `initRef`.\n\nconst InitDataSchema = z.object({\n\tid: z.string(),\n\ttype: z.string(),\n\tdata: z.string(),\n});\n\nconst WireTrackSchema = z.object({\n\t...trackShape,\n\tinitRef: z.optional(z.string()),\n});\n\nconst WireCatalogSchema = z.object({\n\t// draft-00 used the number 1; draft-01 uses a \"draft-XX\" string. Accept both.\n\tversion: z.union([z.literal(1), z.string()]),\n\ttracks: z.optional(z.array(WireTrackSchema)),\n\tinitDataList: z.optional(z.array(InitDataSchema)),\n});\n\n/** Serialize a catalog to its JSON wire representation (draft-01). */\nexport function encode(catalog: Catalog): Uint8Array {\n\t// Hoist inline init payloads into a shared, deduplicated initDataList and\n\t// reference each from its track via initRef (the draft-01 wire shape).\n\tconst initDataList: z.infer<typeof InitDataSchema>[] = [];\n\tconst ids = new Map<string, string>();\n\n\tconst tracks = catalog.tracks.map((track) => {\n\t\tconst { initData, ...rest } = track;\n\t\tconst wireTrack = { ...rest, isLive: rest.isLive ?? false };\n\t\tif (initData === undefined) return wireTrack;\n\n\t\tlet id = ids.get(initData);\n\t\tif (id === undefined) {\n\t\t\tid = `init${initDataList.length}`;\n\t\t\tinitDataList.push({ id, type: \"inline\", data: initData });\n\t\t\tids.set(initData, id);\n\t\t}\n\t\treturn { ...wireTrack, initRef: id };\n\t});\n\n\tconst wire: Record<string, unknown> = { version: VERSION, tracks };\n\tif (initDataList.length > 0) wire.initDataList = initDataList;\n\n\treturn new TextEncoder().encode(JSON.stringify(wire));\n}\n\n/** Parse and validate a catalog from its JSON wire representation. Throws if invalid. */\nexport function decode(raw: Uint8Array): Catalog {\n\tconst str = new TextDecoder().decode(raw);\n\ttry {\n\t\tconst wire = WireCatalogSchema.parse(JSON.parse(str));\n\n\t\t// id -> inline payload, built once so resolution is linear in the number\n\t\t// of tracks rather than tracks x entries.\n\t\tconst inline = new Map((wire.initDataList ?? []).filter((e) => e.type === \"inline\").map((e) => [e.id, e.data]));\n\n\t\tconst tracks = (wire.tracks ?? []).map(({ initRef, ...track }) => {\n\t\t\ttrack.isLive ??= false;\n\n\t\t\t// Resolve draft-01 initRef into inline initData so callers never see\n\t\t\t// the indirection. Inline initData (draft-00) is left untouched.\n\t\t\tif (track.initData === undefined && initRef !== undefined) {\n\t\t\t\t// Only inline entries are resolved here. A non-inline (e.g. `url`) initRef\n\t\t\t\t// legitimately stays undefined; the consumer fetches that init out-of-band.\n\t\t\t\ttrack.initData = inline.get(initRef);\n\t\t\t}\n\t\t\treturn track;\n\t\t});\n\n\t\treturn { tracks };\n\t} catch (error) {\n\t\tconsole.warn(\"invalid MSF catalog\", str);\n\t\tthrow error;\n\t}\n}\n\n/** Read and decode the next catalog frame from a track, or undefined if the track ended. */\nexport async function fetch(track: Moq.Track): Promise<Catalog | undefined> {\n\tconst frame = await track.readFrame();\n\tif (!frame) return undefined;\n\treturn decode(frame);\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=catalog.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.test.d.ts","sourceRoot":"","sources":["../src/catalog.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,147 @@
1
+ /* @ts-self-types="./catalog.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { decode, encode } from "./catalog.js";
4
+ function encodeJson(value) {
5
+ return new TextEncoder().encode(JSON.stringify(value));
6
+ }
7
+ function decodeJson(raw) {
8
+ return JSON.parse(new TextDecoder().decode(raw));
9
+ }
10
+ test("decodes a draft-00 catalog with a numeric version", () => {
11
+ // Example 1 from draft-ietf-moq-msf-00, trimmed. Numeric version plus unmodeled
12
+ // fields (namespace, targetLatency, generatedAt) which must be ignored.
13
+ const catalog = decode(encodeJson({
14
+ version: 1,
15
+ generatedAt: 1746104606044,
16
+ tracks: [
17
+ {
18
+ name: "1080p-video",
19
+ namespace: "conference.example.com/conference123/alice",
20
+ packaging: "loc",
21
+ isLive: true,
22
+ targetLatency: 2000,
23
+ role: "video",
24
+ codec: "av01.0.08M.10.0.110.09",
25
+ width: 1920,
26
+ height: 1080,
27
+ framerate: 30,
28
+ bitrate: 1500000,
29
+ },
30
+ ],
31
+ }));
32
+ expect(catalog.tracks).toHaveLength(1);
33
+ expect(catalog.tracks[0].codec).toBe("av01.0.08M.10.0.110.09");
34
+ });
35
+ test("decodes a draft-00 catalog whose timeline tracks omit isLive", () => {
36
+ // Example 8 from draft-ietf-moq-msf-00: mediatimeline tracks omit isLive/role/codec.
37
+ const catalog = decode(encodeJson({
38
+ version: 1,
39
+ tracks: [
40
+ {
41
+ name: "history",
42
+ packaging: "mediatimeline",
43
+ mimetype: "application/json",
44
+ depends: ["1080p-video"],
45
+ },
46
+ {
47
+ name: "1080p-video",
48
+ packaging: "loc",
49
+ isLive: true,
50
+ role: "video",
51
+ codec: "av01.0.08M.10.0.110.09",
52
+ },
53
+ ],
54
+ }));
55
+ expect(catalog.tracks).toHaveLength(2);
56
+ expect(catalog.tracks[0].isLive).toBe(false);
57
+ expect(catalog.tracks[0].packaging).toBe("mediatimeline");
58
+ });
59
+ test("decodes a draft-01 catalog with a string version", () => {
60
+ const catalog = decode(encodeJson({
61
+ version: "draft-01",
62
+ tracks: [{ name: "audio", packaging: "loc", isLive: true, role: "audio", codec: "opus" }],
63
+ }));
64
+ expect(catalog.tracks[0].role).toBe("audio");
65
+ });
66
+ test("resolves draft-01 initRef into inline initData", () => {
67
+ const catalog = decode(encodeJson({
68
+ version: "draft-01",
69
+ initDataList: [{ id: "v0", type: "inline", data: "AQID" }],
70
+ tracks: [
71
+ { name: "video0", packaging: "cmaf", isLive: true, role: "video", codec: "avc1.640028", initRef: "v0" },
72
+ ],
73
+ }));
74
+ expect(catalog.tracks[0].initData).toBe("AQID");
75
+ });
76
+ test("leaves initData undefined for a dangling or non-inline initRef", () => {
77
+ const catalog = decode(encodeJson({
78
+ version: "draft-01",
79
+ initDataList: [{ id: "v0", type: "url", data: "https://example.com/init" }],
80
+ tracks: [
81
+ { name: "a", packaging: "cmaf", isLive: true, role: "video", codec: "avc1.640028", initRef: "missing" },
82
+ { name: "b", packaging: "cmaf", isLive: true, role: "video", codec: "avc1.640028", initRef: "v0" },
83
+ ],
84
+ }));
85
+ expect(catalog.tracks[0].initData).toBeUndefined();
86
+ expect(catalog.tracks[1].initData).toBeUndefined();
87
+ });
88
+ test("rejects an unsupported numeric version", () => {
89
+ // Mirrors the Rust side: any number other than 1 is rejected.
90
+ expect(() => decode(encodeJson({ version: 2, tracks: [] }))).toThrow();
91
+ });
92
+ test("encode hoists and dedups init data, then round-trips", () => {
93
+ const catalog = {
94
+ tracks: [
95
+ { name: "a", packaging: "cmaf", isLive: true, role: "video", codec: "avc1.640028", initData: "AQID" },
96
+ { name: "b", packaging: "cmaf", isLive: true, role: "video", codec: "avc1.640028", initData: "AQID" },
97
+ ],
98
+ };
99
+ const wire = decodeJson(encode(catalog));
100
+ const list = wire.initDataList;
101
+ expect(list).toHaveLength(1);
102
+ expect(list[0].data).toBe("AQID");
103
+ expect(wire.version).toBe("draft-01");
104
+ const wireTracks = wire.tracks;
105
+ for (const t of wireTracks) {
106
+ expect(t.initRef).toBe(list[0].id);
107
+ expect(t.initData).toBeUndefined();
108
+ }
109
+ const parsed = decode(encode(catalog));
110
+ expect(parsed.tracks[0].initData).toBe("AQID");
111
+ expect(parsed.tracks[1].initData).toBe("AQID");
112
+ });
113
+ test("encode emits isLive when omitted", () => {
114
+ const wire = decodeJson(encode({
115
+ tracks: [{ name: "history", packaging: "mediatimeline" }],
116
+ }));
117
+ const wireTracks = wire.tracks;
118
+ expect(wireTracks[0].isLive).toBe(false);
119
+ const parsed = decode(encodeJson(wire));
120
+ expect(parsed.tracks[0].isLive).toBe(false);
121
+ });
122
+ test("preserves SAP fields through decode and encode", () => {
123
+ const catalog = decode(encodeJson({
124
+ version: "draft-01",
125
+ tracks: [
126
+ {
127
+ name: "video0",
128
+ packaging: "cmaf",
129
+ isLive: true,
130
+ role: "video",
131
+ codec: "avc1.640028",
132
+ maxGrpSapStartingType: 1,
133
+ maxObjSapStartingType: 2,
134
+ jitter: 15.0,
135
+ },
136
+ ],
137
+ }));
138
+ expect(catalog.tracks[0].maxGrpSapStartingType).toBe(1);
139
+ expect(catalog.tracks[0].maxObjSapStartingType).toBe(2);
140
+ expect(catalog.tracks[0].jitter).toBe(15);
141
+ const wire = decodeJson(encode(catalog));
142
+ const wireTracks = wire.tracks;
143
+ expect(wireTracks[0].maxGrpSapStartingType).toBe(1);
144
+ expect(wireTracks[0].maxObjSapStartingType).toBe(2);
145
+ expect(wireTracks[0].jitter).toBe(15);
146
+ });
147
+ //# sourceMappingURL=catalog.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.test.js","sourceRoot":"","sources":["../src/catalog.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { decode, encode } from \"./catalog.ts\";\n\nfunction encodeJson(value: unknown): Uint8Array {\n\treturn new TextEncoder().encode(JSON.stringify(value));\n}\n\nfunction decodeJson(raw: Uint8Array): Record<string, unknown> {\n\treturn JSON.parse(new TextDecoder().decode(raw));\n}\n\ntest(\"decodes a draft-00 catalog with a numeric version\", () => {\n\t// Example 1 from draft-ietf-moq-msf-00, trimmed. Numeric version plus unmodeled\n\t// fields (namespace, targetLatency, generatedAt) which must be ignored.\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: 1,\n\t\t\tgeneratedAt: 1746104606044,\n\t\t\ttracks: [\n\t\t\t\t{\n\t\t\t\t\tname: \"1080p-video\",\n\t\t\t\t\tnamespace: \"conference.example.com/conference123/alice\",\n\t\t\t\t\tpackaging: \"loc\",\n\t\t\t\t\tisLive: true,\n\t\t\t\t\ttargetLatency: 2000,\n\t\t\t\t\trole: \"video\",\n\t\t\t\t\tcodec: \"av01.0.08M.10.0.110.09\",\n\t\t\t\t\twidth: 1920,\n\t\t\t\t\theight: 1080,\n\t\t\t\t\tframerate: 30,\n\t\t\t\t\tbitrate: 1500000,\n\t\t\t\t},\n\t\t\t],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks).toHaveLength(1);\n\texpect(catalog.tracks[0].codec).toBe(\"av01.0.08M.10.0.110.09\");\n});\n\ntest(\"decodes a draft-00 catalog whose timeline tracks omit isLive\", () => {\n\t// Example 8 from draft-ietf-moq-msf-00: mediatimeline tracks omit isLive/role/codec.\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: 1,\n\t\t\ttracks: [\n\t\t\t\t{\n\t\t\t\t\tname: \"history\",\n\t\t\t\t\tpackaging: \"mediatimeline\",\n\t\t\t\t\tmimetype: \"application/json\",\n\t\t\t\t\tdepends: [\"1080p-video\"],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: \"1080p-video\",\n\t\t\t\t\tpackaging: \"loc\",\n\t\t\t\t\tisLive: true,\n\t\t\t\t\trole: \"video\",\n\t\t\t\t\tcodec: \"av01.0.08M.10.0.110.09\",\n\t\t\t\t},\n\t\t\t],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks).toHaveLength(2);\n\texpect(catalog.tracks[0].isLive).toBe(false);\n\texpect(catalog.tracks[0].packaging).toBe(\"mediatimeline\");\n});\n\ntest(\"decodes a draft-01 catalog with a string version\", () => {\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: \"draft-01\",\n\t\t\ttracks: [{ name: \"audio\", packaging: \"loc\", isLive: true, role: \"audio\", codec: \"opus\" }],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks[0].role).toBe(\"audio\");\n});\n\ntest(\"resolves draft-01 initRef into inline initData\", () => {\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: \"draft-01\",\n\t\t\tinitDataList: [{ id: \"v0\", type: \"inline\", data: \"AQID\" }],\n\t\t\ttracks: [\n\t\t\t\t{ name: \"video0\", packaging: \"cmaf\", isLive: true, role: \"video\", codec: \"avc1.640028\", initRef: \"v0\" },\n\t\t\t],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks[0].initData).toBe(\"AQID\");\n});\n\ntest(\"leaves initData undefined for a dangling or non-inline initRef\", () => {\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: \"draft-01\",\n\t\t\tinitDataList: [{ id: \"v0\", type: \"url\", data: \"https://example.com/init\" }],\n\t\t\ttracks: [\n\t\t\t\t{ name: \"a\", packaging: \"cmaf\", isLive: true, role: \"video\", codec: \"avc1.640028\", initRef: \"missing\" },\n\t\t\t\t{ name: \"b\", packaging: \"cmaf\", isLive: true, role: \"video\", codec: \"avc1.640028\", initRef: \"v0\" },\n\t\t\t],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks[0].initData).toBeUndefined();\n\texpect(catalog.tracks[1].initData).toBeUndefined();\n});\n\ntest(\"rejects an unsupported numeric version\", () => {\n\t// Mirrors the Rust side: any number other than 1 is rejected.\n\texpect(() => decode(encodeJson({ version: 2, tracks: [] }))).toThrow();\n});\n\ntest(\"encode hoists and dedups init data, then round-trips\", () => {\n\tconst catalog = {\n\t\ttracks: [\n\t\t\t{ name: \"a\", packaging: \"cmaf\", isLive: true, role: \"video\", codec: \"avc1.640028\", initData: \"AQID\" },\n\t\t\t{ name: \"b\", packaging: \"cmaf\", isLive: true, role: \"video\", codec: \"avc1.640028\", initData: \"AQID\" },\n\t\t],\n\t};\n\n\tconst wire = decodeJson(encode(catalog));\n\tconst list = wire.initDataList as { id: string; type: string; data: string }[];\n\texpect(list).toHaveLength(1);\n\texpect(list[0].data).toBe(\"AQID\");\n\texpect(wire.version).toBe(\"draft-01\");\n\n\tconst wireTracks = wire.tracks as { initRef?: string; initData?: string }[];\n\tfor (const t of wireTracks) {\n\t\texpect(t.initRef).toBe(list[0].id);\n\t\texpect(t.initData).toBeUndefined();\n\t}\n\n\tconst parsed = decode(encode(catalog));\n\texpect(parsed.tracks[0].initData).toBe(\"AQID\");\n\texpect(parsed.tracks[1].initData).toBe(\"AQID\");\n});\n\ntest(\"encode emits isLive when omitted\", () => {\n\tconst wire = decodeJson(\n\t\tencode({\n\t\t\ttracks: [{ name: \"history\", packaging: \"mediatimeline\" }],\n\t\t}),\n\t);\n\n\tconst wireTracks = wire.tracks as { isLive?: boolean }[];\n\texpect(wireTracks[0].isLive).toBe(false);\n\n\tconst parsed = decode(encodeJson(wire));\n\texpect(parsed.tracks[0].isLive).toBe(false);\n});\n\ntest(\"preserves SAP fields through decode and encode\", () => {\n\tconst catalog = decode(\n\t\tencodeJson({\n\t\t\tversion: \"draft-01\",\n\t\t\ttracks: [\n\t\t\t\t{\n\t\t\t\t\tname: \"video0\",\n\t\t\t\t\tpackaging: \"cmaf\",\n\t\t\t\t\tisLive: true,\n\t\t\t\t\trole: \"video\",\n\t\t\t\t\tcodec: \"avc1.640028\",\n\t\t\t\t\tmaxGrpSapStartingType: 1,\n\t\t\t\t\tmaxObjSapStartingType: 2,\n\t\t\t\t\tjitter: 15.0,\n\t\t\t\t},\n\t\t\t],\n\t\t}),\n\t);\n\n\texpect(catalog.tracks[0].maxGrpSapStartingType).toBe(1);\n\texpect(catalog.tracks[0].maxObjSapStartingType).toBe(2);\n\texpect(catalog.tracks[0].jitter).toBe(15);\n\n\tconst wire = decodeJson(encode(catalog));\n\tconst wireTracks = wire.tracks as {\n\t\tmaxGrpSapStartingType?: number;\n\t\tmaxObjSapStartingType?: number;\n\t\tjitter?: number;\n\t}[];\n\texpect(wireTracks[0].maxGrpSapStartingType).toBe(1);\n\texpect(wireTracks[0].maxObjSapStartingType).toBe(2);\n\texpect(wireTracks[0].jitter).toBe(15);\n});\n"]}
package/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * MSF (MOQT Streaming Format) catalog: Zod schemas and types describing the
3
+ * tracks in a broadcast (packaging, role, codec, and media properties).
4
+ *
5
+ * @module
6
+ */
7
+ export * from "./catalog";
8
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,WAAW,CAAC"}
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /* @ts-self-types="./index.d.ts" */
2
+ /**
3
+ * MSF (MOQT Streaming Format) catalog: Zod schemas and types describing the
4
+ * tracks in a broadcast (packaging, role, codec, and media properties).
5
+ *
6
+ * @module
7
+ */
8
+ export * from "./catalog";
9
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,WAAW,CAAC","sourcesContent":["/**\n * MSF (MOQT Streaming Format) catalog: Zod schemas and types describing the\n * tracks in a broadcast (packaging, role, codec, and media properties).\n *\n * @module\n */\nexport * from \"./catalog\";\n"]}
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@norskvideo/moq-msf",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "MSF (MOQT Streaming Format) catalog types for MoQ",
6
+ "license": "(MIT OR Apache-2.0)",
7
+ "repository": "github:moq-dev/moq",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
15
+ "dependencies": {
16
+ "zod": "^4.4.3",
17
+ "@norskvideo/moq-net": "^0.1.0"
18
+ }
19
+ }