mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
implemented flush-back-buffer-for-looped-streams
This commit is contained in:
committed by
Rob Walch
parent
47527a3a18
commit
86e7131b30
@@ -778,6 +778,7 @@ export type BufferControllerConfig = {
|
||||
appendTimeout: number;
|
||||
backBufferLength: number;
|
||||
frontBufferFlushThreshold: number;
|
||||
loopBackBufferFlush?: boolean;
|
||||
liveDurationInfinity: boolean;
|
||||
liveBackBufferLength: number | null;
|
||||
};
|
||||
@@ -1812,7 +1813,9 @@ export interface FragBufferedData {
|
||||
// @public (undocumented)
|
||||
export interface FragChangedData {
|
||||
// (undocumented)
|
||||
frag: Fragment;
|
||||
frag: MediaFragment;
|
||||
// (undocumented)
|
||||
previousFrag: MediaFragment | null;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "FragDecryptedData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -30,6 +30,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
|
||||
- [`maxBufferLength`](#maxbufferlength)
|
||||
- [`backBufferLength`](#backbufferlength)
|
||||
- [`frontBufferFlushThreshold`](#frontbufferflushthreshold)
|
||||
- [`loopBackBufferFlush`](#loopbackbufferflush)
|
||||
- [`startOnSegmentBoundary`](#startonsegmentboundary)
|
||||
- [`maxBufferSize`](#maxbuffersize)
|
||||
- [`maxBufferHole`](#maxbufferhole)
|
||||
@@ -648,6 +649,12 @@ The maximum duration of buffered media to keep once it has been played, in secon
|
||||
|
||||
The maximum duration of buffered media, in seconds, from the play position to keep before evicting non-contiguous forward ranges. A value of `Infinity` means no active eviction will take place; This value will always be at least the `maxBufferLength`.
|
||||
|
||||
### `loopBackBufferFlush`
|
||||
|
||||
(default: `undefined`)
|
||||
|
||||
Controls back-buffer flushing on quality upgrades when the underlying `HTMLMediaElement` has `loop` set to `true`. When the player switches up to a higher-quality level during looped playback, the back buffer holds lower-quality segments that will be played again on the next loop. By default (`undefined`), HLS.js flushes those segments so the loop replays at the new, higher quality. Set this to `false` to opt out and preserve the existing back buffer across loops. Has no effect when `media.loop` is `false`.
|
||||
|
||||
### `startOnSegmentBoundary`
|
||||
|
||||
(default: `false`)
|
||||
|
||||
@@ -69,6 +69,7 @@ export type BufferControllerConfig = {
|
||||
appendTimeout: number;
|
||||
backBufferLength: number;
|
||||
frontBufferFlushThreshold: number;
|
||||
loopBackBufferFlush?: boolean;
|
||||
liveDurationInfinity: boolean;
|
||||
/**
|
||||
* @deprecated use backBufferLength
|
||||
@@ -426,6 +427,7 @@ export const hlsDefaultConfig: HlsConfig = {
|
||||
maxBufferLength: 30, // used by stream-controller
|
||||
backBufferLength: Infinity, // used by buffer-controller
|
||||
frontBufferFlushThreshold: Infinity,
|
||||
loopBackBufferFlush: undefined, // used by buffer-controller
|
||||
startOnSegmentBoundary: false, // used by stream-controller
|
||||
nextAudioTrackBufferFlushForwardOffset: 0.25, // used by stream-controller
|
||||
maxBufferSize: 60 * 1000 * 1000, // used by stream-controller
|
||||
|
||||
@@ -2,7 +2,7 @@ import BufferOperationQueue from './buffer-operation-queue';
|
||||
import { createDoNothingErrorAction } from './error-controller';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import { Events } from '../events';
|
||||
import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment';
|
||||
import { ElementaryStreamTypes } from '../loader/fragment';
|
||||
import { DEFAULT_TARGET_DURATION } from '../loader/level-details';
|
||||
import { PlaylistLevelType } from '../types/loader';
|
||||
import { BufferHelper } from '../utils/buffer-helper';
|
||||
@@ -70,6 +70,8 @@ const VIDEO_CODEC_PROFILE_REPLACE =
|
||||
|
||||
const TRACK_REMOVED_ERROR_NAME = 'HlsJsTrackRemovedError';
|
||||
|
||||
const LOOP_FLUSH_SAFETY_MARGIN = 0.25;
|
||||
|
||||
class HlsJsTrackRemovedError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
@@ -1131,36 +1133,39 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
return;
|
||||
}
|
||||
const { backBufferLength, frontBufferFlushThreshold } = config;
|
||||
this.trimBuffers(frontBufferFlushThreshold, backBufferLength);
|
||||
this.trimBuffers(
|
||||
frontBufferFlushThreshold,
|
||||
backBufferLength,
|
||||
data.frag,
|
||||
data.previousFrag,
|
||||
);
|
||||
|
||||
// Only clear append errors on successful encounter of buffered media. Init segments may complete without error for unsupported media.
|
||||
if (isMediaFragment(data.frag)) {
|
||||
const elementaryStreams = data.frag.elementaryStreams;
|
||||
const { appendErrors } = this;
|
||||
const appendErrorType = this.appendError?.sourceBufferName;
|
||||
const elementaryStreams = data.frag.elementaryStreams;
|
||||
const { appendErrors } = this;
|
||||
const appendErrorType = this.appendError?.sourceBufferName;
|
||||
|
||||
Object.keys(elementaryStreams).forEach((type) => {
|
||||
if (!elementaryStreams[type]) {
|
||||
return;
|
||||
}
|
||||
appendErrors[type] = 0;
|
||||
if (type === appendErrorType) {
|
||||
Object.keys(elementaryStreams).forEach((type) => {
|
||||
if (!elementaryStreams[type]) {
|
||||
return;
|
||||
}
|
||||
appendErrors[type] = 0;
|
||||
if (type === appendErrorType) {
|
||||
this.appendError = undefined;
|
||||
}
|
||||
if (type === 'audio' || type === 'video') {
|
||||
appendErrors.audiovideo = 0;
|
||||
if (appendErrorType === 'audiovideo') {
|
||||
this.appendError = undefined;
|
||||
}
|
||||
if (type === 'audio' || type === 'video') {
|
||||
appendErrors.audiovideo = 0;
|
||||
if (appendErrorType === 'audiovideo') {
|
||||
this.appendError = undefined;
|
||||
}
|
||||
} else {
|
||||
appendErrors.audio = 0;
|
||||
appendErrors.video = 0;
|
||||
if (appendErrorType !== 'audiovideo') {
|
||||
this.appendError = undefined;
|
||||
}
|
||||
} else {
|
||||
appendErrors.audio = 0;
|
||||
appendErrors.video = 0;
|
||||
if (appendErrorType !== 'audiovideo') {
|
||||
this.appendError = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get bufferedToEnd(): boolean {
|
||||
@@ -1303,6 +1308,8 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
private trimBuffers(
|
||||
frontBufferFlushThreshold: number,
|
||||
backBufferLength: number,
|
||||
frag?: MediaFragment,
|
||||
previousFrag?: MediaFragment | null,
|
||||
) {
|
||||
const { hls, details, media } = this;
|
||||
if (!media || details === null) {
|
||||
@@ -1323,12 +1330,30 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
? config.liveBackBufferLength
|
||||
: backBufferLength;
|
||||
|
||||
let targetBackBufferPosition = -Infinity;
|
||||
if (Number.isFinite(backBufferLength) && backBufferLength >= 0) {
|
||||
const maxBackBufferLength = Math.max(backBufferLength, targetDuration);
|
||||
const targetBackBufferPosition =
|
||||
targetBackBufferPosition =
|
||||
Math.floor(currentTime / targetDuration) * targetDuration -
|
||||
maxBackBufferLength;
|
||||
}
|
||||
|
||||
// For looped media with a quality upgrade, extend the flush position
|
||||
// to remove lower-quality segments from the back buffer.
|
||||
if (frag) {
|
||||
const loopFlushEnd = this.getLoopBackBufferFlushEnd(
|
||||
frag,
|
||||
previousFrag ?? null,
|
||||
);
|
||||
if (loopFlushEnd > 0) {
|
||||
targetBackBufferPosition = Math.max(
|
||||
targetBackBufferPosition,
|
||||
loopFlushEnd,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBackBufferPosition > 0) {
|
||||
this.flushBackBuffer(
|
||||
currentTime,
|
||||
targetDuration,
|
||||
@@ -1358,6 +1383,57 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For looped media, determine the back buffer flush position to remove
|
||||
* lower-quality segments on a quality upgrade. Returns 0 if no flush is needed.
|
||||
*/
|
||||
private getLoopBackBufferFlushEnd(
|
||||
frag: MediaFragment,
|
||||
previousFrag: MediaFragment | null,
|
||||
): number {
|
||||
const { media } = this;
|
||||
if (
|
||||
this.hls?.config.loopBackBufferFlush === false ||
|
||||
!media?.loop ||
|
||||
!previousFrag ||
|
||||
frag.level <= previousFrag.level
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { video, audiovideo } = frag.elementaryStreams;
|
||||
if (video?.partial || audiovideo?.partial) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const flushEnd =
|
||||
this.getEarliestElementaryStreamStart(frag) - LOOP_FLUSH_SAFETY_MARGIN;
|
||||
if (flushEnd <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Flushing lower quality back buffer for loop: level ${frag.level}, range [0-${flushEnd.toFixed(3)}]`,
|
||||
);
|
||||
return flushEnd;
|
||||
}
|
||||
|
||||
private getEarliestElementaryStreamStart(frag: MediaFragment): number {
|
||||
const { audio, video, audiovideo } = frag.elementaryStreams;
|
||||
let earliest = frag.start;
|
||||
if (audiovideo) {
|
||||
earliest = Math.min(earliest, audiovideo.startDTS);
|
||||
} else {
|
||||
if (audio) {
|
||||
earliest = Math.min(earliest, audio.startDTS);
|
||||
}
|
||||
if (video) {
|
||||
earliest = Math.min(earliest, video.startDTS);
|
||||
}
|
||||
}
|
||||
return earliest;
|
||||
}
|
||||
|
||||
private flushBackBuffer(
|
||||
currentTime: number,
|
||||
targetDuration: number,
|
||||
@@ -1381,7 +1457,12 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
|
||||
bufferEnd: targetBackBufferPosition,
|
||||
});
|
||||
} else if (track?.ended) {
|
||||
} else if (
|
||||
track?.ended &&
|
||||
!(
|
||||
this.media?.loop && this.hls?.config.loopBackBufferFlush !== false
|
||||
)
|
||||
) {
|
||||
this.log(
|
||||
`Cannot flush ${type} back buffer while SourceBuffer is in ended state`,
|
||||
);
|
||||
|
||||
@@ -444,7 +444,10 @@ export default class StreamController
|
||||
const fragPlaying = this.fragPlaying;
|
||||
if (fragPlaying) {
|
||||
const fragCurrentLevel = fragPlaying.level;
|
||||
this.hls.trigger(Events.FRAG_CHANGED, { frag: fragPlaying });
|
||||
this.hls.trigger(Events.FRAG_CHANGED, {
|
||||
frag: fragPlaying,
|
||||
previousFrag,
|
||||
});
|
||||
if (previousFrag?.level !== fragCurrentLevel) {
|
||||
this.hls.trigger(Events.LEVEL_SWITCHED, {
|
||||
level: fragCurrentLevel,
|
||||
|
||||
+2
-1
@@ -298,7 +298,8 @@ export interface SubtitleFragProcessed {
|
||||
}
|
||||
|
||||
export interface FragChangedData {
|
||||
frag: Fragment;
|
||||
frag: MediaFragment;
|
||||
previousFrag: MediaFragment | null;
|
||||
}
|
||||
|
||||
export interface FPSDropData {
|
||||
|
||||
@@ -6,7 +6,11 @@ import { FragmentTracker } from '../../../src/controller/fragment-tracker';
|
||||
import { ErrorDetails, ErrorTypes } from '../../../src/errors';
|
||||
import { Events } from '../../../src/events';
|
||||
import Hls from '../../../src/hls';
|
||||
import { ElementaryStreamTypes, Fragment } from '../../../src/loader/fragment';
|
||||
import {
|
||||
ElementaryStreamTypes,
|
||||
Fragment,
|
||||
type MediaFragment,
|
||||
} from '../../../src/loader/fragment';
|
||||
import M3U8Parser from '../../../src/loader/m3u8-parser';
|
||||
import { PlaylistLevelType } from '../../../src/types/loader';
|
||||
import { ChunkMetadata } from '../../../src/types/transmuxer';
|
||||
@@ -62,7 +66,11 @@ function setSourceBufferBufferedRange(
|
||||
|
||||
function evokeTrimBuffers(hls: HlsTestable) {
|
||||
const frag = new Fragment(PlaylistLevelType.MAIN, '');
|
||||
hls.trigger(Events.FRAG_CHANGED, { frag });
|
||||
frag.sn = 0;
|
||||
hls.trigger(Events.FRAG_CHANGED, {
|
||||
frag: frag as MediaFragment,
|
||||
previousFrag: null,
|
||||
});
|
||||
}
|
||||
|
||||
describe('BufferController with attached media', function () {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { State } from '../../../src/controller/base-stream-controller';
|
||||
import { FragmentState } from '../../../src/controller/fragment-tracker';
|
||||
import { Events } from '../../../src/events';
|
||||
import Hls from '../../../src/hls';
|
||||
import { Fragment } from '../../../src/loader/fragment';
|
||||
import { ElementaryStreamTypes, Fragment } from '../../../src/loader/fragment';
|
||||
import { LevelDetails } from '../../../src/loader/level-details';
|
||||
import { LoadStats } from '../../../src/loader/load-stats';
|
||||
import M3U8Parser from '../../../src/loader/m3u8-parser';
|
||||
@@ -16,6 +16,7 @@ import { PlaylistLevelType } from '../../../src/types/loader';
|
||||
import { AttrList } from '../../../src/utils/attr-list';
|
||||
import { mockFragments as mockFragmentArray } from '../../mocks/data';
|
||||
import { TimeRangesMock } from '../../mocks/time-ranges.mock';
|
||||
import type BufferController from '../../../src/controller/buffer-controller';
|
||||
import type { FragmentTracker } from '../../../src/controller/fragment-tracker';
|
||||
import type StreamController from '../../../src/controller/stream-controller';
|
||||
import type { MediaFragment } from '../../../src/loader/fragment';
|
||||
@@ -774,6 +775,297 @@ describe('StreamController', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush lower quality back buffer when loopBackBufferFlush and media is looped', function () {
|
||||
let triggerSpy: sinon.SinonSpy;
|
||||
let bufferController: BufferController;
|
||||
|
||||
function createMediaMock(options: {
|
||||
loop: boolean;
|
||||
currentTime: number;
|
||||
}): HTMLMediaElement {
|
||||
return {
|
||||
readyState: 4,
|
||||
seeking: false,
|
||||
currentTime: options.currentTime,
|
||||
loop: options.loop,
|
||||
buffered: new TimeRangesMock([0, options.currentTime + 10]),
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
} as unknown as HTMLMediaElement;
|
||||
}
|
||||
|
||||
function createFragment(
|
||||
sn: number,
|
||||
level: number,
|
||||
start: number,
|
||||
options?: { partial?: boolean },
|
||||
): MediaFragment {
|
||||
const frag = new Fragment(PlaylistLevelType.MAIN, `frag-${sn}.ts`);
|
||||
frag.sn = sn;
|
||||
frag.level = level;
|
||||
frag.setStart(start);
|
||||
frag.duration = 10;
|
||||
frag.setElementaryStreamInfo(
|
||||
ElementaryStreamTypes.VIDEO,
|
||||
start,
|
||||
start + 10,
|
||||
start,
|
||||
start + 10,
|
||||
options?.partial ?? false,
|
||||
);
|
||||
return frag as MediaFragment;
|
||||
}
|
||||
|
||||
function getBufferFlushingCalls() {
|
||||
return triggerSpy
|
||||
.getCalls()
|
||||
.filter((call) => call.args[0] === Events.BUFFER_FLUSHING);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
triggerSpy = sinon.spy(hls, 'trigger');
|
||||
bufferController = hls['bufferController'] as BufferController;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
triggerSpy.restore();
|
||||
});
|
||||
|
||||
function setupLevelDetails(fragments: MediaFragment[]) {
|
||||
const details = new LevelDetails('');
|
||||
details.fragments = fragments;
|
||||
details.startSN = fragments[0]?.sn as number;
|
||||
details.endSN = fragments[fragments.length - 1]?.sn as number;
|
||||
bufferController['details'] = details;
|
||||
}
|
||||
|
||||
function attachMediaAndStubTracker(
|
||||
media: HTMLMediaElement,
|
||||
frag: MediaFragment,
|
||||
) {
|
||||
hls.trigger(Events.MEDIA_ATTACHED, { media });
|
||||
// Set media directly on buffer-controller to avoid MEDIA_ATTACHING
|
||||
// side effects (MediaSource creation) that leak into other tests.
|
||||
bufferController['media'] = media;
|
||||
// Set up a mock source buffer so flushBackBuffer can iterate and flush
|
||||
const mockSourceBuffer = {
|
||||
buffered: media.buffered,
|
||||
updating: false,
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
remove: sinon.stub(),
|
||||
};
|
||||
bufferController['sourceBuffers'] = [
|
||||
['video', mockSourceBuffer as any],
|
||||
[null, null],
|
||||
];
|
||||
bufferController['tracks'] = {
|
||||
video: { buffer: mockSourceBuffer },
|
||||
} as any;
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag);
|
||||
}
|
||||
|
||||
it('should flush up to the start of fragPlaying on level upgrade', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
const newFrag = createFragment(2, 2, 20);
|
||||
|
||||
setupLevelDetails([frag0, frag1, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
const flushCalls = getBufferFlushingCalls();
|
||||
expect(flushCalls).to.have.length(1);
|
||||
expect(flushCalls[0].args[1]).to.deep.include({
|
||||
startOffset: 0,
|
||||
endOffset: 19.75,
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger BUFFER_FLUSHING on level upgrade when loopBackBufferFlush is unset and media.loop is true', function () {
|
||||
hls.config.loopBackBufferFlush = undefined;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
const newFrag = createFragment(2, 2, 20);
|
||||
|
||||
setupLevelDetails([frag0, frag1, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
const flushCalls = getBufferFlushingCalls();
|
||||
expect(flushCalls).to.have.length(1);
|
||||
expect(flushCalls[0].args[1]).to.deep.include({
|
||||
startOffset: 0,
|
||||
endOffset: 19.75,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when loopBackBufferFlush is unset and media.loop is false', function () {
|
||||
hls.config.loopBackBufferFlush = undefined;
|
||||
const media = createMediaMock({ loop: false, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const newFrag = createFragment(2, 2, 20);
|
||||
|
||||
setupLevelDetails([frag0, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when loopBackBufferFlush is disabled', function () {
|
||||
hls.config.loopBackBufferFlush = false;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const newFrag = createFragment(2, 2, 20);
|
||||
|
||||
setupLevelDetails([frag0, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when media.loop is false', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: false, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const newFrag = createFragment(2, 2, 20);
|
||||
|
||||
setupLevelDetails([frag0, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when level has not changed', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 15 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
|
||||
setupLevelDetails([frag0, frag1]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag1);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when fragPlaying is the first fragment', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 5 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const newFrag0 = createFragment(0, 2, 0);
|
||||
|
||||
setupLevelDetails([frag0]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
// Replace level details with the new level's fragment at position 0
|
||||
setupLevelDetails([newFrag0]);
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag0);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING twice for the same level upgrade', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
const frag2 = createFragment(2, 2, 20);
|
||||
const frag3 = createFragment(3, 2, 30);
|
||||
|
||||
setupLevelDetails([frag0, frag1, frag2, frag3]);
|
||||
|
||||
// Level 0→2: should flush
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag2);
|
||||
streamController.tick();
|
||||
expect(getBufferFlushingCalls()).to.have.length(1);
|
||||
|
||||
// Still level 2, no level change: should not flush again
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag3);
|
||||
streamController.tick();
|
||||
expect(getBufferFlushingCalls()).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should trigger BUFFER_FLUSHING again after level downgrade then upgrade', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
const frag2 = createFragment(2, 2, 20);
|
||||
const frag3 = createFragment(3, 1, 30);
|
||||
const frag4 = createFragment(4, 2, 40);
|
||||
|
||||
setupLevelDetails([frag0, frag1, frag2, frag3, frag4]);
|
||||
|
||||
// Level 0→2: first flush
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag2);
|
||||
streamController.tick();
|
||||
expect(getBufferFlushingCalls()).to.have.length(1);
|
||||
|
||||
// Level 2→1: downgrade, no flush
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag3);
|
||||
streamController.tick();
|
||||
expect(getBufferFlushingCalls()).to.have.length(1);
|
||||
|
||||
// Level 1→2: upgrade again, should flush
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(frag4);
|
||||
streamController.tick();
|
||||
|
||||
const flushCalls = getBufferFlushingCalls();
|
||||
expect(flushCalls).to.have.length(2);
|
||||
expect(flushCalls[0].args[1]).to.deep.include({
|
||||
startOffset: 0,
|
||||
endOffset: 19.75,
|
||||
});
|
||||
// Second flush: frag4 is independent (start=40)
|
||||
expect(flushCalls[1].args[1]).to.deep.include({
|
||||
startOffset: 0,
|
||||
endOffset: 39.75,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not trigger BUFFER_FLUSHING when playing fragment is partial', function () {
|
||||
hls.config.loopBackBufferFlush = true;
|
||||
const media = createMediaMock({ loop: true, currentTime: 25 });
|
||||
const frag0 = createFragment(0, 0, 0);
|
||||
const frag1 = createFragment(1, 0, 10);
|
||||
const newFrag = createFragment(2, 2, 20, { partial: true });
|
||||
|
||||
setupLevelDetails([frag0, frag1, newFrag]);
|
||||
attachMediaAndStubTracker(media, frag0);
|
||||
streamController.tick();
|
||||
fragmentTracker.getAppendedFrag = sinon.stub().returns(newFrag);
|
||||
streamController.tick();
|
||||
|
||||
expect(getBufferFlushingCalls()).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('abortCurrentFrag override', function () {
|
||||
it('should clear backtrackFragment and call super', function () {
|
||||
const mockFrag = new Fragment(PlaylistLevelType.MAIN, 'test.ts');
|
||||
|
||||
Reference in New Issue
Block a user