@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPEG-TS Muxer
|
|
3
|
+
*
|
|
4
|
+
* Creates MPEG-TS container from H.264 video and AAC audio.
|
|
5
|
+
* Converts WebCodecs encoder output (AVCC format) to MPEG-TS with Annex B NAL units.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { TSMuxer } from 'tomp4';
|
|
9
|
+
*
|
|
10
|
+
* const muxer = new TSMuxer();
|
|
11
|
+
* muxer.setSpsPps(sps, pps);
|
|
12
|
+
* muxer.setHasAudio(true);
|
|
13
|
+
*
|
|
14
|
+
* // Add audio samples (ADTS format)
|
|
15
|
+
* muxer.addAudioSample(adtsData, pts90k);
|
|
16
|
+
*
|
|
17
|
+
* // Add video samples (AVCC format from WebCodecs)
|
|
18
|
+
* muxer.addVideoSample(avccData, isKeyframe, pts90k);
|
|
19
|
+
*
|
|
20
|
+
* // Finalize and get output
|
|
21
|
+
* muxer.flush();
|
|
22
|
+
* const tsData = muxer.build();
|
|
23
|
+
*
|
|
24
|
+
* @module muxers/mpegts
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// Utility Functions
|
|
29
|
+
// ============================================
|
|
30
|
+
|
|
31
|
+
function concat(arrays) {
|
|
32
|
+
const len = arrays.reduce((s, a) => s + a.length, 0);
|
|
33
|
+
const r = new Uint8Array(len);
|
|
34
|
+
let o = 0;
|
|
35
|
+
for (const a of arrays) { r.set(a, o); o += a.length; }
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// MPEG-TS Muxer
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* MPEG-TS Muxer for H.264 video and AAC audio
|
|
45
|
+
*/
|
|
46
|
+
export class TSMuxer {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.packets = [];
|
|
49
|
+
this.cc = { pat: 0, pmt: 0, video: 0, audio: 0 };
|
|
50
|
+
this.sps = null;
|
|
51
|
+
this.pps = null;
|
|
52
|
+
this.hasAudio = false;
|
|
53
|
+
this.pendingAudio = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set SPS/PPS from encoder output
|
|
58
|
+
* @param {Uint8Array} sps - Sequence Parameter Set
|
|
59
|
+
* @param {Uint8Array} pps - Picture Parameter Set
|
|
60
|
+
*/
|
|
61
|
+
setSpsPps(sps, pps) {
|
|
62
|
+
this.sps = new Uint8Array(sps);
|
|
63
|
+
this.pps = new Uint8Array(pps);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enable audio track in PMT
|
|
68
|
+
* @param {boolean} hasAudio
|
|
69
|
+
*/
|
|
70
|
+
setHasAudio(hasAudio) {
|
|
71
|
+
this.hasAudio = hasAudio;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add AAC audio frame (ADTS format)
|
|
76
|
+
* @param {Uint8Array} adtsData - ADTS-wrapped AAC frame
|
|
77
|
+
* @param {number} pts90k - Presentation timestamp in 90kHz ticks
|
|
78
|
+
*/
|
|
79
|
+
addAudioSample(adtsData, pts90k) {
|
|
80
|
+
this.pendingAudio.push({ data: new Uint8Array(adtsData), pts: pts90k });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add H.264 video sample from WebCodecs encoder (AVCC format with length prefixes)
|
|
85
|
+
* @param {Uint8Array} avccData - AVCC-formatted NAL units
|
|
86
|
+
* @param {boolean} isKey - Is this a keyframe
|
|
87
|
+
* @param {number} pts90k - Presentation timestamp in 90kHz ticks
|
|
88
|
+
*/
|
|
89
|
+
addVideoSample(avccData, isKey, pts90k) {
|
|
90
|
+
const nalUnits = [];
|
|
91
|
+
|
|
92
|
+
// Add AUD (Access Unit Delimiter) at start of each access unit
|
|
93
|
+
nalUnits.push(new Uint8Array([0, 0, 0, 1, 0x09, 0xF0]));
|
|
94
|
+
|
|
95
|
+
// If keyframe, prepend SPS/PPS
|
|
96
|
+
if (isKey && this.sps && this.pps) {
|
|
97
|
+
nalUnits.push(new Uint8Array([0, 0, 0, 1]));
|
|
98
|
+
nalUnits.push(this.sps);
|
|
99
|
+
nalUnits.push(new Uint8Array([0, 0, 0, 1]));
|
|
100
|
+
nalUnits.push(this.pps);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse AVCC NALs and convert to Annex B
|
|
104
|
+
let offset = 0;
|
|
105
|
+
while (offset < avccData.length - 4) {
|
|
106
|
+
const len = (avccData[offset] << 24) | (avccData[offset + 1] << 16) |
|
|
107
|
+
(avccData[offset + 2] << 8) | avccData[offset + 3];
|
|
108
|
+
offset += 4;
|
|
109
|
+
if (len > 0 && offset + len <= avccData.length) {
|
|
110
|
+
nalUnits.push(new Uint8Array([0, 0, 0, 1]));
|
|
111
|
+
nalUnits.push(avccData.slice(offset, offset + len));
|
|
112
|
+
}
|
|
113
|
+
offset += len;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build PES packet
|
|
117
|
+
const annexB = concat(nalUnits);
|
|
118
|
+
const pes = this._buildVideoPES(annexB, pts90k);
|
|
119
|
+
|
|
120
|
+
// Write PAT/PMT before keyframes
|
|
121
|
+
if (isKey) {
|
|
122
|
+
this.packets.push(this._buildPAT());
|
|
123
|
+
this.packets.push(this._buildPMT());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Write pending audio with PTS <= this video frame
|
|
127
|
+
while (this.pendingAudio.length > 0 && this.pendingAudio[0].pts <= pts90k) {
|
|
128
|
+
const audio = this.pendingAudio.shift();
|
|
129
|
+
const audioPes = this._buildAudioPES(audio.data, audio.pts);
|
|
130
|
+
this._packetizePES(audioPes, 0x102, false, audio.pts, 'audio');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Packetize video PES into 188-byte TS packets
|
|
134
|
+
this._packetizePES(pes, 0x101, isKey, pts90k, 'video');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Flush remaining audio samples
|
|
139
|
+
*/
|
|
140
|
+
flush() {
|
|
141
|
+
while (this.pendingAudio.length > 0) {
|
|
142
|
+
const audio = this.pendingAudio.shift();
|
|
143
|
+
const audioPes = this._buildAudioPES(audio.data, audio.pts);
|
|
144
|
+
this._packetizePES(audioPes, 0x102, false, audio.pts, 'audio');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build final MPEG-TS data
|
|
150
|
+
* @returns {Uint8Array}
|
|
151
|
+
*/
|
|
152
|
+
build() {
|
|
153
|
+
const total = this.packets.length * 188;
|
|
154
|
+
const result = new Uint8Array(total);
|
|
155
|
+
for (let i = 0; i < this.packets.length; i++) {
|
|
156
|
+
result.set(this.packets[i], i * 188);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Private methods ---
|
|
162
|
+
|
|
163
|
+
_buildVideoPES(payload, pts90k) {
|
|
164
|
+
const pes = new Uint8Array(14 + payload.length);
|
|
165
|
+
pes[0] = 0; pes[1] = 0; pes[2] = 1; // Start code
|
|
166
|
+
pes[3] = 0xE0; // Stream ID (video)
|
|
167
|
+
pes[4] = 0; pes[5] = 0; // Length = 0 (unbounded)
|
|
168
|
+
pes[6] = 0x80; // Flags
|
|
169
|
+
pes[7] = 0x80; // PTS present
|
|
170
|
+
pes[8] = 5; // Header length
|
|
171
|
+
this._writePTS(pes, 9, pts90k, 0x21);
|
|
172
|
+
pes.set(payload, 14);
|
|
173
|
+
return pes;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_buildAudioPES(payload, pts90k) {
|
|
177
|
+
const pes = new Uint8Array(14 + payload.length);
|
|
178
|
+
pes[0] = 0; pes[1] = 0; pes[2] = 1; // Start code
|
|
179
|
+
pes[3] = 0xC0; // Stream ID (audio)
|
|
180
|
+
const pesLen = 3 + 5 + payload.length;
|
|
181
|
+
pes[4] = (pesLen >> 8) & 0xFF;
|
|
182
|
+
pes[5] = pesLen & 0xFF;
|
|
183
|
+
pes[6] = 0x80;
|
|
184
|
+
pes[7] = 0x80; // PTS present
|
|
185
|
+
pes[8] = 5;
|
|
186
|
+
this._writePTS(pes, 9, pts90k, 0x21);
|
|
187
|
+
pes.set(payload, 14);
|
|
188
|
+
return pes;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_writePTS(buf, offset, pts90k, marker) {
|
|
192
|
+
const pts = BigInt(pts90k);
|
|
193
|
+
buf[offset] = marker | ((Number(pts >> 30n) & 0x07) << 1);
|
|
194
|
+
buf[offset + 1] = Number((pts >> 22n) & 0xFFn);
|
|
195
|
+
buf[offset + 2] = ((Number((pts >> 15n) & 0x7Fn) << 1) | 1);
|
|
196
|
+
buf[offset + 3] = Number((pts >> 7n) & 0xFFn);
|
|
197
|
+
buf[offset + 4] = ((Number(pts & 0x7Fn) << 1) | 1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_packetizePES(pes, pid, isKey, pts90k, type) {
|
|
201
|
+
let offset = 0;
|
|
202
|
+
let first = true;
|
|
203
|
+
const cc = type === 'audio' ? 'audio' : 'video';
|
|
204
|
+
|
|
205
|
+
while (offset < pes.length) {
|
|
206
|
+
const pkt = new Uint8Array(188);
|
|
207
|
+
pkt[0] = 0x47; // Sync byte
|
|
208
|
+
|
|
209
|
+
const payloadStart = first ? 1 : 0;
|
|
210
|
+
pkt[1] = (payloadStart << 6) | ((pid >> 8) & 0x1F);
|
|
211
|
+
pkt[2] = pid & 0xFF;
|
|
212
|
+
|
|
213
|
+
const remaining = pes.length - offset;
|
|
214
|
+
|
|
215
|
+
// First packet of video keyframe gets adaptation field with PCR + RAI
|
|
216
|
+
if (first && isKey && type === 'video') {
|
|
217
|
+
const afLen = 7;
|
|
218
|
+
const payloadSpace = 188 - 4 - 1 - afLen;
|
|
219
|
+
const payloadLen = Math.min(remaining, payloadSpace);
|
|
220
|
+
|
|
221
|
+
pkt[3] = 0x30 | (this.cc[cc] & 0x0F);
|
|
222
|
+
pkt[4] = afLen;
|
|
223
|
+
pkt[5] = 0x50; // PCR + random_access
|
|
224
|
+
const pcrBase = BigInt(pts90k);
|
|
225
|
+
pkt[6] = Number((pcrBase >> 25n) & 0xFFn);
|
|
226
|
+
pkt[7] = Number((pcrBase >> 17n) & 0xFFn);
|
|
227
|
+
pkt[8] = Number((pcrBase >> 9n) & 0xFFn);
|
|
228
|
+
pkt[9] = Number((pcrBase >> 1n) & 0xFFn);
|
|
229
|
+
pkt[10] = (Number(pcrBase & 1n) << 7) | 0x7E;
|
|
230
|
+
pkt[11] = 0;
|
|
231
|
+
|
|
232
|
+
pkt.set(pes.slice(offset, offset + payloadLen), 12);
|
|
233
|
+
offset += payloadLen;
|
|
234
|
+
} else if (remaining < 184) {
|
|
235
|
+
// Need stuffing
|
|
236
|
+
const payloadLen = remaining;
|
|
237
|
+
const afLen = 184 - payloadLen - 1;
|
|
238
|
+
|
|
239
|
+
pkt[3] = 0x30 | (this.cc[cc] & 0x0F);
|
|
240
|
+
pkt[4] = afLen;
|
|
241
|
+
if (afLen > 0) {
|
|
242
|
+
pkt[5] = 0x00;
|
|
243
|
+
for (let i = 6; i < 5 + afLen; i++) pkt[i] = 0xFF;
|
|
244
|
+
}
|
|
245
|
+
pkt.set(pes.slice(offset, offset + payloadLen), 4 + 1 + afLen);
|
|
246
|
+
offset += payloadLen;
|
|
247
|
+
} else {
|
|
248
|
+
// Full payload, no adaptation field
|
|
249
|
+
pkt[3] = 0x10 | (this.cc[cc] & 0x0F);
|
|
250
|
+
pkt.set(pes.slice(offset, offset + 184), 4);
|
|
251
|
+
offset += 184;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.cc[cc] = (this.cc[cc] + 1) & 0x0F;
|
|
255
|
+
this.packets.push(pkt);
|
|
256
|
+
first = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_buildPAT() {
|
|
261
|
+
const pkt = new Uint8Array(188);
|
|
262
|
+
pkt[0] = 0x47;
|
|
263
|
+
pkt[1] = 0x40;
|
|
264
|
+
pkt[2] = 0x00;
|
|
265
|
+
pkt[3] = 0x10 | (this.cc.pat & 0x0F);
|
|
266
|
+
this.cc.pat = (this.cc.pat + 1) & 0x0F;
|
|
267
|
+
|
|
268
|
+
pkt[4] = 0; // Pointer
|
|
269
|
+
pkt[5] = 0x00; // table_id
|
|
270
|
+
pkt[6] = 0xB0;
|
|
271
|
+
pkt[7] = 13; // section_length
|
|
272
|
+
pkt[8] = 0x00; pkt[9] = 0x01; // transport_stream_id
|
|
273
|
+
pkt[10] = 0xC1;
|
|
274
|
+
pkt[11] = 0x00;
|
|
275
|
+
pkt[12] = 0x00;
|
|
276
|
+
pkt[13] = 0x00; pkt[14] = 0x01; // program_number
|
|
277
|
+
pkt[15] = 0xE1; pkt[16] = 0x00; // PMT PID = 0x100
|
|
278
|
+
const crc = this._crc32(pkt.slice(5, 17));
|
|
279
|
+
pkt[17] = (crc >> 24) & 0xFF;
|
|
280
|
+
pkt[18] = (crc >> 16) & 0xFF;
|
|
281
|
+
pkt[19] = (crc >> 8) & 0xFF;
|
|
282
|
+
pkt[20] = crc & 0xFF;
|
|
283
|
+
pkt.fill(0xFF, 21);
|
|
284
|
+
return pkt;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_buildPMT() {
|
|
288
|
+
const pkt = new Uint8Array(188);
|
|
289
|
+
pkt[0] = 0x47;
|
|
290
|
+
pkt[1] = 0x41; // PID 0x100 MSB
|
|
291
|
+
pkt[2] = 0x00;
|
|
292
|
+
pkt[3] = 0x10 | (this.cc.pmt & 0x0F);
|
|
293
|
+
this.cc.pmt = (this.cc.pmt + 1) & 0x0F;
|
|
294
|
+
|
|
295
|
+
pkt[4] = 0; // Pointer
|
|
296
|
+
pkt[5] = 0x02; // table_id
|
|
297
|
+
pkt[6] = 0xB0;
|
|
298
|
+
|
|
299
|
+
if (this.hasAudio) {
|
|
300
|
+
pkt[7] = 23; // section_length with audio
|
|
301
|
+
pkt[8] = 0x00; pkt[9] = 0x01;
|
|
302
|
+
pkt[10] = 0xC1;
|
|
303
|
+
pkt[11] = 0x00;
|
|
304
|
+
pkt[12] = 0x00;
|
|
305
|
+
pkt[13] = 0xE1; pkt[14] = 0x01; // PCR_PID = 0x101
|
|
306
|
+
pkt[15] = 0xF0; pkt[16] = 0x00;
|
|
307
|
+
// Video stream (H.264)
|
|
308
|
+
pkt[17] = 0x1B;
|
|
309
|
+
pkt[18] = 0xE1; pkt[19] = 0x01; // PID 0x101
|
|
310
|
+
pkt[20] = 0xF0; pkt[21] = 0x00;
|
|
311
|
+
// Audio stream (AAC)
|
|
312
|
+
pkt[22] = 0x0F;
|
|
313
|
+
pkt[23] = 0xE1; pkt[24] = 0x02; // PID 0x102
|
|
314
|
+
pkt[25] = 0xF0; pkt[26] = 0x00;
|
|
315
|
+
const crc = this._crc32(pkt.slice(5, 27));
|
|
316
|
+
pkt[27] = (crc >> 24) & 0xFF;
|
|
317
|
+
pkt[28] = (crc >> 16) & 0xFF;
|
|
318
|
+
pkt[29] = (crc >> 8) & 0xFF;
|
|
319
|
+
pkt[30] = crc & 0xFF;
|
|
320
|
+
pkt.fill(0xFF, 31);
|
|
321
|
+
} else {
|
|
322
|
+
pkt[7] = 18; // section_length video only
|
|
323
|
+
pkt[8] = 0x00; pkt[9] = 0x01;
|
|
324
|
+
pkt[10] = 0xC1;
|
|
325
|
+
pkt[11] = 0x00;
|
|
326
|
+
pkt[12] = 0x00;
|
|
327
|
+
pkt[13] = 0xE1; pkt[14] = 0x01;
|
|
328
|
+
pkt[15] = 0xF0; pkt[16] = 0x00;
|
|
329
|
+
pkt[17] = 0x1B;
|
|
330
|
+
pkt[18] = 0xE1; pkt[19] = 0x01;
|
|
331
|
+
pkt[20] = 0xF0; pkt[21] = 0x00;
|
|
332
|
+
const crc = this._crc32(pkt.slice(5, 22));
|
|
333
|
+
pkt[22] = (crc >> 24) & 0xFF;
|
|
334
|
+
pkt[23] = (crc >> 16) & 0xFF;
|
|
335
|
+
pkt[24] = (crc >> 8) & 0xFF;
|
|
336
|
+
pkt[25] = crc & 0xFF;
|
|
337
|
+
pkt.fill(0xFF, 26);
|
|
338
|
+
}
|
|
339
|
+
return pkt;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_crc32(data) {
|
|
343
|
+
let crc = 0xFFFFFFFF;
|
|
344
|
+
for (let i = 0; i < data.length; i++) {
|
|
345
|
+
crc ^= data[i] << 24;
|
|
346
|
+
for (let j = 0; j < 8; j++) {
|
|
347
|
+
crc = (crc & 0x80000000) ? ((crc << 1) ^ 0x04C11DB7) : (crc << 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return crc >>> 0;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default TSMuxer;
|
|
355
|
+
|
|
356
|
+
|