Skip to content

Commit 86c63bb

Browse files
author
Rob Walch
committed
Merge changes from v0.14.1
1 parent c789bd8 commit 86c63bb

File tree

3 files changed

+55
-38
lines changed

3 files changed

+55
-38
lines changed

.github/stale.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ exemptLabels:
99
- pinned
1010
- security
1111
- Confirmed
12+
- Chore
1213
- Enhancement
1314
- Feature proposal
1415
- Question

src/controller/stream-controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,8 @@ export default class StreamController extends BaseStreamController implements Ne
967967
// Avoid buffering if backtracking this fragment
968968
if (video) {
969969
if (level.details && _hasDroppedFrames(frag, video.dropped, level.details.startSN)) {
970+
// Clear demuxer to reset nextAvc which could have been set for dropped frames
971+
this.resetTransmuxer();
970972
this.backtrack(frag, video.startPTS);
971973
return;
972974
} else {

src/remux/mp4-remuxer.ts

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export default class MP4Remuxer implements Remuxer {
189189
};
190190
if (computePTSDTS) {
191191
// remember first PTS of this demuxing context. for audio, PTS = DTS
192-
initPTS = initDTS = audioSamples[0].pts - audioTrack.inputTimeScale * timeOffset;
192+
initPTS = initDTS = audioSamples[0].pts - Math.round(audioTrack.inputTimeScale * timeOffset);
193193
}
194194
}
195195

@@ -208,8 +208,9 @@ export default class MP4Remuxer implements Remuxer {
208208
}
209209
};
210210
if (computePTSDTS) {
211-
initPTS = Math.min(initPTS, videoSamples[0].pts - inputTimeScale * timeOffset);
212-
initDTS = Math.min(initDTS, videoSamples[0].dts - inputTimeScale * timeOffset);
211+
const startPTS = Math.round(inputTimeScale * timeOffset);
212+
initPTS = Math.min(initPTS, videoSamples[0].pts - startPTS);
213+
initDTS = Math.min(initDTS, videoSamples[0].dts - startPTS);
213214
}
214215
}
215216

