@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.
@@ -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
+