mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
8b86ba5f06
This PR changes the way that timestamp offsets are stored in the media source engine. Instead of a single value, it now has a map of values. For DASH, it should contain a single value, which is the last timestamp offset available. For HLS, it'll keep track of the timestamp offsets per discontinuity sequence. If content is appended and we don't yet have a timestamp offset for that discontinuity sequence number, it'll defer creating the cues until the timestamp offset is set, otherwise, the times for the cues may not be correct. Fixes #9470
1719 lines
64 KiB
JavaScript
1719 lines
64 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* length: number,
|
|
* start: jasmine.Spy,
|
|
* end: jasmine.Spy
|
|
* }}
|
|
*/
|
|
let MockTimeRanges;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* abort: jasmine.Spy,
|
|
* appendBuffer: jasmine.Spy,
|
|
* remove: jasmine.Spy,
|
|
* updating: boolean,
|
|
* addEventListener: jasmine.Spy,
|
|
* removeEventListener: jasmine.Spy,
|
|
* buffered: (MockTimeRanges|TimeRanges),
|
|
* timestampOffset: number,
|
|
* appendWindowStart: number,
|
|
* appendWindowEnd: number,
|
|
* updateend: function(),
|
|
* error: function(),
|
|
* mode: string
|
|
* }}
|
|
*/
|
|
let MockSourceBuffer;
|
|
|
|
|
|
describe('MediaSourceEngine', () => {
|
|
const Util = shaka.test.Util;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
const originalIsTypeSupported = window.MediaSource.isTypeSupported;
|
|
let originalIsTypeSupportedManaged;
|
|
if (window.ManagedMediaSource) {
|
|
originalIsTypeSupportedManaged = window.ManagedMediaSource.isTypeSupported;
|
|
}
|
|
const originalTextEngine = shaka.text.TextEngine;
|
|
const originalCreateMediaSource =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.media.MediaSourceEngine.prototype.createMediaSource;
|
|
|
|
const originalFindTransmuxer =
|
|
shaka.transmuxer.TransmuxerEngine.findTransmuxer;
|
|
const originalConvertCodecs =
|
|
shaka.transmuxer.TransmuxerEngine.convertCodecs;
|
|
const originalIsSupported =
|
|
shaka.transmuxer.TransmuxerEngine.isSupported;
|
|
|
|
// Jasmine Spies don't handle toHaveBeenCalledWith well with objects, so use
|
|
// some numbers instead.
|
|
const buffer = /** @type {!ArrayBuffer} */ (/** @type {?} */ (1));
|
|
const buffer2 = /** @type {!ArrayBuffer} */ (/** @type {?} */ (2));
|
|
const buffer3 = /** @type {!ArrayBuffer} */ (/** @type {?} */ (3));
|
|
|
|
const makeFakeStream = (mimeType) => {
|
|
const segmentIndex = {
|
|
isEmpty: () => false,
|
|
};
|
|
segmentIndex[Symbol.iterator] = () => {
|
|
let nextPosition = 0;
|
|
|
|
return {
|
|
next: () => {
|
|
if (nextPosition == 0) {
|
|
nextPosition += 1;
|
|
return {
|
|
value: {mimeType},
|
|
done: false,
|
|
};
|
|
} else {
|
|
return {
|
|
value: null,
|
|
done: true,
|
|
};
|
|
}
|
|
},
|
|
current: () => {
|
|
return {mimeType};
|
|
},
|
|
};
|
|
};
|
|
return {mimeType, drmInfos: [{}], segmentIndex};
|
|
};
|
|
|
|
const fakeVideoStream = makeFakeStream('video/mp4');
|
|
const fakeAudioStream = makeFakeStream('audio/mp4');
|
|
const fakeTextStream = makeFakeStream('text/mp4');
|
|
const fakeTransportStream = makeFakeStream('tsMimetype');
|
|
const fakeStreams =
|
|
[fakeVideoStream, fakeAudioStream, fakeTextStream, fakeTransportStream];
|
|
for (const fakeStream of fakeStreams) {
|
|
fakeStream.fullMimeTypes = new Set([fakeStream.mimeType]);
|
|
}
|
|
|
|
/** @type {shaka.extern.Stream} */
|
|
const fakeStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);
|
|
|
|
let audioSourceBuffer;
|
|
let videoSourceBuffer;
|
|
let mockVideo;
|
|
|
|
/** @type {HTMLMediaElement} */
|
|
let video;
|
|
let mockMediaSource;
|
|
|
|
let mockTextEngine;
|
|
/** @type {!shaka.test.FakeTextDisplayer} */
|
|
let mockTextDisplayer;
|
|
/** @type {!shaka.test.FakeClosedCaptionParser} */
|
|
let mockClosedCaptionParser;
|
|
/** @type {!shaka.test.FakeTransmuxer} */
|
|
let mockTransmuxer;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let createMediaSourceSpy;
|
|
/** @type {!jasmine.Spy} */
|
|
let requiresEncryptionInfoInAllInitSegmentsSpy;
|
|
/** @type {!jasmine.Spy} */
|
|
let requiresEC3InitSegments;
|
|
/** @type {!jasmine.Spy} */
|
|
let fakeEncryptionSpy;
|
|
|
|
/** @type {!shaka.media.MediaSourceEngine} */
|
|
let mediaSourceEngine;
|
|
|
|
beforeAll(() => {
|
|
// Since this is not an integration test, we don't want MediaSourceEngine to
|
|
// fail assertions based on browser support for types. Pretend that all
|
|
// video and audio types are supported.
|
|
window.MediaSource.isTypeSupported = (mimeType) => {
|
|
const type = mimeType.split('/')[0];
|
|
return type == 'video' || type == 'audio';
|
|
};
|
|
if (window.ManagedMediaSource) {
|
|
window.ManagedMediaSource.isTypeSupported = (mimeType) => {
|
|
const type = mimeType.split('/')[0];
|
|
return type == 'video' || type == 'audio';
|
|
};
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
window.MediaSource.isTypeSupported = originalIsTypeSupported;
|
|
if (window.ManagedMediaSource) {
|
|
window.ManagedMediaSource.isTypeSupported =
|
|
originalIsTypeSupportedManaged;
|
|
}
|
|
shaka.transmuxer.TransmuxerEngine.findTransmuxer =
|
|
originalFindTransmuxer;
|
|
shaka.transmuxer.TransmuxerEngine.convertCodecs =
|
|
originalConvertCodecs;
|
|
shaka.transmuxer.TransmuxerEngine.isSupported =
|
|
originalIsSupported;
|
|
});
|
|
|
|
beforeEach(/** @suppress {invalidCasts} */ () => {
|
|
audioSourceBuffer = createMockSourceBuffer();
|
|
videoSourceBuffer = createMockSourceBuffer();
|
|
mockMediaSource = createMockMediaSource();
|
|
mockMediaSource.addSourceBuffer.and.callFake((mimeType) => {
|
|
if (mockMediaSource.readyState !== 'open') {
|
|
// https://w3c.github.io/media-source/#addsourcebuffer-method
|
|
throw new Error('InvalidStateError');
|
|
}
|
|
const type = mimeType.split('/')[0];
|
|
const buffer = type == 'audio' ? audioSourceBuffer : videoSourceBuffer;
|
|
|
|
// reset buffer params
|
|
buffer.timestampOffset = 0;
|
|
buffer.appendWindowEnd = Infinity;
|
|
buffer.appendWindowStart = 0;
|
|
|
|
// send a simple mock of the 'addsourcebuffer' event, after returning.
|
|
Util.shortDelay().then(() => {
|
|
mockMediaSource.sourceBuffers.dispatchEvent(
|
|
new Event('addsourcebuffer'));
|
|
});
|
|
|
|
return buffer;
|
|
});
|
|
mockTransmuxer = new shaka.test.FakeTransmuxer();
|
|
shaka.transmuxer.TransmuxerEngine.findTransmuxer =
|
|
(mimeType) => {
|
|
return () => mockTransmuxer;
|
|
};
|
|
shaka.transmuxer.TransmuxerEngine.convertCodecs =
|
|
(mimeType, contentType) => {
|
|
return 'video/mp4; codecs="avc1.42E01E"';
|
|
};
|
|
shaka.transmuxer.TransmuxerEngine.isSupported =
|
|
(mimeType, contentType) => {
|
|
return mimeType == 'tsMimetype';
|
|
};
|
|
|
|
shaka.text.TextEngine = createMockTextEngineCtor();
|
|
|
|
createMediaSourceSpy = jasmine.createSpy('createMediaSource');
|
|
createMediaSourceSpy.and.callFake((p) => {
|
|
p.resolve();
|
|
mockMediaSource.readyState = 'open';
|
|
return mockMediaSource;
|
|
});
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.media.MediaSourceEngine.prototype.createMediaSource =
|
|
Util.spyFunc(createMediaSourceSpy);
|
|
|
|
requiresEncryptionInfoInAllInitSegmentsSpy = spyOn(deviceDetected,
|
|
'requiresEncryptionInfoInAllInitSegments').and.returnValue(false);
|
|
|
|
requiresEC3InitSegments = spyOn(deviceDetected,
|
|
'requiresEC3InitSegments').and.returnValue(false);
|
|
|
|
fakeEncryptionSpy = spyOn(shaka.media.ContentWorkarounds, 'fakeEncryption')
|
|
.and.callFake((stream, data) => data + 100);
|
|
|
|
// MediaSourceEngine uses video to:
|
|
// - set src attribute
|
|
// - read error codes when operations fail
|
|
// - seek to flush the pipeline on some platforms
|
|
// - check buffered.length to assert that flushing the pipeline is okay
|
|
mockVideo = {
|
|
firstElementChild: undefined,
|
|
error: null,
|
|
currentTime: 0,
|
|
buffered: {
|
|
length: 0,
|
|
},
|
|
appendChild: (element) => {
|
|
mockVideo.firstElementChild = element;
|
|
},
|
|
removeChild: () => {
|
|
mockVideo.firstElementChild = undefined;
|
|
},
|
|
removeAttribute: jasmine.createSpy('removeAttribute'),
|
|
addEventListener: jasmine.createSpy('addVideoEventListener'),
|
|
removeEventListener: jasmine.createSpy('removeVideoEventListener'),
|
|
load: jasmine.createSpy('load'),
|
|
play: jasmine.createSpy('play'),
|
|
paused: true,
|
|
autoplay: false,
|
|
};
|
|
video = /** @type {HTMLMediaElement} */(mockVideo);
|
|
mockClosedCaptionParser = new shaka.test.FakeClosedCaptionParser();
|
|
mockTextDisplayer = new shaka.test.FakeTextDisplayer();
|
|
const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
|
|
|
|
mediaSourceEngine = new shaka.media.MediaSourceEngine(
|
|
video,
|
|
mockTextDisplayer,
|
|
{
|
|
getKeySystem: () => null,
|
|
onMetadata: () => {},
|
|
onEmsg: () => {},
|
|
onEvent: () => {},
|
|
onManifestUpdate: () => {},
|
|
},
|
|
config);
|
|
mediaSourceEngine.getCaptionParser = () => {
|
|
return /** @type {!shaka.media.ClosedCaptionParser} */(
|
|
mockClosedCaptionParser);
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
mockTextEngine = null;
|
|
shaka.text.TextEngine = originalTextEngine;
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.media.MediaSourceEngine.prototype.createMediaSource =
|
|
originalCreateMediaSource;
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
const originalCreateObjectURL =
|
|
shaka.media.MediaSourceEngine.createObjectURL;
|
|
const originalRevokeObjectURL = window.URL.revokeObjectURL;
|
|
const originalMediaSource = window.MediaSource;
|
|
const originalManagedMediaSource = window.ManagedMediaSource;
|
|
/** @type {jasmine.Spy} */
|
|
let createObjectURLSpy;
|
|
/** @type {jasmine.Spy} */
|
|
let revokeObjectURLSpy;
|
|
|
|
beforeEach(async () => {
|
|
// Mock out MediaSource so we can test the production version of
|
|
// createMediaSource. To do this, the test must call the
|
|
// MediaSourceEngine constructor again. The call beforeEach was done with
|
|
// a mocked createMediaSource.
|
|
createMediaSourceSpy.calls.reset();
|
|
createMediaSourceSpy.and.callFake(originalCreateMediaSource);
|
|
|
|
createObjectURLSpy = jasmine.createSpy('createObjectURL');
|
|
createObjectURLSpy.and.returnValue('blob:foo');
|
|
shaka.media.MediaSourceEngine.createObjectURL =
|
|
Util.spyFunc(createObjectURLSpy);
|
|
|
|
revokeObjectURLSpy = jasmine.createSpy('revokeObjectURL');
|
|
window.URL.revokeObjectURL = Util.spyFunc(revokeObjectURLSpy);
|
|
|
|
const mediaSourceSpy = jasmine.createSpy('MediaSource');
|
|
// Because this is a fake constructor, it must be callable with "new".
|
|
// This will cause jasmine to invoke the callback with "new" as well, so
|
|
// the callback must be a "function". This detail is hidden when babel
|
|
// transpiles the tests.
|
|
// eslint-disable-next-line prefer-arrow-callback, no-restricted-syntax
|
|
mediaSourceSpy.and.callFake(function() {
|
|
return mockMediaSource;
|
|
});
|
|
window.MediaSource = Util.spyFunc(mediaSourceSpy);
|
|
if (window.ManagedMediaSource) {
|
|
window.ManagedMediaSource = Util.spyFunc(mediaSourceSpy);
|
|
}
|
|
|
|
await mediaSourceEngine.destroy();
|
|
});
|
|
|
|
afterAll(() => {
|
|
shaka.media.MediaSourceEngine.createObjectURL = originalCreateObjectURL;
|
|
window.MediaSource = originalMediaSource;
|
|
window.ManagedMediaSource = originalManagedMediaSource;
|
|
window.URL.revokeObjectURL = originalRevokeObjectURL;
|
|
});
|
|
const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
|
|
|
|
it('creates a MediaSource object and sets video.src', () => {
|
|
mediaSourceEngine = new shaka.media.MediaSourceEngine(
|
|
video,
|
|
new shaka.test.FakeTextDisplayer(),
|
|
{
|
|
getKeySystem: () => null,
|
|
onMetadata: () => {},
|
|
onEmsg: () => {},
|
|
onEvent: () => {},
|
|
onManifestUpdate: () => {},
|
|
},
|
|
config);
|
|
|
|
expect(createMediaSourceSpy).toHaveBeenCalled();
|
|
expect(createObjectURLSpy).toHaveBeenCalled();
|
|
expect(mockVideo.firstElementChild.src).toBe('blob:foo');
|
|
});
|
|
|
|
it('revokes object URL after MediaSource opens', () => {
|
|
let onSourceOpenListener;
|
|
|
|
mockMediaSource.addEventListener.and.callFake((event, callback, _) => {
|
|
if (event == 'sourceopen') {
|
|
onSourceOpenListener = callback;
|
|
}
|
|
});
|
|
const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
|
|
|
|
mediaSourceEngine = new shaka.media.MediaSourceEngine(
|
|
video,
|
|
new shaka.test.FakeTextDisplayer(),
|
|
{
|
|
getKeySystem: () => null,
|
|
onMetadata: () => {},
|
|
onEmsg: () => {},
|
|
onEvent: () => {},
|
|
onManifestUpdate: () => {},
|
|
},
|
|
config);
|
|
|
|
if (window.ManagedMediaSource) {
|
|
expect(mockMediaSource.addEventListener).toHaveBeenCalledTimes(3);
|
|
} else {
|
|
expect(mockMediaSource.addEventListener).toHaveBeenCalledTimes(1);
|
|
}
|
|
expect(mockMediaSource.addEventListener.calls.mostRecent().args[0])
|
|
.toBe('sourceopen');
|
|
expect(typeof onSourceOpenListener).toBe(typeof Function);
|
|
|
|
onSourceOpenListener();
|
|
|
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:foo');
|
|
});
|
|
});
|
|
|
|
describe('init', () => {
|
|
it('creates SourceBuffers for the given types', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith('audio/mp4');
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith('video/mp4');
|
|
expect(shaka.text.TextEngine).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates SourceBuffers with extra features', async () => {
|
|
const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
|
|
config.addExtraFeaturesToSourceBuffer = (mimeType) => {
|
|
if (mimeType.includes('audio')) {
|
|
return '; extra_audio_param';
|
|
}
|
|
if (mimeType.includes('video')) {
|
|
return '; extra_video_param';
|
|
}
|
|
return '';
|
|
};
|
|
mediaSourceEngine.configure(config);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith(
|
|
'audio/mp4; extra_audio_param');
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith(
|
|
'video/mp4; extra_video_param');
|
|
expect(shaka.text.TextEngine).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates SourceBuffers when MediaSource readyState is closed',
|
|
async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
|
|
await mediaSourceEngine.open();
|
|
|
|
mockMediaSource.readyState = 'closed';
|
|
await expectAsync(
|
|
mediaSourceEngine.init(initObject, false)).not.toBeRejected();
|
|
});
|
|
|
|
it('creates SourceBuffers when MediaSource readyState is ended',
|
|
async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
|
|
await mediaSourceEngine.open();
|
|
|
|
mockMediaSource.readyState = 'ended';
|
|
await expectAsync(
|
|
mediaSourceEngine.init(initObject, false)).not.toBeRejected();
|
|
});
|
|
|
|
it('creates TextEngines for text types', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
expect(mockMediaSource.addSourceBuffer).not.toHaveBeenCalled();
|
|
expect(shaka.text.TextEngine).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('bufferStart and bufferEnd', () => {
|
|
beforeEach(async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('returns correct timestamps for one range', () => {
|
|
audioSourceBuffer.buffered = createFakeBuffered([{start: 0, end: 10}]);
|
|
|
|
expect(mediaSourceEngine.bufferStart(ContentType.AUDIO)).toBeCloseTo(0);
|
|
expect(mediaSourceEngine.bufferEnd(ContentType.AUDIO)).toBeCloseTo(10);
|
|
});
|
|
|
|
it('returns correct timestamps for multiple ranges', () => {
|
|
audioSourceBuffer.buffered =
|
|
createFakeBuffered([{start: 5, end: 10}, {start: 20, end: 30}]);
|
|
|
|
expect(mediaSourceEngine.bufferStart(ContentType.AUDIO)).toBeCloseTo(5);
|
|
expect(mediaSourceEngine.bufferEnd(ContentType.AUDIO)).toBeCloseTo(30);
|
|
});
|
|
|
|
it('returns null if there are no ranges', () => {
|
|
audioSourceBuffer.buffered = createFakeBuffered([]);
|
|
|
|
expect(mediaSourceEngine.bufferStart(ContentType.AUDIO)).toBeNull();
|
|
expect(mediaSourceEngine.bufferEnd(ContentType.AUDIO)).toBeNull();
|
|
});
|
|
|
|
it('will forward to TextEngine', () => {
|
|
mockTextEngine.bufferStart.and.returnValue(10);
|
|
mockTextEngine.bufferEnd.and.returnValue(20);
|
|
|
|
expect(mockTextEngine.bufferStart).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.bufferStart(ContentType.TEXT)).toBe(10);
|
|
expect(mockTextEngine.bufferStart).toHaveBeenCalled();
|
|
|
|
expect(mockTextEngine.bufferEnd).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.bufferEnd(ContentType.TEXT)).toBe(20);
|
|
expect(mockTextEngine.bufferEnd).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('bufferedAheadOf', () => {
|
|
beforeEach(async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('returns the amount of data ahead of the given position', () => {
|
|
audioSourceBuffer.buffered = createFakeBuffered([{start: 0, end: 10}]);
|
|
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 0))
|
|
.toBeCloseTo(10);
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 5))
|
|
.toBeCloseTo(5);
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 9.9))
|
|
.toBeCloseTo(0.1);
|
|
});
|
|
|
|
it('returns zero when given an unbuffered time', () => {
|
|
audioSourceBuffer.buffered = createFakeBuffered([{start: 5, end: 10}]);
|
|
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 10))
|
|
.toBeCloseTo(0);
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 100))
|
|
.toBeCloseTo(0);
|
|
});
|
|
|
|
it('returns the correct amount with multiple ranges', () => {
|
|
audioSourceBuffer.buffered =
|
|
createFakeBuffered([{start: 1, end: 3}, {start: 6, end: 10}]);
|
|
|
|
// in range 0
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 1))
|
|
.toBeCloseTo(6);
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 2.5))
|
|
.toBeCloseTo(4.5);
|
|
|
|
// between range 0 and 1
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 5))
|
|
.toBeCloseTo(4);
|
|
|
|
// in range 1
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 6))
|
|
.toBeCloseTo(4);
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.AUDIO, 9.9))
|
|
.toBeCloseTo(0.1);
|
|
});
|
|
|
|
it('will forward to TextEngine', () => {
|
|
mockTextEngine.bufferedAheadOf.and.returnValue(10);
|
|
|
|
expect(mockTextEngine.bufferedAheadOf).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.bufferedAheadOf(ContentType.TEXT, 5)).toBe(10);
|
|
expect(mockTextEngine.bufferedAheadOf).toHaveBeenCalledWith(5);
|
|
});
|
|
});
|
|
|
|
describe('appendBuffer', () => {
|
|
beforeEach(async () => {
|
|
requiresEC3InitSegments.and.returnValue(false);
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('should apply fake encryption by default', async () => {
|
|
requiresEncryptionInfoInAllInitSegmentsSpy.and.returnValue(true);
|
|
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
|
|
expect(fakeEncryptionSpy).toHaveBeenCalled();
|
|
|
|
expect(videoSourceBuffer.appendBuffer)
|
|
.toHaveBeenCalledWith((buffer + 100));
|
|
videoSourceBuffer.updateend();
|
|
|
|
await p;
|
|
});
|
|
|
|
it('should not apply fake encryption when config is off', async () => {
|
|
requiresEncryptionInfoInAllInitSegmentsSpy.and.returnValue(true);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
|
|
config.insertFakeEncryptionInInit = false;
|
|
|
|
mediaSourceEngine.configure(config);
|
|
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
|
|
expect(fakeEncryptionSpy).not.toHaveBeenCalled();
|
|
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
videoSourceBuffer.updateend();
|
|
|
|
await p;
|
|
});
|
|
|
|
it('appends the given data', async () => {
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
audioSourceBuffer.updateend();
|
|
await p;
|
|
});
|
|
|
|
it('rejects promise when operation throws', async () => {
|
|
const reference = dummyReference(0, 1000);
|
|
audioSourceBuffer.appendBuffer.and.throwError('fail!');
|
|
mockVideo.error = {code: 5, message: 'something failed'};
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
|
|
jasmine.objectContaining({message: 'fail!'}),
|
|
jasmine.objectContaining({code: 5, message: 'something failed'}),
|
|
reference.getUris()[0]));
|
|
await expectAsync(
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, reference, fakeStream,
|
|
/* hasClosedCaptions= */ false))
|
|
.toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
});
|
|
|
|
it('rejects promise when op. throws QuotaExceededError', async () => {
|
|
const fakeDOMException = {name: 'QuotaExceededError'};
|
|
audioSourceBuffer.appendBuffer.and.callFake(() => {
|
|
throw fakeDOMException;
|
|
});
|
|
mockVideo.error = {code: 5};
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
|
|
ContentType.AUDIO));
|
|
await expectAsync(
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false))
|
|
.toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
});
|
|
|
|
it('handles QuotaExceededError for pending operations', async () => {
|
|
const fakeDOMException = {name: 'QuotaExceededError'};
|
|
audioSourceBuffer.appendBuffer.and.callFake(() => {
|
|
if (audioSourceBuffer.appendBuffer.calls.count() > 1) {
|
|
throw fakeDOMException;
|
|
}
|
|
});
|
|
mockVideo.error = {code: 5};
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
|
|
ContentType.AUDIO));
|
|
|
|
const p1 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const p2 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
audioSourceBuffer.updateend();
|
|
await expectAsync(p1).toBeResolved();
|
|
await expectAsync(p2).toBeRejectedWith(expected);
|
|
});
|
|
|
|
it('rejects the promise if this operation fails async', async () => {
|
|
const reference = dummyReference(0, 1000);
|
|
mockVideo.error = {code: 5};
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, reference, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
audioSourceBuffer.error();
|
|
audioSourceBuffer.updateend();
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
|
|
5,
|
|
reference.getUris()[0]));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
});
|
|
|
|
it('queues operations on a single SourceBuffer', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer2);
|
|
audioSourceBuffer.updateend();
|
|
await p2;
|
|
});
|
|
|
|
it('queues operations independently for different types', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p3 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer3, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer3);
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
videoSourceBuffer.updateend();
|
|
// Wait a tick between each updateend() and the status check that follows.
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer2);
|
|
await p3;
|
|
audioSourceBuffer.updateend();
|
|
await p2;
|
|
});
|
|
|
|
it('continues if an operation throws', async () => {
|
|
audioSourceBuffer.appendBuffer.and.callFake((value) => {
|
|
if (value == 2) {
|
|
// throw synchronously.
|
|
throw new Error();
|
|
} else {
|
|
// complete successfully asynchronously.
|
|
Promise.resolve().then(() => {
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
}
|
|
});
|
|
|
|
const p1 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const p2 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const p3 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer3, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
|
|
await expectAsync(p1).toBeResolved();
|
|
await expectAsync(p2).toBeRejected();
|
|
await expectAsync(p3).toBeResolved();
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer2);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer3);
|
|
});
|
|
|
|
it('forwards to TextEngine', async () => {
|
|
const data = new ArrayBuffer(0);
|
|
expect(mockTextEngine.appendBuffer).not.toHaveBeenCalled();
|
|
const reference = dummyReference(0, 10);
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.TEXT, data, reference, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith(
|
|
data, 0, 10, 'foo://bar', -1);
|
|
});
|
|
|
|
it('forwards to TextEngine HLS discontinuity sequence', async () => {
|
|
const data = new ArrayBuffer(0);
|
|
expect(mockTextEngine.appendBuffer).not.toHaveBeenCalled();
|
|
const reference = dummyReference(0, 10);
|
|
reference.discontinuitySequence = 1;
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.TEXT, data, reference, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith(
|
|
data, 0, 10, 'foo://bar', 1);
|
|
});
|
|
|
|
it('appends transmuxed data', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeTransportStream);
|
|
|
|
const output = {
|
|
data: new Uint8Array(1),
|
|
captions: [{}],
|
|
};
|
|
mockTransmuxer.transmux.and.returnValue(Promise.resolve(output));
|
|
|
|
const init = async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalled();
|
|
};
|
|
|
|
// The 'updateend' event fires once the data is done appending to the
|
|
// media source. We only append to the media source once transmuxing is
|
|
// done. Since transmuxing is done using Promises, we need to delay the
|
|
// event until MediaSourceEngine calls appendBuffer.
|
|
const delay = async () => {
|
|
await Util.shortDelay();
|
|
videoSourceBuffer.updateend();
|
|
};
|
|
await Promise.all([init(), delay()]);
|
|
});
|
|
|
|
it('appends parsed closed captions from CaptionParser', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
|
|
mockClosedCaptionParser.parseFromSpy.and.callFake((data) => {
|
|
return ['foo', 'bar'];
|
|
});
|
|
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
// Initialize the closed caption parser.
|
|
const appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true);
|
|
// In MediaSourceEngine, appendBuffer() is async and Promise-based, but
|
|
// at the browser level, it's event-based.
|
|
// MediaSourceEngine waits for the 'updateend' event from the
|
|
// SourceBuffer, and uses that to resolve the appendBuffer Promise.
|
|
// Here, we must trigger the event on the fake/mock SourceBuffer before
|
|
// waiting on the appendBuffer Promise.
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
expect(mockTextEngine.storeAndAppendClosedCaptions).not
|
|
.toHaveBeenCalled();
|
|
// Parse and append the closed captions embedded in video stream.
|
|
const reference = dummyReference(0, 1000);
|
|
const appendVideo = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, reference, fakeStream,
|
|
/* hasClosedCaptions= */ true);
|
|
videoSourceBuffer.updateend();
|
|
await appendVideo;
|
|
|
|
expect(mockTextEngine.storeAndAppendClosedCaptions).toHaveBeenCalled();
|
|
});
|
|
|
|
it('passed continuity timeline to caption parser', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
|
|
mockClosedCaptionParser.parseFromSpy.and.callFake((data) => {
|
|
return ['foo', 'bar'];
|
|
});
|
|
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
// Initialize the closed caption parser.
|
|
let appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true, /* seeked= */ false,
|
|
/* adaptation= */ false, /* isChunkedData= */ false,
|
|
/* fromSplit= */ false, /* continuityTimeline= */ 0);
|
|
// In MediaSourceEngine, appendBuffer() is async and Promise-based, but
|
|
// at the browser level, it's event-based.
|
|
// MediaSourceEngine waits for the 'updateend' event from the
|
|
// SourceBuffer, and uses that to resolve the appendBuffer Promise.
|
|
// Here, we must trigger the event on the fake/mock SourceBuffer before
|
|
// waiting on the appendBuffer Promise.
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
expect(mockClosedCaptionParser.initSpy)
|
|
.toHaveBeenCalledWith(buffer, false, 0);
|
|
|
|
// new init segment
|
|
appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true, /* seeked= */ false,
|
|
/* adaptation= */ false, /* isChunkedData= */ false,
|
|
/* fromSplit= */ false, /* continuityTimeline= */ 1);
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
expect(mockClosedCaptionParser.initSpy)
|
|
.toHaveBeenCalledWith(buffer, false, 1);
|
|
|
|
// new init segment
|
|
appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true, /* seeked= */ false,
|
|
/* adaptation= */ true, /* isChunkedData= */ false,
|
|
/* fromSplit= */ false, /* continuityTimeline= */ 1);
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
expect(mockClosedCaptionParser.initSpy)
|
|
.toHaveBeenCalledWith(buffer, true, 1);
|
|
});
|
|
|
|
it('sets timestampOffset on adaptations in sequence mode', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
videoSourceBuffer.mode = 'sequence';
|
|
|
|
await mediaSourceEngine.init(initObject, /* sequenceMode= */ true);
|
|
|
|
expect(videoSourceBuffer.timestampOffset).toBe(0);
|
|
|
|
// Mocks appending a segment from a newly adapted variant with a 0.50
|
|
// second misalignment from the old variant.
|
|
const reference = dummyReference(0, 1000);
|
|
reference.startTime = 0.50;
|
|
const appendVideo = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, reference, fakeStream,
|
|
/* hasClosedCaptions= */ false,
|
|
/* seeked= */ false, /* adaptation= */ true);
|
|
videoSourceBuffer.updateend();
|
|
await appendVideo;
|
|
|
|
expect(videoSourceBuffer.timestampOffset).toBe(0.50);
|
|
});
|
|
});
|
|
|
|
describe('remove', () => {
|
|
beforeEach(async () => {
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('removes the given data', async () => {
|
|
const p = mediaSourceEngine.remove(ContentType.AUDIO, 1, 5);
|
|
audioSourceBuffer.updateend();
|
|
|
|
await p;
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 5);
|
|
});
|
|
|
|
it('caption parser is not updated on audio remove', async () => {
|
|
// init caption parser
|
|
const appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true, /* seeked= */ false,
|
|
/* adaptation= */ false, /* isChunkedData= */ false,
|
|
/* fromSplit= */ false, /* continuityTimeline= */ 0);
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
const p = mediaSourceEngine.remove(ContentType.AUDIO, 1, 5, [0]);
|
|
audioSourceBuffer.updateend();
|
|
|
|
await p;
|
|
expect(mockClosedCaptionParser.removeSpy).not.toHaveBeenCalledWith([0]);
|
|
});
|
|
|
|
it('caption parser is updated on video remove', async () => {
|
|
// init caption parser
|
|
const appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ true, /* seeked= */ false,
|
|
/* adaptation= */ false, /* isChunkedData= */ false,
|
|
/* fromSplit= */ false, /* continuityTimeline= */ 0);
|
|
videoSourceBuffer.updateend();
|
|
await appendInit;
|
|
|
|
const p = mediaSourceEngine.remove(ContentType.VIDEO, 1, 5, [0]);
|
|
videoSourceBuffer.updateend();
|
|
|
|
await p;
|
|
expect(mockClosedCaptionParser.removeSpy).toHaveBeenCalledWith([0]);
|
|
});
|
|
|
|
it('rejects promise when operation throws', async () => {
|
|
audioSourceBuffer.remove.and.throwError('fail!');
|
|
mockVideo.error = {code: 5};
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
|
|
jasmine.objectContaining({message: 'fail!'}),
|
|
jasmine.objectContaining({code: 5}),
|
|
null));
|
|
await expectAsync(mediaSourceEngine.remove(ContentType.AUDIO, 1, 5))
|
|
.toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 5);
|
|
});
|
|
|
|
it('rejects the promise if this operation fails async', async () => {
|
|
mockVideo.error = {code: 5};
|
|
const p = mediaSourceEngine.remove(ContentType.AUDIO, 1, 5);
|
|
audioSourceBuffer.error();
|
|
audioSourceBuffer.updateend();
|
|
|
|
const expected = Util.jasmineError(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
|
|
5,
|
|
null));
|
|
await expectAsync(p).toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 5);
|
|
});
|
|
|
|
it('queues operations on a single SourceBuffer', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(
|
|
mediaSourceEngine.remove(ContentType.AUDIO, 1, 5));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(
|
|
mediaSourceEngine.remove(ContentType.AUDIO, 6, 10));
|
|
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 5);
|
|
expect(audioSourceBuffer.remove).not.toHaveBeenCalledWith(6, 10);
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(6, 10);
|
|
audioSourceBuffer.updateend();
|
|
await p2;
|
|
});
|
|
|
|
it('queues operations independently for different types', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(
|
|
mediaSourceEngine.remove(ContentType.AUDIO, 1, 5));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(
|
|
mediaSourceEngine.remove(ContentType.AUDIO, 6, 10));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p3 = new shaka.test.StatusPromise(
|
|
mediaSourceEngine.remove(ContentType.VIDEO, 3, 8));
|
|
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 5);
|
|
expect(audioSourceBuffer.remove).not.toHaveBeenCalledWith(6, 10);
|
|
expect(videoSourceBuffer.remove).toHaveBeenCalledWith(3, 8);
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
videoSourceBuffer.updateend();
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(6, 10);
|
|
await p3;
|
|
audioSourceBuffer.updateend();
|
|
await p2;
|
|
});
|
|
|
|
it('continues if an operation throws', async () => {
|
|
audioSourceBuffer.remove.and.callFake((start, end) => {
|
|
if (start == 2) {
|
|
// throw synchronously.
|
|
throw new Error();
|
|
} else {
|
|
// complete successfully asynchronously.
|
|
Promise.resolve().then(() => {
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
}
|
|
});
|
|
|
|
const p1 = mediaSourceEngine.remove(ContentType.AUDIO, 1, 2);
|
|
const p2 = mediaSourceEngine.remove(ContentType.AUDIO, 2, 3);
|
|
const p3 = mediaSourceEngine.remove(ContentType.AUDIO, 3, 4);
|
|
|
|
await expectAsync(p1).toBeResolved();
|
|
await expectAsync(p2).toBeRejected();
|
|
await expectAsync(p3).toBeResolved();
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(1, 2);
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(2, 3);
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(3, 4);
|
|
});
|
|
|
|
it('will forward to TextEngine', async () => {
|
|
expect(mockTextEngine.remove).not.toHaveBeenCalled();
|
|
await mediaSourceEngine.remove(ContentType.TEXT, 10, 20);
|
|
expect(mockTextEngine.remove).toHaveBeenCalledWith(10, 20);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
beforeEach(async () => {
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
initObject.set(ContentType.TEXT, fakeTextStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('clears the given data', async () => {
|
|
mockMediaSource.durationGetter.and.returnValue(20);
|
|
const p = mediaSourceEngine.clear(ContentType.AUDIO);
|
|
audioSourceBuffer.updateend();
|
|
|
|
await p;
|
|
expect(audioSourceBuffer.remove).toHaveBeenCalledTimes(1);
|
|
expect(audioSourceBuffer.remove.calls.argsFor(0)[0]).toBe(0);
|
|
expect(audioSourceBuffer.remove.calls.argsFor(0)[1] >= 20).toBeTruthy();
|
|
});
|
|
|
|
it('does not seek', async () => {
|
|
// We had a bug in which we got into a seek loop. Seeking caused
|
|
// StreamingEngine to call clear(). Clearing triggered a pipeline flush
|
|
// which was implemented by seeking. See issue #569.
|
|
|
|
// This loop is difficult to test for directly.
|
|
|
|
// A unit test on StreamingEngine would not suffice, since reproduction of
|
|
// the bug would involve making the mock MediaSourceEngine seek on clear.
|
|
// Since the fix was to remove the implicit seek, this behavior would then
|
|
// be removed from the mock, which would render the test useless.
|
|
|
|
// An integration test involving both StreamingEngine
|
|
// and MediaSourceEngine would also be problematic. The bug involved
|
|
// a race, so it would be difficult to reproduce the necessary timing.
|
|
// And if we succeeded, it would be tough to detect that we were
|
|
// definitely in a seek loop, since nothing was mocked.
|
|
|
|
// So the best option seems to be to enforce that clear() does not result
|
|
// in a seek. This can be done here, in a unit test on MediaSourceEngine.
|
|
// It does not reproduce the seek loop, but it does ensure that the test
|
|
// would fail if we ever reintroduced this behavior.
|
|
|
|
const originalTime = 10;
|
|
mockVideo.currentTime = originalTime;
|
|
|
|
mockMediaSource.durationGetter.and.returnValue(20);
|
|
const p = mediaSourceEngine.clear(ContentType.AUDIO);
|
|
audioSourceBuffer.updateend();
|
|
|
|
await p;
|
|
expect(mockVideo.currentTime).toBe(originalTime);
|
|
});
|
|
|
|
it('will forward to TextEngine', async () => {
|
|
expect(mockTextEngine.setTimestampOffset).not.toHaveBeenCalled();
|
|
expect(mockTextEngine.setAppendWindow).not.toHaveBeenCalled();
|
|
await mediaSourceEngine.setStreamProperties(ContentType.TEXT,
|
|
/* timestampOffset= */ 10,
|
|
/* appendWindowStart= */ 0,
|
|
/* appendWindowEnd= */ 20,
|
|
/* sequenceMode= */ false,
|
|
fakeStream.mimeType,
|
|
fakeStream.codecs,
|
|
/* streamsByType= */ new Map());
|
|
expect(mockTextEngine.setTimestampOffset).toHaveBeenCalledWith(10);
|
|
expect(mockTextEngine.setAppendWindow).toHaveBeenCalledWith(0, 20);
|
|
});
|
|
});
|
|
|
|
describe('endOfStream', () => {
|
|
beforeEach(async () => {
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('ends the MediaSource stream with the given reason', async () => {
|
|
await mediaSourceEngine.endOfStream('foo');
|
|
expect(mockMediaSource.endOfStream).toHaveBeenCalledWith('foo');
|
|
});
|
|
|
|
it('waits for all previous operations to complete', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p3 = new shaka.test.StatusPromise(mediaSourceEngine.endOfStream());
|
|
|
|
expect(mockMediaSource.endOfStream).not.toHaveBeenCalled();
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
videoSourceBuffer.updateend();
|
|
await p2;
|
|
await p3;
|
|
expect(mockMediaSource.endOfStream).toHaveBeenCalled();
|
|
});
|
|
|
|
it('makes subsequent operations wait', async () => {
|
|
/** @type {!Promise} */
|
|
const p1 = mediaSourceEngine.endOfStream();
|
|
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
// endOfStream hasn't been called yet because blocking multiple queues
|
|
// takes an extra tick, even when they are empty.
|
|
expect(mockMediaSource.endOfStream).not.toHaveBeenCalled();
|
|
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
expect(videoSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
|
|
await p1;
|
|
expect(mockMediaSource.endOfStream).toHaveBeenCalled();
|
|
// The next operations have already been kicked off.
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
// This one is still in queue.
|
|
expect(videoSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
audioSourceBuffer.updateend();
|
|
videoSourceBuffer.updateend();
|
|
await Promise.resolve();
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer2);
|
|
videoSourceBuffer.updateend();
|
|
});
|
|
|
|
it('runs subsequent operations if this operation throws', async () => {
|
|
mockMediaSource.endOfStream.and.throwError(new Error());
|
|
/** @type {!Promise} */
|
|
const p1 = mediaSourceEngine.endOfStream();
|
|
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
|
|
await expectAsync(p1).toBeRejected();
|
|
expect(mockMediaSource.endOfStream).toHaveBeenCalled();
|
|
await Util.shortDelay();
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(1);
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
});
|
|
|
|
describe('setDuration', () => {
|
|
beforeEach(async () => {
|
|
mockMediaSource.durationGetter.and.returnValue(0);
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('sets the given duration', async () => {
|
|
await mediaSourceEngine.setDuration(100);
|
|
expect(mockMediaSource.durationSetter).toHaveBeenCalledWith(100);
|
|
});
|
|
|
|
it('waits for all previous operations to complete', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p3 =
|
|
new shaka.test.StatusPromise(mediaSourceEngine.setDuration(100));
|
|
|
|
expect(mockMediaSource.durationSetter).not.toHaveBeenCalled();
|
|
expect(p1.status).toBe('pending');
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
|
|
audioSourceBuffer.updateend();
|
|
await p1;
|
|
expect(p2.status).toBe('pending');
|
|
expect(p3.status).toBe('pending');
|
|
videoSourceBuffer.updateend();
|
|
await p2;
|
|
await p3;
|
|
expect(mockMediaSource.durationSetter).toHaveBeenCalledWith(100);
|
|
});
|
|
|
|
it('makes subsequent operations wait', async () => {
|
|
/** @type {!Promise} */
|
|
const p1 = mediaSourceEngine.setDuration(100);
|
|
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
// The setter hasn't been called yet because blocking multiple queues
|
|
// takes an extra tick, even when they are empty.
|
|
expect(mockMediaSource.durationSetter).not.toHaveBeenCalled();
|
|
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
expect(videoSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
|
|
await p1;
|
|
expect(mockMediaSource.durationSetter).toHaveBeenCalled();
|
|
// The next operations have already been kicked off.
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
// This one is still in queue.
|
|
expect(videoSourceBuffer.appendBuffer)
|
|
.not.toHaveBeenCalledWith(buffer2);
|
|
audioSourceBuffer.updateend();
|
|
videoSourceBuffer.updateend();
|
|
await Promise.resolve();
|
|
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer2);
|
|
videoSourceBuffer.updateend();
|
|
});
|
|
|
|
it('runs subsequent operations if this operation throws', async () => {
|
|
mockMediaSource.durationSetter.and.throwError(new Error());
|
|
/** @type {!Promise} */
|
|
const p1 = mediaSourceEngine.setDuration(100);
|
|
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
|
|
await expectAsync(p1).toBeRejected();
|
|
expect(mockMediaSource.durationSetter).toHaveBeenCalled();
|
|
await Util.shortDelay();
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
|
|
it('allows duration to be shrunk', async () => {
|
|
// Pretend the initial duration was 100.
|
|
mockMediaSource.durationGetter.and.returnValue(100);
|
|
|
|
// When duration is shrunk, 'updateend' events are generated. This is
|
|
// because reducing the duration triggers the MSE removal algorithm to
|
|
// run.
|
|
mockMediaSource.durationSetter.and.callFake((duration) => {
|
|
expect(duration).toBe(50);
|
|
videoSourceBuffer.updateend();
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
|
|
audioSourceBuffer.appendBuffer.and.callFake(() => {
|
|
audioSourceBuffer.updateend();
|
|
});
|
|
videoSourceBuffer.appendBuffer.and.callFake(() => {
|
|
videoSourceBuffer.updateend();
|
|
});
|
|
|
|
/** @type {!Promise} */
|
|
const p1 = mediaSourceEngine.setDuration(50);
|
|
expect(mockMediaSource.durationSetter).not.toHaveBeenCalled();
|
|
|
|
// These operations should be blocked until after duration is shrunk.
|
|
// This is tested because shrinking duration generates 'updateend'
|
|
// events, and we want to show that the queue still operates correctly.
|
|
const a1 = mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
const a2 = mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
await p1;
|
|
await a1;
|
|
await a2;
|
|
});
|
|
});
|
|
|
|
describe('reload codec switching', () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
|
|
/**
|
|
* @param {!Map<shaka.util.ManifestParserUtils.ContentType,
|
|
* shaka.extern.Stream>} initObject
|
|
* @suppress {visibility}
|
|
*/
|
|
async function resetMSE(initObject) {
|
|
await mediaSourceEngine.reset_(initObject);
|
|
}
|
|
|
|
/** @suppress {visibility} */
|
|
function simulatePlaybackBeginning() {
|
|
mediaSourceEngine.playbackHasBegun_ = true;
|
|
}
|
|
|
|
it('should re-create a new MediaSource', async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
await resetMSE(initObject);
|
|
expect(createMediaSourceSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should re-create the audio & video source buffers', async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
mockMediaSource.addSourceBuffer.calls.reset();
|
|
await resetMSE(initObject);
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should preserve autoplay and paused state', async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
mockVideo.autoplay = true;
|
|
mockVideo.paused = true;
|
|
|
|
let canPlayThroughListener = null;
|
|
mockVideo.addEventListener.and.callFake((eventName, callback, _) => {
|
|
if (eventName == 'canplaythrough') {
|
|
canPlayThroughListener = callback;
|
|
}
|
|
});
|
|
|
|
simulatePlaybackBeginning();
|
|
await resetMSE(initObject);
|
|
|
|
expect(canPlayThroughListener).not.toBe(null);
|
|
if (!canPlayThroughListener) {
|
|
return;
|
|
}
|
|
canPlayThroughListener({target: mockVideo});
|
|
|
|
expect(mockVideo.autoplay).toBe(true);
|
|
expect(mockVideo.paused).toBe(true);
|
|
expect(mockVideo.play).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not clear autoplay if playback has not begun', async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
mockVideo.autoplay = true;
|
|
|
|
let setCount = 0;
|
|
Object.defineProperty(mockVideo, 'autoplay', {
|
|
get: () => true,
|
|
set: () => {
|
|
setCount++;
|
|
},
|
|
});
|
|
|
|
await resetMSE(initObject);
|
|
expect(setCount).toBe(0);
|
|
});
|
|
|
|
it('should preserve playing state', async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
mockVideo.autoplay = false;
|
|
mockVideo.paused = false;
|
|
|
|
let canPlayThroughListener = null;
|
|
mockVideo.addEventListener.and.callFake((eventName, callback, _) => {
|
|
if (eventName == 'canplaythrough') {
|
|
canPlayThroughListener = callback;
|
|
}
|
|
});
|
|
|
|
simulatePlaybackBeginning();
|
|
await resetMSE(initObject);
|
|
|
|
expect(canPlayThroughListener).not.toBe(null);
|
|
if (!canPlayThroughListener) {
|
|
return;
|
|
}
|
|
canPlayThroughListener({target: mockVideo});
|
|
|
|
expect(mockVideo.autoplay).toBe(false);
|
|
expect(mockVideo.paused).toBe(false);
|
|
expect(mockVideo.play).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
beforeEach(async () => {
|
|
captureEvents(audioSourceBuffer, ['updateend', 'error']);
|
|
captureEvents(videoSourceBuffer, ['updateend', 'error']);
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.AUDIO, fakeAudioStream);
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
});
|
|
|
|
it('waits for all operations to complete', async () => {
|
|
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
|
|
fakeStream, /* hasClosedCaptions= */ false);
|
|
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const d = new shaka.test.StatusPromise(mediaSourceEngine.destroy());
|
|
|
|
expect(d.status).toBe('pending');
|
|
await Util.shortDelay();
|
|
expect(d.status).toBe('pending');
|
|
audioSourceBuffer.updateend();
|
|
await Util.shortDelay();
|
|
expect(d.status).toBe('pending');
|
|
videoSourceBuffer.updateend();
|
|
await d;
|
|
});
|
|
|
|
it('resolves even when a pending operation fails', async () => {
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const d = mediaSourceEngine.destroy();
|
|
|
|
audioSourceBuffer.error();
|
|
audioSourceBuffer.updateend();
|
|
await expectAsync(p).toBeRejected();
|
|
await d;
|
|
});
|
|
|
|
it('waits for blocking operations to complete', async () => {
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p = new shaka.test.StatusPromise(mediaSourceEngine.endOfStream());
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const d = new shaka.test.StatusPromise(mediaSourceEngine.destroy());
|
|
|
|
expect(p.status).toBe('pending');
|
|
expect(d.status).toBe('pending');
|
|
await p;
|
|
expect(d.status).toBe('pending');
|
|
await d;
|
|
});
|
|
|
|
it('cancels operations that have not yet started', async () => {
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const rejected = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
// Create the expectation first so we don't get unhandled rejection errors
|
|
const expected = expectAsync(rejected).toBeRejected();
|
|
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const d = new shaka.test.StatusPromise(mediaSourceEngine.destroy());
|
|
|
|
expect(d.status).toBe('pending');
|
|
await Util.shortDelay();
|
|
expect(d.status).toBe('pending');
|
|
await expected;
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
audioSourceBuffer.updateend();
|
|
await d;
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalledWith(buffer2);
|
|
});
|
|
|
|
it('cancels blocking operations that have not yet started', async () => {
|
|
const p1 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false);
|
|
const p2 = mediaSourceEngine.endOfStream();
|
|
const d = mediaSourceEngine.destroy();
|
|
|
|
audioSourceBuffer.updateend();
|
|
await expectAsync(p1).toBeResolved();
|
|
// Note: Canceling a blocking operation means doing nothing more about
|
|
// the operation.
|
|
await expectAsync(p2).toBeResolved();
|
|
await d;
|
|
});
|
|
|
|
it('prevents new operations from being added', async () => {
|
|
const d = mediaSourceEngine.destroy();
|
|
await expectAsync(
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, fakeStream,
|
|
/* hasClosedCaptions= */ false))
|
|
.toBeRejected();
|
|
await d;
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('destroys text engines', async () => {
|
|
mediaSourceEngine.reinitText('text/vtt', false);
|
|
|
|
await mediaSourceEngine.destroy();
|
|
expect(mockTextEngine).toBeTruthy();
|
|
expect(mockTextEngine.destroy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function createMockMediaSource() {
|
|
const mediaSource = {
|
|
readyState: 'open',
|
|
sourceBuffers: document.createElement('div'),
|
|
addSourceBuffer: jasmine.createSpy('addSourceBuffer'),
|
|
endOfStream: jasmine.createSpy('endOfStream'),
|
|
durationGetter: jasmine.createSpy('duration getter'),
|
|
durationSetter: jasmine.createSpy('duration setter'),
|
|
addEventListener: jasmine.createSpy('addEventListener'),
|
|
removeEventListener: () => {},
|
|
};
|
|
Object.defineProperty(mediaSource, 'duration', {
|
|
get: Util.spyFunc(mediaSource.durationGetter),
|
|
set: Util.spyFunc(mediaSource.durationSetter),
|
|
});
|
|
return mediaSource;
|
|
}
|
|
|
|
/** @return {MockSourceBuffer} */
|
|
function createMockSourceBuffer() {
|
|
return {
|
|
abort: jasmine.createSpy('abort'),
|
|
appendBuffer: jasmine.createSpy('appendBuffer'),
|
|
remove: jasmine.createSpy('remove'),
|
|
updating: false,
|
|
addEventListener: jasmine.createSpy('addEventListener'),
|
|
removeEventListener: jasmine.createSpy('removeEventListener'),
|
|
buffered: {
|
|
length: 0,
|
|
start: jasmine.createSpy('buffered.start'),
|
|
end: jasmine.createSpy('buffered.end'),
|
|
},
|
|
timestampOffset: 0,
|
|
appendWindowStart: 0,
|
|
appendWindowEnd: Infinity,
|
|
updateend: () => {},
|
|
error: () => {},
|
|
mode: 'segments',
|
|
};
|
|
}
|
|
|
|
function createMockTextEngineCtor() {
|
|
const ctor = jasmine.createSpy('TextEngine');
|
|
ctor['isTypeSupported'] = () => true;
|
|
// Because this is a fake constructor, it must be callable with "new".
|
|
// This will cause jasmine to invoke the callback with "new" as well, so
|
|
// the callback must be a "function". This detail is hidden when babel
|
|
// transpiles the tests.
|
|
// eslint-disable-next-line prefer-arrow-callback, no-restricted-syntax
|
|
ctor.and.callFake(function() {
|
|
expect(mockTextEngine).toBeFalsy();
|
|
mockTextEngine = jasmine.createSpyObj('TextEngine', [
|
|
'initParser', 'destroy', 'appendBuffer', 'remove', 'setTimestampOffset',
|
|
'setAppendWindow', 'bufferStart', 'bufferEnd', 'bufferedAheadOf',
|
|
'storeAndAppendClosedCaptions', 'setModifyCueCallback',
|
|
]);
|
|
|
|
const resolve = () => Promise.resolve();
|
|
mockTextEngine.destroy.and.callFake(resolve);
|
|
mockTextEngine.appendBuffer.and.callFake(resolve);
|
|
mockTextEngine.remove.and.callFake(resolve);
|
|
return mockTextEngine;
|
|
});
|
|
return ctor;
|
|
}
|
|
|
|
function captureEvents(object, targetEventNames) {
|
|
object.addEventListener.and.callFake((eventName, listener) => {
|
|
if (targetEventNames.includes(eventName)) {
|
|
object[eventName] = listener;
|
|
}
|
|
});
|
|
object.removeEventListener.and.callFake((eventName, listener) => {
|
|
if (targetEventNames.includes(eventName)) {
|
|
expect(object[eventName]).toBe(listener);
|
|
object[eventName] = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
function dummyReference(startTime, endTime) {
|
|
return new shaka.media.SegmentReference(
|
|
startTime, endTime,
|
|
/* uris= */ () => ['foo://bar'],
|
|
/* startByte= */ 0,
|
|
/* endByte= */ null,
|
|
/* initSegmentReference= */ null,
|
|
/* timestampOffset= */ 0,
|
|
/* appendWindowStart= */ 0,
|
|
/* appendWindowEnd= */ Infinity);
|
|
}
|
|
});
|