@@ -235,6 +236,8 @@ export default class MP4Remuxer implements Remuxer {
235236
const initPTS: number = this._initPTS;
236237
let nextAvcDts = this.nextAvcDts;
237238
let offset = 8;
239+
let minPTS: number = Number.MAX_SAFE_INTEGER;
240+
let maxPTS: number = -Number.MAX_SAFE_INTEGER;
238241
let mp4SampleDuration!: number;
239242

240243
// Safari does not like overlapping DTS on consecutive fragments. let's use nextAvcDts to overcome this if fragments are consecutive
@@ -262,51 +265,54 @@ export default class MP4Remuxer implements Remuxer {
262265
inputSamples.forEach(function (sample) {
263266
sample.pts = PTSNormalize(sample.pts - initPTS, nextAvcDts);
264267
sample.dts = PTSNormalize(sample.dts - initPTS, nextAvcDts);
268+
minPTS = Math.min(sample.pts, minPTS);
269+
maxPTS = Math.max(sample.pts, maxPTS);
265270
});
266271

267-
// handle broken streams with PTS < DTS, tolerance up 200ms (18000 in 90kHz timescale)
268-
const PTSDTSshift = inputSamples.reduce((prev, curr) => Math.max(Math.min(prev, curr.pts - curr.dts), -18000), 0);
269-
if (PTSDTSshift < 0) {
270-
logger.log(`[mp4-remuxer]: PTS < DTS detected in video samples, shifting DTS by ${Math.round(PTSDTSshift / 90)} ms to overcome this issue`);
271-
for (let i = 0; i < inputSamples.length; i++) {
272-
inputSamples[i].dts += PTSDTSshift;
273-
}
274-
}
275-
276-
const firstSample = inputSamples[0];
277-
let firstDTS = Math.max(firstSample.dts, 0);
278-
let firstPTS = Math.max(firstSample.pts, 0);
272+
// Get first/last DTS
273+
let firstDTS = inputSamples[0].dts;
274+
const lastDTS = inputSamples[inputSamples.length - 1].dts;
279275

280276
// Check timestamp continuity across consecutive fragments, and modify timing in order to remove gaps or overlaps.
281-
const millisecondDelta = Math.round((firstDTS - nextAvcDts) / 90);
282277
if (contiguous) {
283-
if (millisecondDelta) {
284-
if (millisecondDelta > 1) {
285-
logger.log(`[mp4-remuxer]: AVC:${millisecondDelta} ms hole between fragments detected,filling it`);
286-
} else if (millisecondDelta < -1) {
287-
logger.log(`[mp4-remuxer]: AVC:${(-millisecondDelta)} ms overlapping between fragments detected`);
278+
const delta = firstDTS - nextAvcDts;
279+
const foundHole = delta > 2;
280+
const foundOverlap = delta < -1;
281+
if (foundHole || foundOverlap) {
282+
const millisecondDelta = Math.round(delta / 90);
283+
if (foundHole) {
284+
logger.warn(`AVC: ${millisecondDelta}ms (${delta}dts) hole between fragments detected, filling it`);
285+
} else {
286+
logger.warn(`AVC: ${-millisecondDelta}ms (${delta}dts) overlapping between fragments detected`);
288287
}
289-
290-
// remove hole/gap : set DTS to next expected DTS
291-
firstSample.dts = firstDTS = nextAvcDts;
292-
firstSample.pts = firstPTS = Math.max(firstSample.pts - millisecondDelta, nextAvcDts);
293-
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
294-
logger.log(`[mp4-remuxer]: Video/PTS/DTS adjusted: ${Math.round(firstSample.pts / 90)}/${Math.round(firstDTS / 90)}, delta:${millisecondDelta} ms`);
288+
firstDTS = nextAvcDts;
289+
minPTS -= delta;
290+
inputSamples[0].dts = firstDTS;
291+
inputSamples[0].pts = minPTS;
292+
logger.log(`Video: PTS/DTS adjusted: ${Math.round(minPTS / 90)}/${Math.round(firstDTS / 90)}, delta: ${millisecondDelta} ms`);
295293
}
296294
}
297295

298-
// compute lastPTS/lastDTS
299-
const lastSample = inputSamples[inputSamples.length - 1];
300-
const lastDTS = Math.max(lastSample.dts, 0);
301-
const lastPTS = Math.max(lastSample.pts, 0, lastDTS);
302-
303296
// on Safari let's signal the same sample duration for all samples
304297
// sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS
305298
// set this constant duration as being the avg delta between consecutive DTS.
306299
if (isSafari) {
307300
mp4SampleDuration = Math.round((lastDTS - firstDTS) / (inputSamples.length - 1));
308301
}
309302

303+
// handle broken streams with PTS < DTS, tolerance up 200ms (18000 in 90kHz timescale)
304+
const PTSDTSshift = inputSamples.reduce((prev, curr) => Math.max(Math.min(prev, curr.pts - curr.dts), -18000), 0);
305+
if (PTSDTSshift < 0) {
306+
logger.warn(`[mp4-remuxer]: PTS < DTS detected in video samples, shifting DTS by ${Math.round(PTSDTSshift / 90)} ms to overcome this issue`);
307+
for (let i = 0; i < inputSamples.length; i++) {
308+
inputSamples[i].dts = Math.max(0, inputSamples[i].dts + PTSDTSshift);
309+
}
310+
}
311+
312+
// Clamp first DTS to 0 so that we're still aligning on initPTS,
313+
// and not passing negative values to MP4.traf. This will change initial frame compositionTimeOffset!
314+
firstDTS = Math.max(inputSamples[0].dts, 0);
315+
310316
let nbNalu = 0;
311317
let naluLen = 0;
312318
for (let i = 0; i < nbSamples; i++) {
@@ -386,7 +392,7 @@ export default class MP4Remuxer implements Remuxer {
386392
// If so, playback would potentially get stuck, so we artificially inflate
387393
// the duration of the last frame to minimize any potential gap between segments.
388394
const gapTolerance = Math.floor(config.maxBufferHole * timeScale);
389-
const deltaToFrameEnd = (audioTrackLength ? firstPTS + audioTrackLength * timeScale : this.nextAudioPts) - avcSample.pts;
395+
const deltaToFrameEnd = (audioTrackLength ? minPTS + audioTrackLength * timeScale : this.nextAudioPts) - avcSample.pts;
390396
if (deltaToFrameEnd > gapTolerance) {
391397
// We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video
392398
// frame overlap. maxBufferHole should be >> lastFrameDuration anyway.
@@ -429,8 +435,8 @@ export default class MP4Remuxer implements Remuxer {
429435
const data = {
430436
data1: moof,
431437
data2: mdat,
432-
startPTS: firstPTS / timeScale,
433-
endPTS: (lastPTS + mp4SampleDuration) / timeScale,
438+
startPTS: minPTS / timeScale,
439+
endPTS: (maxPTS + mp4SampleDuration) / timeScale,
434440
startDTS: firstDTS / timeScale,
435441
endDTS: nextAvcDts / timeScale,
436442
type,
@@ -518,9 +524,17 @@ export default class MP4Remuxer implements Remuxer {
518524

519525
// If we're overlapping by more than a duration, drop this sample
520526
if (delta <= -maxAudioFramesDrift * inputSampleDuration) {
521-
logger.warn(`[mp4-remuxer]: Dropping 1 audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(duration)} ms overlap.`);
522-
inputSamples.splice(i, 1);
523-
// Don't touch nextPtsNorm or i
527+
if (contiguous) {
528+
logger.warn(`[mp4-remuxer]: Dropping 1 audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(duration)} ms overlap.`);
529+
inputSamples.splice(i, 1);
530+
// Don't touch nextPtsNorm or i
531+
} else {
532+
// When changing qualities we can't trust that audio has been appended up to nextAudioPts
533+
// Warn about the overlap but do not drop samples as that can introduce buffer gaps
534+
logger.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps nextAudioPts by ${Math.round(1000 * delta / inputTimeScale)} ms.`);
535+
nextPts = pts + inputSampleDuration;
536+
i++;
537+
}
524538
} // eslint-disable-line brace-style
525539

526540
// Insert missing frames if:

0 commit comments

Comments
 (0)