mirror of
https://github.com/video-dev/hls.js.git
synced 2026-05-17 13:30:38 +00:00
987320e051
* 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
244 lines
6.9 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|