@invintusmedia/tomp4 1.0.7 → 1.0.9

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,691 @@
1
+ /**
2
+ * MP4 Parser
3
+ *
4
+ * Parse MP4 files to extract tracks, samples, and metadata.
5
+ * Works with both local data (Uint8Array) and can be extended for remote sources.
6
+ *
7
+ * @example
8
+ * // Parse local MP4 data
9
+ * const parser = new MP4Parser(uint8ArrayData);
10
+ * console.log(parser.duration, parser.videoTrack, parser.audioTrack);
11
+ *
12
+ * // Get sample table for a track
13
+ * const samples = parser.getVideoSamples();
14
+ *
15
+ * // Build HLS segments
16
+ * const segments = parser.buildSegments(4); // 4 second segments
17
+ *
18
+ * @module parsers/mp4
19
+ */
20
+
21
+ // ============================================================================
22
+ // Binary Reading Utilities
23
+ // ============================================================================
24
+
25
+ export function readUint32(data, offset) {
26
+ return (data[offset] << 24) | (data[offset + 1] << 16) |
27
+ (data[offset + 2] << 8) | data[offset + 3];
28
+ }
29
+
30
+ export function readUint64(data, offset) {
31
+ // For simplicity, only handle lower 32 bits (files < 4GB)
32
+ return readUint32(data, offset + 4);
33
+ }
34
+
35
+ export function readInt32(data, offset) {
36
+ const val = readUint32(data, offset);
37
+ return (val & 0x80000000) ? val - 0x100000000 : val;
38
+ }
39
+
40
+ export function boxType(data, offset) {
41
+ return String.fromCharCode(
42
+ data[offset], data[offset + 1],
43
+ data[offset + 2], data[offset + 3]
44
+ );
45
+ }
46
+
47
+ // ============================================================================
48
+ // Box Finding
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Find a box by type within a data range
53
+ * @param {Uint8Array} data - MP4 data
54
+ * @param {string} type - 4-character box type (e.g., 'moov', 'trak')
55
+ * @param {number} start - Start offset
56
+ * @param {number} end - End offset
57
+ * @returns {object|null} Box info {offset, size, headerSize} or null
58
+ */
59
+ export function findBox(data, type, start = 0, end = data.length) {
60
+ let offset = start;
61
+ while (offset < end - 8) {
62
+ const size = readUint32(data, offset);
63
+ const btype = boxType(data, offset + 4);
64
+
65
+ if (size === 0 || size > end - offset) break;
66
+
67
+ const headerSize = size === 1 ? 16 : 8;
68
+ const actualSize = size === 1 ? readUint64(data, offset + 8) : size;
69
+
70
+ if (btype === type) {
71
+ return { offset, size: actualSize, headerSize };
72
+ }
73
+ offset += actualSize;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Find all boxes of a type within a data range
80
+ */
81
+ export function findAllBoxes(data, type, start = 0, end = data.length) {
82
+ const boxes = [];
83
+ let offset = start;
84
+ while (offset < end - 8) {
85
+ const size = readUint32(data, offset);
86
+ const btype = boxType(data, offset + 4);
87
+
88
+ if (size === 0 || size > end - offset) break;
89
+
90
+ const headerSize = size === 1 ? 16 : 8;
91
+ const actualSize = size === 1 ? readUint64(data, offset + 8) : size;
92
+
93
+ if (btype === type) {
94
+ boxes.push({ offset, size: actualSize, headerSize });
95
+ }
96
+ offset += actualSize;
97
+ }
98
+ return boxes;
99
+ }
100
+
101
+ // ============================================================================
102
+ // Sample Table Box Parsing
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Parse stts (time-to-sample) box
107
+ */
108
+ export function parseStts(data, offset) {
109
+ const entryCount = readUint32(data, offset + 12);
110
+ const entries = [];
111
+ let pos = offset + 16;
112
+ for (let i = 0; i < entryCount; i++) {
113
+ entries.push({
114
+ sampleCount: readUint32(data, pos),
115
+ sampleDelta: readUint32(data, pos + 4)
116
+ });
117
+ pos += 8;
118
+ }
119
+ return entries;
120
+ }
121
+
122
+ /**
123
+ * Parse stss (sync sample / keyframe) box
124
+ */
125
+ export function parseStss(data, offset) {
126
+ const entryCount = readUint32(data, offset + 12);
127
+ const keyframes = [];
128
+ let pos = offset + 16;
129
+ for (let i = 0; i < entryCount; i++) {
130
+ keyframes.push(readUint32(data, pos));
131
+ pos += 4;
132
+ }
133
+ return keyframes;
134
+ }
135
+
136
+ /**
137
+ * Parse stsz (sample size) box
138
+ */
139
+ export function parseStsz(data, offset) {
140
+ const sampleSize = readUint32(data, offset + 12);
141
+ const sampleCount = readUint32(data, offset + 16);
142
+
143
+ if (sampleSize !== 0) {
144
+ return Array(sampleCount).fill(sampleSize);
145
+ }
146
+
147
+ const sizes = [];
148
+ let pos = offset + 20;
149
+ for (let i = 0; i < sampleCount; i++) {
150
+ sizes.push(readUint32(data, pos));
151
+ pos += 4;
152
+ }
153
+ return sizes;
154
+ }
155
+
156
+ /**
157
+ * Parse stco (chunk offset 32-bit) box
158
+ */
159
+ export function parseStco(data, offset) {
160
+ const entryCount = readUint32(data, offset + 12);
161
+ const offsets = [];
162
+ let pos = offset + 16;
163
+ for (let i = 0; i < entryCount; i++) {
164
+ offsets.push(readUint32(data, pos));
165
+ pos += 4;
166
+ }
167
+ return offsets;
168
+ }
169
+
170
+ /**
171
+ * Parse co64 (chunk offset 64-bit) box
172
+ */
173
+ export function parseCo64(data, offset) {
174
+ const entryCount = readUint32(data, offset + 12);
175
+ const offsets = [];
176
+ let pos = offset + 16;
177
+ for (let i = 0; i < entryCount; i++) {
178
+ offsets.push(readUint64(data, pos));
179
+ pos += 8;
180
+ }
181
+ return offsets;
182
+ }
183
+
184
+ /**
185
+ * Parse stsc (sample-to-chunk) box
186
+ */
187
+ export function parseStsc(data, offset) {
188
+ const entryCount = readUint32(data, offset + 12);
189
+ const entries = [];
190
+ let pos = offset + 16;
191
+ for (let i = 0; i < entryCount; i++) {
192
+ entries.push({
193
+ firstChunk: readUint32(data, pos),
194
+ samplesPerChunk: readUint32(data, pos + 4),
195
+ sampleDescriptionIndex: readUint32(data, pos + 8)
196
+ });
197
+ pos += 12;
198
+ }
199
+ return entries;
200
+ }
201
+
202
+ /**
203
+ * Parse ctts (composition time offset) box for B-frames
204
+ */
205
+ export function parseCtts(data, offset) {
206
+ const version = data[offset + 8];
207
+ const entryCount = readUint32(data, offset + 12);
208
+ const entries = [];
209
+ let pos = offset + 16;
210
+ for (let i = 0; i < entryCount; i++) {
211
+ const sampleCount = readUint32(data, pos);
212
+ let sampleOffset;
213
+ if (version === 0) {
214
+ sampleOffset = readUint32(data, pos + 4);
215
+ } else {
216
+ // Version 1: signed offset
217
+ sampleOffset = readInt32(data, pos + 4);
218
+ }
219
+ entries.push({ sampleCount, sampleOffset });
220
+ pos += 8;
221
+ }
222
+ return entries;
223
+ }
224
+
225
+ /**
226
+ * Parse mdhd (media header) box
227
+ */
228
+ export function parseMdhd(data, offset) {
229
+ const version = data[offset + 8];
230
+ if (version === 0) {
231
+ return {
232
+ timescale: readUint32(data, offset + 20),
233
+ duration: readUint32(data, offset + 24)
234
+ };
235
+ }
236
+ return {
237
+ timescale: readUint32(data, offset + 28),
238
+ duration: readUint64(data, offset + 32)
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Parse tkhd (track header) box
244
+ */
245
+ export function parseTkhd(data, offset) {
246
+ const version = data[offset + 8];
247
+ if (version === 0) {
248
+ return {
249
+ trackId: readUint32(data, offset + 20),
250
+ duration: readUint32(data, offset + 28),
251
+ width: readUint32(data, offset + 84) / 65536,
252
+ height: readUint32(data, offset + 88) / 65536
253
+ };
254
+ }
255
+ return {
256
+ trackId: readUint32(data, offset + 28),
257
+ duration: readUint64(data, offset + 36),
258
+ width: readUint32(data, offset + 96) / 65536,
259
+ height: readUint32(data, offset + 100) / 65536
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Parse avcC (AVC decoder configuration) box
265
+ */
266
+ export function parseAvcC(data, offset) {
267
+ let pos = offset + 8;
268
+ const configVersion = data[pos++];
269
+ const profile = data[pos++];
270
+ const profileCompat = data[pos++];
271
+ const level = data[pos++];
272
+ const lengthSizeMinusOne = data[pos++] & 0x03;
273
+ const numSPS = data[pos++] & 0x1F;
274
+
275
+ const sps = [];
276
+ for (let i = 0; i < numSPS; i++) {
277
+ const spsLen = (data[pos] << 8) | data[pos + 1];
278
+ pos += 2;
279
+ sps.push(data.slice(pos, pos + spsLen));
280
+ pos += spsLen;
281
+ }
282
+
283
+ const numPPS = data[pos++];
284
+ const pps = [];
285
+ for (let i = 0; i < numPPS; i++) {
286
+ const ppsLen = (data[pos] << 8) | data[pos + 1];
287
+ pos += 2;
288
+ pps.push(data.slice(pos, pos + ppsLen));
289
+ pos += ppsLen;
290
+ }
291
+
292
+ return {
293
+ profile,
294
+ level,
295
+ sps,
296
+ pps,
297
+ nalLengthSize: lengthSizeMinusOne + 1
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Parse mp4a audio sample entry for sample rate and channels
303
+ */
304
+ export function parseMp4a(data, offset) {
305
+ const channels = (data[offset + 24] << 8) | data[offset + 25];
306
+ const sampleRate = (data[offset + 32] << 8) | data[offset + 33];
307
+ return { channels, sampleRate };
308
+ }
309
+
310
+ // ============================================================================
311
+ // Track Analysis
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Analyze a single track from moov data
316
+ */
317
+ export function analyzeTrack(moov, trakOffset, trakSize) {
318
+ // Get track header
319
+ const tkhd = findBox(moov, 'tkhd', trakOffset + 8, trakOffset + trakSize);
320
+ const tkhdInfo = tkhd ? parseTkhd(moov, tkhd.offset) : { trackId: 0, width: 0, height: 0 };
321
+
322
+ const mdia = findBox(moov, 'mdia', trakOffset + 8, trakOffset + trakSize);
323
+ if (!mdia) return null;
324
+
325
+ const mdhd = findBox(moov, 'mdhd', mdia.offset + 8, mdia.offset + mdia.size);
326
+ const mediaInfo = mdhd ? parseMdhd(moov, mdhd.offset) : { timescale: 90000, duration: 0 };
327
+
328
+ const hdlr = findBox(moov, 'hdlr', mdia.offset + 8, mdia.offset + mdia.size);
329
+ const handlerType = hdlr ? boxType(moov, hdlr.offset + 16) : 'unkn';
330
+
331
+ const minf = findBox(moov, 'minf', mdia.offset + 8, mdia.offset + mdia.size);
332
+ if (!minf) return null;
333
+
334
+ const stbl = findBox(moov, 'stbl', minf.offset + 8, minf.offset + minf.size);
335
+ if (!stbl) return null;
336
+
337
+ const stblStart = stbl.offset + 8;
338
+ const stblEnd = stbl.offset + stbl.size;
339
+
340
+ // Parse sample tables
341
+ const sttsBox = findBox(moov, 'stts', stblStart, stblEnd);
342
+ const stssBox = findBox(moov, 'stss', stblStart, stblEnd);
343
+ const stszBox = findBox(moov, 'stsz', stblStart, stblEnd);
344
+ const stcoBox = findBox(moov, 'stco', stblStart, stblEnd);
345
+ const co64Box = findBox(moov, 'co64', stblStart, stblEnd);
346
+ const stscBox = findBox(moov, 'stsc', stblStart, stblEnd);
347
+ const stsdBox = findBox(moov, 'stsd', stblStart, stblEnd);
348
+ const cttsBox = findBox(moov, 'ctts', stblStart, stblEnd);
349
+
350
+ // Parse codec config
351
+ let codecConfig = null;
352
+ let audioConfig = null;
353
+
354
+ if (stsdBox && handlerType === 'vide') {
355
+ const avc1 = findBox(moov, 'avc1', stsdBox.offset + 16, stsdBox.offset + stsdBox.size);
356
+ if (avc1) {
357
+ const avcC = findBox(moov, 'avcC', avc1.offset + 86, avc1.offset + avc1.size);
358
+ if (avcC) {
359
+ codecConfig = parseAvcC(moov, avcC.offset);
360
+ }
361
+ }
362
+ }
363
+
364
+ if (stsdBox && handlerType === 'soun') {
365
+ const mp4a = findBox(moov, 'mp4a', stsdBox.offset + 16, stsdBox.offset + stsdBox.size);
366
+ if (mp4a) {
367
+ audioConfig = parseMp4a(moov, mp4a.offset);
368
+ }
369
+ }
370
+
371
+ return {
372
+ trackId: tkhdInfo.trackId,
373
+ type: handlerType,
374
+ width: tkhdInfo.width,
375
+ height: tkhdInfo.height,
376
+ timescale: mediaInfo.timescale,
377
+ duration: mediaInfo.duration,
378
+ durationSeconds: mediaInfo.duration / mediaInfo.timescale,
379
+ stts: sttsBox ? parseStts(moov, sttsBox.offset) : [],
380
+ stss: stssBox ? parseStss(moov, stssBox.offset) : [],
381
+ stsz: stszBox ? parseStsz(moov, stszBox.offset) : [],
382
+ stco: stcoBox ? parseStco(moov, stcoBox.offset) :
383
+ co64Box ? parseCo64(moov, co64Box.offset) : [],
384
+ stsc: stscBox ? parseStsc(moov, stscBox.offset) : [],
385
+ ctts: cttsBox ? parseCtts(moov, cttsBox.offset) : [],
386
+ codecConfig,
387
+ audioConfig
388
+ };
389
+ }
390
+
391
+ // ============================================================================
392
+ // Sample Table Building
393
+ // ============================================================================
394
+
395
+ /**
396
+ * Build a flat sample table with byte offsets and timestamps
397
+ * @param {object} track - Track metadata from analyzeTrack
398
+ * @returns {Array} Array of sample objects
399
+ */
400
+ export function buildSampleTable(track) {
401
+ const { stsz, stco, stsc, stts, stss, ctts, timescale } = track;
402
+ const samples = [];
403
+
404
+ // Build ctts lookup (composition time offset for B-frames)
405
+ const cttsOffsets = [];
406
+ if (ctts && ctts.length > 0) {
407
+ for (const entry of ctts) {
408
+ for (let i = 0; i < entry.sampleCount; i++) {
409
+ cttsOffsets.push(entry.sampleOffset);
410
+ }
411
+ }
412
+ }
413
+
414
+ let sampleIndex = 0;
415
+ let currentDts = 0;
416
+ let sttsEntryIndex = 0;
417
+ let sttsRemaining = stts[0]?.sampleCount || 0;
418
+
419
+ for (let chunkIndex = 0; chunkIndex < stco.length; chunkIndex++) {
420
+ // Find samples per chunk for this chunk
421
+ let samplesInChunk = 1;
422
+ for (let i = stsc.length - 1; i >= 0; i--) {
423
+ if (stsc[i].firstChunk <= chunkIndex + 1) {
424
+ samplesInChunk = stsc[i].samplesPerChunk;
425
+ break;
426
+ }
427
+ }
428
+
429
+ let chunkOffset = stco[chunkIndex];
430
+
431
+ for (let s = 0; s < samplesInChunk && sampleIndex < stsz.length; s++) {
432
+ const size = stsz[sampleIndex];
433
+ const duration = stts[sttsEntryIndex]?.sampleDelta || 0;
434
+
435
+ // PTS = DTS + composition offset (ctts)
436
+ const compositionOffset = cttsOffsets[sampleIndex] || 0;
437
+ const pts = Math.max(currentDts, currentDts + compositionOffset);
438
+
439
+ samples.push({
440
+ index: sampleIndex,
441
+ offset: chunkOffset,
442
+ size,
443
+ dts: currentDts / timescale,
444
+ pts: pts / timescale,
445
+ time: pts / timescale,
446
+ duration: duration / timescale,
447
+ isKeyframe: stss.length === 0 || stss.includes(sampleIndex + 1)
448
+ });
449
+
450
+ chunkOffset += size;
451
+ currentDts += duration;
452
+ sampleIndex++;
453
+
454
+ sttsRemaining--;
455
+ if (sttsRemaining === 0 && sttsEntryIndex < stts.length - 1) {
456
+ sttsEntryIndex++;
457
+ sttsRemaining = stts[sttsEntryIndex].sampleCount;
458
+ }
459
+ }
460
+ }
461
+
462
+ return samples;
463
+ }
464
+
465
+ /**
466
+ * Build HLS-style segments from video samples
467
+ * @param {Array} videoSamples - Video sample table
468
+ * @param {number} targetDuration - Target segment duration in seconds
469
+ * @returns {Array} Array of segment definitions
470
+ */
471
+ export function buildSegments(videoSamples, targetDuration = 4) {
472
+ const segments = [];
473
+ const keyframes = videoSamples.filter(s => s.isKeyframe);
474
+
475
+ for (let i = 0; i < keyframes.length; i++) {
476
+ const start = keyframes[i];
477
+ const end = keyframes[i + 1] || videoSamples[videoSamples.length - 1];
478
+
479
+ const videoStart = start.index;
480
+ const videoEnd = end ? end.index : videoSamples.length;
481
+
482
+ const duration = (end ? end.time :
483
+ videoSamples[videoSamples.length - 1].time +
484
+ videoSamples[videoSamples.length - 1].duration) - start.time;
485
+
486
+ // Combine short segments
487
+ if (segments.length > 0 &&
488
+ segments[segments.length - 1].duration + duration < targetDuration) {
489
+ const prev = segments[segments.length - 1];
490
+ prev.videoEnd = videoEnd;
491
+ prev.duration += duration;
492
+ prev.endTime = start.time + duration;
493
+ } else {
494
+ segments.push({
495
+ index: segments.length,
496
+ startTime: start.time,
497
+ endTime: start.time + duration,
498
+ duration,
499
+ videoStart,
500
+ videoEnd
501
+ });
502
+ }
503
+ }
504
+
505
+ return segments;
506
+ }
507
+
508
+ /**
509
+ * Calculate byte ranges needed for a set of samples
510
+ * @param {Array} samples - Array of samples with offset and size
511
+ * @param {number} maxGap - Maximum gap to coalesce (default 64KB)
512
+ * @returns {Array} Array of {start, end, samples} ranges
513
+ */
514
+ export function calculateByteRanges(samples, maxGap = 65536) {
515
+ if (samples.length === 0) return [];
516
+
517
+ const ranges = [];
518
+ let currentRange = {
519
+ start: samples[0].offset,
520
+ end: samples[0].offset + samples[0].size,
521
+ samples: [samples[0]]
522
+ };
523
+
524
+ for (let i = 1; i < samples.length; i++) {
525
+ const sample = samples[i];
526
+
527
+ if (sample.offset <= currentRange.end + maxGap) {
528
+ currentRange.end = Math.max(currentRange.end, sample.offset + sample.size);
529
+ currentRange.samples.push(sample);
530
+ } else {
531
+ ranges.push(currentRange);
532
+ currentRange = {
533
+ start: sample.offset,
534
+ end: sample.offset + sample.size,
535
+ samples: [sample]
536
+ };
537
+ }
538
+ }
539
+ ranges.push(currentRange);
540
+
541
+ return ranges;
542
+ }
543
+
544
+ // ============================================================================
545
+ // MP4Parser Class
546
+ // ============================================================================
547
+
548
+ /**
549
+ * MP4 Parser - Parse MP4 files to extract tracks and samples
550
+ */
551
+ export class MP4Parser {
552
+ /**
553
+ * Create parser from MP4 data
554
+ * @param {Uint8Array} data - Complete MP4 file data
555
+ */
556
+ constructor(data) {
557
+ this.data = data;
558
+ this.moov = null;
559
+ this.videoTrack = null;
560
+ this.audioTrack = null;
561
+ this.videoSamples = [];
562
+ this.audioSamples = [];
563
+
564
+ this._parse();
565
+ }
566
+
567
+ _parse() {
568
+ // Find moov box
569
+ const moov = findBox(this.data, 'moov');
570
+ if (!moov) {
571
+ throw new Error('No moov box found - not a valid MP4 file');
572
+ }
573
+
574
+ this.moov = this.data.slice(moov.offset, moov.offset + moov.size);
575
+
576
+ // Parse tracks
577
+ let trackOffset = 8;
578
+ while (trackOffset < this.moov.length) {
579
+ const trak = findBox(this.moov, 'trak', trackOffset);
580
+ if (!trak) break;
581
+
582
+ const track = analyzeTrack(this.moov, trak.offset, trak.size);
583
+ if (track) {
584
+ if (track.type === 'vide' && !this.videoTrack) {
585
+ this.videoTrack = track;
586
+ this.videoSamples = buildSampleTable(track);
587
+ } else if (track.type === 'soun' && !this.audioTrack) {
588
+ this.audioTrack = track;
589
+ this.audioSamples = buildSampleTable(track);
590
+ }
591
+ }
592
+ trackOffset = trak.offset + trak.size;
593
+ }
594
+
595
+ if (!this.videoTrack) {
596
+ throw new Error('No video track found');
597
+ }
598
+ }
599
+
600
+ /** Duration in seconds */
601
+ get duration() {
602
+ return this.videoTrack?.durationSeconds || 0;
603
+ }
604
+
605
+ /** Video width */
606
+ get width() {
607
+ return this.videoTrack?.width || 0;
608
+ }
609
+
610
+ /** Video height */
611
+ get height() {
612
+ return this.videoTrack?.height || 0;
613
+ }
614
+
615
+ /** Whether source has audio */
616
+ get hasAudio() {
617
+ return !!this.audioTrack;
618
+ }
619
+
620
+ /** Whether video has B-frames */
621
+ get hasBframes() {
622
+ return this.videoTrack?.ctts?.length > 0;
623
+ }
624
+
625
+ /** Video codec config (SPS/PPS) */
626
+ get videoCodecConfig() {
627
+ return this.videoTrack?.codecConfig;
628
+ }
629
+
630
+ /** Audio config (sample rate, channels) */
631
+ get audioCodecConfig() {
632
+ return this.audioTrack?.audioConfig;
633
+ }
634
+
635
+ /**
636
+ * Get video samples
637
+ * @returns {Array} Video sample table
638
+ */
639
+ getVideoSamples() {
640
+ return this.videoSamples;
641
+ }
642
+
643
+ /**
644
+ * Get audio samples
645
+ * @returns {Array} Audio sample table
646
+ */
647
+ getAudioSamples() {
648
+ return this.audioSamples;
649
+ }
650
+
651
+ /**
652
+ * Build HLS-style segments
653
+ * @param {number} targetDuration - Target segment duration in seconds
654
+ * @returns {Array} Segment definitions
655
+ */
656
+ buildSegments(targetDuration = 4) {
657
+ return buildSegments(this.videoSamples, targetDuration);
658
+ }
659
+
660
+ /**
661
+ * Get sample data for a range of samples
662
+ * @param {Array} samples - Samples to extract (must have offset and size)
663
+ * @returns {Array} Samples with data property added
664
+ */
665
+ getSampleData(samples) {
666
+ return samples.map(sample => ({
667
+ ...sample,
668
+ data: this.data.slice(sample.offset, sample.offset + sample.size)
669
+ }));
670
+ }
671
+
672
+ /**
673
+ * Get parser info
674
+ */
675
+ getInfo() {
676
+ return {
677
+ duration: this.duration,
678
+ width: this.width,
679
+ height: this.height,
680
+ hasAudio: this.hasAudio,
681
+ hasBframes: this.hasBframes,
682
+ videoSampleCount: this.videoSamples.length,
683
+ audioSampleCount: this.audioSamples.length,
684
+ keyframeCount: this.videoTrack?.stss?.length ||
685
+ this.videoSamples.filter(s => s.isKeyframe).length
686
+ };
687
+ }
688
+ }
689
+
690
+ export default MP4Parser;
691
+