From a9863a5a88b6006cc074c39414055e1136c71f8a Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 31 Jan 2025 16:59:33 -0800 Subject: [PATCH] Fix seeking into and over multiple EXT-X-GAP segments Fixes #6814 --- api-extractor/report/hls.js.api.md | 2 +- src/controller/base-stream-controller.ts | 8 ++++---- src/controller/fragment-tracker.ts | 2 +- src/controller/gap-controller.ts | 21 +++++++++++++++++---- src/controller/stream-controller.ts | 2 +- tests/unit/controller/gap-controller.ts | 4 ++-- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 102cf25a5c0..f28443b561f 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1866,7 +1866,7 @@ export class FragmentTracker implements ComponentAPI { getBufferedFrag(position: number, levelType: PlaylistLevelType): MediaFragment | null; // (undocumented) getFragAtPos(position: number, levelType: PlaylistLevelType, buffered?: boolean): MediaFragment | null; - getPartialFragment(time: number): Fragment | null; + getPartialFragment(time: number): MediaFragment | null; // (undocumented) getState(fragment: Fragment): FragmentState; // (undocumented) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 9862c0559e8..2343dc89680 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -1215,11 +1215,11 @@ export default class BaseStreamController bufferedFragAtPos && (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap) ) { - return BufferHelper.bufferInfo( - bufferable, - pos, - Math.max(bufferInfo.nextStart, maxBufferHole), + const gapDuration = Math.max( + Math.min(bufferInfo.nextStart, bufferedFragAtPos.end) - pos, + maxBufferHole, ); + return BufferHelper.bufferInfo(bufferable, pos, gapDuration); } } return bufferInfo; diff --git a/src/controller/fragment-tracker.ts b/src/controller/fragment-tracker.ts index 7f324a04525..3ef5c3a18c9 100644 --- a/src/controller/fragment-tracker.ts +++ b/src/controller/fragment-tracker.ts @@ -321,7 +321,7 @@ export class FragmentTracker implements ComponentAPI { /** * Gets the partial fragment for a certain time */ - public getPartialFragment(time: number): Fragment | null { + public getPartialFragment(time: number): MediaFragment | null { let bestFragment: Fragment | null = null; let timePadding: number; let startTime: number; diff --git a/src/controller/gap-controller.ts b/src/controller/gap-controller.ts index 18a4e8e5c67..3ea4450e206 100644 --- a/src/controller/gap-controller.ts +++ b/src/controller/gap-controller.ts @@ -12,7 +12,7 @@ import type { InFlightData } from './base-stream-controller'; import type { InFlightFragments } from '../hls'; import type Hls from '../hls'; import type { FragmentTracker } from './fragment-tracker'; -import type { Fragment } from '../loader/fragment'; +import type { Fragment, MediaFragment } from '../loader/fragment'; import type { SourceBufferName } from '../types/buffer'; import type { BufferAppendedData, @@ -503,7 +503,7 @@ export default class GapController extends TaskLoop { * @param partial - The partial fragment found at the current time (where playback is stalling). * @private */ - private _trySkipBufferHole(partial: Fragment | null): number { + private _trySkipBufferHole(partial: MediaFragment | null): number { const { fragmentTracker, media } = this; const config = this.hls?.config; if (!media || !fragmentTracker || !config) { @@ -515,7 +515,7 @@ export default class GapController extends TaskLoop { const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart; - if (startTime) { + if (startTime && this.hls) { const bufferStarved = bufferInfo.len <= config.maxBufferHole; const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3; @@ -541,6 +541,19 @@ export default class GapController extends TaskLoop { PlaylistLevelType.MAIN, ); if (startProvisioned) { + // Do not seek when switching variants + if (!this.hls.levels[this.hls.loadLevel]?.details) { + return 0; + } + // Do not seek when loading frags + const inFlightDependency = getInFlightDependency( + this.hls.inFlightFragments, + startTime, + ); + if (inFlightDependency) { + return 0; + } + // Do not seek if we can't walk tracked fragments to end of gap let moreToLoad = false; let pos = startProvisioned.end; while (pos < startTime) { @@ -567,7 +580,7 @@ export default class GapController extends TaskLoop { ); this.moved = true; media.currentTime = targetTime; - if (!partial?.gap && this.hls) { + if (!partial?.gap) { const error = new Error( `fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`, ); diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 3364c75e188..ec8954767ee 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -237,7 +237,7 @@ export default class StreamController protected onTickEnd() { super.onTickEnd(); - if (this.media?.readyState) { + if (this.media?.readyState && this.media.seeking === false) { this.lastCurrentTime = this.media.currentTime; } this.checkFragmentChanged(); diff --git a/tests/unit/controller/gap-controller.ts b/tests/unit/controller/gap-controller.ts index f594d603232..4d08112655d 100644 --- a/tests/unit/controller/gap-controller.ts +++ b/tests/unit/controller/gap-controller.ts @@ -16,7 +16,7 @@ import { import { MockMediaElement, MockMediaSource } from '../utils/mock-media'; import type { HlsConfig } from '../../../src/config'; import type StreamController from '../../../src/controller/stream-controller'; -import type { Fragment } from '../../../src/loader/fragment'; +import type { Fragment, MediaFragment } from '../../../src/loader/fragment'; chai.use(sinonChai); const expect = chai.expect; @@ -202,7 +202,7 @@ describe('GapController', function () { const bufferInfo = BufferHelper.bufferedInfo([], 0, 0); sandbox .stub(gapController.fragmentTracker, 'getPartialFragment') - .returns({} as unknown as Fragment); + .returns({} as unknown as MediaFragment); const skipHoleStub = sandbox.stub(gapController, '_trySkipBufferHole'); gapController._tryFixBufferStall(bufferInfo, 100); expect(skipHoleStub).to.have.been.calledOnce;