@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,376 @@
1
+ /**
2
+ * MPEG-TS Parser
3
+ *
4
+ * Parses MPEG Transport Stream data and extracts video/audio access units.
5
+ * Supports H.264/H.265 video and AAC audio.
6
+ *
7
+ * @example
8
+ * import { TSParser } from 'tomp4';
9
+ *
10
+ * const parser = new TSParser();
11
+ * parser.parse(tsData);
12
+ * parser.finalize();
13
+ *
14
+ * console.log(parser.videoAccessUnits.length); // Number of video frames
15
+ * console.log(parser.audioAccessUnits.length); // Number of audio frames
16
+ *
17
+ * @module parsers/mpegts
18
+ */
19
+
20
+ // ============================================
21
+ // Constants
22
+ // ============================================
23
+
24
+ const TS_PACKET_SIZE = 188;
25
+ const TS_SYNC_BYTE = 0x47;
26
+ const PAT_PID = 0x0000;
27
+
28
+ // Stream type info
29
+ export const STREAM_TYPES = {
30
+ 0x01: { name: 'MPEG-1 Video', supported: false },
31
+ 0x02: { name: 'MPEG-2 Video', supported: false },
32
+ 0x03: { name: 'MPEG-1 Audio (MP3)', supported: false },
33
+ 0x04: { name: 'MPEG-2 Audio', supported: false },
34
+ 0x0F: { name: 'AAC', supported: true },
35
+ 0x11: { name: 'AAC-LATM', supported: true },
36
+ 0x1B: { name: 'H.264/AVC', supported: true },
37
+ 0x24: { name: 'H.265/HEVC', supported: true },
38
+ 0x81: { name: 'AC-3 (Dolby)', supported: false },
39
+ 0x87: { name: 'E-AC-3', supported: false }
40
+ };
41
+
42
+ // ============================================
43
+ // MPEG-TS Parser
44
+ // ============================================
45
+
46
+ /**
47
+ * MPEG-TS Parser
48
+ * Extracts video and audio access units from MPEG Transport Stream data.
49
+ */
50
+ export class TSParser {
51
+ constructor() {
52
+ this.pmtPid = null;
53
+ this.videoPid = null;
54
+ this.audioPid = null;
55
+ this.videoStreamType = null;
56
+ this.audioStreamType = null;
57
+ this.videoPesBuffer = [];
58
+ this.audioPesBuffer = [];
59
+ this.videoAccessUnits = [];
60
+ this.audioAccessUnits = [];
61
+ this.videoPts = [];
62
+ this.videoDts = [];
63
+ this.audioPts = [];
64
+ this.lastAudioPts = null;
65
+ this.adtsPartial = null;
66
+ this.audioSampleRate = null;
67
+ this.audioChannels = null;
68
+ this.videoWidth = null;
69
+ this.videoHeight = null;
70
+ this.debug = { packets: 0, patFound: false, pmtFound: false };
71
+ }
72
+
73
+ /**
74
+ * Parse MPEG-TS data
75
+ * @param {Uint8Array} data - MPEG-TS data
76
+ */
77
+ parse(data) {
78
+ let offset = 0;
79
+ // Find first sync byte
80
+ while (offset < data.byteLength && data[offset] !== TS_SYNC_BYTE) offset++;
81
+ if (offset > 0) this.debug.skippedBytes = offset;
82
+
83
+ // Parse all packets
84
+ while (offset + TS_PACKET_SIZE <= data.byteLength) {
85
+ if (data[offset] !== TS_SYNC_BYTE) {
86
+ // Try to resync
87
+ const nextSync = data.indexOf(TS_SYNC_BYTE, offset + 1);
88
+ if (nextSync === -1) break;
89
+ offset = nextSync;
90
+ continue;
91
+ }
92
+ this.parsePacket(data.subarray(offset, offset + TS_PACKET_SIZE));
93
+ this.debug.packets++;
94
+ offset += TS_PACKET_SIZE;
95
+ }
96
+ }
97
+
98
+ parsePacket(packet) {
99
+ const pid = ((packet[1] & 0x1F) << 8) | packet[2];
100
+ const payloadStart = (packet[1] & 0x40) !== 0;
101
+ const adaptationField = (packet[3] & 0x30) >> 4;
102
+ let payloadOffset = 4;
103
+ if (adaptationField === 2 || adaptationField === 3) {
104
+ const adaptLen = packet[4];
105
+ payloadOffset = 5 + adaptLen;
106
+ if (payloadOffset >= TS_PACKET_SIZE) return;
107
+ }
108
+ if (adaptationField === 2) return;
109
+ if (payloadOffset >= packet.length) return;
110
+
111
+ const payload = packet.subarray(payloadOffset);
112
+ if (payload.length === 0) return;
113
+
114
+ if (pid === PAT_PID) this.parsePAT(payload);
115
+ else if (pid === this.pmtPid) this.parsePMT(payload);
116
+ else if (pid === this.videoPid) this.collectPES(payload, payloadStart, 'video');
117
+ else if (pid === this.audioPid) this.collectPES(payload, payloadStart, 'audio');
118
+ }
119
+
120
+ parsePAT(payload) {
121
+ if (payload.length < 12) return;
122
+ let offset = payload[0] + 1;
123
+ if (offset + 8 > payload.length) return;
124
+
125
+ offset += 8;
126
+
127
+ while (offset + 4 <= payload.length - 4) {
128
+ const programNum = (payload[offset] << 8) | payload[offset + 1];
129
+ const pmtPid = ((payload[offset + 2] & 0x1F) << 8) | payload[offset + 3];
130
+ if (programNum !== 0 && pmtPid !== 0) {
131
+ this.pmtPid = pmtPid;
132
+ this.debug.patFound = true;
133
+ break;
134
+ }
135
+ offset += 4;
136
+ }
137
+ }
138
+
139
+ parsePMT(payload) {
140
+ if (payload.length < 16) return;
141
+ let offset = payload[0] + 1;
142
+ if (offset + 12 > payload.length) return;
143
+
144
+ offset++;
145
+ const sectionLength = ((payload[offset] & 0x0F) << 8) | payload[offset + 1];
146
+ offset += 2;
147
+ offset += 5;
148
+ offset += 2;
149
+
150
+ if (offset + 2 > payload.length) return;
151
+ const programInfoLength = ((payload[offset] & 0x0F) << 8) | payload[offset + 1];
152
+ offset += 2 + programInfoLength;
153
+
154
+ const sectionEnd = Math.min(payload.length - 4, 1 + payload[0] + 3 + sectionLength - 4);
155
+
156
+ while (offset + 5 <= sectionEnd) {
157
+ const streamType = payload[offset];
158
+ const elementaryPid = ((payload[offset + 1] & 0x1F) << 8) | payload[offset + 2];
159
+ const esInfoLength = ((payload[offset + 3] & 0x0F) << 8) | payload[offset + 4];
160
+
161
+ if (!this.videoPid && (streamType === 0x01 || streamType === 0x02 || streamType === 0x1B || streamType === 0x24)) {
162
+ this.videoPid = elementaryPid;
163
+ this.videoStreamType = streamType;
164
+ this.debug.pmtFound = true;
165
+ }
166
+ else if (!this.audioPid && (streamType === 0x03 || streamType === 0x04 || streamType === 0x0F || streamType === 0x11 || streamType === 0x81 || streamType === 0x87)) {
167
+ this.audioPid = elementaryPid;
168
+ this.audioStreamType = streamType;
169
+ }
170
+
171
+ offset += 5 + esInfoLength;
172
+ }
173
+ }
174
+
175
+ collectPES(payload, isStart, type) {
176
+ const buffer = type === 'video' ? this.videoPesBuffer : this.audioPesBuffer;
177
+ if (isStart) {
178
+ if (type === 'audio') this.debug.audioPesStarts = (this.debug.audioPesStarts || 0) + 1;
179
+ if (buffer.length > 0) this.processPES(this.concatenateBuffers(buffer), type);
180
+ buffer.length = 0;
181
+ }
182
+ buffer.push(payload.slice());
183
+ }
184
+
185
+ processPES(pesData, type) {
186
+ if (pesData.length < 9) return;
187
+ if (pesData[0] !== 0 || pesData[1] !== 0 || pesData[2] !== 1) return;
188
+ const flags = pesData[7];
189
+ const headerDataLength = pesData[8];
190
+ let pts = null, dts = null;
191
+ if (flags & 0x80) pts = this.parsePTS(pesData, 9);
192
+ if (flags & 0x40) dts = this.parsePTS(pesData, 14);
193
+ const payload = pesData.subarray(9 + headerDataLength);
194
+ if (type === 'video') this.processVideoPayload(payload, pts, dts);
195
+ else this.processAudioPayload(payload, pts);
196
+ }
197
+
198
+ parsePTS(data, offset) {
199
+ return ((data[offset] & 0x0E) << 29) |
200
+ ((data[offset + 1]) << 22) |
201
+ ((data[offset + 2] & 0xFE) << 14) |
202
+ ((data[offset + 3]) << 7) |
203
+ ((data[offset + 4] & 0xFE) >> 1);
204
+ }
205
+
206
+ processVideoPayload(payload, pts, dts) {
207
+ const nalUnits = this.extractNALUnits(payload);
208
+ if (nalUnits.length > 0 && pts !== null) {
209
+ this.videoAccessUnits.push({ nalUnits, pts, dts: dts !== null ? dts : pts });
210
+ this.videoPts.push(pts);
211
+ this.videoDts.push(dts !== null ? dts : pts);
212
+ }
213
+ }
214
+
215
+ extractNALUnits(data) {
216
+ const nalUnits = [];
217
+ let i = 0;
218
+ while (i < data.length - 3) {
219
+ if (data[i] === 0 && data[i + 1] === 0) {
220
+ let startCodeLen = 0;
221
+ if (data[i + 2] === 1) startCodeLen = 3;
222
+ else if (data[i + 2] === 0 && i + 3 < data.length && data[i + 3] === 1) startCodeLen = 4;
223
+ if (startCodeLen > 0) {
224
+ let end = i + startCodeLen;
225
+ while (end < data.length - 2) {
226
+ if (data[end] === 0 && data[end + 1] === 0 &&
227
+ (data[end + 2] === 1 || (data[end + 2] === 0 && end + 3 < data.length && data[end + 3] === 1))) break;
228
+ end++;
229
+ }
230
+ if (end >= data.length - 2) end = data.length;
231
+ const nalUnit = data.subarray(i + startCodeLen, end);
232
+ if (nalUnit.length > 0) nalUnits.push(nalUnit);
233
+ i = end;
234
+ continue;
235
+ }
236
+ }
237
+ i++;
238
+ }
239
+ return nalUnits;
240
+ }
241
+
242
+ processAudioPayload(payload, pts) {
243
+ const frames = this.extractADTSFrames(payload);
244
+
245
+ this.debug.audioPesCount = (this.debug.audioPesCount || 0) + 1;
246
+ this.debug.audioFramesInPes = (this.debug.audioFramesInPes || 0) + frames.length;
247
+
248
+ if (pts !== null) {
249
+ this.lastAudioPts = pts;
250
+ } else if (this.lastAudioPts !== null) {
251
+ pts = this.lastAudioPts;
252
+ } else {
253
+ this.debug.audioSkipped = (this.debug.audioSkipped || 0) + frames.length;
254
+ return;
255
+ }
256
+
257
+ const sampleRate = this.audioSampleRate || 48000;
258
+ const ptsIncrement = Math.round(1024 * 90000 / sampleRate);
259
+
260
+ for (const frame of frames) {
261
+ this.audioAccessUnits.push({ data: frame.data, pts });
262
+ this.audioPts.push(pts);
263
+ pts += ptsIncrement;
264
+ this.lastAudioPts = pts;
265
+ }
266
+ }
267
+
268
+ extractADTSFrames(data) {
269
+ const SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
270
+
271
+ const frames = [];
272
+ let i = 0;
273
+
274
+ if (this.adtsPartial && this.adtsPartial.length > 0) {
275
+ const combined = new Uint8Array(this.adtsPartial.length + data.length);
276
+ combined.set(this.adtsPartial);
277
+ combined.set(data, this.adtsPartial.length);
278
+ data = combined;
279
+ this.adtsPartial = null;
280
+ }
281
+
282
+ while (i < data.length - 7) {
283
+ if (data[i] === 0xFF && (data[i + 1] & 0xF0) === 0xF0) {
284
+ const protectionAbsent = data[i + 1] & 0x01;
285
+ const frameLength = ((data[i + 3] & 0x03) << 11) | (data[i + 4] << 3) | ((data[i + 5] & 0xE0) >> 5);
286
+
287
+ if (!this.audioSampleRate && frameLength > 0) {
288
+ const samplingFreqIndex = ((data[i + 2] & 0x3C) >> 2);
289
+ const channelConfig = ((data[i + 2] & 0x01) << 2) | ((data[i + 3] & 0xC0) >> 6);
290
+ if (samplingFreqIndex < SAMPLE_RATES.length) {
291
+ this.audioSampleRate = SAMPLE_RATES[samplingFreqIndex];
292
+ this.audioChannels = channelConfig;
293
+ }
294
+ }
295
+
296
+ if (frameLength > 0) {
297
+ if (i + frameLength <= data.length) {
298
+ const headerSize = protectionAbsent ? 7 : 9;
299
+ frames.push({ header: data.subarray(i, i + headerSize), data: data.subarray(i + headerSize, i + frameLength) });
300
+ i += frameLength;
301
+ continue;
302
+ } else {
303
+ this.adtsPartial = data.slice(i);
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ i++;
309
+ }
310
+ return frames;
311
+ }
312
+
313
+ concatenateBuffers(buffers) {
314
+ const totalLength = buffers.reduce((sum, b) => sum + b.length, 0);
315
+ const result = new Uint8Array(totalLength);
316
+ let offset = 0;
317
+ for (const buf of buffers) { result.set(buf, offset); offset += buf.length; }
318
+ return result;
319
+ }
320
+
321
+ /**
322
+ * Finalize parsing - process remaining buffers and normalize timestamps
323
+ */
324
+ finalize() {
325
+ if (this.videoPesBuffer.length > 0) this.processPES(this.concatenateBuffers(this.videoPesBuffer), 'video');
326
+ if (this.audioPesBuffer.length > 0) this.processPES(this.concatenateBuffers(this.audioPesBuffer), 'audio');
327
+
328
+ this.normalizeTimestamps();
329
+ }
330
+
331
+ normalizeTimestamps() {
332
+ let minPts = Infinity;
333
+
334
+ if (this.videoPts.length > 0) {
335
+ minPts = Math.min(minPts, Math.min(...this.videoPts));
336
+ }
337
+ if (this.audioPts.length > 0) {
338
+ minPts = Math.min(minPts, Math.min(...this.audioPts));
339
+ }
340
+
341
+ if (minPts === Infinity || minPts === 0) return;
342
+
343
+ for (let i = 0; i < this.videoPts.length; i++) {
344
+ this.videoPts[i] -= minPts;
345
+ }
346
+ for (let i = 0; i < this.videoDts.length; i++) {
347
+ this.videoDts[i] -= minPts;
348
+ }
349
+ for (let i = 0; i < this.audioPts.length; i++) {
350
+ this.audioPts[i] -= minPts;
351
+ }
352
+
353
+ for (const au of this.videoAccessUnits) {
354
+ au.pts -= minPts;
355
+ au.dts -= minPts;
356
+ }
357
+ for (const au of this.audioAccessUnits) {
358
+ au.pts -= minPts;
359
+ }
360
+
361
+ this.debug.timestampOffset = minPts;
362
+ this.debug.timestampNormalized = true;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Get codec info for a stream type
368
+ * @param {number} streamType - MPEG-TS stream type
369
+ * @returns {object} Codec info with name and supported flag
370
+ */
371
+ export function getCodecInfo(streamType) {
372
+ return STREAM_TYPES[streamType] || { name: `Unknown (0x${streamType?.toString(16)})`, supported: false };
373
+ }
374
+
375
+ export default TSParser;
376
+