@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.
- package/README.md +65 -0
- package/catalog/audio.d.ts +139 -0
- package/catalog/audio.d.ts.map +1 -0
- package/catalog/audio.js +55 -0
- package/catalog/audio.js.map +1 -0
- package/catalog/consumer.d.ts +34 -0
- package/catalog/consumer.d.ts.map +1 -0
- package/catalog/consumer.js +30 -0
- package/catalog/consumer.js.map +1 -0
- package/catalog/container.d.ts +24 -0
- package/catalog/container.d.ts.map +1 -0
- package/catalog/container.js +30 -0
- package/catalog/container.js.map +1 -0
- package/catalog/format.d.ts +13 -0
- package/catalog/format.d.ts.map +1 -0
- package/catalog/format.js +24 -0
- package/catalog/format.js.map +1 -0
- package/catalog/index.d.ts +18 -0
- package/catalog/index.d.ts.map +1 -0
- package/catalog/index.js +19 -0
- package/catalog/index.js.map +1 -0
- package/catalog/integers.d.ts +23 -0
- package/catalog/integers.d.ts.map +1 -0
- package/catalog/integers.js +24 -0
- package/catalog/integers.js.map +1 -0
- package/catalog/priority.d.ts +7 -0
- package/catalog/priority.d.ts.map +1 -0
- package/catalog/priority.js +9 -0
- package/catalog/priority.js.map +1 -0
- package/catalog/producer.d.ts +31 -0
- package/catalog/producer.d.ts.map +1 -0
- package/catalog/producer.js +25 -0
- package/catalog/producer.js.map +1 -0
- package/catalog/root.d.ts +252 -0
- package/catalog/root.d.ts.map +1 -0
- package/catalog/root.js +17 -0
- package/catalog/root.js.map +1 -0
- package/catalog/root.test.d.ts +2 -0
- package/catalog/root.test.d.ts.map +1 -0
- package/catalog/root.test.js +20 -0
- package/catalog/root.test.js.map +1 -0
- package/catalog/timeline.d.ts +26 -0
- package/catalog/timeline.d.ts.map +1 -0
- package/catalog/timeline.js +33 -0
- package/catalog/timeline.js.map +1 -0
- package/catalog/track.d.ts +8 -0
- package/catalog/track.d.ts.map +1 -0
- package/catalog/track.js +7 -0
- package/catalog/track.js.map +1 -0
- package/catalog/video.d.ts +171 -0
- package/catalog/video.d.ts.map +1 -0
- package/catalog/video.js +87 -0
- package/catalog/video.js.map +1 -0
- package/container/cmaf/decode.d.ts +68 -0
- package/container/cmaf/decode.d.ts.map +1 -0
- package/container/cmaf/decode.js +303 -0
- package/container/cmaf/decode.js.map +1 -0
- package/container/cmaf/encode.d.ts +51 -0
- package/container/cmaf/encode.d.ts.map +1 -0
- package/container/cmaf/encode.js +834 -0
- package/container/cmaf/encode.js.map +1 -0
- package/container/cmaf/format.d.ts +12 -0
- package/container/cmaf/format.d.ts.map +1 -0
- package/container/cmaf/format.js +19 -0
- package/container/cmaf/format.js.map +1 -0
- package/container/cmaf/index.d.ts +9 -0
- package/container/cmaf/index.d.ts.map +1 -0
- package/container/cmaf/index.js +10 -0
- package/container/cmaf/index.js.map +1 -0
- package/container/consumer.d.ts +41 -0
- package/container/consumer.d.ts.map +1 -0
- package/container/consumer.js +424 -0
- package/container/consumer.js.map +1 -0
- package/container/consumer.test.d.ts +2 -0
- package/container/consumer.test.d.ts.map +1 -0
- package/container/consumer.test.js +396 -0
- package/container/consumer.test.js.map +1 -0
- package/container/format.d.ts +12 -0
- package/container/format.d.ts.map +1 -0
- package/container/format.js +3 -0
- package/container/format.js.map +1 -0
- package/container/index.d.ts +14 -0
- package/container/index.d.ts.map +1 -0
- package/container/index.js +18 -0
- package/container/index.js.map +1 -0
- package/container/legacy.d.ts +39 -0
- package/container/legacy.d.ts.map +1 -0
- package/container/legacy.js +53 -0
- package/container/legacy.js.map +1 -0
- package/container/timeline.d.ts +67 -0
- package/container/timeline.d.ts.map +1 -0
- package/container/timeline.js +87 -0
- package/container/timeline.js.map +1 -0
- package/container/types.d.ts +22 -0
- package/container/types.d.ts.map +1 -0
- package/container/types.js +22 -0
- package/container/types.js.map +1 -0
- package/index.d.ts +13 -0
- package/index.d.ts.map +1 -0
- package/index.js +18 -0
- package/index.js.map +1 -0
- package/package.json +37 -0
- package/util/aac.d.ts +8 -0
- package/util/aac.d.ts.map +1 -0
- package/util/aac.js +57 -0
- package/util/aac.js.map +1 -0
- package/util/aac.test.d.ts +2 -0
- package/util/aac.test.d.ts.map +1 -0
- package/util/aac.test.js +34 -0
- package/util/aac.test.js.map +1 -0
- package/util/hacks.d.ts +5 -0
- package/util/hacks.d.ts.map +1 -0
- package/util/hacks.js +6 -0
- package/util/hacks.js.map +1 -0
- package/util/hex.d.ts +5 -0
- package/util/hex.d.ts.map +1 -0
- package/util/hex.js +18 -0
- package/util/hex.js.map +1 -0
- package/util/index.d.ts +12 -0
- package/util/index.d.ts.map +1 -0
- package/util/index.js +18 -0
- package/util/index.js.map +1 -0
- package/util/latency.d.ts +38 -0
- package/util/latency.d.ts.map +1 -0
- package/util/latency.js +49 -0
- package/util/latency.js.map +1 -0
- package/util/libav.d.ts +3 -0
- package/util/libav.d.ts.map +1 -0
- package/util/libav.js +26 -0
- 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
|