Files
hls.js/tests/unit/controller/base-stream-controller.ts
Rob WalchandGitHub 305a3a7477 Flush MSE rendering pipeline when crossing video buffer holes with audio (#6972)
* Flush MSE rendering pipeline when crossing video buffer holes with audio
Resolves #5631
* Don't wait to nudge when stalling with waiting event
Fixes #6169
* Add `hls.inFlightFragments` to Hls API. Used to determine if gap-controller should wait for in-flight media segments before seeking over gaps.
2025-01-31 16:23:13 -08:00

123 lines
4.0 KiB
TypeScript

import chai from 'chai';
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 { MediaFragment, Part } from '../../../src/loader/fragment';
import type { BufferInfo } from '../../../src/utils/buffer-helper';
chai.use(sinonChai);
const expect = chai.expect;
type BaseStreamControllerTestable = Omit<
BaseStreamController,
'media' | '_streamEnded'
> & {
media: HTMLMediaElement | null;
_streamEnded: (bufferInfo: BufferInfo, levelDetails: LevelDetails) => boolean;
};
describe('BaseStreamController', function () {
let hls: Hls;
let baseStreamController: BaseStreamControllerTestable;
let bufferInfo: BufferInfo;
let fragmentTracker;
let media;
beforeEach(function () {
hls = new Hls({});
fragmentTracker = {
state: null,
getState() {
return this.state;
},
isEndListAppended() {
return true;
},
};
baseStreamController = new BaseStreamController(
hls,
fragmentTracker,
new KeyLoader(hlsDefaultConfig),
) as unknown as BaseStreamControllerTestable;
bufferInfo = {
len: 1,
nextStart: 0,
start: 0,
end: 1,
bufferedIndex: 0,
buffered: [{ start: 0, end: 1 }],
};
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 () {
const levelDetails = levelDetailsWithEndSequenceVodOrLive(3, true);
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
.false;
});
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]);
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
levelDetails.partList = [{ start: 0, duration: 1 } as unknown as Part];
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
.true;
});
it('depends on fragment-tracker to determine if last fragment is buffered', function () {
media.buffered = new TimeRangesMock([0, 1]);
const levelDetails = levelDetailsWithEndSequenceVodOrLive(10);
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
.true;
fragmentTracker.isEndListAppended = () => false;
expect(baseStreamController._streamEnded(bufferInfo, levelDetails)).to.be
.false;
});
});
});