Skip to content

Commit 523402d

Browse files
committed
Fix stream ended check in interstitial asset players sharing a media-source timeline
1 parent a58f164 commit 523402d

File tree

2 files changed

+48
-30
lines changed

2 files changed

+48
-30
lines changed

src/controller/base-stream-controller.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,21 @@ export default class BaseStreamController
198198
bufferInfo: BufferInfo,
199199
levelDetails: LevelDetails,
200200
): boolean {
201-
// If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached,
202-
// of nothing loading/loaded return false
203-
const hasTimelineOffset = this.config.timelineOffset !== undefined;
201+
// Stream is never "ended" when playlist is live or media is detached
202+
if (levelDetails.live || !this.media) {
203+
return false;
204+
}
205+
// Stream is not "ended" when nothing is buffered past the start
206+
const bufferEnd = bufferInfo.end || 0;
207+
const timelineStart = this.config.timelineOffset || 0;
208+
if (bufferEnd <= timelineStart) {
209+
return false;
210+
}
211+
// Stream is not "ended" when there is a second buffered range starting before the end of the playlist
204212
const nextStart = bufferInfo.nextStart;
205213
const hasSecondBufferedRange =
206-
nextStart && (!hasTimelineOffset || nextStart < levelDetails.edge);
207-
if (
208-
levelDetails.live ||
209-
hasSecondBufferedRange ||
210-
!bufferInfo.end ||
211-
!this.media
212-
) {
214+
nextStart && nextStart > timelineStart && nextStart < levelDetails.edge;
215+
if (hasSecondBufferedRange) {
213216
return false;
214217
}
215218
const partList = levelDetails.partList;

tests/unit/controller/base-stream-controller.ts

+35-20
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import sinonChai from 'sinon-chai';
33
import { hlsDefaultConfig } from '../../../src/config';
44
import BaseStreamController from '../../../src/controller/stream-controller';
55
import Hls from '../../../src/hls';
6+
import { Fragment } from '../../../src/loader/fragment';
67
import KeyLoader from '../../../src/loader/key-loader';
8+
import { LevelDetails } from '../../../src/loader/level-details';
9+
import { PlaylistLevelType } from '../../../src/types/loader';
710
import { TimeRangesMock } from '../../mocks/time-ranges.mock';
8-
import type { Fragment, Part } from '../../../src/loader/fragment';
9-
import type { LevelDetails } from '../../../src/loader/level-details';
11+
import type { MediaFragment, Part } from '../../../src/loader/fragment';
1012
import type { BufferInfo } from '../../../src/utils/buffer-helper';
1113

1214
chai.use(sinonChai);
@@ -24,7 +26,6 @@ describe('BaseStreamController', function () {
2426
let hls: Hls;
2527
let baseStreamController: BaseStreamControllerTestable;
2628
let bufferInfo: BufferInfo;
27-
let levelDetails: LevelDetails;
2829
let fragmentTracker;
2930
let media;
3031
beforeEach(function () {
@@ -49,41 +50,55 @@ describe('BaseStreamController', function () {
4950
start: 0,
5051
end: 1,
5152
};
52-
levelDetails = {
53-
endSN: 0,
54-
live: false,
55-
get fragments() {
56-
const frags: Fragment[] = [];
57-
for (let i = 0; i < this.endSN; i++) {
58-
frags.push({ sn: i, type: 'main' } as unknown as Fragment);
59-
}
60-
return frags;
61-
},
62-
} as unknown as LevelDetails;
6353
media = {
6454
duration: 0,
6555
buffered: new TimeRangesMock(),
6656
} as unknown as HTMLMediaElement;
6757
baseStreamController.media = media;
6858
});
6959

60+
function levelDetailsWithEndSequenceVodOrLive(
61+
endSN: number = 1,
62+
live: boolean = false,
63+
) {
64+
const details = new LevelDetails('');
65+
for (let i = 0; i < endSN; i++) {
66+
const frag = new Fragment(PlaylistLevelType.MAIN, '') as MediaFragment;
67+
frag.duration = 5;
68+
frag.sn = i;
69+
frag.start = i * 5;
70+
details.fragments.push(frag);
71+
}
72+
details.live = live;
73+
return details;
74+
}
75+
7076
describe('_streamEnded', function () {
7177
it('returns false if the stream is live', function () {
72-
levelDetails.live = true;
78+
const levelDetails = levelDetailsWithEndSequenceVodOrLive(3, true);
7379
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
7480
.false;
7581
});
7682

77-
it('returns false if there is subsequently buffered range', function () {
78-
levelDetails.endSN = 10;
79-
bufferInfo.nextStart = 100;
83+
it('returns false if there is subsequently buffered range within program range', function () {
84+
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
85+
expect(levelDetails.edge).to.eq(50);
86+
bufferInfo.nextStart = 45;
8087
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
8188
.false;
8289
});
8390

91+
it('returns true if complete and subsequently buffered range is outside program range', function () {
92+
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
93+
expect(levelDetails.edge).to.eq(50);
94+
bufferInfo.nextStart = 100;
95+
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
96+
.true;
97+
});
98+
8499
it('returns true if parts are buffered for low latency content', function () {
85100
media.buffered = new TimeRangesMock([0, 1]);
86-
levelDetails.endSN = 10;
101+
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
87102
levelDetails.partList = [{ start: 0, duration: 1 } as unknown as Part];
88103

89104
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
@@ -92,7 +107,7 @@ describe('BaseStreamController', function () {
92107

93108
it('depends on fragment-tracker to determine if last fragment is buffered', function () {
94109
media.buffered = new TimeRangesMock([0, 1]);
95-
levelDetails.endSN = 10;
110+
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
96111

97112
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
98113
.true;

0 commit comments

Comments
 (0)