@invintusmedia/tomp4 1.0.4 → 1.0.6
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 +16 -0
- package/dist/tomp4.js +17 -1048
- package/package.json +2 -2
- package/src/hls.js +11 -5
- package/src/index.js +16 -2
- package/src/muxers/mp4.js +692 -0
- package/src/muxers/mpegts.js +356 -0
- package/src/parsers/mpegts.js +376 -0
- package/src/transcode.js +838 -0
- package/src/ts-to-mp4.js +15 -1046
- package/src/index.d.ts +0 -135
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MP4 Muxer
|
|
3
|
+
*
|
|
4
|
+
* Creates MP4 container from parsed video/audio access units.
|
|
5
|
+
* Takes TSParser output and builds a complete MP4 file.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { MP4Muxer } from 'tomp4';
|
|
9
|
+
* import { TSParser } from 'tomp4';
|
|
10
|
+
*
|
|
11
|
+
* const parser = new TSParser();
|
|
12
|
+
* parser.parse(tsData);
|
|
13
|
+
* parser.finalize();
|
|
14
|
+
*
|
|
15
|
+
* const muxer = new MP4Muxer(parser);
|
|
16
|
+
* const mp4Data = muxer.build();
|
|
17
|
+
*
|
|
18
|
+
* @module muxers/mp4
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// MP4 BOX HELPERS
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
export function createBox(type, ...payloads) {
|
|
26
|
+
let size = 8;
|
|
27
|
+
for (const p of payloads) size += p.byteLength;
|
|
28
|
+
const result = new Uint8Array(size);
|
|
29
|
+
const view = new DataView(result.buffer);
|
|
30
|
+
view.setUint32(0, size);
|
|
31
|
+
result[4] = type.charCodeAt(0);
|
|
32
|
+
result[5] = type.charCodeAt(1);
|
|
33
|
+
result[6] = type.charCodeAt(2);
|
|
34
|
+
result[7] = type.charCodeAt(3);
|
|
35
|
+
let offset = 8;
|
|
36
|
+
for (const p of payloads) {
|
|
37
|
+
result.set(p, offset);
|
|
38
|
+
offset += p.byteLength;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createFullBox(type, version, flags, ...payloads) {
|
|
44
|
+
const header = new Uint8Array(4);
|
|
45
|
+
header[0] = version;
|
|
46
|
+
header[1] = (flags >> 16) & 0xFF;
|
|
47
|
+
header[2] = (flags >> 8) & 0xFF;
|
|
48
|
+
header[3] = flags & 0xFF;
|
|
49
|
+
return createBox(type, header, ...payloads);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// H.264 SPS Parser
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse H.264 SPS to extract video dimensions
|
|
58
|
+
* @param {Uint8Array} sps - SPS NAL unit
|
|
59
|
+
* @returns {object} { width, height }
|
|
60
|
+
*/
|
|
61
|
+
export function parseSPS(sps) {
|
|
62
|
+
const result = { width: 1920, height: 1080 };
|
|
63
|
+
if (!sps || sps.length < 4) return result;
|
|
64
|
+
|
|
65
|
+
let offset = 1;
|
|
66
|
+
const profile = sps[offset++];
|
|
67
|
+
offset++; // constraint flags
|
|
68
|
+
offset++; // level_idc
|
|
69
|
+
|
|
70
|
+
let bitPos = offset * 8;
|
|
71
|
+
const getBit = () => (sps[Math.floor(bitPos / 8)] >> (7 - (bitPos++ % 8))) & 1;
|
|
72
|
+
const readUE = () => {
|
|
73
|
+
let zeros = 0;
|
|
74
|
+
while (bitPos < sps.length * 8 && getBit() === 0) zeros++;
|
|
75
|
+
let val = (1 << zeros) - 1;
|
|
76
|
+
for (let i = 0; i < zeros; i++) val += getBit() << (zeros - 1 - i);
|
|
77
|
+
return val;
|
|
78
|
+
};
|
|
79
|
+
const readSE = () => {
|
|
80
|
+
const val = readUE();
|
|
81
|
+
return (val & 1) ? (val + 1) >> 1 : -(val >> 1);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
readUE(); // seq_parameter_set_id
|
|
86
|
+
|
|
87
|
+
if (profile === 100 || profile === 110 || profile === 122 || profile === 244 ||
|
|
88
|
+
profile === 44 || profile === 83 || profile === 86 || profile === 118 || profile === 128) {
|
|
89
|
+
const chromaFormat = readUE();
|
|
90
|
+
if (chromaFormat === 3) getBit();
|
|
91
|
+
readUE(); readUE();
|
|
92
|
+
getBit();
|
|
93
|
+
if (getBit()) {
|
|
94
|
+
for (let i = 0; i < (chromaFormat !== 3 ? 8 : 12); i++) {
|
|
95
|
+
if (getBit()) {
|
|
96
|
+
const size = i < 6 ? 16 : 64;
|
|
97
|
+
for (let j = 0; j < size; j++) readSE();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readUE(); // log2_max_frame_num_minus4
|
|
104
|
+
const pocType = readUE();
|
|
105
|
+
if (pocType === 0) {
|
|
106
|
+
readUE();
|
|
107
|
+
} else if (pocType === 1) {
|
|
108
|
+
getBit(); readSE(); readSE();
|
|
109
|
+
const numRefFrames = readUE();
|
|
110
|
+
for (let i = 0; i < numRefFrames; i++) readSE();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
readUE(); getBit();
|
|
114
|
+
|
|
115
|
+
const picWidthMbs = readUE() + 1;
|
|
116
|
+
const picHeightMapUnits = readUE() + 1;
|
|
117
|
+
const frameMbsOnly = getBit();
|
|
118
|
+
|
|
119
|
+
if (!frameMbsOnly) getBit();
|
|
120
|
+
getBit();
|
|
121
|
+
|
|
122
|
+
let cropLeft = 0, cropRight = 0, cropTop = 0, cropBottom = 0;
|
|
123
|
+
if (getBit()) {
|
|
124
|
+
cropLeft = readUE();
|
|
125
|
+
cropRight = readUE();
|
|
126
|
+
cropTop = readUE();
|
|
127
|
+
cropBottom = readUE();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mbWidth = 16;
|
|
131
|
+
const mbHeight = frameMbsOnly ? 16 : 32;
|
|
132
|
+
result.width = picWidthMbs * mbWidth - (cropLeft + cropRight) * 2;
|
|
133
|
+
result.height = (2 - frameMbsOnly) * picHeightMapUnits * mbHeight / (frameMbsOnly ? 1 : 2) - (cropTop + cropBottom) * 2;
|
|
134
|
+
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Fall back to defaults
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================
|
|
143
|
+
// MP4 Muxer
|
|
144
|
+
// ============================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* MP4 Muxer - creates MP4 container from parsed access units
|
|
148
|
+
*/
|
|
149
|
+
export class MP4Muxer {
|
|
150
|
+
/**
|
|
151
|
+
* @param {TSParser} parser - Parser with video/audio access units
|
|
152
|
+
*/
|
|
153
|
+
constructor(parser) {
|
|
154
|
+
this.parser = parser;
|
|
155
|
+
this.videoTimescale = 90000;
|
|
156
|
+
this.audioTimescale = parser.audioSampleRate || 48000;
|
|
157
|
+
this.audioSampleDuration = 1024;
|
|
158
|
+
this.videoDimensions = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getVideoDimensions() {
|
|
162
|
+
if (this.videoDimensions) return this.videoDimensions;
|
|
163
|
+
|
|
164
|
+
for (const au of this.parser.videoAccessUnits) {
|
|
165
|
+
for (const nalUnit of au.nalUnits) {
|
|
166
|
+
const nalType = nalUnit[0] & 0x1F;
|
|
167
|
+
if (nalType === 7) {
|
|
168
|
+
this.videoDimensions = parseSPS(nalUnit);
|
|
169
|
+
return this.videoDimensions;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.videoDimensions = { width: 1920, height: 1080 };
|
|
175
|
+
return this.videoDimensions;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build complete MP4 file
|
|
180
|
+
* @returns {Uint8Array}
|
|
181
|
+
*/
|
|
182
|
+
build() {
|
|
183
|
+
const mdatContent = this.buildMdatContent();
|
|
184
|
+
const moov = this.buildMoov(mdatContent.byteLength);
|
|
185
|
+
const ftyp = this.buildFtyp();
|
|
186
|
+
const mdatOffset = ftyp.byteLength + moov.byteLength + 8;
|
|
187
|
+
this.updateChunkOffsets(moov, mdatOffset);
|
|
188
|
+
const mdat = createBox('mdat', mdatContent);
|
|
189
|
+
const result = new Uint8Array(ftyp.byteLength + moov.byteLength + mdat.byteLength);
|
|
190
|
+
result.set(ftyp, 0);
|
|
191
|
+
result.set(moov, ftyp.byteLength);
|
|
192
|
+
result.set(mdat, ftyp.byteLength + moov.byteLength);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
buildFtyp() {
|
|
197
|
+
const data = new Uint8Array(16);
|
|
198
|
+
data[0] = 'i'.charCodeAt(0); data[1] = 's'.charCodeAt(0); data[2] = 'o'.charCodeAt(0); data[3] = 'm'.charCodeAt(0);
|
|
199
|
+
data[7] = 1;
|
|
200
|
+
data[8] = 'i'.charCodeAt(0); data[9] = 's'.charCodeAt(0); data[10] = 'o'.charCodeAt(0); data[11] = 'm'.charCodeAt(0);
|
|
201
|
+
data[12] = 'a'.charCodeAt(0); data[13] = 'v'.charCodeAt(0); data[14] = 'c'.charCodeAt(0); data[15] = '1'.charCodeAt(0);
|
|
202
|
+
return createBox('ftyp', data);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
buildMdatContent() {
|
|
206
|
+
const chunks = [];
|
|
207
|
+
this.videoSampleSizes = [];
|
|
208
|
+
this.videoSampleOffsets = [];
|
|
209
|
+
let currentOffset = 0;
|
|
210
|
+
for (const au of this.parser.videoAccessUnits) {
|
|
211
|
+
this.videoSampleOffsets.push(currentOffset);
|
|
212
|
+
let sampleSize = 0;
|
|
213
|
+
for (const nalUnit of au.nalUnits) {
|
|
214
|
+
const prefixed = new Uint8Array(4 + nalUnit.length);
|
|
215
|
+
new DataView(prefixed.buffer).setUint32(0, nalUnit.length);
|
|
216
|
+
prefixed.set(nalUnit, 4);
|
|
217
|
+
chunks.push(prefixed);
|
|
218
|
+
sampleSize += prefixed.length;
|
|
219
|
+
}
|
|
220
|
+
this.videoSampleSizes.push(sampleSize);
|
|
221
|
+
currentOffset += sampleSize;
|
|
222
|
+
}
|
|
223
|
+
this.videoChunkOffset = 0;
|
|
224
|
+
this.audioChunkOffset = currentOffset;
|
|
225
|
+
this.audioSampleSizes = [];
|
|
226
|
+
for (const frame of this.parser.audioAccessUnits) {
|
|
227
|
+
chunks.push(frame.data);
|
|
228
|
+
this.audioSampleSizes.push(frame.data.length);
|
|
229
|
+
currentOffset += frame.data.length;
|
|
230
|
+
}
|
|
231
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
232
|
+
const result = new Uint8Array(totalSize);
|
|
233
|
+
let offset = 0;
|
|
234
|
+
for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; }
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
buildMoov(mdatSize) {
|
|
239
|
+
const mvhd = this.buildMvhd();
|
|
240
|
+
const videoTrak = this.buildVideoTrak();
|
|
241
|
+
const audioTrak = this.buildAudioTrak();
|
|
242
|
+
const udta = this.buildUdta();
|
|
243
|
+
return createBox('moov', mvhd, videoTrak, audioTrak, udta);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
buildUdta() {
|
|
247
|
+
const toolName = 'toMp4.js';
|
|
248
|
+
const toolBytes = new TextEncoder().encode(toolName);
|
|
249
|
+
const dataBox = new Uint8Array(16 + toolBytes.length);
|
|
250
|
+
const dataView = new DataView(dataBox.buffer);
|
|
251
|
+
dataView.setUint32(0, 16 + toolBytes.length);
|
|
252
|
+
dataBox[4] = 'd'.charCodeAt(0); dataBox[5] = 'a'.charCodeAt(0); dataBox[6] = 't'.charCodeAt(0); dataBox[7] = 'a'.charCodeAt(0);
|
|
253
|
+
dataView.setUint32(8, 1); dataView.setUint32(12, 0);
|
|
254
|
+
dataBox.set(toolBytes, 16);
|
|
255
|
+
const tooBox = createBox('©too', dataBox);
|
|
256
|
+
const ilst = createBox('ilst', tooBox);
|
|
257
|
+
const hdlrData = new Uint8Array(21);
|
|
258
|
+
hdlrData[4] = 'm'.charCodeAt(0); hdlrData[5] = 'd'.charCodeAt(0); hdlrData[6] = 'i'.charCodeAt(0); hdlrData[7] = 'r'.charCodeAt(0);
|
|
259
|
+
const metaHdlr = createFullBox('hdlr', 0, 0, hdlrData);
|
|
260
|
+
const meta = createFullBox('meta', 0, 0, new Uint8Array(0), metaHdlr, ilst);
|
|
261
|
+
return createBox('udta', meta);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
buildMvhd() {
|
|
265
|
+
const data = new Uint8Array(96);
|
|
266
|
+
const view = new DataView(data.buffer);
|
|
267
|
+
view.setUint32(8, this.videoTimescale);
|
|
268
|
+
view.setUint32(12, this.calculateVideoDuration());
|
|
269
|
+
view.setUint32(16, 0x00010000);
|
|
270
|
+
view.setUint16(20, 0x0100);
|
|
271
|
+
view.setUint32(32, 0x00010000);
|
|
272
|
+
view.setUint32(48, 0x00010000);
|
|
273
|
+
view.setUint32(64, 0x40000000);
|
|
274
|
+
view.setUint32(92, 258);
|
|
275
|
+
return createFullBox('mvhd', 0, 0, data);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
calculateVideoDuration() {
|
|
279
|
+
if (this.parser.videoDts.length < 2) return 0;
|
|
280
|
+
const firstDts = this.parser.videoDts[0];
|
|
281
|
+
const lastDts = this.parser.videoDts[this.parser.videoDts.length - 1];
|
|
282
|
+
const avgDuration = (lastDts - firstDts) / (this.parser.videoDts.length - 1);
|
|
283
|
+
return Math.round(lastDts - firstDts + avgDuration);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
buildVideoTrak() {
|
|
287
|
+
const edts = this.buildVideoEdts();
|
|
288
|
+
if (edts) {
|
|
289
|
+
return createBox('trak', this.buildVideoTkhd(), edts, this.buildVideoMdia());
|
|
290
|
+
}
|
|
291
|
+
return createBox('trak', this.buildVideoTkhd(), this.buildVideoMdia());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
buildVideoEdts() {
|
|
295
|
+
if (this.parser.videoAccessUnits.length === 0) return null;
|
|
296
|
+
|
|
297
|
+
const firstAU = this.parser.videoAccessUnits[0];
|
|
298
|
+
const firstVideoPts = firstAU.pts;
|
|
299
|
+
|
|
300
|
+
if (firstVideoPts === 0) return null;
|
|
301
|
+
|
|
302
|
+
const duration = this.calculateVideoDuration();
|
|
303
|
+
const mediaTime = firstVideoPts;
|
|
304
|
+
|
|
305
|
+
const elstData = new Uint8Array(16);
|
|
306
|
+
const view = new DataView(elstData.buffer);
|
|
307
|
+
view.setUint32(0, 1);
|
|
308
|
+
view.setUint32(4, duration);
|
|
309
|
+
view.setInt32(8, mediaTime);
|
|
310
|
+
view.setUint16(12, 1);
|
|
311
|
+
view.setUint16(14, 0);
|
|
312
|
+
|
|
313
|
+
const elst = createFullBox('elst', 0, 0, elstData);
|
|
314
|
+
return createBox('edts', elst);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
buildVideoTkhd() {
|
|
318
|
+
const { width, height } = this.getVideoDimensions();
|
|
319
|
+
const data = new Uint8Array(80);
|
|
320
|
+
const view = new DataView(data.buffer);
|
|
321
|
+
view.setUint32(8, 256);
|
|
322
|
+
view.setUint32(16, this.calculateVideoDuration());
|
|
323
|
+
view.setUint16(32, 0);
|
|
324
|
+
view.setUint32(36, 0x00010000);
|
|
325
|
+
view.setUint32(52, 0x00010000);
|
|
326
|
+
view.setUint32(68, 0x40000000);
|
|
327
|
+
view.setUint32(72, width << 16);
|
|
328
|
+
view.setUint32(76, height << 16);
|
|
329
|
+
return createFullBox('tkhd', 0, 3, data);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
buildVideoMdia() {
|
|
333
|
+
return createBox('mdia', this.buildVideoMdhd(), this.buildVideoHdlr(), this.buildVideoMinf());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
buildVideoMdhd() {
|
|
337
|
+
const data = new Uint8Array(20);
|
|
338
|
+
const view = new DataView(data.buffer);
|
|
339
|
+
view.setUint32(8, this.videoTimescale);
|
|
340
|
+
view.setUint32(12, this.calculateVideoDuration());
|
|
341
|
+
view.setUint16(16, 0x55C4);
|
|
342
|
+
return createFullBox('mdhd', 0, 0, data);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
buildVideoHdlr() {
|
|
346
|
+
const data = new Uint8Array(21);
|
|
347
|
+
data[4] = 'v'.charCodeAt(0); data[5] = 'i'.charCodeAt(0); data[6] = 'd'.charCodeAt(0); data[7] = 'e'.charCodeAt(0);
|
|
348
|
+
return createFullBox('hdlr', 0, 0, data);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
buildVideoMinf() {
|
|
352
|
+
return createBox('minf', this.buildVmhd(), this.buildDinf(), this.buildVideoStbl());
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
buildVmhd() { return createFullBox('vmhd', 0, 1, new Uint8Array(8)); }
|
|
356
|
+
|
|
357
|
+
buildDinf() {
|
|
358
|
+
const urlBox = createFullBox('url ', 0, 1, new Uint8Array(0));
|
|
359
|
+
const dref = createFullBox('dref', 0, 0, new Uint8Array([0, 0, 0, 1]), urlBox);
|
|
360
|
+
return createBox('dinf', dref);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
buildVideoStbl() {
|
|
364
|
+
const boxes = [this.buildVideoStsd(), this.buildVideoStts(), this.buildVideoCtts(), this.buildVideoStsc(), this.buildVideoStsz(), this.buildVideoStco()];
|
|
365
|
+
const stss = this.buildVideoStss();
|
|
366
|
+
if (stss) boxes.push(stss);
|
|
367
|
+
return createBox('stbl', ...boxes);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
buildVideoStsd() {
|
|
371
|
+
const { width, height } = this.getVideoDimensions();
|
|
372
|
+
const avcC = this.buildAvcC();
|
|
373
|
+
const btrtData = new Uint8Array(12);
|
|
374
|
+
const btrtView = new DataView(btrtData.buffer);
|
|
375
|
+
btrtView.setUint32(4, 2000000); btrtView.setUint32(8, 2000000);
|
|
376
|
+
const btrt = createBox('btrt', btrtData);
|
|
377
|
+
const paspData = new Uint8Array(8);
|
|
378
|
+
const paspView = new DataView(paspData.buffer);
|
|
379
|
+
paspView.setUint32(0, 1); paspView.setUint32(4, 1);
|
|
380
|
+
const pasp = createBox('pasp', paspData);
|
|
381
|
+
const avc1Data = new Uint8Array(78 + avcC.byteLength + btrt.byteLength + pasp.byteLength);
|
|
382
|
+
const view = new DataView(avc1Data.buffer);
|
|
383
|
+
view.setUint16(6, 1); view.setUint16(24, width); view.setUint16(26, height);
|
|
384
|
+
view.setUint32(28, 0x00480000); view.setUint32(32, 0x00480000);
|
|
385
|
+
view.setUint16(40, 1); view.setUint16(74, 0x0018); view.setInt16(76, -1);
|
|
386
|
+
avc1Data.set(avcC, 78); avc1Data.set(btrt, 78 + avcC.byteLength); avc1Data.set(pasp, 78 + avcC.byteLength + btrt.byteLength);
|
|
387
|
+
const avc1 = createBox('avc1', avc1Data);
|
|
388
|
+
const stsdHeader = new Uint8Array(4);
|
|
389
|
+
new DataView(stsdHeader.buffer).setUint32(0, 1);
|
|
390
|
+
return createFullBox('stsd', 0, 0, stsdHeader, avc1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
buildAvcC() {
|
|
394
|
+
let sps = null, pps = null;
|
|
395
|
+
for (const au of this.parser.videoAccessUnits) {
|
|
396
|
+
for (const nalUnit of au.nalUnits) {
|
|
397
|
+
const nalType = nalUnit[0] & 0x1F;
|
|
398
|
+
if (nalType === 7 && !sps) sps = nalUnit;
|
|
399
|
+
if (nalType === 8 && !pps) pps = nalUnit;
|
|
400
|
+
if (sps && pps) break;
|
|
401
|
+
}
|
|
402
|
+
if (sps && pps) break;
|
|
403
|
+
}
|
|
404
|
+
if (!sps || !pps) {
|
|
405
|
+
sps = new Uint8Array([0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x78, 0x02, 0x27, 0xe5, 0xc0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc6, 0x58]);
|
|
406
|
+
pps = new Uint8Array([0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0]);
|
|
407
|
+
}
|
|
408
|
+
const data = new Uint8Array(11 + sps.length + pps.length);
|
|
409
|
+
const view = new DataView(data.buffer);
|
|
410
|
+
data[0] = 1; data[1] = sps[1]; data[2] = sps[2]; data[3] = sps[3]; data[4] = 0xFF; data[5] = 0xE1;
|
|
411
|
+
view.setUint16(6, sps.length); data.set(sps, 8);
|
|
412
|
+
data[8 + sps.length] = 1; view.setUint16(9 + sps.length, pps.length); data.set(pps, 11 + sps.length);
|
|
413
|
+
return createBox('avcC', data);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
buildVideoStts() {
|
|
417
|
+
const entries = [];
|
|
418
|
+
let lastDuration = -1, count = 0;
|
|
419
|
+
for (let i = 0; i < this.parser.videoDts.length; i++) {
|
|
420
|
+
const duration = i < this.parser.videoDts.length - 1
|
|
421
|
+
? this.parser.videoDts[i + 1] - this.parser.videoDts[i]
|
|
422
|
+
: (entries.length > 0 ? entries[entries.length - 1].duration : 3003);
|
|
423
|
+
if (duration === lastDuration) count++;
|
|
424
|
+
else { if (count > 0) entries.push({ count, duration: lastDuration }); lastDuration = duration; count = 1; }
|
|
425
|
+
}
|
|
426
|
+
if (count > 0) entries.push({ count, duration: lastDuration });
|
|
427
|
+
const data = new Uint8Array(4 + entries.length * 8);
|
|
428
|
+
const view = new DataView(data.buffer);
|
|
429
|
+
view.setUint32(0, entries.length);
|
|
430
|
+
for (let i = 0; i < entries.length; i++) { view.setUint32(4 + i * 8, entries[i].count); view.setUint32(8 + i * 8, entries[i].duration); }
|
|
431
|
+
return createFullBox('stts', 0, 0, data);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
buildVideoCtts() {
|
|
435
|
+
const entries = [];
|
|
436
|
+
for (const au of this.parser.videoAccessUnits) {
|
|
437
|
+
const cts = au.pts - au.dts;
|
|
438
|
+
if (entries.length > 0 && entries[entries.length - 1].offset === cts) entries[entries.length - 1].count++;
|
|
439
|
+
else entries.push({ count: 1, offset: cts });
|
|
440
|
+
}
|
|
441
|
+
const data = new Uint8Array(4 + entries.length * 8);
|
|
442
|
+
const view = new DataView(data.buffer);
|
|
443
|
+
view.setUint32(0, entries.length);
|
|
444
|
+
for (let i = 0; i < entries.length; i++) { view.setUint32(4 + i * 8, entries[i].count); view.setUint32(8 + i * 8, entries[i].offset); }
|
|
445
|
+
return createFullBox('ctts', 0, 0, data);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
buildVideoStsc() {
|
|
449
|
+
const data = new Uint8Array(4 + 12);
|
|
450
|
+
const view = new DataView(data.buffer);
|
|
451
|
+
view.setUint32(0, 1); view.setUint32(4, 1); view.setUint32(8, this.videoSampleSizes.length); view.setUint32(12, 1);
|
|
452
|
+
return createFullBox('stsc', 0, 0, data);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
buildVideoStsz() {
|
|
456
|
+
const data = new Uint8Array(8 + this.videoSampleSizes.length * 4);
|
|
457
|
+
const view = new DataView(data.buffer);
|
|
458
|
+
view.setUint32(0, 0); view.setUint32(4, this.videoSampleSizes.length);
|
|
459
|
+
for (let i = 0; i < this.videoSampleSizes.length; i++) view.setUint32(8 + i * 4, this.videoSampleSizes[i]);
|
|
460
|
+
return createFullBox('stsz', 0, 0, data);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
buildVideoStco() {
|
|
464
|
+
const data = new Uint8Array(8);
|
|
465
|
+
const view = new DataView(data.buffer);
|
|
466
|
+
view.setUint32(0, 1); view.setUint32(4, 0);
|
|
467
|
+
return createFullBox('stco', 0, 0, data);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
buildVideoStss() {
|
|
471
|
+
const keyframes = [];
|
|
472
|
+
for (let i = 0; i < this.parser.videoAccessUnits.length; i++) {
|
|
473
|
+
for (const nalUnit of this.parser.videoAccessUnits[i].nalUnits) {
|
|
474
|
+
if ((nalUnit[0] & 0x1F) === 5) { keyframes.push(i + 1); break; }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (keyframes.length === 0) return null;
|
|
478
|
+
const data = new Uint8Array(4 + keyframes.length * 4);
|
|
479
|
+
const view = new DataView(data.buffer);
|
|
480
|
+
view.setUint32(0, keyframes.length);
|
|
481
|
+
for (let i = 0; i < keyframes.length; i++) view.setUint32(4 + i * 4, keyframes[i]);
|
|
482
|
+
return createFullBox('stss', 0, 0, data);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
buildAudioTrak() {
|
|
486
|
+
const edts = this.buildAudioEdts();
|
|
487
|
+
if (edts) {
|
|
488
|
+
return createBox('trak', this.buildAudioTkhd(), edts, this.buildAudioMdia());
|
|
489
|
+
}
|
|
490
|
+
return createBox('trak', this.buildAudioTkhd(), this.buildAudioMdia());
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
buildAudioEdts() {
|
|
494
|
+
if (this.parser.audioPts.length === 0) return null;
|
|
495
|
+
|
|
496
|
+
const firstAudioPts = this.parser.audioPts[0];
|
|
497
|
+
if (firstAudioPts === 0) return null;
|
|
498
|
+
|
|
499
|
+
const mediaTime = Math.round(firstAudioPts * this.audioTimescale / 90000);
|
|
500
|
+
const duration = this.audioSampleSizes.length * this.audioSampleDuration;
|
|
501
|
+
|
|
502
|
+
const elstData = new Uint8Array(16);
|
|
503
|
+
const view = new DataView(elstData.buffer);
|
|
504
|
+
view.setUint32(0, 1);
|
|
505
|
+
view.setUint32(4, Math.round(duration * this.videoTimescale / this.audioTimescale));
|
|
506
|
+
view.setInt32(8, mediaTime);
|
|
507
|
+
view.setUint16(12, 1);
|
|
508
|
+
view.setUint16(14, 0);
|
|
509
|
+
|
|
510
|
+
const elst = createFullBox('elst', 0, 0, elstData);
|
|
511
|
+
return createBox('edts', elst);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
buildAudioTkhd() {
|
|
515
|
+
const data = new Uint8Array(80);
|
|
516
|
+
const view = new DataView(data.buffer);
|
|
517
|
+
view.setUint32(8, 257);
|
|
518
|
+
const audioDuration = this.audioSampleSizes.length * this.audioSampleDuration;
|
|
519
|
+
view.setUint32(16, Math.round(audioDuration * this.videoTimescale / this.audioTimescale));
|
|
520
|
+
view.setUint16(32, 0x0100);
|
|
521
|
+
view.setUint32(36, 0x00010000); view.setUint32(52, 0x00010000); view.setUint32(68, 0x40000000);
|
|
522
|
+
return createFullBox('tkhd', 0, 3, data);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
buildAudioMdia() { return createBox('mdia', this.buildAudioMdhd(), this.buildAudioHdlr(), this.buildAudioMinf()); }
|
|
526
|
+
|
|
527
|
+
buildAudioMdhd() {
|
|
528
|
+
const data = new Uint8Array(20);
|
|
529
|
+
const view = new DataView(data.buffer);
|
|
530
|
+
view.setUint32(8, this.audioTimescale);
|
|
531
|
+
view.setUint32(12, this.audioSampleSizes.length * this.audioSampleDuration);
|
|
532
|
+
view.setUint16(16, 0x55C4);
|
|
533
|
+
return createFullBox('mdhd', 0, 0, data);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
buildAudioHdlr() {
|
|
537
|
+
const data = new Uint8Array(21);
|
|
538
|
+
data[4] = 's'.charCodeAt(0); data[5] = 'o'.charCodeAt(0); data[6] = 'u'.charCodeAt(0); data[7] = 'n'.charCodeAt(0);
|
|
539
|
+
return createFullBox('hdlr', 0, 0, data);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
buildAudioMinf() { return createBox('minf', this.buildSmhd(), this.buildDinf(), this.buildAudioStbl()); }
|
|
543
|
+
buildSmhd() { return createFullBox('smhd', 0, 0, new Uint8Array(4)); }
|
|
544
|
+
|
|
545
|
+
buildAudioStbl() {
|
|
546
|
+
return createBox('stbl', this.buildAudioStsd(), this.buildAudioStts(), this.buildAudioStsc(), this.buildAudioStsz(), this.buildAudioStco());
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
buildAudioStsd() {
|
|
550
|
+
const esds = this.buildEsds();
|
|
551
|
+
const channels = this.parser.audioChannels || 2;
|
|
552
|
+
const mp4aData = new Uint8Array(28 + esds.byteLength);
|
|
553
|
+
const view = new DataView(mp4aData.buffer);
|
|
554
|
+
view.setUint16(6, 1);
|
|
555
|
+
view.setUint16(16, channels);
|
|
556
|
+
view.setUint16(18, 16);
|
|
557
|
+
view.setUint32(24, this.audioTimescale << 16);
|
|
558
|
+
mp4aData.set(esds, 28);
|
|
559
|
+
const mp4a = createBox('mp4a', mp4aData);
|
|
560
|
+
const stsdHeader = new Uint8Array(4);
|
|
561
|
+
new DataView(stsdHeader.buffer).setUint32(0, 1);
|
|
562
|
+
return createFullBox('stsd', 0, 0, stsdHeader, mp4a);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
buildEsds() {
|
|
566
|
+
const SAMPLE_RATE_INDEX = {
|
|
567
|
+
96000: 0, 88200: 1, 64000: 2, 48000: 3, 44100: 4, 32000: 5,
|
|
568
|
+
24000: 6, 22050: 7, 16000: 8, 12000: 9, 11025: 10, 8000: 11, 7350: 12
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const sampleRate = this.audioTimescale;
|
|
572
|
+
const channels = this.parser.audioChannels || 2;
|
|
573
|
+
const samplingFreqIndex = SAMPLE_RATE_INDEX[sampleRate] ?? 4;
|
|
574
|
+
|
|
575
|
+
const audioConfig = ((2 << 11) | (samplingFreqIndex << 7) | (channels << 3)) & 0xFFFF;
|
|
576
|
+
const audioConfigHigh = (audioConfig >> 8) & 0xFF;
|
|
577
|
+
const audioConfigLow = audioConfig & 0xFF;
|
|
578
|
+
|
|
579
|
+
const data = new Uint8Array([
|
|
580
|
+
0x00, 0x00, 0x00, 0x00,
|
|
581
|
+
0x03, 0x19,
|
|
582
|
+
0x00, 0x02,
|
|
583
|
+
0x00,
|
|
584
|
+
0x04, 0x11,
|
|
585
|
+
0x40,
|
|
586
|
+
0x15,
|
|
587
|
+
0x00, 0x00, 0x00,
|
|
588
|
+
0x00, 0x01, 0xF4, 0x00,
|
|
589
|
+
0x00, 0x01, 0xF4, 0x00,
|
|
590
|
+
0x05, 0x02,
|
|
591
|
+
audioConfigHigh, audioConfigLow,
|
|
592
|
+
0x06, 0x01, 0x02
|
|
593
|
+
]);
|
|
594
|
+
return createBox('esds', data);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
buildAudioStts() {
|
|
598
|
+
const audioPts = this.parser.audioPts;
|
|
599
|
+
|
|
600
|
+
if (audioPts.length < 2) {
|
|
601
|
+
const data = new Uint8Array(12);
|
|
602
|
+
const view = new DataView(data.buffer);
|
|
603
|
+
view.setUint32(0, 1);
|
|
604
|
+
view.setUint32(4, this.audioSampleSizes.length);
|
|
605
|
+
view.setUint32(8, this.audioSampleDuration);
|
|
606
|
+
return createFullBox('stts', 0, 0, data);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const entries = [];
|
|
610
|
+
let lastDuration = -1, count = 0;
|
|
611
|
+
|
|
612
|
+
for (let i = 0; i < audioPts.length; i++) {
|
|
613
|
+
let duration;
|
|
614
|
+
if (i < audioPts.length - 1) {
|
|
615
|
+
const ptsDiff = audioPts[i + 1] - audioPts[i];
|
|
616
|
+
duration = Math.round(ptsDiff * this.audioTimescale / 90000);
|
|
617
|
+
} else {
|
|
618
|
+
duration = this.audioSampleDuration;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (duration <= 0 || duration > this.audioSampleDuration * 2) {
|
|
622
|
+
duration = this.audioSampleDuration;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (duration === lastDuration) {
|
|
626
|
+
count++;
|
|
627
|
+
} else {
|
|
628
|
+
if (count > 0) entries.push({ count, duration: lastDuration });
|
|
629
|
+
lastDuration = duration;
|
|
630
|
+
count = 1;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (count > 0) entries.push({ count, duration: lastDuration });
|
|
634
|
+
|
|
635
|
+
const data = new Uint8Array(4 + entries.length * 8);
|
|
636
|
+
const view = new DataView(data.buffer);
|
|
637
|
+
view.setUint32(0, entries.length);
|
|
638
|
+
for (let i = 0; i < entries.length; i++) {
|
|
639
|
+
view.setUint32(4 + i * 8, entries[i].count);
|
|
640
|
+
view.setUint32(8 + i * 8, entries[i].duration);
|
|
641
|
+
}
|
|
642
|
+
return createFullBox('stts', 0, 0, data);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
buildAudioStsc() {
|
|
646
|
+
const data = new Uint8Array(4 + 12);
|
|
647
|
+
const view = new DataView(data.buffer);
|
|
648
|
+
view.setUint32(0, 1); view.setUint32(4, 1); view.setUint32(8, this.audioSampleSizes.length); view.setUint32(12, 1);
|
|
649
|
+
return createFullBox('stsc', 0, 0, data);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
buildAudioStsz() {
|
|
653
|
+
const data = new Uint8Array(8 + this.audioSampleSizes.length * 4);
|
|
654
|
+
const view = new DataView(data.buffer);
|
|
655
|
+
view.setUint32(0, 0); view.setUint32(4, this.audioSampleSizes.length);
|
|
656
|
+
for (let i = 0; i < this.audioSampleSizes.length; i++) view.setUint32(8 + i * 4, this.audioSampleSizes[i]);
|
|
657
|
+
return createFullBox('stsz', 0, 0, data);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
buildAudioStco() {
|
|
661
|
+
const data = new Uint8Array(8);
|
|
662
|
+
const view = new DataView(data.buffer);
|
|
663
|
+
view.setUint32(0, 1); view.setUint32(4, 0);
|
|
664
|
+
return createFullBox('stco', 0, 0, data);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
updateChunkOffsets(moov, mdatOffset) { this.updateStcoInBox(moov, mdatOffset, 0); }
|
|
668
|
+
|
|
669
|
+
updateStcoInBox(data, mdatOffset, trackIndex) {
|
|
670
|
+
let offset = 8;
|
|
671
|
+
while (offset < data.byteLength - 8) {
|
|
672
|
+
const view = new DataView(data.buffer, data.byteOffset + offset);
|
|
673
|
+
const size = view.getUint32(0);
|
|
674
|
+
const type = String.fromCharCode(data[offset+4], data[offset+5], data[offset+6], data[offset+7]);
|
|
675
|
+
if (size < 8 || offset + size > data.byteLength) break;
|
|
676
|
+
if (type === 'stco') {
|
|
677
|
+
view.setUint32(16, trackIndex === 0 ? mdatOffset + this.videoChunkOffset : mdatOffset + this.audioChunkOffset);
|
|
678
|
+
trackIndex++;
|
|
679
|
+
} else if (['moov', 'trak', 'mdia', 'minf', 'stbl'].includes(type)) {
|
|
680
|
+
trackIndex = this.updateStcoInBox(data.subarray(offset, offset + size), mdatOffset, trackIndex);
|
|
681
|
+
}
|
|
682
|
+
offset += size;
|
|
683
|
+
}
|
|
684
|
+
return trackIndex;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Alias for backwards compatibility
|
|
689
|
+
export { MP4Muxer as MP4Builder };
|
|
690
|
+
|
|
691
|
+
export default MP4Muxer;
|
|
692
|
+
|