diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index 14c36c1ade3..bb78313e70a 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -210,16 +210,10 @@ export default class TransmuxerInterface { snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === lastFrag?.stats.chunkCount; - const nextInSequence = - snDiff === 1 || - (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0))); - const contiguous = !trackSwitch && nextInSequence; - const consecutive = - nextInSequence || - (trackSwitch && - lastFrag !== null && - Math.abs(timeOffset - (this.part ? this.part.end : lastFrag.end)) <= - this.hls.config.maxFragLookUpTolerance); + const contiguous = + !trackSwitch && + (snDiff === 1 || + (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0)))); const now = self.performance.now(); if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { @@ -238,7 +232,6 @@ export default class TransmuxerInterface { trackSwitch, timeOffset, initSegmentChange, - consecutive, ); if (!contiguous || discontinuity || initSegmentChange) { this.hls.logger @@ -248,7 +241,6 @@ export default class TransmuxerInterface { discontinuity: ${discontinuity} trackSwitch: ${trackSwitch} contiguous: ${contiguous} - consecutive: ${consecutive} accurateTimeOffset: ${accurateTimeOffset} timeOffset: ${timeOffset} initSegmentChange: ${initSegmentChange}`); diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index 97e134bb79b..d7e0d30853b 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -106,7 +106,6 @@ export default class Transmuxer { const { contiguous, - consecutive, discontinuity, trackSwitch, accurateTimeOffset, @@ -182,7 +181,7 @@ export default class Transmuxer { } if (discontinuity || initSegmentChange || resetMuxers) { - this.resetInitialTimestamp(defaultInitPts, consecutive); + this.resetInitialTimestamp(defaultInitPts); } if (!contiguous) { @@ -201,7 +200,6 @@ export default class Transmuxer { const currentState = this.currentTransmuxState; currentState.contiguous = true; - currentState.consecutive = true; currentState.discontinuity = false; currentState.trackSwitch = false; @@ -306,16 +304,13 @@ export default class Transmuxer { chunkMeta.transmuxing.executeEnd = now(); } - resetInitialTimestamp( - defaultInitPts: TimestampOffset | null, - consecutive: boolean = false, - ) { + resetInitialTimestamp(defaultInitPts: TimestampOffset | null) { const { demuxer, remuxer } = this; if (!demuxer || !remuxer) { return; } demuxer.resetTimeStamp(defaultInitPts); - remuxer.resetTimeStamp(defaultInitPts, consecutive); + remuxer.resetTimeStamp(defaultInitPts); } resetContiguity() { @@ -575,7 +570,6 @@ export class TransmuxState { public trackSwitch: boolean; public timeOffset: number; public initSegmentChange: boolean; - public consecutive: boolean; constructor( discontinuity: boolean, @@ -584,7 +578,6 @@ export class TransmuxState { trackSwitch: boolean, timeOffset: number, initSegmentChange: boolean, - consecutive: boolean = contiguous, ) { this.discontinuity = discontinuity; this.contiguous = contiguous; @@ -592,6 +585,5 @@ export class TransmuxState { this.trackSwitch = trackSwitch; this.timeOffset = timeOffset; this.initSegmentChange = initSegmentChange; - this.consecutive = consecutive; } } diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index 1cb016bef10..b91d86afecd 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -77,7 +77,6 @@ export default class MP4Remuxer extends Logger implements Remuxer { private _initDTS: TimestampOffset | null = null; private nextVideoTs: number | null = null; private nextAudioTs: number | null = null; - private isConsecutive: boolean = false; private videoSampleDuration: number | null = null; private isAudioContiguous: boolean = false; private isVideoContiguous: boolean = false; @@ -105,10 +104,7 @@ export default class MP4Remuxer extends Logger implements Remuxer { this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; } - resetTimeStamp( - defaultTimeStamp: TimestampOffset | null, - consecutive: boolean = false, - ) { + resetTimeStamp(defaultTimeStamp: TimestampOffset | null) { const initPTS = this._initPTS; if ( !initPTS || @@ -119,11 +115,10 @@ export default class MP4Remuxer extends Logger implements Remuxer { defaultTimeStamp.timescale !== initPTS.timescale ) { this.log( - `Reset initPTS: ${initPTS ? timestampToString(initPTS) : initPTS} > ${defaultTimeStamp ? timestampToString(defaultTimeStamp) : defaultTimeStamp} consecutive: ${consecutive}`, + `Reset initPTS: ${initPTS ? timestampToString(initPTS) : initPTS} > ${defaultTimeStamp ? timestampToString(defaultTimeStamp) : defaultTimeStamp}`, ); } - this.isConsecutive = consecutive; this._initPTS = this._initDTS = defaultTimeStamp; } @@ -492,7 +487,7 @@ export default class MP4Remuxer extends Logger implements Remuxer { initDTS = Math.min(initDTS as number, videoInitDTS); // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps const videoInit = - !_initPTS && this.isConsecutive ? videoInitPTS : videoInitDTS; + !_initPTS && this.isVideoContiguous ? videoInitPTS : videoInitDTS; initPTS = Math.min(initPTS as number, videoInit); } } diff --git a/src/types/remuxer.ts b/src/types/remuxer.ts index 32e09aeae36..0686726f8e9 100644 --- a/src/types/remuxer.ts +++ b/src/types/remuxer.ts @@ -35,10 +35,7 @@ export interface Remuxer { videoCodec: string | undefined, decryptdata: DecryptData | null, ): void; - resetTimeStamp( - defaultInitPTS: TimestampOffset | null, - consecutive?: boolean, - ): void; + resetTimeStamp(defaultInitPTS: TimestampOffset | null): void; resetNextTimestamp(): void; destroy(): void; } diff --git a/tests/unit/demuxer/transmuxer.ts b/tests/unit/demuxer/transmuxer.ts index 2728f62068d..733e08eee5e 100644 --- a/tests/unit/demuxer/transmuxer.ts +++ b/tests/unit/demuxer/transmuxer.ts @@ -4,14 +4,11 @@ import sinonChai from 'sinon-chai'; import { TransmuxConfig, TransmuxState } from '../../../src/demux/transmuxer'; import TransmuxerInterface from '../../../src/demux/transmuxer-interface'; import Hls from '../../../src/hls'; -import { Fragment, Part } from '../../../src/loader/fragment'; +import { Fragment } from '../../../src/loader/fragment'; import { PlaylistLevelType } from '../../../src/types/loader'; import { ChunkMetadata } from '../../../src/types/transmuxer'; -import { AttrList } from '../../../src/utils/attr-list'; -import type Transmuxer from '../../../src/demux/transmuxer'; import type { MediaFragment } from '../../../src/loader/fragment'; import type { TransmuxerResult } from '../../../src/types/transmuxer'; -import type { TimestampOffset } from '../../../src/utils/timescale-conversion'; use(sinonChai); @@ -261,298 +258,6 @@ describe('TransmuxerInterface tests', function () { ); }); - it('flags a discontinuity reached with a level switch as consecutive but not contiguous', function () { - const config = { enableWorker: false }; - hls = new Hls(config); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - onTransmuxComplete, - onFlush, - ); - const transmuxerInterfacePrivates = transmuxerInterface as any; - - const currentFrag = new Fragment(PlaylistLevelType.MAIN, ''); - currentFrag.cc = 100; - currentFrag.sn = 5; - currentFrag.level = 1; - transmuxerInterfacePrivates.frag = currentFrag; - - // next sequence number, new discontinuity sequence, different level - const newFrag = new Fragment(PlaylistLevelType.MAIN, ''); - newFrag.cc = 101; - newFrag.sn = 6; - newFrag.level = 2; - newFrag.start = 1000; - const data = new ArrayBuffer(8); - const initSegmentData = new Uint8Array(0); - const chunkMeta = new ChunkMetadata(newFrag.level, newFrag.sn, 0); - - sinon.stub(transmuxerInterfacePrivates.transmuxer, 'configure'); - const pushStub = sinon.stub(transmuxerInterfacePrivates.transmuxer, 'push'); - pushStub.returns(Promise.reject(new Error('Stubbed transmux result'))); - transmuxerInterface.push( - data, - initSegmentData, - '', - '', - newFrag as MediaFragment, - null, - 0, - true, - chunkMeta, - ); - - const state = new TransmuxState(true, false, true, true, 1000, false, true); - expect(pushStub).to.have.been.calledOnce; - expect(pushStub).to.have.been.calledWith( - data, - newFrag.decryptdata, - chunkMeta, - state, - ); - }); - - it('flags a discontinuity switch to a level with a different sequence-number base as consecutive when it starts at the previous fragment end', function () { - const config = { enableWorker: false }; - hls = new Hls(config); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - onTransmuxComplete, - onFlush, - ); - const transmuxerInterfacePrivates = transmuxerInterface as any; - - const currentFrag = new Fragment(PlaylistLevelType.MAIN, ''); - currentFrag.cc = 100; - currentFrag.sn = 5; - currentFrag.level = 0; - currentFrag.start = 990; - currentFrag.duration = 10; - transmuxerInterfacePrivates.frag = currentFrag; - - // Variant Streams may number Media Segments independently (RFC 8216 6.3.2) - const newFrag = new Fragment(PlaylistLevelType.MAIN, ''); - newFrag.cc = 101; - newFrag.sn = 106; - newFrag.level = 1; - newFrag.start = currentFrag.end; - const data = new ArrayBuffer(8); - const initSegmentData = new Uint8Array(0); - const chunkMeta = new ChunkMetadata(newFrag.level, newFrag.sn, 0); - - sinon.stub(transmuxerInterfacePrivates.transmuxer, 'configure'); - const pushStub = sinon.stub(transmuxerInterfacePrivates.transmuxer, 'push'); - pushStub.returns(Promise.reject(new Error('Stubbed transmux result'))); - transmuxerInterface.push( - data, - initSegmentData, - '', - '', - newFrag as MediaFragment, - null, - 0, - true, - chunkMeta, - ); - - const state = new TransmuxState(true, false, true, true, 1000, false, true); - expect(pushStub).to.have.been.calledOnce; - expect(pushStub).to.have.been.calledWith( - data, - newFrag.decryptdata, - chunkMeta, - state, - ); - }); - - it('does not flag a level switch as consecutive when the fragment does not start at the previous fragment end', function () { - const config = { enableWorker: false }; - hls = new Hls(config); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - onTransmuxComplete, - onFlush, - ); - const transmuxerInterfacePrivates = transmuxerInterface as any; - - const currentFrag = new Fragment(PlaylistLevelType.MAIN, ''); - currentFrag.cc = 100; - currentFrag.sn = 5; - currentFrag.level = 0; - currentFrag.start = 990; - currentFrag.duration = 10; - transmuxerInterfacePrivates.frag = currentFrag; - - // seek: one fragment ahead of the previous fragment end, on another level with a different base - const newFrag = new Fragment(PlaylistLevelType.MAIN, ''); - newFrag.cc = 101; - newFrag.sn = 107; - newFrag.level = 1; - newFrag.start = currentFrag.end + 10; - const data = new ArrayBuffer(8); - const initSegmentData = new Uint8Array(0); - const chunkMeta = new ChunkMetadata(newFrag.level, newFrag.sn, 0); - - sinon.stub(transmuxerInterfacePrivates.transmuxer, 'configure'); - const pushStub = sinon.stub(transmuxerInterfacePrivates.transmuxer, 'push'); - pushStub.returns(Promise.reject(new Error('Stubbed transmux result'))); - transmuxerInterface.push( - data, - initSegmentData, - '', - '', - newFrag as MediaFragment, - null, - 0, - true, - chunkMeta, - ); - - const state = new TransmuxState( - true, - false, - true, - true, - 1010, - false, - false, - ); - expect(pushStub).to.have.been.calledOnce; - expect(pushStub).to.have.been.calledWith( - data, - newFrag.decryptdata, - chunkMeta, - state, - ); - }); - - it('flags a level switch at a discontinuity as consecutive when the first part of the new fragment starts at the previous part end', function () { - const config = { enableWorker: false }; - hls = new Hls(config); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - onTransmuxComplete, - onFlush, - ); - const transmuxerInterfacePrivates = transmuxerInterface as any; - - // last part of the last fragment of the previous discontinuity sequence - const currentFrag = new Fragment(PlaylistLevelType.MAIN, ''); - currentFrag.cc = 100; - currentFrag.sn = 5; - currentFrag.level = 0; - currentFrag.start = 990; - currentFrag.duration = 10; - const partAttrs = new AttrList('DURATION=2'); - let currentPart = new Part(partAttrs, currentFrag as MediaFragment, '', 0); - for (let index = 1; index < 5; index++) { - currentPart = new Part( - partAttrs, - currentFrag as MediaFragment, - '', - index, - currentPart, - ); - } - expect(currentPart.end).to.equal(1000); - transmuxerInterfacePrivates.frag = currentFrag; - transmuxerInterfacePrivates.part = currentPart; - - // first part of the first fragment of the next sequence, on a level with its own sequence-number base - const newFrag = new Fragment(PlaylistLevelType.MAIN, ''); - newFrag.cc = 101; - newFrag.sn = 106; - newFrag.level = 1; - newFrag.start = 1000; - const newPart = new Part(partAttrs, newFrag as MediaFragment, '', 0); - const chunkMeta = new ChunkMetadata( - newFrag.level, - newFrag.sn, - 0, - 0, - newPart.index, - ); - - sinon.stub(transmuxerInterfacePrivates.transmuxer, 'configure'); - const pushStub = sinon.stub(transmuxerInterfacePrivates.transmuxer, 'push'); - pushStub.returns(Promise.reject(new Error('Stubbed transmux result'))); - transmuxerInterface.push( - new ArrayBuffer(8), - new Uint8Array(0), - '', - '', - newFrag as MediaFragment, - newPart, - 0, - true, - chunkMeta, - ); - - expect(pushStub).to.have.been.calledOnce; - const state = pushStub.firstCall.args[3] as TransmuxState; - expect(state).to.include({ - discontinuity: true, - trackSwitch: true, - contiguous: false, - consecutive: true, - timeOffset: 1000, - }); - }); - - it('does not flag the first fragment of a session as consecutive', function () { - const config = { enableWorker: false }; - hls = new Hls(config); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - onTransmuxComplete, - onFlush, - ); - const transmuxerInterfacePrivates = transmuxerInterface as any; - - // nothing was pushed before: a start position in the middle of the playlist - const frag = new Fragment(PlaylistLevelType.MAIN, ''); - frag.cc = 0; - frag.sn = 10; - frag.level = 2; - frag.start = 100; - const chunkMeta = new ChunkMetadata(frag.level, frag.sn, 0); - - sinon.stub(transmuxerInterfacePrivates.transmuxer, 'configure'); - const pushStub = sinon.stub(transmuxerInterfacePrivates.transmuxer, 'push'); - pushStub.returns(Promise.reject(new Error('Stubbed transmux result'))); - transmuxerInterface.push( - new ArrayBuffer(8), - new Uint8Array(0), - '', - '', - frag as MediaFragment, - null, - 0, - true, - chunkMeta, - ); - - expect(pushStub).to.have.been.calledOnce; - const state = pushStub.firstCall.args[3] as TransmuxState; - expect(state).to.include({ - discontinuity: true, - trackSwitch: true, - contiguous: false, - consecutive: false, - timeOffset: 100, - }); - }); - it('sends worker generic message', function () { const config = { enableWorker: true }; // Option debug : true crashes mocha hls = new Hls(config); @@ -636,120 +341,3 @@ describe('TransmuxerInterface tests', function () { expect(spy).to.have.been.calledWith(evt.data.data.message); }); }); - -describe('Transmuxer', function () { - let hls: Hls; - - afterEach(function () { - sinon.restore(); - if (hls) { - hls.destroy(); - } - }); - - // A Transmuxer whose demuxer and remuxer are stubs, so push() can be driven through the - // discontinuity and track-switch branches without media data - function createTransmuxer(defaultInitPts?: TimestampOffset) { - hls = new Hls({ enableWorker: false }); - sinon.stub(hls, 'trigger'); - const transmuxerInterface = new TransmuxerInterface( - hls, - PlaylistLevelType.MAIN, - () => {}, - () => {}, - ) as any; - const transmuxer = transmuxerInterface.transmuxer as Transmuxer; - const demuxer = { - resetTimeStamp: sinon.stub(), - resetContiguity: sinon.stub(), - resetInitSegment: sinon.stub(), - destroy: sinon.stub(), - }; - const remuxer = { - resetTimeStamp: sinon.stub(), - resetNextTimestamp: sinon.stub(), - resetInitSegment: sinon.stub(), - destroy: sinon.stub(), - }; - const transmuxerPrivates = transmuxer as any; - transmuxerPrivates.demuxer = demuxer; - transmuxerPrivates.remuxer = remuxer; - sinon.stub(transmuxerPrivates, 'configureTransmuxer').returns(undefined); - sinon - .stub(transmuxerPrivates, 'transmux') - .callsFake( - (data, keyData, timeOffset, accurateTimeOffset, chunkMeta) => ({ - remuxResult: {}, - chunkMeta, - }), - ); - transmuxer.configure( - new TransmuxConfig('', '', new Uint8Array(0), 0, defaultInitPts), - ); - return { transmuxer, demuxer, remuxer }; - } - - function pushWithState(transmuxer: Transmuxer, state: TransmuxState) { - const chunkMeta = new ChunkMetadata(1, 6, 0); - const result = transmuxer.push(new ArrayBuffer(8), null, chunkMeta, state); - // the stubbed transmux result comes back synchronously - expect(result).to.have.property('chunkMeta', chunkMeta); - } - - it('resets the remuxer timestamp as consecutive for a discontinuity reached in sequence on the same level', function () { - const { transmuxer, remuxer } = createTransmuxer(); - // consecutive defaults to contiguous - pushWithState( - transmuxer, - new TransmuxState(true, true, true, false, 1000, false), - ); - expect(remuxer.resetTimeStamp).to.have.been.calledOnce; - expect(remuxer.resetTimeStamp).to.have.been.calledWithExactly(null, true); - expect(remuxer.resetNextTimestamp).to.not.have.been.called; - }); - - it('resets the remuxer timestamp as consecutive for a discontinuity reached with a level switch', function () { - const { transmuxer, remuxer } = createTransmuxer(); - pushWithState( - transmuxer, - new TransmuxState(true, false, true, true, 1000, false, true), - ); - expect(remuxer.resetTimeStamp).to.have.been.calledOnce; - expect(remuxer.resetTimeStamp).to.have.been.calledWithExactly(null, true); - // samples from another level are not stitched to the previous fragment - expect(remuxer.resetNextTimestamp).to.have.been.calledOnce; - }); - - it('resets the remuxer timestamp as not consecutive for a discontinuity reached by a seek', function () { - const { transmuxer, remuxer } = createTransmuxer(); - pushWithState( - transmuxer, - new TransmuxState(true, false, true, false, 1000, false, false), - ); - expect(remuxer.resetTimeStamp).to.have.been.calledOnce; - expect(remuxer.resetTimeStamp).to.have.been.calledWithExactly(null, false); - }); - - it('forwards the cached offset of a discontinuity sequence with the consecutive flag', function () { - const defaultInitPts = { baseTime: 900000, timescale: 90000, trackId: 1 }; - const { transmuxer, remuxer } = createTransmuxer(defaultInitPts); - pushWithState( - transmuxer, - new TransmuxState(true, false, true, false, 1000, false, false), - ); - expect(remuxer.resetTimeStamp).to.have.been.calledOnce; - expect(remuxer.resetTimeStamp).to.have.been.calledWithExactly( - defaultInitPts, - false, - ); - }); - - it('does not reset the remuxer timestamp for a contiguous push within a discontinuity sequence', function () { - const { transmuxer, remuxer } = createTransmuxer(); - pushWithState( - transmuxer, - new TransmuxState(false, true, true, false, 1000, false), - ); - expect(remuxer.resetTimeStamp).to.not.have.been.called; - }); -}); diff --git a/tests/unit/remux/mp4-remuxer.ts b/tests/unit/remux/mp4-remuxer.ts index 0bf61ac8899..c763f325390 100644 --- a/tests/unit/remux/mp4-remuxer.ts +++ b/tests/unit/remux/mp4-remuxer.ts @@ -174,9 +174,9 @@ describe('mp4-remuxer', function () { { dts: 118800, cts: 7200, audioOffset: 7200 }, ]; sequences.forEach(({ dts, cts, audioOffset }, i) => { - // discontinuity with consecutive sn on the same level: the transmuxer resets the init segment and the timestamp offset (consecutive), but not contiguity + // contiguous discontinuity: the transmuxer resets the init segment and the timestamp offset, but not contiguity mp4Remuxer.resetInitSegment(); - mp4Remuxer.resetTimeStamp(null, true); + mp4Remuxer.resetTimeStamp(null); result = remuxFragment( mp4Remuxer, i + 1, @@ -193,7 +193,7 @@ describe('mp4-remuxer', function () { }); }); - it('does not shift the timeline at a discontinuity reached with a level switch (consecutive but not contiguous)', function () { + it('anchors on video DTS at a discontinuity reached with a level switch (not contiguous)', function () { let result = remuxFragment( mp4Remuxer, 0, @@ -203,9 +203,9 @@ describe('mp4-remuxer', function () { ); const timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); // level switch at the discontinuity: the transmuxer resets the init segment, the timestamp - // offset (still consecutive) and contiguity + // offset and contiguity mp4Remuxer.resetInitSegment(); - mp4Remuxer.resetTimeStamp(null, true); + mp4Remuxer.resetTimeStamp(null); mp4Remuxer.resetNextTimestamp(); // timestamps above the playlist time, so computeInitPts needs no 33-bit rollover adjustment const dts = 9000000; @@ -217,12 +217,12 @@ describe('mp4-remuxer', function () { makeAudioTrack(dts + 9119, 185), timeOffset, ); - // anchored on the first PTS: the sequence starts at timeOffset and ends one fragment later + // the DTS anchor keeps its headroom across the switch, at the cost of one composition offset expect(result.initSegment?.initPTS).to.equal( - dts + cts - Math.round(timeOffset * 90000), + dts - Math.round(timeOffset * 90000), ); expect(result.video!.endPTS).to.be.closeTo( - timeOffset + fragmentDuration, + timeOffset + fragmentDuration + cts / 90000, 0.001, ); });