From 523402db946db715331aca782066fe60f4de423a Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 13 Dec 2024 12:30:11 -0800 Subject: [PATCH] Fix stream ended check in interstitial asset players sharing a media-source timeline --- src/controller/base-stream-controller.ts | 23 ++++---- .../unit/controller/base-stream-controller.ts | 55 ++++++++++++------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 0d75b3b5700..5157b15b098 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -198,18 +198,21 @@ export default class BaseStreamController bufferInfo: BufferInfo, levelDetails: LevelDetails, ): boolean { - // If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached, - // of nothing loading/loaded return false - const hasTimelineOffset = this.config.timelineOffset !== undefined; + // Stream is never "ended" when playlist is live or media is detached + if (levelDetails.live || !this.media) { + return false; + } + // Stream is not "ended" when nothing is buffered past the start + const bufferEnd = bufferInfo.end || 0; + const timelineStart = this.config.timelineOffset || 0; + if (bufferEnd <= timelineStart) { + return false; + } + // Stream is not "ended" when there is a second buffered range starting before the end of the playlist const nextStart = bufferInfo.nextStart; const hasSecondBufferedRange = - nextStart && (!hasTimelineOffset || nextStart < levelDetails.edge); - if ( - levelDetails.live || - hasSecondBufferedRange || - !bufferInfo.end || - !this.media - ) { + nextStart && nextStart > timelineStart && nextStart < levelDetails.edge; + if (hasSecondBufferedRange) { return false; } const partList = levelDetails.partList; diff --git a/tests/unit/controller/base-stream-controller.ts b/tests/unit/controller/base-stream-controller.ts index b79ff5e947c..6bc3b6cb3c2 100644 --- a/tests/unit/controller/base-stream-controller.ts +++ b/tests/unit/controller/base-stream-controller.ts @@ -3,10 +3,12 @@ import sinonChai from 'sinon-chai'; import { hlsDefaultConfig } from '../../../src/config'; import BaseStreamController from '../../../src/controller/stream-controller'; import Hls from '../../../src/hls'; +import { Fragment } from '../../../src/loader/fragment'; import KeyLoader from '../../../src/loader/key-loader'; +import { LevelDetails } from '../../../src/loader/level-details'; +import { PlaylistLevelType } from '../../../src/types/loader'; import { TimeRangesMock } from '../../mocks/time-ranges.mock'; -import type { Fragment, Part } from '../../../src/loader/fragment'; -import type { LevelDetails } from '../../../src/loader/level-details'; +import type { MediaFragment, Part } from '../../../src/loader/fragment'; import type { BufferInfo } from '../../../src/utils/buffer-helper'; chai.use(sinonChai); @@ -24,7 +26,6 @@ describe('BaseStreamController', function () { let hls: Hls; let baseStreamController: BaseStreamControllerTestable; let bufferInfo: BufferInfo; - let levelDetails: LevelDetails; let fragmentTracker; let media; beforeEach(function () { @@ -49,17 +50,6 @@ describe('BaseStreamController', function () { start: 0, end: 1, }; - levelDetails = { - endSN: 0, - live: false, - get fragments() { - const frags: Fragment[] = []; - for (let i = 0; i < this.endSN; i++) { - frags.push({ sn: i, type: 'main' } as unknown as Fragment); - } - return frags; - }, - } as unknown as LevelDetails; media = { duration: 0, buffered: new TimeRangesMock(), @@ -67,23 +57,48 @@ describe('BaseStreamController', function () { baseStreamController.media = media; }); + function levelDetailsWithEndSequenceVodOrLive( + endSN: number = 1, + live: boolean = false, + ) { + const details = new LevelDetails(''); + for (let i = 0; i < endSN; i++) { + const frag = new Fragment(PlaylistLevelType.MAIN, '') as MediaFragment; + frag.duration = 5; + frag.sn = i; + frag.start = i * 5; + details.fragments.push(frag); + } + details.live = live; + return details; + } + describe('_streamEnded', function () { it('returns false if the stream is live', function () { - levelDetails.live = true; + const levelDetails = levelDetailsWithEndSequenceVodOrLive(3, true); expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be .false; }); - it('returns false if there is subsequently buffered range', function () { - levelDetails.endSN = 10; - bufferInfo.nextStart = 100; + it('returns false if there is subsequently buffered range within program range', function () { + const levelDetails = levelDetailsWithEndSequenceVodOrLive(10); + expect(levelDetails.edge).to.eq(50); + bufferInfo.nextStart = 45; expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be .false; }); + it('returns true if complete and subsequently buffered range is outside program range', function () { + const levelDetails = levelDetailsWithEndSequenceVodOrLive(10); + expect(levelDetails.edge).to.eq(50); + bufferInfo.nextStart = 100; + expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be + .true; + }); + it('returns true if parts are buffered for low latency content', function () { media.buffered = new TimeRangesMock([0, 1]); - levelDetails.endSN = 10; + const levelDetails = levelDetailsWithEndSequenceVodOrLive(10); levelDetails.partList = [{ start: 0, duration: 1 } as unknown as Part]; expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be @@ -92,7 +107,7 @@ describe('BaseStreamController', function () { it('depends on fragment-tracker to determine if last fragment is buffered', function () { media.buffered = new TimeRangesMock([0, 1]); - levelDetails.endSN = 10; + const levelDetails = levelDetailsWithEndSequenceVodOrLive(10); expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be .true;