@alessmicrosystems/mpegts.js 1.8.1 → 1.8.2
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 +65 -0
- package/d.ts/src/demux/ts-demuxer.d.ts +7 -0
- package/dist/mpegts.js +1 -1
- package/dist/mpegts.js.map +1 -1
- package/package.json +1 -1
- package/src/demux/ts-demuxer.ts +117 -6
- package/src/remux/mp4-remuxer.js +38 -8
package/package.json
CHANGED
package/src/demux/ts-demuxer.ts
CHANGED
|
@@ -99,7 +99,7 @@ type AudioData = {
|
|
|
99
99
|
type StashedAudioPayload = {
|
|
100
100
|
codec: 'aac' | 'aac-loas' | 'ac-3' | 'ec-3' | 'opus' | 'mp3';
|
|
101
101
|
data: Uint8Array;
|
|
102
|
-
pts: number;
|
|
102
|
+
pts: number | undefined;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
class TSDemuxer extends BaseDemuxer {
|
|
@@ -164,6 +164,10 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
164
164
|
private stashed_audio_before_video_init_: Array<StashedAudioPayload> = [];
|
|
165
165
|
private _last_dispatch_block_reason_: string = '';
|
|
166
166
|
private video_init_dispatch_time_: number = 0;
|
|
167
|
+
private audio_wrap_log_count_: number = 0;
|
|
168
|
+
private audio_stale_drop_log_count_: number = 0;
|
|
169
|
+
private audio_startup_align_log_count_: number = 0;
|
|
170
|
+
private audio_startup_drop_log_count_: number = 0;
|
|
167
171
|
// The PID currently being decoded as the active audio stream.
|
|
168
172
|
// 0 means "use whatever common_pids picks" (default first audio).
|
|
169
173
|
private active_audio_pid_: number = 0;
|
|
@@ -1072,6 +1076,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1072
1076
|
pmt.common_pids.eac3 = 0;
|
|
1073
1077
|
this.selectAudioPid(fallback.pid);
|
|
1074
1078
|
}
|
|
1079
|
+
} else if (is_new_pmt) {
|
|
1080
|
+
const cfgPref = this.config_?.preferredAudioPid;
|
|
1081
|
+
if (cfgPref && pmt.all_audio_pids.some(a => a.pid === cfgPref)) {
|
|
1082
|
+
this.selectAudioPid(cfgPref);
|
|
1083
|
+
}
|
|
1075
1084
|
}
|
|
1076
1085
|
// Emit available track info to the player layer
|
|
1077
1086
|
if (is_new_pmt && this.onTracksUpdated && (pmt.all_audio_pids.length > 1 || pmt.subtitle_pids.length > 0)) {
|
|
@@ -1458,6 +1467,61 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1458
1467
|
}
|
|
1459
1468
|
}
|
|
1460
1469
|
|
|
1470
|
+
private normalizeIncomingAudioPtsMilliseconds(base_pts_ms: number | undefined): number | undefined {
|
|
1471
|
+
if (base_pts_ms == undefined || this.audio_last_sample_pts_ == undefined) {
|
|
1472
|
+
return base_pts_ms;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const halfWrapMs = 0x100000000 / this.timescale_;
|
|
1476
|
+
const fullWrapMs = 0x200000000 / this.timescale_;
|
|
1477
|
+
|
|
1478
|
+
if (base_pts_ms + halfWrapMs < this.audio_last_sample_pts_) {
|
|
1479
|
+
const normalized = base_pts_ms + fullWrapMs;
|
|
1480
|
+
this.audio_wrap_log_count_++;
|
|
1481
|
+
if (this.audio_wrap_log_count_ <= 3 || this.audio_wrap_log_count_ % 25 === 0) {
|
|
1482
|
+
Log.w(this.TAG, `[muvie] Audio timestamp wrap normalized (#${this.audio_wrap_log_count_}): ${base_pts_ms}ms -> ${normalized}ms (last=${this.audio_last_sample_pts_}ms)`);
|
|
1483
|
+
}
|
|
1484
|
+
return normalized;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (base_pts_ms > this.audio_last_sample_pts_ + halfWrapMs) {
|
|
1488
|
+
this.audio_stale_drop_log_count_++;
|
|
1489
|
+
if (this.audio_stale_drop_log_count_ <= 3 || this.audio_stale_drop_log_count_ % 25 === 0) {
|
|
1490
|
+
Log.w(this.TAG, `[muvie] Dropping stale pre-wrap audio timestamp (#${this.audio_stale_drop_log_count_}) ${base_pts_ms}ms (last=${this.audio_last_sample_pts_}ms)`);
|
|
1491
|
+
}
|
|
1492
|
+
return undefined;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
return base_pts_ms;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
private getStartupAudioAnchorMilliseconds(): number | undefined {
|
|
1499
|
+
if (this.video_track_.samples.length > 0) {
|
|
1500
|
+
return this.video_track_.samples[0].dts;
|
|
1501
|
+
}
|
|
1502
|
+
return undefined;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
private normalizeStartupAudioPtsMilliseconds(base_pts_ms: number | undefined): number | undefined {
|
|
1506
|
+
if (base_pts_ms == undefined || this.audio_last_sample_pts_ != undefined) {
|
|
1507
|
+
return base_pts_ms;
|
|
1508
|
+
}
|
|
1509
|
+
const anchor = this.getStartupAudioAnchorMilliseconds();
|
|
1510
|
+
if (anchor == undefined) {
|
|
1511
|
+
return base_pts_ms;
|
|
1512
|
+
}
|
|
1513
|
+
const maxStartupDriftMs = 5000;
|
|
1514
|
+
const drift = base_pts_ms - anchor;
|
|
1515
|
+
if (Math.abs(drift) > maxStartupDriftMs) {
|
|
1516
|
+
this.audio_startup_align_log_count_++;
|
|
1517
|
+
if (this.audio_startup_align_log_count_ <= 3 || this.audio_startup_align_log_count_ % 25 === 0) {
|
|
1518
|
+
Log.w(this.TAG, `[muvie] Startup audio anchor normalize (#${this.audio_startup_align_log_count_}): ${base_pts_ms}ms -> ${anchor}ms (video=${anchor}ms drift=${Math.round(drift)}ms)`);
|
|
1519
|
+
}
|
|
1520
|
+
return anchor;
|
|
1521
|
+
}
|
|
1522
|
+
return base_pts_ms;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1461
1525
|
private stashAudioBeforeVideoInit(codec: StashedAudioPayload['codec'], data: Uint8Array, pts: number) {
|
|
1462
1526
|
this.stashed_audio_before_video_init_.push({codec, data, pts});
|
|
1463
1527
|
}
|
|
@@ -1465,9 +1529,22 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1465
1529
|
private dispatchAudioVideoMediaSegment() {
|
|
1466
1530
|
// Flush any audio that was stashed before video init dispatched.
|
|
1467
1531
|
if (this.video_init_segment_dispatched_ && this.stashed_audio_before_video_init_.length > 0) {
|
|
1468
|
-
const
|
|
1532
|
+
const anchor = this.getStartupAudioAnchorMilliseconds();
|
|
1533
|
+
let stash = this.stashed_audio_before_video_init_;
|
|
1534
|
+
if (anchor != undefined) {
|
|
1535
|
+
const maxStartupLeadMs = 1500;
|
|
1536
|
+
const before = stash.length;
|
|
1537
|
+
stash = stash.filter((item) => item.pts == undefined || (item.pts / this.timescale_) >= (anchor - maxStartupLeadMs));
|
|
1538
|
+
const dropped = before - stash.length;
|
|
1539
|
+
if (dropped > 0) {
|
|
1540
|
+
this.audio_startup_drop_log_count_++;
|
|
1541
|
+
if (this.audio_startup_drop_log_count_ <= 3 || this.audio_startup_drop_log_count_ % 25 === 0) {
|
|
1542
|
+
Log.w(this.TAG, `[muvie] Dropped ${dropped} stale startup audio payload(s) before video anchor ${anchor}ms (#${this.audio_startup_drop_log_count_})`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const count = stash.length;
|
|
1469
1547
|
Log.v(this.TAG, `[muvie] Flushing ${count} stashed audio payload(s) now that video init is dispatched`);
|
|
1470
|
-
const stash = this.stashed_audio_before_video_init_;
|
|
1471
1548
|
this.stashed_audio_before_video_init_ = [];
|
|
1472
1549
|
for (const item of stash) {
|
|
1473
1550
|
switch (item.codec) {
|
|
@@ -1558,6 +1635,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1558
1635
|
|
|
1559
1636
|
if (pts != undefined) {
|
|
1560
1637
|
base_pts_ms = pts / this.timescale_;
|
|
1638
|
+
base_pts_ms = this.normalizeStartupAudioPtsMilliseconds(base_pts_ms);
|
|
1639
|
+
base_pts_ms = this.normalizeIncomingAudioPtsMilliseconds(base_pts_ms);
|
|
1640
|
+
if (base_pts_ms == undefined) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1561
1643
|
}
|
|
1562
1644
|
if (this.audio_metadata_.codec === 'aac') {
|
|
1563
1645
|
if (pts == undefined && this.audio_last_sample_pts_ != undefined) {
|
|
@@ -1652,6 +1734,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1652
1734
|
|
|
1653
1735
|
if (pts != undefined) {
|
|
1654
1736
|
base_pts_ms = pts / this.timescale_;
|
|
1737
|
+
base_pts_ms = this.normalizeStartupAudioPtsMilliseconds(base_pts_ms);
|
|
1738
|
+
base_pts_ms = this.normalizeIncomingAudioPtsMilliseconds(base_pts_ms);
|
|
1739
|
+
if (base_pts_ms == undefined) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1655
1742
|
}
|
|
1656
1743
|
if (this.audio_metadata_.codec === 'aac') {
|
|
1657
1744
|
if (pts == undefined && this.audio_last_sample_pts_ != undefined) {
|
|
@@ -1738,6 +1825,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1738
1825
|
|
|
1739
1826
|
if (pts != undefined) {
|
|
1740
1827
|
base_pts_ms = pts / this.timescale_;
|
|
1828
|
+
base_pts_ms = this.normalizeStartupAudioPtsMilliseconds(base_pts_ms);
|
|
1829
|
+
base_pts_ms = this.normalizeIncomingAudioPtsMilliseconds(base_pts_ms);
|
|
1830
|
+
if (base_pts_ms == undefined) {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1741
1833
|
}
|
|
1742
1834
|
|
|
1743
1835
|
if (this.audio_metadata_.codec === 'ac-3') {
|
|
@@ -1811,6 +1903,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1811
1903
|
|
|
1812
1904
|
if (pts != undefined) {
|
|
1813
1905
|
base_pts_ms = pts / this.timescale_;
|
|
1906
|
+
base_pts_ms = this.normalizeStartupAudioPtsMilliseconds(base_pts_ms);
|
|
1907
|
+
base_pts_ms = this.normalizeIncomingAudioPtsMilliseconds(base_pts_ms);
|
|
1908
|
+
if (base_pts_ms == undefined) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1814
1911
|
}
|
|
1815
1912
|
|
|
1816
1913
|
if (this.audio_metadata_.codec === 'ec-3') {
|
|
@@ -1884,6 +1981,11 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1884
1981
|
|
|
1885
1982
|
if (pts != undefined) {
|
|
1886
1983
|
base_pts_ms = pts / this.timescale_;
|
|
1984
|
+
base_pts_ms = this.normalizeStartupAudioPtsMilliseconds(base_pts_ms);
|
|
1985
|
+
base_pts_ms = this.normalizeIncomingAudioPtsMilliseconds(base_pts_ms);
|
|
1986
|
+
if (base_pts_ms == undefined) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1887
1989
|
}
|
|
1888
1990
|
if (this.audio_metadata_.codec === 'opus') {
|
|
1889
1991
|
if (pts == undefined && this.audio_last_sample_pts_ != undefined) {
|
|
@@ -1961,7 +2063,6 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
1961
2063
|
let bit_rate = 0;
|
|
1962
2064
|
let object_type = 34; // Layer-3, listed in MPEG-4 Audio Object Types
|
|
1963
2065
|
|
|
1964
|
-
let codec = 'mp3';
|
|
1965
2066
|
switch (ver) {
|
|
1966
2067
|
case 0: // MPEG 2.5
|
|
1967
2068
|
sample_rate = _mpegAudioV25SampleRateTable[sampling_freq_index];
|
|
@@ -2005,6 +2106,16 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
2005
2106
|
data: sample
|
|
2006
2107
|
} as const;
|
|
2007
2108
|
|
|
2109
|
+
let pts_ms: number | undefined;
|
|
2110
|
+
if (pts != undefined) {
|
|
2111
|
+
pts_ms = pts / this.timescale_;
|
|
2112
|
+
pts_ms = this.normalizeStartupAudioPtsMilliseconds(pts_ms);
|
|
2113
|
+
pts_ms = this.normalizeIncomingAudioPtsMilliseconds(pts_ms);
|
|
2114
|
+
if (pts_ms == undefined) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2008
2119
|
|
|
2009
2120
|
if (this.audio_init_segment_dispatched_ == false) {
|
|
2010
2121
|
this.audio_metadata_ = {
|
|
@@ -2024,8 +2135,8 @@ class TSDemuxer extends BaseDemuxer {
|
|
|
2024
2135
|
let mp3_sample = {
|
|
2025
2136
|
unit: data,
|
|
2026
2137
|
length: data.byteLength,
|
|
2027
|
-
pts:
|
|
2028
|
-
dts:
|
|
2138
|
+
pts: pts_ms,
|
|
2139
|
+
dts: pts_ms
|
|
2029
2140
|
};
|
|
2030
2141
|
this.audio_track_.samples.push(mp3_sample);
|
|
2031
2142
|
this.audio_track_.length += data.byteLength;
|
package/src/remux/mp4-remuxer.js
CHANGED
|
@@ -65,6 +65,8 @@ class MP4Remuxer {
|
|
|
65
65
|
this._mp3UseMpegAudio = !Browser.firefox;
|
|
66
66
|
|
|
67
67
|
this._fillAudioTimestampGap = this._config.fixAudioTimestampGap;
|
|
68
|
+
this._audioDiscontinuityResetLogCount = 0;
|
|
69
|
+
this._audioGapFillLogCount = 0;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
destroy() {
|
|
@@ -86,6 +88,11 @@ class MP4Remuxer {
|
|
|
86
88
|
return this;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
_shouldLogLimited(counterName) {
|
|
92
|
+
this[counterName] = (this[counterName] || 0) + 1;
|
|
93
|
+
return this[counterName] <= 3 || this[counterName] % 25 === 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
/* prototype: function onInitSegment(type: string, initSegment: ArrayBuffer): void
|
|
90
97
|
InitSegment: {
|
|
91
98
|
type: string,
|
|
@@ -251,6 +258,10 @@ class MP4Remuxer {
|
|
|
251
258
|
let dtsCorrection = undefined;
|
|
252
259
|
let firstDts = -1, lastDts = -1, lastPts = -1;
|
|
253
260
|
let refSampleDuration = this._audioMeta.refSampleDuration;
|
|
261
|
+
const hardDiscontinuityThreshold = Math.max(
|
|
262
|
+
30000,
|
|
263
|
+
refSampleDuration != null ? refSampleDuration * 180 : 30000
|
|
264
|
+
);
|
|
254
265
|
|
|
255
266
|
let mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio;
|
|
256
267
|
let firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined;
|
|
@@ -376,19 +387,35 @@ class MP4Remuxer {
|
|
|
376
387
|
|
|
377
388
|
dtsCorrection = originalDts - curRefDts;
|
|
378
389
|
if (dtsCorrection <= -maxAudioFramesDrift * refSampleDuration) {
|
|
390
|
+
if (Math.abs(dtsCorrection) > hardDiscontinuityThreshold) {
|
|
391
|
+
if (this._shouldLogLimited('_audioDiscontinuityResetLogCount')) {
|
|
392
|
+
Log.w(this.TAG, `Audio discontinuity reset (#${this._audioDiscontinuityResetLogCount}): backward ${Math.round(dtsCorrection)} ms, codec=${this._audioMeta.codec}`);
|
|
393
|
+
}
|
|
394
|
+
dts = Math.floor(originalDts);
|
|
395
|
+
sampleDuration = Math.floor(refSampleDuration);
|
|
396
|
+
this._audioNextDts = dts + sampleDuration;
|
|
397
|
+
} else {
|
|
379
398
|
// If we're overlapping by more than maxAudioFramesDrift number of frame, drop this sample
|
|
380
|
-
|
|
381
|
-
|
|
399
|
+
Log.w(this.TAG, `Dropping 1 audio frame (originalDts: ${originalDts} ms ,curRefDts: ${curRefDts} ms) due to dtsCorrection: ${dtsCorrection} ms overlap.`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
382
402
|
}
|
|
383
403
|
else if (dtsCorrection >= maxAudioFramesDrift * refSampleDuration && this._fillAudioTimestampGap && !Browser.safari) {
|
|
384
404
|
// Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap
|
|
405
|
+
let frameCount = Math.floor(dtsCorrection / refSampleDuration);
|
|
406
|
+
if (frameCount > 180 || dtsCorrection > hardDiscontinuityThreshold) {
|
|
407
|
+
if (this._shouldLogLimited('_audioDiscontinuityResetLogCount')) {
|
|
408
|
+
Log.w(this.TAG, `Audio discontinuity reset (#${this._audioDiscontinuityResetLogCount}): forward ${Math.round(dtsCorrection)} ms, frames=${frameCount}, codec=${this._audioMeta.codec}`);
|
|
409
|
+
}
|
|
410
|
+
dts = Math.floor(originalDts);
|
|
411
|
+
sampleDuration = Math.floor(refSampleDuration);
|
|
412
|
+
this._audioNextDts = dts + sampleDuration;
|
|
413
|
+
} else {
|
|
385
414
|
needFillSilentFrames = true;
|
|
386
415
|
// We need to insert silent frames to fill timestamp gap
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
`originalDts: ${originalDts} ms, curRefDts: ${curRefDts} ms, ` +
|
|
391
|
-
`dtsCorrection: ${Math.round(dtsCorrection)} ms, generate: ${frameCount} frames`);
|
|
416
|
+
if (this._shouldLogLimited('_audioGapFillLogCount')) {
|
|
417
|
+
Log.w(this.TAG, `Audio gap fill (#${this._audioGapFillLogCount}): correction=${Math.round(dtsCorrection)} ms, frames=${frameCount}, codec=${this._audioMeta.codec}`);
|
|
418
|
+
}
|
|
392
419
|
|
|
393
420
|
|
|
394
421
|
dts = Math.floor(curRefDts);
|
|
@@ -428,6 +455,7 @@ class MP4Remuxer {
|
|
|
428
455
|
}
|
|
429
456
|
|
|
430
457
|
this._audioNextDts = curRefDts + refSampleDuration;
|
|
458
|
+
}
|
|
431
459
|
|
|
432
460
|
} else {
|
|
433
461
|
|
|
@@ -478,7 +506,9 @@ class MP4Remuxer {
|
|
|
478
506
|
|
|
479
507
|
if (needFillSilentFrames) {
|
|
480
508
|
// Silent frames should be inserted after wrong-duration frame
|
|
481
|
-
|
|
509
|
+
for (let j = 0; j < silentFrames.length; j++) {
|
|
510
|
+
mp4Samples.push(silentFrames[j]);
|
|
511
|
+
}
|
|
482
512
|
}
|
|
483
513
|
}
|
|
484
514
|
|