mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
83de1ad634
This ensures that we get the expected types and that type coercion doesn't convert between types. This also ensures we are consistent in how we check for equality of primitives in tests. Change-Id: I9f3aacdf25ab1afe5e8d6e4b895b5299ee687d54
475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
describe('TextEngine', () => {
|
|
const TextEngine = shaka.text.TextEngine;
|
|
|
|
const dummyData = new ArrayBuffer(0);
|
|
const dummyMimeType = 'text/fake';
|
|
|
|
/** @type {!Function} */
|
|
let mockParserPlugIn;
|
|
|
|
/** @type {!shaka.test.FakeTextDisplayer} */
|
|
let mockDisplayer;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let mockParseInit;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let mockParseMedia;
|
|
|
|
/** @type {!shaka.text.TextEngine} */
|
|
let textEngine;
|
|
|
|
beforeEach(() => {
|
|
mockParseInit = jasmine.createSpy('mockParseInit');
|
|
mockParseMedia = jasmine.createSpy('mockParseMedia');
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
mockParserPlugIn = function() {
|
|
return {
|
|
parseInit: mockParseInit,
|
|
parseMedia: mockParseMedia,
|
|
};
|
|
};
|
|
|
|
mockDisplayer = new shaka.test.FakeTextDisplayer();
|
|
mockDisplayer.removeSpy.and.returnValue(true);
|
|
|
|
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
|
|
textEngine = new TextEngine(mockDisplayer);
|
|
textEngine.initParser(dummyMimeType);
|
|
});
|
|
|
|
afterEach(() => {
|
|
TextEngine.unregisterParser(dummyMimeType);
|
|
});
|
|
|
|
describe('isTypeSupported', () => {
|
|
it('reports support only when a parser is installed', () => {
|
|
TextEngine.unregisterParser(dummyMimeType);
|
|
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
|
|
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
|
|
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(true);
|
|
TextEngine.unregisterParser(dummyMimeType);
|
|
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
|
|
});
|
|
|
|
it('reports support when it\'s closed captions and muxjs is available',
|
|
() => {
|
|
const closedCaptionsType =
|
|
shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE;
|
|
const originalMuxjs = window.muxjs;
|
|
expect(TextEngine.isTypeSupported(closedCaptionsType)).toBe(true);
|
|
try {
|
|
window['muxjs'] = null;
|
|
expect(TextEngine.isTypeSupported(closedCaptionsType)).toBe(false);
|
|
} finally {
|
|
window['muxjs'] = originalMuxjs;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('appendBuffer', () => {
|
|
it('works asynchronously', async () => {
|
|
mockParseMedia.and.returnValue([1, 2, 3]);
|
|
const p = textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(mockDisplayer.appendSpy).not.toHaveBeenCalled();
|
|
await p;
|
|
});
|
|
|
|
it('calls displayer.append()', async () => {
|
|
const cue1 = createFakeCue(1, 2);
|
|
const cue2 = createFakeCue(2, 3);
|
|
const cue3 = createFakeCue(3, 4);
|
|
const cue4 = createFakeCue(4, 5);
|
|
mockParseMedia.and.returnValue([cue1, cue2]);
|
|
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
|
|
new Uint8Array(dummyData),
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 3},
|
|
]);
|
|
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[cue1, cue2],
|
|
]);
|
|
|
|
expect(mockDisplayer.removeSpy).not.toHaveBeenCalled();
|
|
|
|
mockParseMedia.and.returnValue([cue3, cue4]);
|
|
|
|
await textEngine.appendBuffer(dummyData, 3, 5);
|
|
|
|
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
|
|
new Uint8Array(dummyData),
|
|
{periodStart: 0, segmentStart: 3, segmentEnd: 5},
|
|
]);
|
|
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[cue3, cue4],
|
|
]);
|
|
});
|
|
|
|
it('does not throw if called right before destroy', async () => {
|
|
mockParseMedia.and.returnValue([1, 2, 3]);
|
|
const p = textEngine.appendBuffer(dummyData, 0, 3);
|
|
textEngine.destroy();
|
|
await p;
|
|
});
|
|
});
|
|
|
|
describe('storeAndAppendClosedCaptions', () => {
|
|
it('appends closed captions with selected id', () => {
|
|
const caption = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 0,
|
|
endTime: 1,
|
|
stream: 'CC1',
|
|
text: 'captions',
|
|
};
|
|
|
|
textEngine.setSelectedClosedCaptionId('CC1', 0);
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption], /* startTime */ 0, /* endTime */ 2, /* offset */ 0);
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not append closed captions without selected id', () => {
|
|
const caption = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 1,
|
|
endTime: 2,
|
|
stream: 'CC1',
|
|
text: 'caption2',
|
|
};
|
|
|
|
textEngine.setSelectedClosedCaptionId('CC3', 0);
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption], /* startTime */ 0, /* endTime */ 2, /* offset */ 0);
|
|
expect(mockDisplayer.appendSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('stores closed captions', () => {
|
|
const caption0 = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 0,
|
|
endTime: 1,
|
|
stream: 'CC1',
|
|
text: 'caption1',
|
|
};
|
|
const caption1 = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 1,
|
|
endTime: 2,
|
|
stream: 'CC1',
|
|
text: 'caption2',
|
|
};
|
|
const caption2 = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 1,
|
|
endTime: 2,
|
|
stream: 'CC3',
|
|
text: 'caption3',
|
|
};
|
|
|
|
textEngine.setSelectedClosedCaptionId('CC1', 0);
|
|
// Text Engine stores all the closed captions as a two layer map.
|
|
// {closed caption id -> {start and end time -> cues}}
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption0], /* startTime */ 0, /* endTime */ 1, /* offset */ 0);
|
|
expect(textEngine.getNumberOfClosedCaptionChannels()).toBe(1);
|
|
expect(textEngine.getNumberOfClosedCaptionsInChannel('CC1')).toBe(1);
|
|
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption1], /* startTime */ 1, /* endTime */ 2, /* offset */ 0);
|
|
// Caption1 has the same stream id with caption0, but different start and
|
|
// end time. The closed captions map should have 1 key CC1, and two values
|
|
// for two start and end times.
|
|
expect(textEngine.getNumberOfClosedCaptionChannels()).toBe(1);
|
|
expect(textEngine.getNumberOfClosedCaptionsInChannel('CC1')).toBe(2);
|
|
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption2], /* startTime */ 1, /* endTime */ 2, /* offset */ 0);
|
|
// Caption2 has a different stream id CC3, so the closed captions map
|
|
// should have two different keys, CC1 and CC3.
|
|
expect(textEngine.getNumberOfClosedCaptionChannels()).toBe(2);
|
|
});
|
|
|
|
it('offsets closed captions to account for video offset', () => {
|
|
const caption = {
|
|
startPts: 0,
|
|
endPts: 100,
|
|
startTime: 0,
|
|
endTime: 1,
|
|
stream: 'CC1',
|
|
text: 'captions',
|
|
};
|
|
|
|
textEngine.setSelectedClosedCaptionId('CC1', 0);
|
|
textEngine.storeAndAppendClosedCaptions(
|
|
[caption], /* startTime */ 0, /* endTime */ 2, /* offset */ 1000);
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledWith([
|
|
jasmine.objectContaining({
|
|
startTime: 1000,
|
|
endTime: 1001,
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
|
|
describe('remove', () => {
|
|
let cue1;
|
|
let cue2;
|
|
let cue3;
|
|
|
|
beforeEach(() => {
|
|
cue1 = createFakeCue(0, 1);
|
|
cue2 = createFakeCue(1, 2);
|
|
cue3 = createFakeCue(2, 3);
|
|
mockParseMedia.and.returnValue([cue1, cue2, cue3]);
|
|
});
|
|
|
|
it('works asynchronously', async () => {
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
const p = textEngine.remove(0, 1);
|
|
expect(mockDisplayer.removeSpy).not.toHaveBeenCalled();
|
|
await p;
|
|
});
|
|
|
|
it('calls displayer.remove()', async () => {
|
|
await textEngine.remove(0, 1);
|
|
expect(mockDisplayer.removeSpy).toHaveBeenCalledWith(0, 1);
|
|
});
|
|
|
|
it('does not throw if called right before destroy', async () => {
|
|
const p = textEngine.remove(0, 1);
|
|
textEngine.destroy();
|
|
await p;
|
|
});
|
|
});
|
|
|
|
describe('setTimestampOffset', () => {
|
|
it('passes the offset to the parser', async () => {
|
|
mockParseMedia.and.callFake((data, time) => {
|
|
return [
|
|
createFakeCue(time.periodStart + 0,
|
|
time.periodStart + 1),
|
|
createFakeCue(time.periodStart + 2,
|
|
time.periodStart + 3),
|
|
];
|
|
});
|
|
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
|
|
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
|
|
new Uint8Array(dummyData),
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 3},
|
|
]);
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[
|
|
createFakeCue(0, 1),
|
|
createFakeCue(2, 3),
|
|
],
|
|
]);
|
|
|
|
textEngine.setTimestampOffset(4);
|
|
await textEngine.appendBuffer(dummyData, 4, 7);
|
|
|
|
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
|
|
new Uint8Array(dummyData),
|
|
{periodStart: 4, segmentStart: 4, segmentEnd: 7},
|
|
]);
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[
|
|
createFakeCue(4, 5),
|
|
createFakeCue(6, 7),
|
|
],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('bufferStart/bufferEnd', () => {
|
|
beforeEach(() => {
|
|
mockParseMedia.and.callFake(() => {
|
|
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
|
|
});
|
|
});
|
|
|
|
it('return null when there are no cues', () => {
|
|
expect(textEngine.bufferStart()).toBe(null);
|
|
expect(textEngine.bufferEnd()).toBe(null);
|
|
});
|
|
|
|
it('reflect newly-added cues', async () => {
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(3);
|
|
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(6);
|
|
|
|
await textEngine.appendBuffer(dummyData, 6, 10);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(10);
|
|
});
|
|
|
|
it('reflect newly-removed cues', async () => {
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
await textEngine.appendBuffer(dummyData, 6, 10);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(10);
|
|
|
|
await textEngine.remove(0, 3);
|
|
expect(textEngine.bufferStart()).toBe(3);
|
|
expect(textEngine.bufferEnd()).toBe(10);
|
|
|
|
await textEngine.remove(8, 11);
|
|
expect(textEngine.bufferStart()).toBe(3);
|
|
expect(textEngine.bufferEnd()).toBe(8);
|
|
|
|
await textEngine.remove(11, 20);
|
|
expect(textEngine.bufferStart()).toBe(3);
|
|
expect(textEngine.bufferEnd()).toBe(8);
|
|
|
|
await textEngine.remove(0, Infinity);
|
|
expect(textEngine.bufferStart()).toBe(null);
|
|
expect(textEngine.bufferEnd()).toBe(null);
|
|
});
|
|
|
|
it('does not use timestamp offset', async () => {
|
|
// The start and end times passed to appendBuffer are now absolute, so
|
|
// they already account for timestampOffset and period offset.
|
|
// See https://github.com/google/shaka-player/issues/1562
|
|
textEngine.setTimestampOffset(60);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(3);
|
|
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
expect(textEngine.bufferStart()).toBe(0);
|
|
expect(textEngine.bufferEnd()).toBe(6);
|
|
});
|
|
});
|
|
|
|
describe('bufferedAheadOf', () => {
|
|
beforeEach(() => {
|
|
mockParseMedia.and.callFake(() => {
|
|
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
|
|
});
|
|
});
|
|
|
|
it('returns 0 when there are no cues', () => {
|
|
expect(textEngine.bufferedAheadOf(0)).toBe(0);
|
|
});
|
|
|
|
it('returns 0 if |t| is not buffered', async () => {
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
expect(textEngine.bufferedAheadOf(6.1)).toBe(0);
|
|
});
|
|
|
|
it('ignores gaps in the content', async () => {
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
expect(textEngine.bufferedAheadOf(2)).toBe(3);
|
|
});
|
|
|
|
it('returns the distance to the end if |t| is buffered', async () => {
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferedAheadOf(0)).toBe(3);
|
|
expect(textEngine.bufferedAheadOf(1)).toBe(2);
|
|
expect(textEngine.bufferedAheadOf(2.5)).toBeCloseTo(0.5);
|
|
});
|
|
|
|
it('does not use timestamp offset', async () => {
|
|
// The start and end times passed to appendBuffer are now absolute, so
|
|
// they already account for timestampOffset and period offset.
|
|
// See https://github.com/google/shaka-player/issues/1562
|
|
textEngine.setTimestampOffset(60);
|
|
await textEngine.appendBuffer(dummyData, 3, 6);
|
|
expect(textEngine.bufferedAheadOf(4)).toBe(2);
|
|
expect(textEngine.bufferedAheadOf(64)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('setAppendWindow', () => {
|
|
beforeEach(() => {
|
|
mockParseMedia.and.callFake(() => {
|
|
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
|
|
});
|
|
});
|
|
|
|
it('limits appended cues', async () => {
|
|
textEngine.setAppendWindow(0, 1.9);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[
|
|
createFakeCue(0, 1),
|
|
createFakeCue(1, 2),
|
|
],
|
|
]);
|
|
|
|
textEngine.setAppendWindow(1, 2.1);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
|
|
expect(mockDisplayer.appendSpy).toHaveBeenCalledOnceMoreWith([
|
|
[
|
|
createFakeCue(1, 2),
|
|
createFakeCue(2, 3),
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('limits bufferStart', async () => {
|
|
textEngine.setAppendWindow(1, 9);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferStart()).toBe(1);
|
|
|
|
await textEngine.remove(0, 9);
|
|
textEngine.setAppendWindow(2.1, 9);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferStart()).toBe(2.1);
|
|
});
|
|
|
|
it('limits bufferEnd', async () => {
|
|
textEngine.setAppendWindow(0, 1.9);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferEnd()).toBe(1.9);
|
|
|
|
textEngine.setAppendWindow(0, 2.1);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferEnd()).toBe(2.1);
|
|
|
|
textEngine.setAppendWindow(0, 4.1);
|
|
await textEngine.appendBuffer(dummyData, 0, 3);
|
|
expect(textEngine.bufferEnd()).toBe(3);
|
|
});
|
|
});
|
|
|
|
function createFakeCue(startTime, endTime) {
|
|
return {startTime: startTime, endTime: endTime};
|
|
}
|
|
});
|