Files
hls.js/tests/unit/controller/buffer-operation-queue.ts
Rob Walch 987320e051 Improve audio video append sync (#7842)
* Improve AV sync in buffer-controller
Resolves #7808
* Fix backfilling of #EXT-X-PROGRAM-DATE-TIME with only one PDT tag after segments
* Fix Low-Latency part/fragment toggle when the selected fragment is changed after initial selection
2026-05-11 16:01:12 -07:00

244 lines
6.9 KiB
TypeScript

import { expect, use } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import BufferOperationQueue from '../../../src/controller/buffer-operation-queue';
import type {
BufferOperation,
SourceBufferTrackSet,
} from '../../../src/types/buffer';
use(sinonChai);
const queueNames = ['audio', 'video'];
describe('BufferOperationQueue tests', function () {
const sandbox = sinon.createSandbox();
let operationQueue;
const sbMock = {
audio: {
buffer: { updating: false },
},
video: {
buffer: { updating: false },
},
} as any as SourceBufferTrackSet;
beforeEach(function () {
operationQueue = new BufferOperationQueue(sbMock);
});
afterEach(function () {
sandbox.restore();
});
it('initializes with an audio and video queue', function () {
expect(operationQueue.queues.video).to.exist;
expect(operationQueue.queues.audio).to.exist;
});
describe('append', function () {
it('appends and executes if the queue is empty', function () {
const execute = sandbox.spy();
const operation: BufferOperation = {
label: '',
execute,
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
queueNames.forEach((name, i) => {
operationQueue.append(operation, name);
expect(
execute,
`The ${name} queue operation should have been executed`,
).to.have.callCount(i + 1);
expect(
operationQueue.queues[name],
`The ${name} queue should have a length of 1`,
).to.have.length(1);
});
});
});
it('appends but does not execute if the queue has at least one operation enqueued', function () {
queueNames.forEach((name) => {
operationQueue.queues[name].push({});
});
const execute = sandbox.spy();
const operation: BufferOperation = {
label: '',
execute,
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
queueNames.forEach((name) => {
operationQueue.append(operation, name);
expect(
execute,
`The ${name} queue operation should not have been executed`,
).to.have.not.been.called;
expect(
operationQueue.queues[name],
`The ${name} queue should have a length of 2`,
).to.have.length(2);
});
});
describe('appendBlocker', function () {
it('appends a blocking promise, which resolves upon execution', function () {
const promises: Promise<{}>[] = [];
queueNames.forEach((name) => {
promises.push(operationQueue.appendBlocker(name));
});
return Promise.all(promises).then(() => {
queueNames.forEach((name) => {
expect(
operationQueue.queues[name],
`The ${name} queue should have a length of 1`,
).to.have.length(1);
});
});
});
});
describe('executeNext', function () {
it('does nothing if executing against an empty queue', function () {
queueNames.forEach((name) => {
expect(operationQueue.executeNext(name)).to.not.throw;
});
});
it('should execute the onError callback and shift the operation if it throws an unhandled exception', function () {
const onError = sandbox.spy();
const error = new Error();
const operation: BufferOperation = {
label: '',
execute: () => {
throw error;
},
onStart: () => {},
onComplete: () => {},
onError,
};
queueNames.forEach((name, i) => {
operationQueue.append(operation, name);
expect(onError, 'onError should have been called').to.have.callCount(
i + 1,
);
expect(
onError,
'onError should have been called with the thrown exception',
).to.have.been.calledWith(error);
expect(
operationQueue.queues[name],
`The ${name} queue should have a length of 0`,
).to.have.length(0);
});
});
});
describe('shiftAndExecute', function () {
const execute = sandbox.spy();
const operation: BufferOperation = {
label: '',
execute,
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
it('should dequeue the current operation and execute the next', function () {
queueNames.forEach((name) => {
operationQueue.queues[name].push({}, operation);
});
queueNames.forEach((name, i) => {
operationQueue.shiftAndExecuteNext(name);
expect(
execute,
`The ${name} queue operation should have been executed`,
).to.have.callCount(i + 1);
expect(
operationQueue.queues[name],
`The ${name} queue should have a length of 1`,
).to.have.length(1);
});
});
});
describe('unblockAudio', function () {
it('removes block-audio op from head and executes next', function () {
const nextExecute = sandbox.spy();
const blockOp: BufferOperation = {
label: 'block-audio',
execute: () => {},
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
const nextOp: BufferOperation = {
label: 'append',
execute: nextExecute,
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
operationQueue.queues.audio.push(blockOp, nextOp);
operationQueue.unblockAudio();
expect(operationQueue.queues.audio).to.have.length(1);
expect(operationQueue.queues.audio[0]).to.equal(nextOp);
expect(nextExecute).to.have.been.calledOnce;
});
it('does not remove head op if it is not block-audio', function () {
const headOp: BufferOperation = {
label: 'append',
execute: () => {},
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
operationQueue.queues.audio.push(headOp);
operationQueue.unblockAudio();
expect(operationQueue.queues.audio).to.have.length(1);
expect(operationQueue.queues.audio[0]).to.equal(headOp);
});
it('does nothing if queue is destroyed', function () {
operationQueue.destroy();
expect(() => operationQueue.unblockAudio()).to.not.throw();
});
});
describe('removeBlockers', function () {
it('removes block-audio ops from audio queue head', function () {
const blockOp: BufferOperation = {
label: 'block-audio',
execute: () => {},
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
const nextOp: BufferOperation = {
label: 'append',
execute: () => {},
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
operationQueue.queues.audio.push(blockOp, nextOp);
operationQueue.removeBlockers();
expect(operationQueue.queues.audio).to.have.length(1);
expect(operationQueue.queues.audio[0]).to.equal(nextOp);
});
});
});