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