Skip to content

Commit

Permalink
Fix stream ended check in interstitial asset players sharing a media-…
Browse files Browse the repository at this point in the history
…source timeline
  • Loading branch information
robwalch committed Dec 14, 2024
1 parent a58f164 commit 523402d
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 30 deletions.
23 changes: 13 additions & 10 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
55 changes: 35 additions & 20 deletions tests/unit/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () {
Expand All @@ -49,41 +50,55 @@ 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(),
} as unknown as HTMLMediaElement;
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
Expand All @@ -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;
Expand Down

0 comments on commit 523402d

Please sign in to comment.