From 10c8f33ad97f0971e412094c0c4e9dee3b298a2d Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:39:45 -0700 Subject: [PATCH] Use the remuxer's video contiguity as the signal and drop the cross-level rule Review on video-dev/hls.js#8083: the sequence-number leg assumed aligned numbering across variants, and across a level switch timeOffset is only as good as the playlist alignment, so binding the first PTS to it would turn drift into an overlap or a gap. Same-level discontinuities have an exact start from updateFromToPTS. isVideoContiguous is cleared by resetNextTimestamp exactly when the transmuxer reports a non-contiguous fragment and set when video was remuxed, so it is "contiguous, with previous video to continue from". Remove the consecutive field of TransmuxState, the second argument of Remuxer.resetTimeStamp and the presentation-time rule. A discontinuity that coincides with a level switch keeps the DTS anchor. Co-Authored-By: Claude Fable 5.1 --- src/demux/transmuxer-interface.ts | 16 +- src/demux/transmuxer.ts | 14 +- src/remux/mp4-remuxer.ts | 11 +- src/types/remuxer.ts | 5 +- tests/unit/demuxer/transmuxer.ts | 414 +----------------------------- tests/unit/remux/mp4-remuxer.ts | 16 +- 6 files changed, 20 insertions(+), 456 deletions(-) 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, ); });