mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-18 16:36:56 +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
1177 lines
45 KiB
JavaScript
1177 lines
45 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.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @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,
|
|
* appendWindowEnd: number,
|
|
* updateend: function(),
|
|
* error: function()
|
|
* }}
|
|
*/
|
|
let MockSourceBuffer;
|
|
|
|
|
|
describe('MediaSourceEngine', () => {
|
|
const Util = shaka.test.Util;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
const originalIsTypeSupported = window.MediaSource.isTypeSupported;
|
|
const originalTextEngine = shaka.text.TextEngine;
|
|
const originalCreateMediaSource =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.media.MediaSourceEngine.prototype.createMediaSource;
|
|
const originalTransmuxerIsSupported = shaka.media.Transmuxer.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 fakeVideoStream = {mimeType: 'video/foo'};
|
|
const fakeAudioStream = {mimeType: 'audio/foo'};
|
|
const fakeTextStream = {mimeType: 'text/foo'};
|
|
const fakeTransportStream = {mimeType: 'tsMimetype'};
|
|
|
|
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 {!jasmine.Spy} */
|
|
let createMediaSourceSpy;
|
|
|
|
/** @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';
|
|
};
|
|
|
|
shaka.media.Transmuxer.isSupported = (mimeType, contentType) => {
|
|
return mimeType == 'tsMimetype';
|
|
};
|
|
});
|
|
|
|
afterAll(() => {
|
|
window.MediaSource.isTypeSupported = originalIsTypeSupported;
|
|
shaka.media.Transmuxer.isSupported = originalTransmuxerIsSupported;
|
|
});
|
|
|
|
beforeEach(/** @suppress {invalidCasts} */ () => {
|
|
audioSourceBuffer = createMockSourceBuffer();
|
|
videoSourceBuffer = createMockSourceBuffer();
|
|
mockMediaSource = createMockMediaSource();
|
|
mockMediaSource.addSourceBuffer.and.callFake((mimeType) => {
|
|
const type = mimeType.split('/')[0];
|
|
return type == 'audio' ? audioSourceBuffer : videoSourceBuffer;
|
|
});
|
|
|
|
shaka.text.TextEngine = createMockTextEngineCtor();
|
|
|
|
createMediaSourceSpy = jasmine.createSpy('createMediaSource');
|
|
createMediaSourceSpy.and.callFake((p) => {
|
|
p.resolve();
|
|
return mockMediaSource;
|
|
});
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.media.MediaSourceEngine.prototype.createMediaSource =
|
|
Util.spyFunc(createMediaSourceSpy);
|
|
|
|
// 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 = {
|
|
src: '',
|
|
error: null,
|
|
currentTime: 0,
|
|
buffered: {
|
|
length: 0,
|
|
},
|
|
removeAttribute: /** @this {HTMLVideoElement} */ (attr) => {
|
|
// Only called with attr == 'src'.
|
|
// This assertion alerts us if the requirements for this mock change.
|
|
goog.asserts.assert(attr == 'src', 'Unexpected removeAttribute() call');
|
|
mockVideo.src = '';
|
|
},
|
|
load: /** @this {HTMLVideoElement} */ () => {
|
|
// This assertion alerts us if the requirements for this mock change.
|
|
goog.asserts.assert(mockVideo.src == '', 'Unexpected load() call');
|
|
},
|
|
};
|
|
video = /** @type {HTMLMediaElement} */(mockVideo);
|
|
mockClosedCaptionParser = new shaka.test.FakeClosedCaptionParser();
|
|
mockTextDisplayer = new shaka.test.FakeTextDisplayer();
|
|
mediaSourceEngine = new shaka.media.MediaSourceEngine(
|
|
video,
|
|
mockClosedCaptionParser,
|
|
mockTextDisplayer);
|
|
});
|
|
|
|
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 originalMediaSource = window.MediaSource;
|
|
/** @type {jasmine.Spy} */
|
|
let createObjectURLSpy;
|
|
|
|
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);
|
|
|
|
const mediaSourceSpy =
|
|
jasmine.createSpy('MediaSource').and.callFake(() => {
|
|
return mockMediaSource;
|
|
});
|
|
window.MediaSource = Util.spyFunc(mediaSourceSpy);
|
|
|
|
await mediaSourceEngine.destroy();
|
|
});
|
|
|
|
afterAll(() => {
|
|
shaka.media.MediaSourceEngine.createObjectURL = originalCreateObjectURL;
|
|
window.MediaSource = originalMediaSource;
|
|
});
|
|
|
|
it('creates a MediaSource object and sets video.src', () => {
|
|
mediaSourceEngine = new shaka.media.MediaSourceEngine(
|
|
video,
|
|
new shaka.test.FakeClosedCaptionParser(),
|
|
new shaka.test.FakeTextDisplayer());
|
|
|
|
expect(createMediaSourceSpy).toHaveBeenCalled();
|
|
expect(createObjectURLSpy).toHaveBeenCalled();
|
|
expect(mockVideo.src).toBe('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/foo');
|
|
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith('video/foo');
|
|
expect(shaka.text.TextEngine).not.toHaveBeenCalled();
|
|
});
|
|
|
|
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 () => {
|
|
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('appends the given data', async () => {
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, null, /* hasClosedCaptions */ false);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
audioSourceBuffer.updateend();
|
|
await p;
|
|
});
|
|
|
|
it('rejects promise when operation throws', async () => {
|
|
audioSourceBuffer.appendBuffer.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!'})));
|
|
await expectAsync(
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, null,
|
|
/* 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, null,
|
|
/* hasClosedCaptions */ false))
|
|
.toBeRejectedWith(expected);
|
|
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
|
|
});
|
|
|
|
it('rejects the promise if this operation fails async', async () => {
|
|
mockVideo.error = {code: 5};
|
|
const p = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, null, /* 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));
|
|
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, null,
|
|
/* hasClosedCaptions */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, null,
|
|
/* 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, null,
|
|
/* hasClosedCaptions */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, null,
|
|
/* hasClosedCaptions */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p3 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer3, null, null,
|
|
/* 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, null, /* hasClosedCaptions */ false);
|
|
const p2 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, null,
|
|
/* hasClosedCaptions */ false);
|
|
const p3 = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer3, null, null,
|
|
/* 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();
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.TEXT, data, 0, 10, /* hasClosedCaptions */ false);
|
|
expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith(
|
|
data, 0, 10);
|
|
});
|
|
|
|
it('appends transmuxed data and captions', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeTransportStream);
|
|
|
|
const init = async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null,
|
|
/* hasClosedCaptions */ false);
|
|
expect(mockTextEngine.storeAndAppendClosedCaptions).toHaveBeenCalled();
|
|
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 only transmuxed data without embedded text', async () => {
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeTransportStream);
|
|
|
|
const init = async () => {
|
|
await mediaSourceEngine.init(initObject, false);
|
|
await mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null,
|
|
/* hasClosedCaptions */ false);
|
|
expect(mockTextEngine.appendCues).not.toHaveBeenCalled();
|
|
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, onCaptions) => {
|
|
onCaptions(['foo', 'bar']);
|
|
});
|
|
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
// Initialize the closed caption parser.
|
|
const appendInit = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null, 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 appendVideo = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, 0, Infinity, true);
|
|
videoSourceBuffer.updateend();
|
|
await appendVideo;
|
|
|
|
expect(mockTextEngine.storeAndAppendClosedCaptions).toHaveBeenCalled();
|
|
});
|
|
|
|
it('appends closed caption data only when mux.js is available',
|
|
async () => {
|
|
const originalMuxjs = window.muxjs;
|
|
|
|
try {
|
|
window['muxjs'] = null;
|
|
const initObject = new Map();
|
|
initObject.set(ContentType.VIDEO, fakeVideoStream);
|
|
await mediaSourceEngine.init(initObject, false);
|
|
|
|
const appendBuffer = mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null, 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 appendBuffer;
|
|
expect(mockClosedCaptionParser.initSpy).not.toHaveBeenCalled();
|
|
expect(mockTextEngine.storeAndAppendClosedCaptions).not
|
|
.toHaveBeenCalled();
|
|
} finally {
|
|
window['muxjs'] = originalMuxjs;
|
|
}
|
|
});
|
|
});
|
|
|
|
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('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!'})));
|
|
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));
|
|
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 MediaSourcEngine
|
|
// 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);
|
|
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, null,
|
|
/* hasClosedCaptions */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null,
|
|
/* 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, null,
|
|
/* hasClosedCaptions */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null,
|
|
/* hasClosedCaptions */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, null,
|
|
/* 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, null,
|
|
/* 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, null,
|
|
/* hasClosedCaptions */ false));
|
|
/** @type {!shaka.test.StatusPromise} */
|
|
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
|
|
ContentType.VIDEO, buffer, null, null,
|
|
/* 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, null,
|
|
/* hasClosedCaptions */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null,
|
|
/* hasClosedCaptions */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null, null,
|
|
/* 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, null,
|
|
/* 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();
|
|
});
|
|
});
|
|
|
|
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, null,
|
|
/* hasClosedCaptions */ false);
|
|
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, null,
|
|
/* 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, null, /* 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, null, /* hasClosedCaptions */ false);
|
|
const rejected = mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer2, null, null,
|
|
/* 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, null, /* hasClosedCaptions */ false);
|
|
const p2 = mediaSourceEngine.endOfStream();
|
|
const d = mediaSourceEngine.destroy();
|
|
|
|
audioSourceBuffer.updateend();
|
|
await expectAsync(p1).toBeResolved();
|
|
await expectAsync(p2).toBeRejected();
|
|
await d;
|
|
});
|
|
|
|
it('prevents new operations from being added', async () => {
|
|
const d = mediaSourceEngine.destroy();
|
|
await expectAsync(
|
|
mediaSourceEngine.appendBuffer(
|
|
ContentType.AUDIO, buffer, null, null,
|
|
/* hasClosedCaptions */ false))
|
|
.toBeRejected();
|
|
await d;
|
|
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('destroys text engines', async () => {
|
|
mediaSourceEngine.reinitText('text/vtt');
|
|
|
|
await mediaSourceEngine.destroy();
|
|
expect(mockTextEngine).toBeTruthy();
|
|
expect(mockTextEngine.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
// Regression test for https://github.com/google/shaka-player/issues/984
|
|
it('destroys TextDisplayer on destroy', async () => {
|
|
await mediaSourceEngine.destroy();
|
|
expect(mockTextDisplayer.destroySpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function createMockMediaSource() {
|
|
const mediaSource = {
|
|
readyState: 'open',
|
|
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: mediaSource.durationGetter_, set: 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,
|
|
appendWindowEnd: Infinity,
|
|
updateend: () => {},
|
|
error: () => {},
|
|
};
|
|
}
|
|
|
|
function createMockTextEngineCtor() {
|
|
const ctor = jasmine.createSpy('TextEngine');
|
|
ctor.isTypeSupported = () => true;
|
|
ctor.and.callFake(() => {
|
|
expect(mockTextEngine).toBeFalsy();
|
|
mockTextEngine = jasmine.createSpyObj('TextEngine', [
|
|
'initParser', 'destroy', 'appendBuffer', 'remove', 'setTimestampOffset',
|
|
'setAppendWindow', 'bufferStart', 'bufferEnd', 'bufferedAheadOf',
|
|
'appendCues', 'storeAndAppendClosedCaptions',
|
|
]);
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
});
|