@norskvideo/moq-hang 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.
Files changed (130) hide show
  1. package/README.md +65 -0
  2. package/catalog/audio.d.ts +139 -0
  3. package/catalog/audio.d.ts.map +1 -0
  4. package/catalog/audio.js +55 -0
  5. package/catalog/audio.js.map +1 -0
  6. package/catalog/consumer.d.ts +34 -0
  7. package/catalog/consumer.d.ts.map +1 -0
  8. package/catalog/consumer.js +30 -0
  9. package/catalog/consumer.js.map +1 -0
  10. package/catalog/container.d.ts +24 -0
  11. package/catalog/container.d.ts.map +1 -0
  12. package/catalog/container.js +30 -0
  13. package/catalog/container.js.map +1 -0
  14. package/catalog/format.d.ts +13 -0
  15. package/catalog/format.d.ts.map +1 -0
  16. package/catalog/format.js +24 -0
  17. package/catalog/format.js.map +1 -0
  18. package/catalog/index.d.ts +18 -0
  19. package/catalog/index.d.ts.map +1 -0
  20. package/catalog/index.js +19 -0
  21. package/catalog/index.js.map +1 -0
  22. package/catalog/integers.d.ts +23 -0
  23. package/catalog/integers.d.ts.map +1 -0
  24. package/catalog/integers.js +24 -0
  25. package/catalog/integers.js.map +1 -0
  26. package/catalog/priority.d.ts +7 -0
  27. package/catalog/priority.d.ts.map +1 -0
  28. package/catalog/priority.js +9 -0
  29. package/catalog/priority.js.map +1 -0
  30. package/catalog/producer.d.ts +31 -0
  31. package/catalog/producer.d.ts.map +1 -0
  32. package/catalog/producer.js +25 -0
  33. package/catalog/producer.js.map +1 -0
  34. package/catalog/root.d.ts +252 -0
  35. package/catalog/root.d.ts.map +1 -0
  36. package/catalog/root.js +17 -0
  37. package/catalog/root.js.map +1 -0
  38. package/catalog/root.test.d.ts +2 -0
  39. package/catalog/root.test.d.ts.map +1 -0
  40. package/catalog/root.test.js +20 -0
  41. package/catalog/root.test.js.map +1 -0
  42. package/catalog/timeline.d.ts +26 -0
  43. package/catalog/timeline.d.ts.map +1 -0
  44. package/catalog/timeline.js +33 -0
  45. package/catalog/timeline.js.map +1 -0
  46. package/catalog/track.d.ts +8 -0
  47. package/catalog/track.d.ts.map +1 -0
  48. package/catalog/track.js +7 -0
  49. package/catalog/track.js.map +1 -0
  50. package/catalog/video.d.ts +171 -0
  51. package/catalog/video.d.ts.map +1 -0
  52. package/catalog/video.js +87 -0
  53. package/catalog/video.js.map +1 -0
  54. package/container/cmaf/decode.d.ts +68 -0
  55. package/container/cmaf/decode.d.ts.map +1 -0
  56. package/container/cmaf/decode.js +303 -0
  57. package/container/cmaf/decode.js.map +1 -0
  58. package/container/cmaf/encode.d.ts +51 -0
  59. package/container/cmaf/encode.d.ts.map +1 -0
  60. package/container/cmaf/encode.js +834 -0
  61. package/container/cmaf/encode.js.map +1 -0
  62. package/container/cmaf/format.d.ts +12 -0
  63. package/container/cmaf/format.d.ts.map +1 -0
  64. package/container/cmaf/format.js +19 -0
  65. package/container/cmaf/format.js.map +1 -0
  66. package/container/cmaf/index.d.ts +9 -0
  67. package/container/cmaf/index.d.ts.map +1 -0
  68. package/container/cmaf/index.js +10 -0
  69. package/container/cmaf/index.js.map +1 -0
  70. package/container/consumer.d.ts +41 -0
  71. package/container/consumer.d.ts.map +1 -0
  72. package/container/consumer.js +424 -0
  73. package/container/consumer.js.map +1 -0
  74. package/container/consumer.test.d.ts +2 -0
  75. package/container/consumer.test.d.ts.map +1 -0
  76. package/container/consumer.test.js +396 -0
  77. package/container/consumer.test.js.map +1 -0
  78. package/container/format.d.ts +12 -0
  79. package/container/format.d.ts.map +1 -0
  80. package/container/format.js +3 -0
  81. package/container/format.js.map +1 -0
  82. package/container/index.d.ts +14 -0
  83. package/container/index.d.ts.map +1 -0
  84. package/container/index.js +18 -0
  85. package/container/index.js.map +1 -0
  86. package/container/legacy.d.ts +39 -0
  87. package/container/legacy.d.ts.map +1 -0
  88. package/container/legacy.js +53 -0
  89. package/container/legacy.js.map +1 -0
  90. package/container/timeline.d.ts +67 -0
  91. package/container/timeline.d.ts.map +1 -0
  92. package/container/timeline.js +87 -0
  93. package/container/timeline.js.map +1 -0
  94. package/container/types.d.ts +22 -0
  95. package/container/types.d.ts.map +1 -0
  96. package/container/types.js +22 -0
  97. package/container/types.js.map +1 -0
  98. package/index.d.ts +13 -0
  99. package/index.d.ts.map +1 -0
  100. package/index.js +18 -0
  101. package/index.js.map +1 -0
  102. package/package.json +37 -0
  103. package/util/aac.d.ts +8 -0
  104. package/util/aac.d.ts.map +1 -0
  105. package/util/aac.js +57 -0
  106. package/util/aac.js.map +1 -0
  107. package/util/aac.test.d.ts +2 -0
  108. package/util/aac.test.d.ts.map +1 -0
  109. package/util/aac.test.js +34 -0
  110. package/util/aac.test.js.map +1 -0
  111. package/util/hacks.d.ts +5 -0
  112. package/util/hacks.d.ts.map +1 -0
  113. package/util/hacks.js +6 -0
  114. package/util/hacks.js.map +1 -0
  115. package/util/hex.d.ts +5 -0
  116. package/util/hex.d.ts.map +1 -0
  117. package/util/hex.js +18 -0
  118. package/util/hex.js.map +1 -0
  119. package/util/index.d.ts +12 -0
  120. package/util/index.d.ts.map +1 -0
  121. package/util/index.js +18 -0
  122. package/util/index.js.map +1 -0
  123. package/util/latency.d.ts +38 -0
  124. package/util/latency.d.ts.map +1 -0
  125. package/util/latency.js +49 -0
  126. package/util/latency.js.map +1 -0
  127. package/util/libav.d.ts +3 -0
  128. package/util/libav.d.ts.map +1 -0
  129. package/util/libav.js +26 -0
  130. package/util/libav.js.map +1 -0
