From 5caafed7fa7326d136b404958c2f91d2910bbe04 Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:58:19 -0700 Subject: [PATCH 1/8] Anchor initPTS on video PTS when continuing contiguous content across a discontinuity generateIS binds initPTS to the lowest DTS across tracks (video-dev/hls.js#7880, cherry-picked into 1.6.18 as 209a01ba) so the first segment leaves composition offset headroom for segments loaded later at an earlier playlist time (video-dev/hls.js#7932). At a discontinuity, timeOffset is the previous fragment's presentation end (updateFromToPTS: start + duration, where duration is endPTS - start). Mapping the new sequence's first DTS to that boundary presents its first frame one composition offset late. The next fragment's start is derived from that PTS, so the shift carries forward and repeats at every discontinuity. Keep the DTS anchor for fresh non-contiguous anchors (session start, seeks) and use the PTS anchor only when the remuxer is continuing contiguous content, which is the mapping discontinuities had before 1.6.18. Refs video-dev/hls.js#8064 Co-Authored-By: Claude Fable 5.1 --- src/remux/mp4-remuxer.ts | 10 +- tests/unit/remux/mp4-remuxer.ts | 171 +++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 3 deletions(-) diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index 96b0e59b8c0..c0ca8771cbd 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -485,8 +485,14 @@ export default class MP4Remuxer extends Logger implements Remuxer { (timeOffset === 0 && videoInitPTS < _initPTS.baseTime) ) { initDTS = Math.min(initDTS as number, videoInitDTS); - // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps - initPTS = Math.min(initPTS as number, videoInitDTS); + // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps (#7932). + // When continuing contiguous content (a discontinuity), timeOffset is the previous + // fragment's presentation end, so bind to the first PTS instead: anchoring the first + // DTS there presents the new sequence one composition offset late, and that shift + // repeats at every discontinuity (#8064). + const videoInit = + !_initPTS && this.isVideoContiguous ? videoInitPTS : videoInitDTS; + initPTS = Math.min(initPTS as number, videoInit); } } this.videoTrackConfig = { diff --git a/tests/unit/remux/mp4-remuxer.ts b/tests/unit/remux/mp4-remuxer.ts index 8466fe2e2a4..ebf638eec6d 100644 --- a/tests/unit/remux/mp4-remuxer.ts +++ b/tests/unit/remux/mp4-remuxer.ts @@ -2,14 +2,27 @@ import { expect, use } from 'chai'; import EventEmitter from 'eventemitter3'; import sinonChai from 'sinon-chai'; import { hlsDefaultConfig } from '../../../src/config'; +import { dummyTrack } from '../../../src/demux/dummy-demuxed-track'; import MP4Remuxer from '../../../src/remux/mp4-remuxer'; +import { PlaylistLevelType } from '../../../src/types/loader'; +import { ChunkMetadata } from '../../../src/types/transmuxer'; import { logger } from '../../../src/utils/logger'; import type { HlsEventEmitter } from '../../../src/events'; -import type { VideoSample } from '../../../src/types/demuxer'; +import type { + AudioSample, + DemuxedAudioTrack, + DemuxedMetadataTrack, + DemuxedUserdataTrack, + DemuxedVideoTrack, + VideoSample, +} from '../../../src/types/demuxer'; import type { TypeSupported } from '../../../src/utils/codecs'; use(sinonChai); +const VIDEO_FRAME_DURATION = 3600; // 25 fps in 90 kHz ticks +const AAC_FRAME_DURATION = 1920; // 1024 samples at 48 kHz in 90 kHz ticks + describe('mp4-remuxer', function () { let mp4Remuxer: MP4Remuxer; @@ -102,6 +115,84 @@ describe('mp4-remuxer', function () { ); expect(result).to.be.gt(0); }); + + describe('initPTS anchoring', function () { + const frames = 100; // 4 seconds at 25 fps + const fragmentDuration = (frames * VIDEO_FRAME_DURATION) / 90000; + + it('anchors the first segment of a session on video DTS so earlier segments keep composition headroom', function () { + // start mid-stream at playlist time 20 (#7932): the first DTS maps to 20, the first PTS to 20 + cts + const firstDts = 1800000 + 7830; + const result = remuxFragment( + mp4Remuxer, + 3, + makeVideoTrack(firstDts, 7200, frames), + makeAudioTrack(firstDts + 9200, 185), + 20, + ); + expect(result.initSegment?.initPTS).to.equal(firstDts - 20 * 90000); + expect(result.video?.startPTS).to.be.closeTo(20 + 7200 / 90000, 0.001); + }); + + it('anchors on video DTS after seeking (non-contiguous) into another discontinuity sequence', function () { + remuxFragment( + mp4Remuxer, + 3, + makeVideoTrack(1800000, 7200, frames), + makeAudioTrack(1800000 + 9200, 185), + 20, + ); + // seek: the transmuxer resets the init segment, the timestamp offset (new cc, no cached initPTS) and contiguity + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null); + mp4Remuxer.resetNextTimestamp(); + const firstDts = 30000000; + const result = remuxFragment( + mp4Remuxer, + 40, + makeVideoTrack(firstDts, 7200, frames), + makeAudioTrack(firstDts + 9200, 185), + 300, + ); + expect(result.initSegment?.initPTS).to.equal(firstDts - 300 * 90000); + }); + + it('does not shift the timeline by the composition offset at each discontinuity (#8064)', function () { + // cc 0 mirrors the first segment of the deltatre DAI test stream: cts 40 ms, first audio 99 ms after the first video DTS + let result = remuxFragment( + mp4Remuxer, + 0, + makeVideoTrack(900000, 3600, frames), + makeAudioTrack(900000 + 8951, 185), + 0, + ); + let timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); + // cc 1 to 3 mirror the ad segments: cts 80 ms, first audio 101 ms then 80 ms after the first video DTS + const sequences = [ + { dts: 118800, cts: 7200, audioOffset: 9119 }, + { dts: 118800, cts: 7200, audioOffset: 7200 }, + { dts: 118800, cts: 7200, audioOffset: 7200 }, + ]; + sequences.forEach(({ dts, cts, audioOffset }, i) => { + // discontinuity with consecutive sn: the transmuxer resets the init segment and the timestamp offset, but not contiguity + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null); + result = remuxFragment( + mp4Remuxer, + i + 1, + makeVideoTrack(dts, cts, frames), + makeAudioTrack(dts + audioOffset, 185), + timeOffset, + ); + // the remuxed video must end one fragment duration after the timeOffset it was asked to start at + expect(result.video!.endPTS, `cc ${i + 1} video endPTS`).to.be.closeTo( + timeOffset + fragmentDuration, + 0.001, + ); + timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); + }); + }); + }); }); function ptsDts(pts: number, dts: number): VideoSample { @@ -119,3 +210,81 @@ function ptsDts(pts: number, dts: number): VideoSample { length: 1, }; } + +function makeVideoTrack( + firstDts: number, + cts: number, + frames: number, +): DemuxedVideoTrack { + const samples: VideoSample[] = []; + for (let i = 0; i < frames; i++) { + const dts = firstDts + i * VIDEO_FRAME_DURATION; + samples.push({ + dts, + pts: dts + cts, + key: i === 0, + frame: true, + units: [{ data: new Uint8Array(8), type: i === 0 ? 5 : 1 }], + length: 8, + }); + } + return { + ...dummyTrack('video'), + type: 'video', + id: 1, + pid: 256, + segmentCodec: 'avc', + codec: 'avc1.64001f', + duration: 10, + sps: [new Uint8Array([0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40])], + pps: [new Uint8Array([0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0])], + width: 720, + height: 408, + pixelRatio: [1, 1], + samples, + }; +} + +function makeAudioTrack(firstPts: number, frames: number): DemuxedAudioTrack { + const samples: AudioSample[] = []; + for (let i = 0; i < frames; i++) { + samples.push({ + pts: firstPts + i * AAC_FRAME_DURATION, + unit: new Uint8Array(16), + }); + } + return { + ...dummyTrack('audio'), + type: 'audio', + id: 0, + pid: 257, + segmentCodec: 'aac', + codec: 'mp4a.40.2', + manifestCodec: 'mp4a.40.2', + duration: 10, + config: [0x11, 0x90], + samplerate: 48000, + channelCount: 2, + samples, + }; +} + +function remuxFragment( + remuxer: MP4Remuxer, + sn: number, + video: DemuxedVideoTrack, + audio: DemuxedAudioTrack, + timeOffset: number, +) { + return remuxer.remux( + audio, + video, + dummyTrack('id3') as DemuxedMetadataTrack, + dummyTrack('text') as DemuxedUserdataTrack, + timeOffset, + true, + true, + PlaylistLevelType.MAIN, + new ChunkMetadata(0, sn, 1, 0), + ); +} From bf71d9b8e6f4f624941e42733b5a1818e6cc92f2 Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:39:30 -0700 Subject: [PATCH 2/8] Pass fragment sequence continuity to the remuxer so level switches at discontinuities keep the PTS anchor The remuxer used its own video contiguity flag to decide when a new timestamp offset continues the previous fragment. A level switch at a discontinuity resets contiguity (samples from another level must not be stitched), so that boundary still took the DTS anchor and shifted the timeline once. The transmuxer already distinguishes "consecutive" (next sn or part, any level) from "contiguous" (consecutive and same level). Carry consecutive in TransmuxState and hand it to Remuxer.resetTimeStamp, and let generateIS anchor on the first PTS whenever a fresh offset is computed for a consecutive fragment. Session start and seeks stay on the DTS anchor. Refs video-dev/hls.js#8064 Co-Authored-By: Claude Fable 5.1 --- src/demux/transmuxer-interface.ts | 12 ++++--- src/demux/transmuxer.ts | 16 ++++++++-- src/remux/mp4-remuxer.ts | 22 +++++++++---- src/types/remuxer.ts | 5 ++- tests/unit/demuxer/transmuxer.ts | 53 +++++++++++++++++++++++++++++++ tests/unit/remux/mp4-remuxer.ts | 38 ++++++++++++++++++++-- 6 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index bb78313e70a..faf2f13beec 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -210,10 +210,12 @@ export default class TransmuxerInterface { snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === lastFrag?.stats.chunkCount; - const contiguous = - !trackSwitch && - (snDiff === 1 || - (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0)))); + // consecutive: this fragment or part directly follows the last one pushed, on any level + // contiguous: consecutive and from the same level, so samples can be stitched together + const consecutive = + snDiff === 1 || + (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0))); + const contiguous = !trackSwitch && consecutive; const now = self.performance.now(); if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { @@ -232,6 +234,7 @@ export default class TransmuxerInterface { trackSwitch, timeOffset, initSegmentChange, + consecutive, ); if (!contiguous || discontinuity || initSegmentChange) { this.hls.logger @@ -241,6 +244,7 @@ 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 d7e0d30853b..1d35f39e068 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -106,6 +106,7 @@ export default class Transmuxer { const { contiguous, + consecutive, discontinuity, trackSwitch, accurateTimeOffset, @@ -181,7 +182,7 @@ export default class Transmuxer { } if (discontinuity || initSegmentChange || resetMuxers) { - this.resetInitialTimestamp(defaultInitPts); + this.resetInitialTimestamp(defaultInitPts, consecutive); } if (!contiguous) { @@ -200,6 +201,7 @@ export default class Transmuxer { const currentState = this.currentTransmuxState; currentState.contiguous = true; + currentState.consecutive = true; currentState.discontinuity = false; currentState.trackSwitch = false; @@ -304,13 +306,16 @@ export default class Transmuxer { chunkMeta.transmuxing.executeEnd = now(); } - resetInitialTimestamp(defaultInitPts: TimestampOffset | null) { + resetInitialTimestamp( + defaultInitPts: TimestampOffset | null, + consecutive: boolean = false, + ) { const { demuxer, remuxer } = this; if (!demuxer || !remuxer) { return; } demuxer.resetTimeStamp(defaultInitPts); - remuxer.resetTimeStamp(defaultInitPts); + remuxer.resetTimeStamp(defaultInitPts, consecutive); } resetContiguity() { @@ -570,6 +575,9 @@ export class TransmuxState { public trackSwitch: boolean; public timeOffset: number; public initSegmentChange: boolean; + // the fragment or part directly follows the last one pushed, on any level + // (contiguous implies consecutive; a level switch is consecutive but not contiguous) + public consecutive: boolean; constructor( discontinuity: boolean, @@ -578,6 +586,7 @@ export class TransmuxState { trackSwitch: boolean, timeOffset: number, initSegmentChange: boolean, + consecutive: boolean = contiguous, ) { this.discontinuity = discontinuity; this.contiguous = contiguous; @@ -585,5 +594,6 @@ 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 c0ca8771cbd..7486760c319 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -77,6 +77,9 @@ export default class MP4Remuxer extends Logger implements Remuxer { private _initDTS: TimestampOffset | null = null; private nextVideoTs: number | null = null; private nextAudioTs: number | null = null; + // the fragment being remuxed directly follows the last one (on any level), so a new + // timestamp offset computed for it starts where the previous fragment's presentation ended + private isConsecutive: boolean = false; private videoSampleDuration: number | null = null; private isAudioContiguous: boolean = false; private isVideoContiguous: boolean = false; @@ -104,7 +107,10 @@ export default class MP4Remuxer extends Logger implements Remuxer { this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; } - resetTimeStamp(defaultTimeStamp: TimestampOffset | null) { + resetTimeStamp( + defaultTimeStamp: TimestampOffset | null, + consecutive: boolean = false, + ) { const initPTS = this._initPTS; if ( !initPTS || @@ -115,10 +121,11 @@ export default class MP4Remuxer extends Logger implements Remuxer { defaultTimeStamp.timescale !== initPTS.timescale ) { this.log( - `Reset initPTS: ${initPTS ? timestampToString(initPTS) : initPTS} > ${defaultTimeStamp ? timestampToString(defaultTimeStamp) : defaultTimeStamp}`, + `Reset initPTS: ${initPTS ? timestampToString(initPTS) : initPTS} > ${defaultTimeStamp ? timestampToString(defaultTimeStamp) : defaultTimeStamp} consecutive: ${consecutive}`, ); } + this.isConsecutive = consecutive; this._initPTS = this._initDTS = defaultTimeStamp; } @@ -486,12 +493,13 @@ 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 (#7932). - // When continuing contiguous content (a discontinuity), timeOffset is the previous - // fragment's presentation end, so bind to the first PTS instead: anchoring the first - // DTS there presents the new sequence one composition offset late, and that shift - // repeats at every discontinuity (#8064). + // When the fragment directly follows the last one (a discontinuity reached in + // sequence, with or without a level switch), timeOffset is the previous fragment's + // presentation end, so bind to the first PTS instead: anchoring the first DTS there + // presents the new sequence one composition offset late, and that shift repeats at + // every discontinuity (#8064). const videoInit = - !_initPTS && this.isVideoContiguous ? videoInitPTS : videoInitDTS; + !_initPTS && this.isConsecutive ? videoInitPTS : videoInitDTS; initPTS = Math.min(initPTS as number, videoInit); } } diff --git a/src/types/remuxer.ts b/src/types/remuxer.ts index 0686726f8e9..32e09aeae36 100644 --- a/src/types/remuxer.ts +++ b/src/types/remuxer.ts @@ -35,7 +35,10 @@ export interface Remuxer { videoCodec: string | undefined, decryptdata: DecryptData | null, ): void; - resetTimeStamp(defaultInitPTS: TimestampOffset | null): void; + resetTimeStamp( + defaultInitPTS: TimestampOffset | null, + consecutive?: boolean, + ): void; resetNextTimestamp(): void; destroy(): void; } diff --git a/tests/unit/demuxer/transmuxer.ts b/tests/unit/demuxer/transmuxer.ts index 733e08eee5e..a5c882412b8 100644 --- a/tests/unit/demuxer/transmuxer.ts +++ b/tests/unit/demuxer/transmuxer.ts @@ -258,6 +258,59 @@ 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('sends worker generic message', function () { const config = { enableWorker: true }; // Option debug : true crashes mocha hls = new Hls(config); diff --git a/tests/unit/remux/mp4-remuxer.ts b/tests/unit/remux/mp4-remuxer.ts index ebf638eec6d..0bf61ac8899 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: the transmuxer resets the init segment and the timestamp offset, but not contiguity + // discontinuity with consecutive sn on the same level: the transmuxer resets the init segment and the timestamp offset (consecutive), but not contiguity mp4Remuxer.resetInitSegment(); - mp4Remuxer.resetTimeStamp(null); + mp4Remuxer.resetTimeStamp(null, true); result = remuxFragment( mp4Remuxer, i + 1, @@ -192,6 +192,40 @@ describe('mp4-remuxer', function () { timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); }); }); + + it('does not shift the timeline at a discontinuity reached with a level switch (consecutive but not contiguous)', function () { + let result = remuxFragment( + mp4Remuxer, + 0, + makeVideoTrack(900000, 3600, frames), + makeAudioTrack(900000 + 8951, 185), + 0, + ); + 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 + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null, true); + mp4Remuxer.resetNextTimestamp(); + // timestamps above the playlist time, so computeInitPts needs no 33-bit rollover adjustment + const dts = 9000000; + const cts = 7200; + result = remuxFragment( + mp4Remuxer, + 1, + makeVideoTrack(dts, cts, frames), + makeAudioTrack(dts + 9119, 185), + timeOffset, + ); + // anchored on the first PTS: the sequence starts at timeOffset and ends one fragment later + expect(result.initSegment?.initPTS).to.equal( + dts + cts - Math.round(timeOffset * 90000), + ); + expect(result.video!.endPTS).to.be.closeTo( + timeOffset + fragmentDuration, + 0.001, + ); + }); }); }); From b04b51441ce3f6c3007053e55a0eb349a00c81cc Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:19:35 -0700 Subject: [PATCH 3/8] Establish cross-level continuity from presentation time, not sequence numbers Variant Streams may number Media Segments independently (RFC 8216 6.3.2), so a discontinuity switch to a level with another sequence-number base was marked non-consecutive and kept the DTS anchor, preserving the drift this branch fixes. Across a level switch, treat the fragment as consecutive when it starts where the last pushed fragment or part ends, within maxFragLookUpTolerance. Same-level sequencing and the contiguous flag are unchanged. Refs video-dev/hls.js#8064 Co-Authored-By: Claude Fable 5.1 --- src/demux/transmuxer-interface.ts | 16 +++- src/demux/transmuxer.ts | 5 +- tests/unit/demuxer/transmuxer.ts | 118 ++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index faf2f13beec..c1d675adfe4 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -210,12 +210,20 @@ export default class TransmuxerInterface { snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === lastFrag?.stats.chunkCount; - // consecutive: this fragment or part directly follows the last one pushed, on any level - // contiguous: consecutive and from the same level, so samples can be stitched together - const consecutive = + const nextInSequence = snDiff === 1 || (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0))); - const contiguous = !trackSwitch && consecutive; + // contiguous: next in sequence on the same level, so samples can be stitched together + const contiguous = !trackSwitch && nextInSequence; + // consecutive: this fragment or part directly follows the last one pushed. Variant Streams may + // number Media Segments independently (RFC 8216 6.3.2), so across a level switch the fragment + // follows the last one when it starts where that fragment's playlist time ends + const consecutive = + nextInSequence || + (trackSwitch && + lastFrag !== null && + Math.abs(timeOffset - (this.part ? this.part.end : lastFrag.end)) <= + this.hls.config.maxFragLookUpTolerance); const now = self.performance.now(); if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index 1d35f39e068..4d8755cf2e3 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -575,8 +575,9 @@ export class TransmuxState { public trackSwitch: boolean; public timeOffset: number; public initSegmentChange: boolean; - // the fragment or part directly follows the last one pushed, on any level - // (contiguous implies consecutive; a level switch is consecutive but not contiguous) + // the fragment or part directly follows the last one pushed: next in sequence on any level, or, + // across a level switch, starting where the last fragment ended (Variant Streams may number + // Media Segments independently). Contiguous implies consecutive. public consecutive: boolean; constructor( diff --git a/tests/unit/demuxer/transmuxer.ts b/tests/unit/demuxer/transmuxer.ts index a5c882412b8..b22ece75caa 100644 --- a/tests/unit/demuxer/transmuxer.ts +++ b/tests/unit/demuxer/transmuxer.ts @@ -311,6 +311,124 @@ describe('TransmuxerInterface tests', function () { ); }); + 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('sends worker generic message', function () { const config = { enableWorker: true }; // Option debug : true crashes mocha hls = new Hls(config); From 31fab07ff33b149365122d24663d27bd2c586af1 Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:28:46 -0700 Subject: [PATCH 4/8] Pin the consecutive flag's path through Transmuxer.push Transmuxer.push is the only place TransmuxState.consecutive reaches Remuxer.resetTimeStamp, on both the inline and the worker path, and nothing exercised it: the specs covered the interface computing the flag and the remuxer honoring it. Drive push() with stubbed muxers through the discontinuity branches: a sequence reached in order (same level or level switch) resets the offset with consecutive set, a seek resets it cleared, the cached per-cc offset passes through, and a contiguous push does not reset at all. Also note on MP4Remuxer.isConsecutive that it comes from fragment sequencing, distinct from the sample-timing heuristic remuxAudio already calls "consecutive". Refs video-dev/hls.js#8064 Co-Authored-By: Claude Fable 5.1 --- src/remux/mp4-remuxer.ts | 4 +- tests/unit/demuxer/transmuxer.ts | 119 +++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index 7486760c319..4aa4ad78925 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -78,7 +78,9 @@ export default class MP4Remuxer extends Logger implements Remuxer { private nextVideoTs: number | null = null; private nextAudioTs: number | null = null; // the fragment being remuxed directly follows the last one (on any level), so a new - // timestamp offset computed for it starts where the previous fragment's presentation ended + // timestamp offset computed for it starts where the previous fragment's presentation ended. + // Set from TransmuxState.consecutive (fragment sequencing); not the sample-timing heuristic + // remuxAudio uses to treat near-adjacent audio fragments as contiguous private isConsecutive: boolean = false; private videoSampleDuration: number | null = null; private isAudioContiguous: boolean = false; diff --git a/tests/unit/demuxer/transmuxer.ts b/tests/unit/demuxer/transmuxer.ts index b22ece75caa..68d5950f1d8 100644 --- a/tests/unit/demuxer/transmuxer.ts +++ b/tests/unit/demuxer/transmuxer.ts @@ -7,8 +7,10 @@ import Hls from '../../../src/hls'; import { Fragment } from '../../../src/loader/fragment'; import { PlaylistLevelType } from '../../../src/types/loader'; import { ChunkMetadata } from '../../../src/types/transmuxer'; +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); @@ -512,3 +514,120 @@ 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; + }); +}); From 6a261e674dcbdc9d608bd06c35150a2988afc145 Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:37:08 -0700 Subject: [PATCH 5/8] Cover the part-end leg and the session-start guard of the continuity rule The cross-level continuity term compares the pushed part's start with the last pushed part's end, and only looks at the last fragment when there is one. Neither leg had a spec: add a level switch at a discontinuity between the last part of one fragment and the first part of the next, and a first push with nothing before it. Both assert the TransmuxState fields explicitly. Refs video-dev/hls.js#8064 Co-Authored-By: Claude Fable 5.1 --- tests/unit/demuxer/transmuxer.ts | 124 ++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/tests/unit/demuxer/transmuxer.ts b/tests/unit/demuxer/transmuxer.ts index 68d5950f1d8..2728f62068d 100644 --- a/tests/unit/demuxer/transmuxer.ts +++ b/tests/unit/demuxer/transmuxer.ts @@ -4,9 +4,10 @@ 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 } from '../../../src/loader/fragment'; +import { Fragment, Part } 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'; @@ -431,6 +432,127 @@ describe('TransmuxerInterface tests', function () { ); }); + 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); From f11d6ba88c173f10923055d7d1570a87b09213d5 Mon Sep 17 00:00:00 2001 From: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:22:05 -0700 Subject: [PATCH 6/8] Remove explanatory comment blocks to match surrounding code style Co-Authored-By: Claude Fable 5.1 --- src/demux/transmuxer-interface.ts | 4 ---- src/demux/transmuxer.ts | 3 --- src/remux/mp4-remuxer.ts | 11 +---------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index c1d675adfe4..14c36c1ade3 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -213,11 +213,7 @@ export default class TransmuxerInterface { const nextInSequence = snDiff === 1 || (snDiff === 0 && (partDiff === 1 || (progressive && partDiff <= 0))); - // contiguous: next in sequence on the same level, so samples can be stitched together const contiguous = !trackSwitch && nextInSequence; - // consecutive: this fragment or part directly follows the last one pushed. Variant Streams may - // number Media Segments independently (RFC 8216 6.3.2), so across a level switch the fragment - // follows the last one when it starts where that fragment's playlist time ends const consecutive = nextInSequence || (trackSwitch && diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index 4d8755cf2e3..97e134bb79b 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -575,9 +575,6 @@ export class TransmuxState { public trackSwitch: boolean; public timeOffset: number; public initSegmentChange: boolean; - // the fragment or part directly follows the last one pushed: next in sequence on any level, or, - // across a level switch, starting where the last fragment ended (Variant Streams may number - // Media Segments independently). Contiguous implies consecutive. public consecutive: boolean; constructor( diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index 4aa4ad78925..1cb016bef10 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -77,10 +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; - // the fragment being remuxed directly follows the last one (on any level), so a new - // timestamp offset computed for it starts where the previous fragment's presentation ended. - // Set from TransmuxState.consecutive (fragment sequencing); not the sample-timing heuristic - // remuxAudio uses to treat near-adjacent audio fragments as contiguous private isConsecutive: boolean = false; private videoSampleDuration: number | null = null; private isAudioContiguous: boolean = false; @@ -494,12 +490,7 @@ export default class MP4Remuxer extends Logger implements Remuxer { (timeOffset === 0 && videoInitPTS < _initPTS.baseTime) ) { initDTS = Math.min(initDTS as number, videoInitDTS); - // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps (#7932). - // When the fragment directly follows the last one (a discontinuity reached in - // sequence, with or without a level switch), timeOffset is the previous fragment's - // presentation end, so bind to the first PTS instead: anchoring the first DTS there - // presents the new sequence one composition offset late, and that shift repeats at - // every discontinuity (#8064). + // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps const videoInit = !_initPTS && this.isConsecutive ? videoInitPTS : videoInitDTS; initPTS = Math.min(initPTS as number, videoInit); 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 7/8] 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, ); }); From 5929c184ed0746a42714ce0d1753239b75ab859d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:06:29 +0000 Subject: [PATCH 8/8] Update dependency wrangler to v4.148.0 --- package-lock.json | 132 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d684b8315c..d359381202e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,7 @@ "sinon-chai": "4.0.1", "typescript": "5.9.3", "url-toolkit": "2.2.5", - "wrangler": "4.147.0" + "wrangler": "4.148.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1850,9 +1850,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261001.1.tgz", - "integrity": "sha512-4cgSgDf28JSw/P5Dj5GCS59hzVqS5XnmGAWNkvYHLI6ODU9idGaMMNEuhJXDkEG/lsABmlVlnCgsdh2VbKWepw==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261006.1.tgz", + "integrity": "sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q==", "cpu": [ "x64" ], @@ -1867,9 +1867,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261001.1.tgz", - "integrity": "sha512-8ulAWruEVouNmEIsQsy9WSSCS9zkLu93W2MTwp5esiFyoPp05BNSVFIFsYSPi1pkFmBBd7fsnwMpbLkaJLaVPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261006.1.tgz", + "integrity": "sha512-PyFGjOaldtppnLuKsdkoYWXfLVpDjuf8u+uoykX+1Xvxo2EPmCVViq0iDKm+U7c7PRno85ehS3/MPVXyk2HwHw==", "cpu": [ "arm64" ], @@ -1884,9 +1884,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261001.1.tgz", - "integrity": "sha512-kZbTZJGrhsMOdqZ2BIybjaBRLZjYsRLWj7mcNw/6Y3hodOhp5MrNzcz4iWGwNVWwyIV+7Pl+/LX5VcgnRqdHOg==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261006.1.tgz", + "integrity": "sha512-Ntf9S8eesmVufdjaT2kzWlz6Ng9XGYTc6cHQVRKAg59lFufwsIabGeMT6U4KkrpEeSIxgHq8aPtuLbZXE19tCA==", "cpu": [ "x64" ], @@ -1901,9 +1901,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261001.1.tgz", - "integrity": "sha512-oOk3Zj6k/8oP0FJgZBWDn7+BqbsqIMEMaV95pulHPVbwVu4wYoLfQq2hp0vkbCNsCFLqZb7cqixXdrwD54ZIow==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261006.1.tgz", + "integrity": "sha512-JyeDBymVWw5tVRebSsVE8ojDYxd6UlZrAseHU8G+NpIyd5wF61tXlF/axLeCUqu0pOhBFHb8I5QKi0jwrCK41Q==", "cpu": [ "arm64" ], @@ -1918,9 +1918,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261001.1.tgz", - "integrity": "sha512-uRxm5W4VyBkoSaoP1BfOuH0873tE+vxsknF4sab6/5YIRvIAufChq3QDp+zlAn67PuGPGcn7W8QraA0t4ucmOQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261006.1.tgz", + "integrity": "sha512-nWP0URNqOBN9fa/gZ7eh/w051DwjOXN8j4lDZDYVXyQPsux6sV3DfyYZT+CjR/rt9nKBSmJT9QE9/bWT18+84w==", "cpu": [ "x64" ], @@ -10229,16 +10229,16 @@ } }, "node_modules/miniflare": { - "version": "5.20261001.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261001.0-alpha.tgz", - "integrity": "sha512-GaimS5mSIOMyvd16ga+e1/QkI8cmp3z35QPHFex0DktqNQf2RVZQwMPQXdyoUreUiFP/0Gpe2MoQInTyMAD5xA==", + "version": "5.20261006.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261006.0-alpha.tgz", + "integrity": "sha512-UP3qIeQKHba59GHxBqD8H2GateacluT4vrjIpqEgH78kCSUxdu8jA0f7MH7ftRSAnnB1oOvEsDtav3xevQDbpQ==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.1", - "workerd": "1.20261001.1", + "workerd": "1.20261006.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -13948,9 +13948,9 @@ "dev": true }, "node_modules/workerd": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261001.1.tgz", - "integrity": "sha512-d/SIYHFO0PT/wiFZg8in4NpRIxYuFwslX1HdylOtWkBIIUmSpkGFhK820cV84XACFylwJ48xuRoWW/8DWDPsPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261006.1.tgz", + "integrity": "sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -13961,11 +13961,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20261001.1", - "@cloudflare/workerd-darwin-arm64": "1.20261001.1", - "@cloudflare/workerd-linux-64": "1.20261001.1", - "@cloudflare/workerd-linux-arm64": "1.20261001.1", - "@cloudflare/workerd-windows-64": "1.20261001.1" + "@cloudflare/workerd-darwin-64": "1.20261006.1", + "@cloudflare/workerd-darwin-arm64": "1.20261006.1", + "@cloudflare/workerd-linux-64": "1.20261006.1", + "@cloudflare/workerd-linux-arm64": "1.20261006.1", + "@cloudflare/workerd-windows-64": "1.20261006.1" } }, "node_modules/workerpool": { @@ -13975,9 +13975,9 @@ "dev": true }, "node_modules/wrangler": { - "version": "4.147.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.147.0.tgz", - "integrity": "sha512-pQYRoiq8PTAxphaG69z8+GC1DkSGd19EDZehQ8zxjo/Ko3mRB6Qs1mTrd8ZuKAarLklIjTqr1lUdCK9r4q2hUg==", + "version": "4.148.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.148.0.tgz", + "integrity": "sha512-wgbll8cA/7qOMJSoYQuJrw9M9lmedGzYtg6wvUK2p/7G1KaE88jFmQ/CvtQzVUF9C8PQtC4Y5p9vbpAsJGAlpA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -13985,10 +13985,10 @@ "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20261001.0-alpha", + "miniflare": "5.20261006.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20261001.1" + "workerd": "1.20261006.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -14002,7 +14002,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20261001.1" + "@cloudflare/workers-types": "^5.20261006.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -15526,37 +15526,37 @@ "requires": {} }, "@cloudflare/workerd-darwin-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261001.1.tgz", - "integrity": "sha512-4cgSgDf28JSw/P5Dj5GCS59hzVqS5XnmGAWNkvYHLI6ODU9idGaMMNEuhJXDkEG/lsABmlVlnCgsdh2VbKWepw==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261006.1.tgz", + "integrity": "sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q==", "dev": true, "optional": true }, "@cloudflare/workerd-darwin-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261001.1.tgz", - "integrity": "sha512-8ulAWruEVouNmEIsQsy9WSSCS9zkLu93W2MTwp5esiFyoPp05BNSVFIFsYSPi1pkFmBBd7fsnwMpbLkaJLaVPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261006.1.tgz", + "integrity": "sha512-PyFGjOaldtppnLuKsdkoYWXfLVpDjuf8u+uoykX+1Xvxo2EPmCVViq0iDKm+U7c7PRno85ehS3/MPVXyk2HwHw==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261001.1.tgz", - "integrity": "sha512-kZbTZJGrhsMOdqZ2BIybjaBRLZjYsRLWj7mcNw/6Y3hodOhp5MrNzcz4iWGwNVWwyIV+7Pl+/LX5VcgnRqdHOg==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261006.1.tgz", + "integrity": "sha512-Ntf9S8eesmVufdjaT2kzWlz6Ng9XGYTc6cHQVRKAg59lFufwsIabGeMT6U4KkrpEeSIxgHq8aPtuLbZXE19tCA==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261001.1.tgz", - "integrity": "sha512-oOk3Zj6k/8oP0FJgZBWDn7+BqbsqIMEMaV95pulHPVbwVu4wYoLfQq2hp0vkbCNsCFLqZb7cqixXdrwD54ZIow==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261006.1.tgz", + "integrity": "sha512-JyeDBymVWw5tVRebSsVE8ojDYxd6UlZrAseHU8G+NpIyd5wF61tXlF/axLeCUqu0pOhBFHb8I5QKi0jwrCK41Q==", "dev": true, "optional": true }, "@cloudflare/workerd-windows-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261001.1.tgz", - "integrity": "sha512-uRxm5W4VyBkoSaoP1BfOuH0873tE+vxsknF4sab6/5YIRvIAufChq3QDp+zlAn67PuGPGcn7W8QraA0t4ucmOQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261006.1.tgz", + "integrity": "sha512-nWP0URNqOBN9fa/gZ7eh/w051DwjOXN8j4lDZDYVXyQPsux6sV3DfyYZT+CjR/rt9nKBSmJT9QE9/bWT18+84w==", "dev": true, "optional": true }, @@ -21074,15 +21074,15 @@ "dev": true }, "miniflare": { - "version": "5.20261001.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261001.0-alpha.tgz", - "integrity": "sha512-GaimS5mSIOMyvd16ga+e1/QkI8cmp3z35QPHFex0DktqNQf2RVZQwMPQXdyoUreUiFP/0Gpe2MoQInTyMAD5xA==", + "version": "5.20261006.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261006.0-alpha.tgz", + "integrity": "sha512-UP3qIeQKHba59GHxBqD8H2GateacluT4vrjIpqEgH78kCSUxdu8jA0f7MH7ftRSAnnB1oOvEsDtav3xevQDbpQ==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.1", - "workerd": "1.20261001.1", + "workerd": "1.20261006.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } @@ -23751,16 +23751,16 @@ "dev": true }, "workerd": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261001.1.tgz", - "integrity": "sha512-d/SIYHFO0PT/wiFZg8in4NpRIxYuFwslX1HdylOtWkBIIUmSpkGFhK820cV84XACFylwJ48xuRoWW/8DWDPsPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261006.1.tgz", + "integrity": "sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA==", "dev": true, "requires": { - "@cloudflare/workerd-darwin-64": "1.20261001.1", - "@cloudflare/workerd-darwin-arm64": "1.20261001.1", - "@cloudflare/workerd-linux-64": "1.20261001.1", - "@cloudflare/workerd-linux-arm64": "1.20261001.1", - "@cloudflare/workerd-windows-64": "1.20261001.1" + "@cloudflare/workerd-darwin-64": "1.20261006.1", + "@cloudflare/workerd-darwin-arm64": "1.20261006.1", + "@cloudflare/workerd-linux-64": "1.20261006.1", + "@cloudflare/workerd-linux-arm64": "1.20261006.1", + "@cloudflare/workerd-windows-64": "1.20261006.1" } }, "workerpool": { @@ -23770,9 +23770,9 @@ "dev": true }, "wrangler": { - "version": "4.147.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.147.0.tgz", - "integrity": "sha512-pQYRoiq8PTAxphaG69z8+GC1DkSGd19EDZehQ8zxjo/Ko3mRB6Qs1mTrd8ZuKAarLklIjTqr1lUdCK9r4q2hUg==", + "version": "4.148.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.148.0.tgz", + "integrity": "sha512-wgbll8cA/7qOMJSoYQuJrw9M9lmedGzYtg6wvUK2p/7G1KaE88jFmQ/CvtQzVUF9C8PQtC4Y5p9vbpAsJGAlpA==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.5.0", @@ -23780,10 +23780,10 @@ "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "fsevents": "2.3.3", - "miniflare": "5.20261001.0-alpha", + "miniflare": "5.20261006.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20261001.1" + "workerd": "1.20261006.1" } }, "wrap-ansi": { diff --git a/package.json b/package.json index 9daca509762..594dec2e625 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,6 @@ "sinon-chai": "4.0.1", "typescript": "5.9.3", "url-toolkit": "2.2.5", - "wrangler": "4.147.0" + "wrangler": "4.148.0" } }