@amazon/vinyl-transmux 1.0.0
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/dist/index.cjs +1150 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +1130 -0
- package/dist/index.js.map +7 -0
- package/dist/types/current/aac/adts.d.ts +38 -0
- package/dist/types/current/aac/adts.d.ts.map +1 -0
- package/dist/types/current/aac/adts.js +63 -0
- package/dist/types/current/index.d.ts +7 -0
- package/dist/types/current/index.d.ts.map +1 -0
- package/dist/types/current/index.js +6 -0
- package/dist/types/current/mp4/mp4.d.ts +70 -0
- package/dist/types/current/mp4/mp4.d.ts.map +1 -0
- package/dist/types/current/mp4/mp4.js +338 -0
- package/dist/types/current/mpegts/mpegts.d.ts +68 -0
- package/dist/types/current/mpegts/mpegts.d.ts.map +1 -0
- package/dist/types/current/mpegts/mpegts.js +202 -0
- package/dist/types/current/transmux/sampleEntry.d.ts +25 -0
- package/dist/types/current/transmux/sampleEntry.d.ts.map +1 -0
- package/dist/types/current/transmux/sampleEntry.js +114 -0
- package/dist/types/current/transmux/transmux.d.ts +17 -0
- package/dist/types/current/transmux/transmux.d.ts.map +1 -0
- package/dist/types/current/transmux/transmux.js +568 -0
- package/dist/types/current/tsconfig.tsbuildinfo +1 -0
- package/dist/types/ts3.7/aac/adts.d.ts +38 -0
- package/dist/types/ts3.7/index.d.ts +7 -0
- package/dist/types/ts3.7/mp4/mp4.d.ts +70 -0
- package/dist/types/ts3.7/mpegts/mpegts.d.ts +68 -0
- package/dist/types/ts3.7/transmux/sampleEntry.d.ts +25 -0
- package/dist/types/ts3.7/transmux/transmux.d.ts +17 -0
- package/dist/types/ts4.1/aac/adts.d.ts +38 -0
- package/dist/types/ts4.1/index.d.ts +7 -0
- package/dist/types/ts4.1/mp4/mp4.d.ts +70 -0
- package/dist/types/ts4.1/mpegts/mpegts.d.ts +68 -0
- package/dist/types/ts4.1/transmux/sampleEntry.d.ts +25 -0
- package/dist/types/ts4.1/transmux/transmux.d.ts +17 -0
- package/dist/types/ts4.7/aac/adts.d.ts +38 -0
- package/dist/types/ts4.7/index.d.ts +7 -0
- package/dist/types/ts4.7/mp4/mp4.d.ts +70 -0
- package/dist/types/ts4.7/mpegts/mpegts.d.ts +68 -0
- package/dist/types/ts4.7/transmux/sampleEntry.d.ts +25 -0
- package/dist/types/ts4.7/transmux/transmux.d.ts +17 -0
- package/docs/TRANSMUX.md +131 -0
- package/package.json +82 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Transmuxer that converts MPEG-TS / raw ADTS AAC segments into fMP4
|
|
7
|
+
* fragments consumable by MSE SourceBuffer.
|
|
8
|
+
*
|
|
9
|
+
* The transmuxer produces two outputs:
|
|
10
|
+
* 1. An initialization segment (ftyp + moov) — append once per track.
|
|
11
|
+
* 2. Media segments (moof + mdat) — append per HLS segment.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { adtsSampleRate, buildAudioSpecificConfig, parseAdtsFrames, } from '../aac/adts';
|
|
16
|
+
import { mp4aSampleEntry, avc1SampleEntry, } from '../transmux/sampleEntry';
|
|
17
|
+
import { audioTrak, ftyp, mdat, moov, moof, patchDataOffset, videoTrak, MPEG_TIMESCALE, } from '../mp4/mp4';
|
|
18
|
+
import { STREAM_TYPE_AAC, STREAM_TYPE_H264, concatBuffers, demuxTsSegment, parsePesPts, } from '../mpegts/mpegts';
|
|
19
|
+
/**
|
|
20
|
+
* Creates a stateful transmuxer instance. Call `transmux()` for each HLS
|
|
21
|
+
* segment in order. The init segment is always returned (cached after first
|
|
22
|
+
* call).
|
|
23
|
+
*/
|
|
24
|
+
export function createTransmuxer() {
|
|
25
|
+
const state = {
|
|
26
|
+
initSegment: null,
|
|
27
|
+
sequenceNumber: 1,
|
|
28
|
+
audioConfig: null,
|
|
29
|
+
videoConfig: null,
|
|
30
|
+
audioDecodeTime: 0,
|
|
31
|
+
videoDecodeTime: 0,
|
|
32
|
+
};
|
|
33
|
+
return { transmux: (data) => transmux(state, data) };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Builds the ftyp + moov init segment and stores it in state.
|
|
37
|
+
*/
|
|
38
|
+
function buildInitSegment(state, moovBox) {
|
|
39
|
+
const ftypBox = ftyp();
|
|
40
|
+
const init = new Uint8Array(ftypBox.byteLength + moovBox.byteLength);
|
|
41
|
+
init.set(ftypBox, 0);
|
|
42
|
+
init.set(moovBox, ftypBox.byteLength);
|
|
43
|
+
state.initSegment = init;
|
|
44
|
+
}
|
|
45
|
+
/** Returns the cached init segment as an ArrayBuffer. */
|
|
46
|
+
function getInitSegment(state) {
|
|
47
|
+
const init = state.initSegment;
|
|
48
|
+
return init.buffer.slice(init.byteOffset, init.byteOffset + init.byteLength);
|
|
49
|
+
}
|
|
50
|
+
function transmux(state, data) {
|
|
51
|
+
let segment = new Uint8Array(data);
|
|
52
|
+
// Skip ID3 tags that may precede the audio/video data.
|
|
53
|
+
// ID3 tags are common in HLS for carrying timestamps.
|
|
54
|
+
segment = skipId3Tags(segment);
|
|
55
|
+
// Detect format: MPEG-TS starts with 0x47, ADTS starts with 0xFFF
|
|
56
|
+
const isMpegTs = segment[0] === 0x47;
|
|
57
|
+
const isAdts = segment[0] === 0xff && (segment[1] & 0xf0) === 0xf0;
|
|
58
|
+
if (isMpegTs) {
|
|
59
|
+
return transmuxMpegTs(state, segment);
|
|
60
|
+
}
|
|
61
|
+
else if (isAdts) {
|
|
62
|
+
return transmuxAdts(state, segment);
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Unsupported segment format: expected MPEG-TS or ADTS');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Skips any leading ID3v2 tags. Each tag has a 10-byte header where bytes
|
|
68
|
+
* 6-9 encode the size as a synchsafe integer.
|
|
69
|
+
*/
|
|
70
|
+
function skipId3Tags(data) {
|
|
71
|
+
let offset = 0;
|
|
72
|
+
while (offset + 10 <= data.length &&
|
|
73
|
+
data[offset] === 0x49 && // 'I'
|
|
74
|
+
data[offset + 1] === 0x44 && // 'D'
|
|
75
|
+
data[offset + 2] === 0x33 // '3'
|
|
76
|
+
) {
|
|
77
|
+
const size = ((data[offset + 6] & 0x7f) << 21) |
|
|
78
|
+
((data[offset + 7] & 0x7f) << 14) |
|
|
79
|
+
((data[offset + 8] & 0x7f) << 7) |
|
|
80
|
+
(data[offset + 9] & 0x7f);
|
|
81
|
+
offset += 10 + size;
|
|
82
|
+
}
|
|
83
|
+
return offset > 0 ? data.subarray(offset) : data;
|
|
84
|
+
}
|
|
85
|
+
function transmuxAdts(state, data) {
|
|
86
|
+
const frames = parseAdtsFrames(data);
|
|
87
|
+
if (frames.length === 0) {
|
|
88
|
+
throw new Error('No ADTS frames found in segment');
|
|
89
|
+
}
|
|
90
|
+
const firstFrame = frames[0];
|
|
91
|
+
if (!state.audioConfig) {
|
|
92
|
+
const sampleRate = adtsSampleRate(firstFrame.samplingFrequencyIndex);
|
|
93
|
+
state.audioConfig = {
|
|
94
|
+
audioObjectType: firstFrame.audioObjectType,
|
|
95
|
+
samplingFrequencyIndex: firstFrame.samplingFrequencyIndex,
|
|
96
|
+
channelConfiguration: firstFrame.channelConfiguration,
|
|
97
|
+
sampleRate,
|
|
98
|
+
audioSpecificConfig: buildAudioSpecificConfig(firstFrame),
|
|
99
|
+
};
|
|
100
|
+
buildInitSegment(state, moov(audioTrak({
|
|
101
|
+
trackId: 1,
|
|
102
|
+
sampleRate: state.audioConfig.sampleRate,
|
|
103
|
+
channelCount: state.audioConfig.channelConfiguration,
|
|
104
|
+
sampleEntry: mp4aSampleEntry(state.audioConfig),
|
|
105
|
+
})));
|
|
106
|
+
}
|
|
107
|
+
const sampleRate = state.audioConfig.sampleRate;
|
|
108
|
+
const samplesPerFrame = 1024; // AAC-LC always uses 1024 samples per frame
|
|
109
|
+
// Build sample table and raw frame data
|
|
110
|
+
const samples = [];
|
|
111
|
+
const frameDataParts = [];
|
|
112
|
+
for (const frame of frames) {
|
|
113
|
+
const rawData = data.subarray(frame.offset + frame.headerSize, frame.offset + frame.frameLength);
|
|
114
|
+
samples.push({
|
|
115
|
+
duration: samplesPerFrame,
|
|
116
|
+
size: rawData.byteLength,
|
|
117
|
+
});
|
|
118
|
+
frameDataParts.push(rawData);
|
|
119
|
+
}
|
|
120
|
+
const rawMediaData = concatBuffers(frameDataParts);
|
|
121
|
+
const duration = (frames.length * samplesPerFrame) / sampleRate;
|
|
122
|
+
const moofBox = moof({
|
|
123
|
+
sequenceNumber: state.sequenceNumber++,
|
|
124
|
+
trackId: 1,
|
|
125
|
+
baseDecodeTime: state.audioDecodeTime,
|
|
126
|
+
samples,
|
|
127
|
+
});
|
|
128
|
+
state.audioDecodeTime += frames.length * samplesPerFrame;
|
|
129
|
+
patchDataOffset(moofBox, moofBox.byteLength);
|
|
130
|
+
const mdatBox = mdat(rawMediaData);
|
|
131
|
+
const mediaSegment = new Uint8Array(moofBox.byteLength + mdatBox.byteLength);
|
|
132
|
+
mediaSegment.set(moofBox, 0);
|
|
133
|
+
mediaSegment.set(mdatBox, moofBox.byteLength);
|
|
134
|
+
return {
|
|
135
|
+
initSegment: getInitSegment(state),
|
|
136
|
+
mediaSegment: mediaSegment.buffer.slice(mediaSegment.byteOffset, mediaSegment.byteOffset + mediaSegment.byteLength),
|
|
137
|
+
duration,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function transmuxMpegTs(state, data) {
|
|
141
|
+
const streams = demuxTsSegment(data);
|
|
142
|
+
const audioStream = streams.find((s) => s.streamType === STREAM_TYPE_AAC);
|
|
143
|
+
const videoStream = streams.find((s) => s.streamType === STREAM_TYPE_H264);
|
|
144
|
+
// Process audio
|
|
145
|
+
let audioFrames = [];
|
|
146
|
+
let audioRawData = null;
|
|
147
|
+
let audioConfig = state.audioConfig;
|
|
148
|
+
if (audioStream) {
|
|
149
|
+
const pesData = concatBuffers(audioStream.data);
|
|
150
|
+
const esData = extractElementaryStream(pesData);
|
|
151
|
+
audioFrames = parseAdtsFrames(esData);
|
|
152
|
+
if (audioFrames.length > 0 && !audioConfig) {
|
|
153
|
+
const firstFrame = audioFrames[0];
|
|
154
|
+
const sampleRate = adtsSampleRate(firstFrame.samplingFrequencyIndex);
|
|
155
|
+
audioConfig = {
|
|
156
|
+
audioObjectType: firstFrame.audioObjectType,
|
|
157
|
+
samplingFrequencyIndex: firstFrame.samplingFrequencyIndex,
|
|
158
|
+
channelConfiguration: firstFrame.channelConfiguration,
|
|
159
|
+
sampleRate,
|
|
160
|
+
audioSpecificConfig: buildAudioSpecificConfig(firstFrame),
|
|
161
|
+
};
|
|
162
|
+
state.audioConfig = audioConfig;
|
|
163
|
+
}
|
|
164
|
+
// Strip ADTS headers from frames
|
|
165
|
+
const parts = [];
|
|
166
|
+
for (const frame of audioFrames) {
|
|
167
|
+
parts.push(esData.subarray(frame.offset + frame.headerSize, frame.offset + frame.frameLength));
|
|
168
|
+
}
|
|
169
|
+
audioRawData = concatBuffers(parts);
|
|
170
|
+
}
|
|
171
|
+
// Process video
|
|
172
|
+
let videoNalus = [];
|
|
173
|
+
let videoConfig = state.videoConfig;
|
|
174
|
+
let videoFrameDuration = 3754; // default ~23.976fps at 90kHz
|
|
175
|
+
const videoPtsList = [];
|
|
176
|
+
if (videoStream) {
|
|
177
|
+
const pesData = concatBuffers(videoStream.data);
|
|
178
|
+
// Extract PTS timestamps from PES headers to compute frame duration and CTOs
|
|
179
|
+
let pesOff = 0;
|
|
180
|
+
while (pesOff + 9 < pesData.length) {
|
|
181
|
+
if (pesData[pesOff] === 0 &&
|
|
182
|
+
pesData[pesOff + 1] === 0 &&
|
|
183
|
+
pesData[pesOff + 2] === 1 &&
|
|
184
|
+
pesData[pesOff + 3] >= 0xbc) {
|
|
185
|
+
const pts = parsePesPts(pesData.subarray(pesOff));
|
|
186
|
+
if (pts >= 0)
|
|
187
|
+
videoPtsList.push(pts);
|
|
188
|
+
const pLen = (pesData[pesOff + 4] << 8) | pesData[pesOff + 5];
|
|
189
|
+
if (pLen > 0) {
|
|
190
|
+
pesOff += 6 + pLen;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const hdrLen = pesData[pesOff + 8];
|
|
194
|
+
pesOff += 9 + hdrLen;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
pesOff++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (videoPtsList.length >= 2) {
|
|
202
|
+
// Compute frame duration from sorted PTS deltas
|
|
203
|
+
const sorted = [...videoPtsList].sort((a, b) => a - b);
|
|
204
|
+
const deltas = [];
|
|
205
|
+
for (let i = 1; i < sorted.length; i++)
|
|
206
|
+
deltas.push(sorted[i] - sorted[i - 1]);
|
|
207
|
+
deltas.sort((a, b) => a - b);
|
|
208
|
+
videoFrameDuration = deltas[deltas.length >> 1] || 3754;
|
|
209
|
+
}
|
|
210
|
+
const esData = extractElementaryStream(pesData);
|
|
211
|
+
videoNalus = extractH264Nalus(esData);
|
|
212
|
+
if (!videoConfig) {
|
|
213
|
+
const sps = videoNalus.find((n) => (n[0] & 0x1f) === 7);
|
|
214
|
+
const pps = videoNalus.find((n) => (n[0] & 0x1f) === 8);
|
|
215
|
+
if (sps && pps) {
|
|
216
|
+
const { width, height } = parseSpsInfo(sps);
|
|
217
|
+
videoConfig = {
|
|
218
|
+
width,
|
|
219
|
+
height,
|
|
220
|
+
sps,
|
|
221
|
+
pps,
|
|
222
|
+
profileIdc: sps[1],
|
|
223
|
+
profileCompatibility: sps[2],
|
|
224
|
+
levelIdc: sps[3],
|
|
225
|
+
};
|
|
226
|
+
state.videoConfig = videoConfig;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Generate init segment if needed
|
|
231
|
+
if (!state.initSegment && (audioConfig || videoConfig)) {
|
|
232
|
+
const traks = [];
|
|
233
|
+
if (videoConfig) {
|
|
234
|
+
traks.push(videoTrak({
|
|
235
|
+
trackId: traks.length + 1,
|
|
236
|
+
width: videoConfig.width,
|
|
237
|
+
height: videoConfig.height,
|
|
238
|
+
sampleEntry: avc1SampleEntry(videoConfig),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
if (audioConfig) {
|
|
242
|
+
traks.push(audioTrak({
|
|
243
|
+
trackId: traks.length + 1,
|
|
244
|
+
sampleRate: audioConfig.sampleRate,
|
|
245
|
+
channelCount: audioConfig.channelConfiguration,
|
|
246
|
+
sampleEntry: mp4aSampleEntry(audioConfig),
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
buildInitSegment(state, moov(...traks));
|
|
250
|
+
}
|
|
251
|
+
// Build media segment - for now handle audio track (trackId=1 for video, 2 for audio in muxed)
|
|
252
|
+
const fragments = [];
|
|
253
|
+
let duration = 0;
|
|
254
|
+
if (videoConfig && videoNalus.length > 0) {
|
|
255
|
+
const trackId = 1;
|
|
256
|
+
// Group NAL units into access units. Each IDR or non-IDR slice
|
|
257
|
+
// starts a new access unit. SPS/PPS are prepended to the next IDR.
|
|
258
|
+
const accessUnits = [];
|
|
259
|
+
let pendingParams = [];
|
|
260
|
+
for (const nalu of videoNalus) {
|
|
261
|
+
const naluType = nalu[0] & 0x1f;
|
|
262
|
+
if (naluType === 7 || naluType === 8) {
|
|
263
|
+
// SPS or PPS - buffer for next IDR
|
|
264
|
+
pendingParams.push(nalu);
|
|
265
|
+
}
|
|
266
|
+
else if (naluType === 6) {
|
|
267
|
+
// SEI - attach to next slice's access unit
|
|
268
|
+
pendingParams.push(nalu);
|
|
269
|
+
}
|
|
270
|
+
else if (naluType === 5) {
|
|
271
|
+
// IDR slice - include SPS/PPS/SEI before it
|
|
272
|
+
accessUnits.push({
|
|
273
|
+
nalus: [...pendingParams, nalu],
|
|
274
|
+
isIdr: true,
|
|
275
|
+
});
|
|
276
|
+
pendingParams = [];
|
|
277
|
+
}
|
|
278
|
+
else if (naluType === 1) {
|
|
279
|
+
// Non-IDR slice - include any pending SEI
|
|
280
|
+
accessUnits.push({
|
|
281
|
+
nalus: pendingParams.length > 0
|
|
282
|
+
? [...pendingParams, nalu]
|
|
283
|
+
: [nalu],
|
|
284
|
+
isIdr: false,
|
|
285
|
+
});
|
|
286
|
+
pendingParams = [];
|
|
287
|
+
}
|
|
288
|
+
// Skip other NAL types (SEI, AUD, etc.)
|
|
289
|
+
}
|
|
290
|
+
if (accessUnits.length > 0) {
|
|
291
|
+
const naluParts = [];
|
|
292
|
+
const samples = [];
|
|
293
|
+
const frameDuration = videoFrameDuration;
|
|
294
|
+
// Compute composition time offsets from PTS.
|
|
295
|
+
// DTS increases linearly; CTO = PTS - DTS.
|
|
296
|
+
const basePts = videoPtsList.length > 0 ? Math.min(...videoPtsList) : 0;
|
|
297
|
+
const baseDts = basePts; // first DTS = first PTS for simplicity
|
|
298
|
+
for (let auIdx = 0; auIdx < accessUnits.length; auIdx++) {
|
|
299
|
+
const au = accessUnits[auIdx];
|
|
300
|
+
// Concatenate all NAL units in this access unit with length prefixes
|
|
301
|
+
let auSize = 0;
|
|
302
|
+
const auParts = [];
|
|
303
|
+
for (const nalu of au.nalus) {
|
|
304
|
+
const lengthPrefixed = new Uint8Array(4 + nalu.byteLength);
|
|
305
|
+
lengthPrefixed[0] = (nalu.byteLength >> 24) & 0xff;
|
|
306
|
+
lengthPrefixed[1] = (nalu.byteLength >> 16) & 0xff;
|
|
307
|
+
lengthPrefixed[2] = (nalu.byteLength >> 8) & 0xff;
|
|
308
|
+
lengthPrefixed[3] = nalu.byteLength & 0xff;
|
|
309
|
+
lengthPrefixed.set(nalu, 4);
|
|
310
|
+
auParts.push(lengthPrefixed);
|
|
311
|
+
auSize += lengthPrefixed.byteLength;
|
|
312
|
+
}
|
|
313
|
+
naluParts.push(...auParts);
|
|
314
|
+
samples.push({
|
|
315
|
+
duration: frameDuration,
|
|
316
|
+
size: auSize,
|
|
317
|
+
// is_leading=0, depends_on: IDR=2(no), non-IDR=1(yes),
|
|
318
|
+
// is_depended_on=0, has_redundancy=0
|
|
319
|
+
flags: au.isIdr ? 0x02000000 : 0x01010000,
|
|
320
|
+
compositionTimeOffset: auIdx < videoPtsList.length
|
|
321
|
+
? videoPtsList[auIdx] -
|
|
322
|
+
(baseDts + auIdx * frameDuration)
|
|
323
|
+
: 0,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const rawData = concatBuffers(naluParts);
|
|
327
|
+
const moofBox = moof({
|
|
328
|
+
sequenceNumber: state.sequenceNumber++,
|
|
329
|
+
trackId,
|
|
330
|
+
baseDecodeTime: state.videoDecodeTime,
|
|
331
|
+
samples,
|
|
332
|
+
});
|
|
333
|
+
state.videoDecodeTime += accessUnits.length * frameDuration;
|
|
334
|
+
patchDataOffset(moofBox, moofBox.byteLength);
|
|
335
|
+
const mdatBox = mdat(rawData);
|
|
336
|
+
fragments.push(moofBox, mdatBox);
|
|
337
|
+
duration = (accessUnits.length * frameDuration) / MPEG_TIMESCALE;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (audioConfig && audioRawData && audioFrames.length > 0) {
|
|
341
|
+
const trackId = videoConfig ? 2 : 1;
|
|
342
|
+
const samplesPerFrame = 1024;
|
|
343
|
+
const samples = [];
|
|
344
|
+
const parts = [];
|
|
345
|
+
let offset = 0;
|
|
346
|
+
for (const frame of audioFrames) {
|
|
347
|
+
const size = frame.frameLength - frame.headerSize;
|
|
348
|
+
parts.push(audioRawData.subarray(offset, offset + size));
|
|
349
|
+
samples.push({
|
|
350
|
+
duration: samplesPerFrame,
|
|
351
|
+
size,
|
|
352
|
+
});
|
|
353
|
+
offset += size;
|
|
354
|
+
}
|
|
355
|
+
const rawData = concatBuffers(parts);
|
|
356
|
+
const moofBox = moof({
|
|
357
|
+
sequenceNumber: state.sequenceNumber++,
|
|
358
|
+
trackId,
|
|
359
|
+
baseDecodeTime: state.audioDecodeTime,
|
|
360
|
+
samples,
|
|
361
|
+
});
|
|
362
|
+
state.audioDecodeTime += audioFrames.length * samplesPerFrame;
|
|
363
|
+
patchDataOffset(moofBox, moofBox.byteLength);
|
|
364
|
+
const mdatBox = mdat(rawData);
|
|
365
|
+
fragments.push(moofBox, mdatBox);
|
|
366
|
+
const audioDuration = (audioFrames.length * samplesPerFrame) / audioConfig.sampleRate;
|
|
367
|
+
if (audioDuration > duration)
|
|
368
|
+
duration = audioDuration;
|
|
369
|
+
}
|
|
370
|
+
const mediaSegment = concatBuffers(fragments);
|
|
371
|
+
return {
|
|
372
|
+
initSegment: getInitSegment(state),
|
|
373
|
+
mediaSegment: mediaSegment.buffer.slice(mediaSegment.byteOffset, mediaSegment.byteOffset + mediaSegment.byteLength),
|
|
374
|
+
duration,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extracts the elementary stream from concatenated PES packet data.
|
|
379
|
+
* Handles multiple PES packets within the buffer.
|
|
380
|
+
*/
|
|
381
|
+
function extractElementaryStream(pesData) {
|
|
382
|
+
const parts = [];
|
|
383
|
+
let offset = 0;
|
|
384
|
+
while (offset < pesData.length) {
|
|
385
|
+
if (offset + 9 <= pesData.length &&
|
|
386
|
+
pesData[offset] === 0 &&
|
|
387
|
+
pesData[offset + 1] === 0 &&
|
|
388
|
+
pesData[offset + 2] === 1 &&
|
|
389
|
+
pesData[offset + 3] >= 0xbc // PES stream IDs start at 0xBC
|
|
390
|
+
) {
|
|
391
|
+
const headerDataLength = pesData[offset + 8];
|
|
392
|
+
const payloadStart = offset + 9 + headerDataLength;
|
|
393
|
+
const pesPacketLength = (pesData[offset + 4] << 8) | pesData[offset + 5];
|
|
394
|
+
if (pesPacketLength > 0) {
|
|
395
|
+
const pesEnd = Math.min(offset + 6 + pesPacketLength, pesData.length);
|
|
396
|
+
if (payloadStart < pesEnd) {
|
|
397
|
+
parts.push(pesData.subarray(payloadStart, pesEnd));
|
|
398
|
+
}
|
|
399
|
+
offset = pesEnd;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Unknown length — scan for next PES header (00 00 01 + stream_id >= 0xBC)
|
|
403
|
+
let nextPes = payloadStart;
|
|
404
|
+
while (nextPes + 3 < pesData.length) {
|
|
405
|
+
if (pesData[nextPes] === 0 &&
|
|
406
|
+
pesData[nextPes + 1] === 0 &&
|
|
407
|
+
pesData[nextPes + 2] === 1 &&
|
|
408
|
+
pesData[nextPes + 3] >= 0xbc) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
nextPes++;
|
|
412
|
+
}
|
|
413
|
+
if (nextPes + 3 >= pesData.length)
|
|
414
|
+
nextPes = pesData.length;
|
|
415
|
+
if (payloadStart < nextPes) {
|
|
416
|
+
parts.push(pesData.subarray(payloadStart, nextPes));
|
|
417
|
+
}
|
|
418
|
+
offset = nextPes;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
offset++;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return concatBuffers(parts);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Extracts H.264 NAL units from an Annex B byte stream.
|
|
429
|
+
* Properly handles 3-byte and 4-byte start codes and trims trailing zeros
|
|
430
|
+
* that may result from TS packet padding.
|
|
431
|
+
*/
|
|
432
|
+
function extractH264Nalus(data) {
|
|
433
|
+
const nalus = [];
|
|
434
|
+
// Find all start code positions
|
|
435
|
+
const startPositions = [];
|
|
436
|
+
let i = 0;
|
|
437
|
+
while (i < data.length - 2) {
|
|
438
|
+
if (data[i] === 0 && data[i + 1] === 0) {
|
|
439
|
+
if (i + 3 < data.length && data[i + 2] === 0 && data[i + 3] === 1) {
|
|
440
|
+
startPositions.push({ offset: i, scLen: 4 });
|
|
441
|
+
i += 4;
|
|
442
|
+
}
|
|
443
|
+
else if (data[i + 2] === 1) {
|
|
444
|
+
startPositions.push({ offset: i, scLen: 3 });
|
|
445
|
+
i += 3;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
i++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (let j = 0; j < startPositions.length; j++) {
|
|
456
|
+
const naluStart = startPositions[j].offset + startPositions[j].scLen;
|
|
457
|
+
const naluEnd = j + 1 < startPositions.length
|
|
458
|
+
? startPositions[j + 1].offset
|
|
459
|
+
: data.length;
|
|
460
|
+
// Trim trailing zeros (TS packet padding)
|
|
461
|
+
let trimmedEnd = naluEnd;
|
|
462
|
+
while (trimmedEnd > naluStart + 1 && data[trimmedEnd - 1] === 0) {
|
|
463
|
+
trimmedEnd--;
|
|
464
|
+
}
|
|
465
|
+
if (trimmedEnd > naluStart) {
|
|
466
|
+
nalus.push(data.subarray(naluStart, trimmedEnd));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return nalus;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* H.264 profiles that include the high-profile SPS extensions
|
|
473
|
+
* (chroma format, bit depth, scaling matrices).
|
|
474
|
+
*/
|
|
475
|
+
const HIGH_PROFILE_IDCS = new Set([
|
|
476
|
+
100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134,
|
|
477
|
+
]);
|
|
478
|
+
/**
|
|
479
|
+
* Minimal H.264 SPS parser using exp-golomb decoding.
|
|
480
|
+
* Extracts pic_width_in_mbs_minus1 and pic_height_in_map_units_minus1.
|
|
481
|
+
*/
|
|
482
|
+
function parseSpsInfo(sps) {
|
|
483
|
+
// SPS NAL unit: skip NAL header byte
|
|
484
|
+
let bitOffset = 8; // skip nal_unit_type byte
|
|
485
|
+
function readBit() {
|
|
486
|
+
const byteIndex = bitOffset >> 3;
|
|
487
|
+
const bitIndex = 7 - (bitOffset & 7);
|
|
488
|
+
bitOffset++;
|
|
489
|
+
return (sps[byteIndex] >> bitIndex) & 1;
|
|
490
|
+
}
|
|
491
|
+
function readBits(n) {
|
|
492
|
+
let val = 0;
|
|
493
|
+
for (let i = 0; i < n; i++) {
|
|
494
|
+
val = (val << 1) | readBit();
|
|
495
|
+
}
|
|
496
|
+
return val;
|
|
497
|
+
}
|
|
498
|
+
function readUe() {
|
|
499
|
+
let leadingZeros = 0;
|
|
500
|
+
while (readBit() === 0 && leadingZeros < 32)
|
|
501
|
+
leadingZeros++;
|
|
502
|
+
if (leadingZeros === 0)
|
|
503
|
+
return 0;
|
|
504
|
+
return (1 << leadingZeros) - 1 + readBits(leadingZeros);
|
|
505
|
+
}
|
|
506
|
+
function readSe() {
|
|
507
|
+
const val = readUe();
|
|
508
|
+
return val & 1 ? (val + 1) >> 1 : -(val >> 1);
|
|
509
|
+
}
|
|
510
|
+
// profile_idc
|
|
511
|
+
const profileIdc = readBits(8);
|
|
512
|
+
// constraint_set flags + reserved
|
|
513
|
+
readBits(8);
|
|
514
|
+
// level_idc
|
|
515
|
+
readBits(8);
|
|
516
|
+
// seq_parameter_set_id
|
|
517
|
+
readUe();
|
|
518
|
+
if (HIGH_PROFILE_IDCS.has(profileIdc)) {
|
|
519
|
+
const chromaFormatIdc = readUe();
|
|
520
|
+
if (chromaFormatIdc === 3)
|
|
521
|
+
readBit(); // separate_colour_plane_flag
|
|
522
|
+
readUe(); // bit_depth_luma_minus8
|
|
523
|
+
readUe(); // bit_depth_chroma_minus8
|
|
524
|
+
readBit(); // qpprime_y_zero_transform_bypass_flag
|
|
525
|
+
const seqScalingMatrixPresent = readBit();
|
|
526
|
+
if (seqScalingMatrixPresent) {
|
|
527
|
+
const count = chromaFormatIdc !== 3 ? 8 : 12;
|
|
528
|
+
for (let i = 0; i < count; i++) {
|
|
529
|
+
if (readBit()) {
|
|
530
|
+
// 8x8 scaling lists (i >= 6) require a real H.264 encoder
|
|
531
|
+
// to produce a valid SPS bitstream that survives the full
|
|
532
|
+
// exp-golomb parser to this point.
|
|
533
|
+
const size = i < 6 ? 16 : /* c8 ignore next */ 64;
|
|
534
|
+
let lastScale = 8;
|
|
535
|
+
let nextScale = 8;
|
|
536
|
+
for (let j = 0; j < size; j++) {
|
|
537
|
+
if (nextScale !== 0) {
|
|
538
|
+
const delta = readSe();
|
|
539
|
+
nextScale = (lastScale + delta + 0x100) % 0x100;
|
|
540
|
+
}
|
|
541
|
+
lastScale = nextScale === 0 ? lastScale : nextScale;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
readUe(); // log2_max_frame_num_minus4
|
|
548
|
+
const picOrderCntType = readUe();
|
|
549
|
+
if (picOrderCntType === 0) {
|
|
550
|
+
readUe(); // log2_max_pic_order_cnt_lsb_minus4
|
|
551
|
+
}
|
|
552
|
+
else if (picOrderCntType === 1) {
|
|
553
|
+
readBit(); // delta_pic_order_always_zero_flag
|
|
554
|
+
readSe(); // offset_for_non_ref_pic
|
|
555
|
+
readSe(); // offset_for_top_to_bottom_field
|
|
556
|
+
const numRefFrames = readUe();
|
|
557
|
+
for (let i = 0; i < numRefFrames; i++)
|
|
558
|
+
readSe();
|
|
559
|
+
}
|
|
560
|
+
readUe(); // max_num_ref_frames
|
|
561
|
+
readBit(); // gaps_in_frame_num_value_allowed_flag
|
|
562
|
+
const picWidthInMbsMinus1 = readUe();
|
|
563
|
+
const picHeightInMapUnitsMinus1 = readUe();
|
|
564
|
+
const frameMbsOnlyFlag = readBit();
|
|
565
|
+
const width = (picWidthInMbsMinus1 + 1) * 16;
|
|
566
|
+
const height = (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16;
|
|
567
|
+
return { width, height };
|
|
568
|
+
}
|