@@ -0,0 +1,834 @@
1
+ /* @ts-self-types="./encode.d.ts" */
2
+ /**
3
+ * MP4 encoding utilities for creating fMP4 init and data segments.
4
+ * Used by MSE to create init segments (ftyp+moov) and data segments (moof+mdat).
5
+ */
6
+ import { writeDref, writeFtyp, writeHdlr, writeIsoBoxes, writeMdat, writeMdhd, writeMfhd, writeMvhd, writeSmhd, writeStsd, writeStts, writeTfdt, writeTfhd, writeTkhd, writeTrex, writeTrun, writeUrl, writeVmhd, } from "@svta/cml-iso-bmff";
7
+ import * as Aac from "../../util/aac";
8
+ import * as Hex from "../../util/hex";
9
+ // Identity matrix for tkhd/mvhd (stored as 16.16 fixed point)
10
+ const IDENTITY_MATRIX = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
11
+ // Writers config - maps box types to their writer functions
12
+ const WRITERS = {
13
+ // Init segment boxes
14
+ ftyp: writeFtyp,
15
+ mvhd: writeMvhd,
16
+ tkhd: writeTkhd,
17
+ mdhd: writeMdhd,
18
+ hdlr: writeHdlr,
19
+ vmhd: writeVmhd,
20
+ smhd: writeSmhd,
21
+ "url ": writeUrl,
22
+ dref: writeDref,
23
+ stsd: writeStsd,
24
+ stts: writeStts,
25
+ trex: writeTrex,
26
+ // Data segment boxes
27
+ mfhd: writeMfhd,
28
+ tfhd: writeTfhd,
29
+ tfdt: writeTfdt,
30
+ trun: writeTrun,
31
+ mdat: writeMdat,
32
+ // For boxes without library writers, we create them manually as Uint8Arrays
33
+ };
34
+ /** Write boxes using our WRITERS config. */
35
+ function writeBoxes(boxes) {
36
+ return writeIsoBoxes(boxes, { writers: WRITERS });
37
+ }
38
+ /**
39
+ * Helper to create a simple full box (version + flags + content) as raw bytes.
40
+ * Used for boxes that don't have writers in the library.
41
+ */
42
+ function createFullBox(type, version, flags, content) {
43
+ const size = 8 + 4 + content.length; // header + version/flags + content
44
+ const box = new Uint8Array(size);
45
+ const view = new DataView(box.buffer);
46
+ view.setUint32(0, size, false);
47
+ box[4] = type.charCodeAt(0);
48
+ box[5] = type.charCodeAt(1);
49
+ box[6] = type.charCodeAt(2);
50
+ box[7] = type.charCodeAt(3);
51
+ view.setUint32(8, (version << 24) | flags, false);
52
+ box.set(content, 12);
53
+ return box;
54
+ }
55
+ /**
56
+ * Create an empty stsc (Sample to Chunk) box for fragmented MP4.
57
+ */
58
+ function createEmptyStsc() {
59
+ // stsc: version(1) + flags(3) + entry_count(4) = 4 bytes content
60
+ const content = new Uint8Array(4); // entry_count = 0
61
+ return createFullBox("stsc", 0, 0, content);
62
+ }
63
+ /**
64
+ * Create an empty stsz (Sample Size) box for fragmented MP4.
65
+ */
66
+ function createEmptyStsz() {
67
+ // stsz: version(1) + flags(3) + sample_size(4) + sample_count(4) = 8 bytes content
68
+ const content = new Uint8Array(8); // sample_size = 0, sample_count = 0
69
+ return createFullBox("stsz", 0, 0, content);
70
+ }
71
+ /**
72
+ * Create an empty stco (Chunk Offset) box for fragmented MP4.
73
+ */
74
+ function createEmptyStco() {
75
+ // stco: version(1) + flags(3) + entry_count(4) = 4 bytes content
76
+ const content = new Uint8Array(4); // entry_count = 0
77
+ return createFullBox("stco", 0, 0, content);
78
+ }
79
+ /**
80
+ * Create an avc1 (H.264 Visual Sample Entry) box with embedded avcC.
81
+ * Built manually because the library doesn't properly serialize Uint8Array child boxes.
82
+ */
83
+ function createAvc1Box(width, height, avcC) {
84
+ // avc1 box structure:
85
+ // - 6 bytes reserved (0)
86
+ // - 2 bytes data_reference_index (1)
87
+ // - 2 bytes pre_defined (0)
88
+ // - 2 bytes reserved (0)
89
+ // - 12 bytes pre_defined (0)
90
+ // - 2 bytes width
91
+ // - 2 bytes height
92
+ // - 4 bytes horizresolution (0x00480000 = 72 dpi)
93
+ // - 4 bytes vertresolution (0x00480000 = 72 dpi)
94
+ // - 4 bytes reserved (0)
95
+ // - 2 bytes frame_count (1)
96
+ // - 32 bytes compressorname (null-padded string)
97
+ // - 2 bytes depth (0x0018 = 24)
98
+ // - 2 bytes pre_defined (-1 = 0xFFFF)
99
+ // - child boxes (avcC)
100
+ const avcCSize = 8 + avcC.length; // box header + payload
101
+ const avc1ContentSize = 6 + 2 + 2 + 2 + 12 + 2 + 2 + 4 + 4 + 4 + 2 + 32 + 2 + 2 + avcCSize;
102
+ const avc1Size = 8 + avc1ContentSize; // box header + content
103
+ const box = new Uint8Array(avc1Size);
104
+ const view = new DataView(box.buffer);
105
+ let offset = 0;
106
+ // Box header
107
+ view.setUint32(offset, avc1Size, false);
108
+ offset += 4;
109
+ box[offset++] = 0x61; // 'a'
110
+ box[offset++] = 0x76; // 'v'
111
+ box[offset++] = 0x63; // 'c'
112
+ box[offset++] = 0x31; // '1'
113
+ // SampleEntry fields
114
+ offset += 6; // reserved (6 bytes of 0)
115
+ view.setUint16(offset, 1, false);
116
+ offset += 2; // data_reference_index = 1
117
+ // VisualSampleEntry fields
118
+ view.setUint16(offset, 0, false);
119
+ offset += 2; // pre_defined
120
+ view.setUint16(offset, 0, false);
121
+ offset += 2; // reserved
122
+ offset += 12; // pre_defined (12 bytes of 0)
123
+ view.setUint16(offset, width, false);
124
+ offset += 2;
125
+ view.setUint16(offset, height, false);
126
+ offset += 2;
127
+ view.setUint32(offset, 0x00480000, false);
128
+ offset += 4; // horizresolution (72 dpi)
129
+ view.setUint32(offset, 0x00480000, false);
130
+ offset += 4; // vertresolution (72 dpi)
131
+ view.setUint32(offset, 0, false);
132
+ offset += 4; // reserved
133
+ view.setUint16(offset, 1, false);
134
+ offset += 2; // frame_count = 1
135
+ offset += 32; // compressorname (32 bytes of 0)
136
+ view.setUint16(offset, 0x0018, false);
137
+ offset += 2; // depth = 24
138
+ view.setUint16(offset, 0xffff, false);
139
+ offset += 2; // pre_defined = -1
140
+ // avcC child box
141
+ view.setUint32(offset, avcCSize, false);
142
+ offset += 4;
143
+ box[offset++] = 0x61; // 'a'
144
+ box[offset++] = 0x76; // 'v'
145
+ box[offset++] = 0x63; // 'c'
146
+ box[offset++] = 0x43; // 'C'
147
+ box.set(avcC, offset);
148
+ return box;
149
+ }
150
+ /**
151
+ * Creates an MSE-compatible initialization segment (ftyp + moov) for H.264 video.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * // From WebCodecs EncodedVideoChunkMetadata
156
+ * const config = await encoder.encode(frame);
157
+ * const metadata = config.decoderConfig;
158
+ *
159
+ * const initSegment = createVideoInitSegment({
160
+ * width: metadata.codedWidth,
161
+ * height: metadata.codedHeight,
162
+ * avcC: new Uint8Array(metadata.description),
163
+ * });
164
+ *
165
+ * sourceBuffer.appendBuffer(initSegment);
166
+ * ```
167
+ */
168
+ export function createVideoInitSegment(config) {
169
+ const { codedWidth, codedHeight, description } = config;
170
+ if (!codedWidth || !codedHeight || !description) {
171
+ throw new Error("Missing required fields to create video init segment");
172
+ }
173
+ // Legacy container always uses microsecond timescale and track ID 1.
174
+ // For CMAF, the init segment in the catalog is authoritative; this builder
175
+ // is only used for the legacy path.
176
+ const timescale = 1_000_000;
177
+ const trackId = 1;
178
+ // ftyp - File Type Box
179
+ const ftyp = {
180
+ type: "ftyp",
181
+ majorBrand: "isom",
182
+ minorVersion: 0x200,
183
+ compatibleBrands: ["isom", "iso6", "mp41"],
184
+ };
185
+ // mvhd - Movie Header Box
186
+ const mvhd = {
187
+ type: "mvhd",
188
+ version: 0,
189
+ flags: 0,
190
+ creationTime: 0,
191
+ modificationTime: 0,
192
+ timescale: timescale,
193
+ duration: 0, // Unknown/fragmented
194
+ rate: 0x00010000, // 1.0 in 16.16 fixed point
195
+ volume: 0x0100, // 1.0 in 8.8 fixed point
196
+ reserved1: 0,
197
+ reserved2: [0, 0],
198
+ matrix: IDENTITY_MATRIX,
199
+ preDefined: [0, 0, 0, 0, 0, 0],
200
+ nextTrackId: trackId + 1,
201
+ };
202
+ // tkhd - Track Header Box
203
+ const tkhd = {
204
+ type: "tkhd",
205
+ version: 0,
206
+ flags: 0x000003, // Track enabled + in movie
207
+ creationTime: 0,
208
+ modificationTime: 0,
209
+ trackId: trackId,
210
+ reserved1: 0,
211
+ duration: 0,
212
+ reserved2: [0, 0],
213
+ layer: 0,
214
+ alternateGroup: 0,
215
+ volume: 0, // Video tracks have 0 volume
216
+ reserved3: 0,
217
+ matrix: IDENTITY_MATRIX,
218
+ width: codedWidth * 0x10000, // 16.16 fixed point (avoid << which produces signed int)
219
+ height: codedHeight * 0x10000,
220
+ };
221
+ // mdhd - Media Header Box
222
+ const mdhd = {
223
+ type: "mdhd",
224
+ version: 0,
225
+ flags: 0,
226
+ creationTime: 0,
227
+ modificationTime: 0,
228
+ timescale: timescale,
229
+ duration: 0,
230
+ language: "und",
231
+ preDefined: 0,
232
+ };
233
+ // hdlr - Handler Reference Box
234
+ const hdlr = {
235
+ type: "hdlr",
236
+ version: 0,
237
+ flags: 0,
238
+ preDefined: 0,
239
+ handlerType: "vide",
240
+ reserved: [0, 0, 0],
241
+ name: "VideoHandler",
242
+ };
243
+ // vmhd - Video Media Header Box
244
+ const vmhd = {
245
+ type: "vmhd",
246
+ version: 0,
247
+ flags: 1, // Required to be 1
248
+ graphicsmode: 0,
249
+ opcolor: [0, 0, 0],
250
+ };
251
+ // url - Data Entry URL Box (self-contained)
252
+ const urlBox = {
253
+ type: "url ",
254
+ version: 0,
255
+ flags: 0x000001, // Self-contained flag
256
+ location: "",
257
+ };
258
+ // dref - Data Reference Box
259
+ const dref = {
260
+ type: "dref",
261
+ version: 0,
262
+ flags: 0,
263
+ entryCount: 1,
264
+ entries: [urlBox],
265
+ };
266
+ // dinf - Data Information Box
267
+ const dinf = {
268
+ type: "dinf",
269
+ boxes: [dref],
270
+ };
271
+ // Build the avc1 box manually since the library doesn't properly serialize Uint8Array children
272
+ const avc1Box = createAvc1Box(codedWidth, codedHeight, Hex.toBytes(description));
273
+ // stsd - Sample Description Box
274
+ const stsd = {
275
+ type: "stsd",
276
+ version: 0,
277
+ flags: 0,
278
+ entryCount: 1,
279
+ // biome-ignore lint/suspicious/noExplicitAny: Raw avc1 box since library doesn't handle avcC children
280
+ entries: [avc1Box],
281
+ };
282
+ // stts - Decoding Time to Sample (empty for fragmented)
283
+ const stts = {
284
+ type: "stts",
285
+ version: 0,
286
+ flags: 0,
287
+ entryCount: 0,
288
+ entries: [],
289
+ };
290
+ // Create raw boxes for types without library writers
291
+ const stsc = createEmptyStsc();
292
+ const stsz = createEmptyStsz();
293
+ const stco = createEmptyStco();
294
+ // stbl - Sample Table Box
295
+ // Note: stsc, stsz, stco are raw Uint8Arrays since the library doesn't have writers for them
296
+ const stbl = {
297
+ type: "stbl",
298
+ // biome-ignore lint/suspicious/noExplicitAny: Raw boxes for types without library writers
299
+ boxes: [stsd, stts, stsc, stsz, stco],
300
+ };
301
+ // minf - Media Information Box
302
+ const minf = {
303
+ type: "minf",
304
+ boxes: [vmhd, dinf, stbl],
305
+ };
306
+ // mdia - Media Box
307
+ const mdia = {
308
+ type: "mdia",
309
+ boxes: [mdhd, hdlr, minf],
310
+ };
311
+ // trak - Track Box
312
+ const trak = {
313
+ type: "trak",
314
+ boxes: [tkhd, mdia],
315
+ };
316
+ // trex - Track Extends Box (required for fragmented MP4)
317
+ const trex = {
318
+ type: "trex",
319
+ version: 0,
320
+ flags: 0,
321
+ trackId: trackId,
322
+ defaultSampleDescriptionIndex: 1,
323
+ defaultSampleDuration: 0,
324
+ defaultSampleSize: 0,
325
+ defaultSampleFlags: 0,
326
+ };
327
+ // mvex - Movie Extends Box (signals fragmented MP4)
328
+ const mvex = {
329
+ type: "mvex",
330
+ boxes: [trex],
331
+ };
332
+ // moov - Movie Box
333
+ const moov = {
334
+ type: "moov",
335
+ boxes: [mvhd, trak, mvex],
336
+ };
337
+ // Write all boxes and concatenate
338
+ const buffers = writeBoxes([ftyp, moov]);
339
+ const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
340
+ const result = new Uint8Array(totalLength);
341
+ let offset = 0;
342
+ for (const buf of buffers) {
343
+ result.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
344
+ offset += buf.byteLength;
345
+ }
346
+ return result;
347
+ }
348
+ /**
349
+ * Creates an MSE-compatible initialization segment (ftyp + moov) for audio.
350
+ * Supports AAC (mp4a) and Opus codecs.
351
+ */
352
+ export function createAudioInitSegment(config) {
353
+ const { sampleRate, numberOfChannels, description, codec } = config;
354
+ // Legacy container always uses microsecond timescale and track ID 1.
355
+ const timescale = 1_000_000;
356
+ const trackId = 1;
357
+ // ftyp - File Type Box
358
+ const ftyp = {
359
+ type: "ftyp",
360
+ majorBrand: "isom",
361
+ minorVersion: 0x200,
362
+ compatibleBrands: ["isom", "iso6", "mp41"],
363
+ };
364
+ // mvhd - Movie Header Box
365
+ const mvhd = {
366
+ type: "mvhd",
367
+ version: 0,
368
+ flags: 0,
369
+ creationTime: 0,
370
+ modificationTime: 0,
371
+ timescale: timescale,
372
+ duration: 0,
373
+ rate: 0x00010000,
374
+ volume: 0x0100,
375
+ reserved1: 0,
376
+ reserved2: [0, 0],
377
+ matrix: IDENTITY_MATRIX,
378
+ preDefined: [0, 0, 0, 0, 0, 0],
379
+ nextTrackId: trackId + 1,
380
+ };
381
+ // tkhd - Track Header Box
382
+ const tkhd = {
383
+ type: "tkhd",
384
+ version: 0,
385
+ flags: 0x000003,
386
+ creationTime: 0,
387
+ modificationTime: 0,
388
+ trackId: trackId,
389
+ reserved1: 0,
390
+ duration: 0,
391
+ reserved2: [0, 0],
392
+ layer: 0,
393
+ alternateGroup: 0,
394
+ volume: 0x0100, // Audio tracks have volume (1.0 in 8.8 fixed point)
395
+ reserved3: 0,
396
+ matrix: IDENTITY_MATRIX,
397
+ width: 0,
398
+ height: 0,
399
+ };
400
+ // mdhd - Media Header Box
401
+ const mdhd = {
402
+ type: "mdhd",
403
+ version: 0,
404
+ flags: 0,
405
+ creationTime: 0,
406
+ modificationTime: 0,
407
+ timescale: timescale,
408
+ duration: 0,
409
+ language: "und",
410
+ preDefined: 0,
411
+ };
412
+ // hdlr - Handler Reference Box
413
+ const hdlr = {
414
+ type: "hdlr",
415
+ version: 0,
416
+ flags: 0,
417
+ preDefined: 0,
418
+ handlerType: "soun",
419
+ reserved: [0, 0, 0],
420
+ name: "SoundHandler",
421
+ };
422
+ // smhd - Sound Media Header Box
423
+ const smhd = {
424
+ type: "smhd",
425
+ version: 0,
426
+ flags: 0,
427
+ balance: 0,
428
+ reserved: 0,
429
+ };
430
+ // url - Data Entry URL Box (self-contained)
431
+ const urlBox = {
432
+ type: "url ",
433
+ version: 0,
434
+ flags: 0x000001,
435
+ location: "",
436
+ };
437
+ // dref - Data Reference Box
438
+ const dref = {
439
+ type: "dref",
440
+ version: 0,
441
+ flags: 0,
442
+ entryCount: 1,
443
+ entries: [urlBox],
444
+ };
445
+ // dinf - Data Information Box
446
+ const dinf = {
447
+ type: "dinf",
448
+ boxes: [dref],
449
+ };
450
+ // Build codec-specific sample entry (manually to ensure child boxes are properly serialized)
451
+ const sampleEntry = createAudioSampleEntry(codec, sampleRate, numberOfChannels, description);
452
+ // stsd - Sample Description Box
453
+ const stsd = {
454
+ type: "stsd",
455
+ version: 0,
456
+ flags: 0,
457
+ entryCount: 1,
458
+ // biome-ignore lint/suspicious/noExplicitAny: Raw sample entry box
459
+ entries: [sampleEntry],
460
+ };
461
+ // stts - Decoding Time to Sample (empty for fragmented)
462
+ const stts = {
463
+ type: "stts",
464
+ version: 0,
465
+ flags: 0,
466
+ entryCount: 0,
467
+ entries: [],
468
+ };
469
+ // Create raw boxes for types without library writers
470
+ const stsc = createEmptyStsc();
471
+ const stsz = createEmptyStsz();
472
+ const stco = createEmptyStco();
473
+ // stbl - Sample Table Box
474
+ // Note: stsc, stsz, stco are raw Uint8Arrays since the library doesn't have writers for them
475
+ const stbl = {
476
+ type: "stbl",
477
+ // biome-ignore lint/suspicious/noExplicitAny: Raw boxes for types without library writers
478
+ boxes: [stsd, stts, stsc, stsz, stco],
479
+ };
480
+ // minf - Media Information Box
481
+ const minf = {
482
+ type: "minf",
483
+ boxes: [smhd, dinf, stbl],
484
+ };
485
+ // mdia - Media Box
486
+ const mdia = {
487
+ type: "mdia",
488
+ boxes: [mdhd, hdlr, minf],
489
+ };
490
+ // trak - Track Box
491
+ const trak = {
492
+ type: "trak",
493
+ boxes: [tkhd, mdia],
494
+ };
495
+ // trex - Track Extends Box
496
+ const trex = {
497
+ type: "trex",
498
+ version: 0,
499
+ flags: 0,
500
+ trackId: trackId,
501
+ defaultSampleDescriptionIndex: 1,
502
+ defaultSampleDuration: 0,
503
+ defaultSampleSize: 0,
504
+ defaultSampleFlags: 0,
505
+ };
506
+ // mvex - Movie Extends Box
507
+ const mvex = {
508
+ type: "mvex",
509
+ boxes: [trex],
510
+ };
511
+ // moov - Movie Box
512
+ const moov = {
513
+ type: "moov",
514
+ boxes: [mvhd, trak, mvex],
515
+ };
516
+ // Write all boxes and concatenate
517
+ const buffers = writeBoxes([ftyp, moov]);
518
+ const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
519
+ const result = new Uint8Array(totalLength);
520
+ let offset = 0;
521
+ for (const buf of buffers) {
522
+ result.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
523
+ offset += buf.byteLength;
524
+ }
525
+ return result;
526
+ }
527
+ /**
528
+ * Create an audio sample entry box (mp4a or Opus) with embedded codec config.
529
+ * Built manually because the library doesn't properly serialize Uint8Array child boxes.
530
+ */
531
+ function createAudioSampleEntry(codec, sampleRate, channelCount, description) {
532
+ if (codec.startsWith("mp4a")) {
533
+ return createMp4aBox(sampleRate, channelCount, description);
534
+ }
535
+ else if (codec === "opus") {
536
+ return createOpusBox(sampleRate, channelCount, description);
537
+ }
538
+ throw new Error(`Unsupported audio codec: ${codec}`);
539
+ }
540
+ /**
541
+ * Create an mp4a (AAC Audio Sample Entry) box with embedded esds.
542
+ */
543
+ function createMp4aBox(sampleRate, channelCount, description) {
544
+ const esds = createEsdsBox(sampleRate, channelCount, description);
545
+ // mp4a box structure (AudioSampleEntry):
546
+ // - 6 bytes reserved (0)
547
+ // - 2 bytes data_reference_index (1)
548
+ // - 8 bytes reserved (0) - includes reserved2[2] and pre_defined fields
549
+ // - 2 bytes channelcount
550
+ // - 2 bytes samplesize (16)
551
+ // - 2 bytes pre_defined (0)
552
+ // - 2 bytes reserved (0)
553
+ // - 4 bytes samplerate (16.16 fixed point)
554
+ // - child boxes (esds)
555
+ const mp4aContentSize = 6 + 2 + 8 + 2 + 2 + 2 + 2 + 4 + esds.length;
556
+ const mp4aSize = 8 + mp4aContentSize;
557
+ const box = new Uint8Array(mp4aSize);
558
+ const view = new DataView(box.buffer);
559
+ let offset = 0;
560
+ // Box header
561
+ view.setUint32(offset, mp4aSize, false);
562
+ offset += 4;
563
+ box[offset++] = 0x6d; // 'm'
564
+ box[offset++] = 0x70; // 'p'
565
+ box[offset++] = 0x34; // '4'
566
+ box[offset++] = 0x61; // 'a'
567
+ // SampleEntry fields
568
+ offset += 6; // reserved (6 bytes of 0)
569
+ view.setUint16(offset, 1, false);
570
+ offset += 2; // data_reference_index = 1
571
+ // AudioSampleEntry fields
572
+ offset += 8; // reserved (8 bytes of 0)
573
+ view.setUint16(offset, channelCount, false);
574
+ offset += 2;
575
+ view.setUint16(offset, 16, false);
576
+ offset += 2; // samplesize = 16
577
+ view.setUint16(offset, 0, false);
578
+ offset += 2; // pre_defined
579
+ view.setUint16(offset, 0, false);
580
+ offset += 2; // reserved
581
+ view.setUint32(offset, sampleRate * 0x10000, false);
582
+ offset += 4; // samplerate (16.16 fixed point)
583
+ // esds child box (already includes box header)
584
+ box.set(esds, offset);
585
+ return box;
586
+ }
587
+ /**
588
+ * Create an Opus (Opus Audio Sample Entry) box with embedded dOps.
589
+ */
590
+ function createOpusBox(sampleRate, channelCount, description) {
591
+ const dOps = createDOpsBox(channelCount, sampleRate, description);
592
+ // Opus box structure (AudioSampleEntry):
593
+ // Same structure as mp4a
594
+ const opusContentSize = 6 + 2 + 8 + 2 + 2 + 2 + 2 + 4 + dOps.length;
595
+ const opusSize = 8 + opusContentSize;
596
+ const box = new Uint8Array(opusSize);
597
+ const view = new DataView(box.buffer);
598
+ let offset = 0;
599
+ // Box header
600
+ view.setUint32(offset, opusSize, false);
601
+ offset += 4;
602
+ box[offset++] = 0x4f; // 'O'
603
+ box[offset++] = 0x70; // 'p'
604
+ box[offset++] = 0x75; // 'u'
605
+ box[offset++] = 0x73; // 's'
606
+ // SampleEntry fields
607
+ offset += 6; // reserved (6 bytes of 0)
608
+ view.setUint16(offset, 1, false);
609
+ offset += 2; // data_reference_index = 1
610
+ // AudioSampleEntry fields
611
+ offset += 8; // reserved (8 bytes of 0)
612
+ view.setUint16(offset, channelCount, false);
613
+ offset += 2;
614
+ view.setUint16(offset, 16, false);
615
+ offset += 2; // samplesize = 16
616
+ view.setUint16(offset, 0, false);
617
+ offset += 2; // pre_defined
618
+ view.setUint16(offset, 0, false);
619
+ offset += 2; // reserved
620
+ view.setUint32(offset, sampleRate * 0x10000, false);
621
+ offset += 4; // samplerate (16.16 fixed point)
622
+ // dOps child box (already includes box header)
623
+ box.set(dOps, offset);
624
+ return box;
625
+ }
626
+ /**
627
+ * Creates an esds (Elementary Stream Descriptor) box for AAC.
628
+ * The description from WebCodecs is the AudioSpecificConfig.
629
+ * If no description is provided, generates one from sampleRate and channelCount.
630
+ */
631
+ function createEsdsBox(sampleRate, channelCount, description) {
632
+ const audioSpecificConfig = description
633
+ ? Hex.toBytes(description)
634
+ : Aac.audioSpecificConfig(sampleRate, channelCount);
635
+ // ES_Descriptor structure:
636
+ // - tag (0x03) + size + ES_ID (2) + flags (1)
637
+ // - DecoderConfigDescriptor: tag (0x04) + size + objectTypeIndication (1) + streamType (1) + bufferSizeDB (3) + maxBitrate (4) + avgBitrate (4)
638
+ // - DecoderSpecificInfo: tag (0x05) + size + AudioSpecificConfig
639
+ // - SLConfigDescriptor: tag (0x06) + size + predefined (1)
640
+ const decSpecificInfoSize = audioSpecificConfig.length;
641
+ const decConfigDescSize = 13 + 2 + decSpecificInfoSize; // 13 fixed + tag/size + ASC
642
+ const esDescSize = 3 + 2 + decConfigDescSize + 3; // 3 fixed + tag/size + DCD + SLC (3 bytes)
643
+ const esdsSize = 12 + 2 + esDescSize; // 4 (size) + 4 (type) + 4 (version/flags) + tag/size + ESD
644
+ const esds = new Uint8Array(esdsSize);
645
+ const view = new DataView(esds.buffer);
646
+ let offset = 0;
647
+ // Box header
648
+ view.setUint32(offset, esdsSize, false);
649
+ offset += 4;
650
+ esds[offset++] = 0x65; // 'e'
651
+ esds[offset++] = 0x73; // 's'
652
+ esds[offset++] = 0x64; // 'd'
653
+ esds[offset++] = 0x73; // 's'
654
+ // Version and flags (full box)
655
+ view.setUint32(offset, 0, false);
656
+ offset += 4;
657
+ // ES_Descriptor
658
+ esds[offset++] = 0x03; // tag
659
+ esds[offset++] = esDescSize; // size (assuming < 128)
660
+ view.setUint16(offset, 0, false);
661
+ offset += 2; // ES_ID
662
+ esds[offset++] = 0; // flags
663
+ // DecoderConfigDescriptor
664
+ esds[offset++] = 0x04; // tag
665
+ esds[offset++] = decConfigDescSize; // size
666
+ esds[offset++] = 0x40; // objectTypeIndication: Audio ISO/IEC 14496-3 (AAC)
667
+ esds[offset++] = 0x15; // streamType (5 = audio) << 2 | upstream (0) << 1 | reserved (1)
668
+ esds[offset++] = 0x00; // bufferSizeDB (3 bytes)
669
+ esds[offset++] = 0x00;
670
+ esds[offset++] = 0x00;
671
+ view.setUint32(offset, 0, false);
672
+ offset += 4; // maxBitrate
673
+ view.setUint32(offset, 0, false);
674
+ offset += 4; // avgBitrate
675
+ // DecoderSpecificInfo (AudioSpecificConfig)
676
+ esds[offset++] = 0x05; // tag
677
+ esds[offset++] = decSpecificInfoSize; // size
678
+ esds.set(audioSpecificConfig, offset);
679
+ offset += decSpecificInfoSize;
680
+ // SLConfigDescriptor
681
+ esds[offset++] = 0x06; // tag
682
+ esds[offset++] = 0x01; // size
683
+ esds[offset++] = 0x02; // predefined = MP4
684
+ return esds;
685
+ }
686
+ /**
687
+ * Creates a dOps (Opus Specific) box.
688
+ * See https://opus-codec.org/docs/opus_in_isobmff.html
689
+ */
690
+ function createDOpsBox(channelCount, sampleRate, description) {
691
+ // If description is provided, it's the OpusHead without the magic signature
692
+ if (description) {
693
+ const opusHead = Hex.toBytes(description);
694
+ const dOpsSize = 8 + opusHead.length;
695
+ const dOps = new Uint8Array(dOpsSize);
696
+ const view = new DataView(dOps.buffer);
697
+ view.setUint32(0, dOpsSize, false);
698
+ dOps[4] = 0x64; // 'd'
699
+ dOps[5] = 0x4f; // 'O'
700
+ dOps[6] = 0x70; // 'p'
701
+ dOps[7] = 0x73; // 's'
702
+ dOps.set(opusHead, 8);
703
+ return dOps;
704
+ }
705
+ // Build minimal dOps box
706
+ // dOps structure: Version (1) + OutputChannelCount (1) + PreSkip (2) +
707
+ // InputSampleRate (4) + OutputGain (2) + ChannelMappingFamily (1)
708
+ const dOpsSize = 8 + 11; // box header + content
709
+ const dOps = new Uint8Array(dOpsSize);
710
+ const view = new DataView(dOps.buffer);
711
+ let offset = 0;
712
+ view.setUint32(offset, dOpsSize, false);
713
+ offset += 4;
714
+ dOps[offset++] = 0x64; // 'd'
715
+ dOps[offset++] = 0x4f; // 'O'
716
+ dOps[offset++] = 0x70; // 'p'
717
+ dOps[offset++] = 0x73; // 's'
718
+ dOps[offset++] = 0; // Version
719
+ dOps[offset++] = channelCount;
720
+ view.setUint16(offset, 312, false);
721
+ offset += 2; // PreSkip (typical value)
722
+ view.setUint32(offset, sampleRate, false);
723
+ offset += 4; // InputSampleRate
724
+ view.setInt16(offset, 0, false);
725
+ offset += 2; // OutputGain
726
+ dOps[offset++] = 0; // ChannelMappingFamily (0 = mono/stereo)
727
+ return dOps;
728
+ }
729
+ /**
730
+ * Encode a raw frame into a moof+mdat segment for MSE.
731
+ *
732
+ * @param opts - Options for the data segment
733
+ * @returns The encoded moof+mdat segment
734
+ */
735
+ export function encodeDataSegment(opts) {
736
+ const { data, timestamp, duration, keyframe, sequence, trackId = 1 } = opts;
737
+ // Sample flags:
738
+ // - sample_depends_on: bits 25-24 (2 = does not depend on others for IDR, 1 = depends on others)
739
+ // - sample_is_non_sync_sample: bit 16 (0 = sync/keyframe, 1 = non-sync)
740
+ // For keyframe: depends_on=2 (0x02000000), non_sync=0
741
+ // For non-keyframe: depends_on=1 (0x01000000), non_sync=1 (0x00010000)
742
+ const sampleFlags = keyframe ? 0x02000000 : 0x01010000;
743
+ // mfhd - Movie Fragment Header
744
+ const mfhd = {
745
+ type: "mfhd",
746
+ version: 0,
747
+ flags: 0,
748
+ sequenceNumber: sequence,
749
+ };
750
+ // tfhd - Track Fragment Header
751
+ // Flags: default-base-is-moof (0x020000)
752
+ const tfhd = {
753
+ type: "tfhd",
754
+ version: 0,
755
+ flags: 0x020000,
756
+ trackId,
757
+ };
758
+ // tfdt - Track Fragment Base Media Decode Time
759
+ const tfdt = {
760
+ type: "tfdt",
761
+ version: 1, // version 1 for 64-bit baseMediaDecodeTime
762
+ flags: 0,
763
+ baseMediaDecodeTime: timestamp,
764
+ };
765
+ // trun - Track Run
766
+ // Flags: data-offset-present (0x000001) | sample-duration-present (0x000100) |
767
+ // sample-size-present (0x000200) | sample-flags-present (0x000400)
768
+ const trun = {
769
+ type: "trun",
770
+ version: 0,
771
+ flags: 0x000001 | 0x000100 | 0x000200 | 0x000400,
772
+ sampleCount: 1,
773
+ dataOffset: 0, // Will be calculated after we know moof size
774
+ samples: [
775
+ {
776
+ sampleDuration: duration,
777
+ sampleSize: data.byteLength,
778
+ sampleFlags,
779
+ },
780
+ ],
781
+ };
782
+ // traf - Track Fragment
783
+ const traf = {
784
+ type: "traf",
785
+ boxes: [tfhd, tfdt, trun],
786
+ };
787
+ // moof - Movie Fragment
788
+ const moof = {
789
+ type: "moof",
790
+ boxes: [mfhd, traf],
791
+ };
792
+ // Write moof to calculate its size
793
+ const moofBuffers = writeBoxes([moof]);
794
+ let moofSize = 0;
795
+ for (const buf of moofBuffers) {
796
+ moofSize += buf.byteLength;
797
+ }
798
+ // Update trun.dataOffset to point to mdat data
799
+ // dataOffset is relative to moof start, so it's moofSize + 8 (mdat header)
800
+ trun.dataOffset = moofSize + 8;
801
+ // Re-write moof with correct dataOffset
802
+ const moofBuffersFinal = writeBoxes([moof]);
803
+ moofSize = 0;
804
+ for (const buf of moofBuffersFinal) {
805
+ moofSize += buf.byteLength;
806
+ }
807
+ // mdat - Media Data
808
+ // Need to ensure the data is a proper ArrayBuffer-backed Uint8Array for the library
809
+ const mdatBuffer = new ArrayBuffer(data.byteLength);
810
+ const mdatData = new Uint8Array(mdatBuffer);
811
+ mdatData.set(data);
812
+ const mdat = {
813
+ type: "mdat",
814
+ data: mdatData,
815
+ };
816
+ const mdatBuffers = writeBoxes([mdat]);
817
+ let mdatSize = 0;
818
+ for (const buf of mdatBuffers) {
819
+ mdatSize += buf.byteLength;
820
+ }
821
+ // Concatenate all buffers
822
+ const result = new Uint8Array(moofSize + mdatSize);
823
+ let offset = 0;
824
+ for (const buf of moofBuffersFinal) {
825
+ result.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
826
+ offset += buf.byteLength;
827
+ }
828
+ for (const buf of mdatBuffers) {
829
+ result.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), offset);
830
+ offset += buf.byteLength;
831
+ }
832
+ return result;
833
+ }
834
+ //# sourceMappingURL=encode.js.map