@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.
Files changed (43) hide show
  1. package/dist/index.cjs +1150 -0
  2. package/dist/index.cjs.map +7 -0
  3. package/dist/index.js +1130 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/types/current/aac/adts.d.ts +38 -0
  6. package/dist/types/current/aac/adts.d.ts.map +1 -0
  7. package/dist/types/current/aac/adts.js +63 -0
  8. package/dist/types/current/index.d.ts +7 -0
  9. package/dist/types/current/index.d.ts.map +1 -0
  10. package/dist/types/current/index.js +6 -0
  11. package/dist/types/current/mp4/mp4.d.ts +70 -0
  12. package/dist/types/current/mp4/mp4.d.ts.map +1 -0
  13. package/dist/types/current/mp4/mp4.js +338 -0
  14. package/dist/types/current/mpegts/mpegts.d.ts +68 -0
  15. package/dist/types/current/mpegts/mpegts.d.ts.map +1 -0
  16. package/dist/types/current/mpegts/mpegts.js +202 -0
  17. package/dist/types/current/transmux/sampleEntry.d.ts +25 -0
  18. package/dist/types/current/transmux/sampleEntry.d.ts.map +1 -0
  19. package/dist/types/current/transmux/sampleEntry.js +114 -0
  20. package/dist/types/current/transmux/transmux.d.ts +17 -0
  21. package/dist/types/current/transmux/transmux.d.ts.map +1 -0
  22. package/dist/types/current/transmux/transmux.js +568 -0
  23. package/dist/types/current/tsconfig.tsbuildinfo +1 -0
  24. package/dist/types/ts3.7/aac/adts.d.ts +38 -0
  25. package/dist/types/ts3.7/index.d.ts +7 -0
  26. package/dist/types/ts3.7/mp4/mp4.d.ts +70 -0
  27. package/dist/types/ts3.7/mpegts/mpegts.d.ts +68 -0
  28. package/dist/types/ts3.7/transmux/sampleEntry.d.ts +25 -0
  29. package/dist/types/ts3.7/transmux/transmux.d.ts +17 -0
  30. package/dist/types/ts4.1/aac/adts.d.ts +38 -0
  31. package/dist/types/ts4.1/index.d.ts +7 -0
  32. package/dist/types/ts4.1/mp4/mp4.d.ts +70 -0
  33. package/dist/types/ts4.1/mpegts/mpegts.d.ts +68 -0
  34. package/dist/types/ts4.1/transmux/sampleEntry.d.ts +25 -0
  35. package/dist/types/ts4.1/transmux/transmux.d.ts +17 -0
  36. package/dist/types/ts4.7/aac/adts.d.ts +38 -0
  37. package/dist/types/ts4.7/index.d.ts +7 -0
  38. package/dist/types/ts4.7/mp4/mp4.d.ts +70 -0
  39. package/dist/types/ts4.7/mpegts/mpegts.d.ts +68 -0
  40. package/dist/types/ts4.7/transmux/sampleEntry.d.ts +25 -0
  41. package/dist/types/ts4.7/transmux/transmux.d.ts +17 -0
  42. package/docs/TRANSMUX.md +131 -0
  43. 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
+ }