Skip to content

Commit

Permalink
test: Rewrite MediaSourceEngine codec switching tests (#6462)
Browse files Browse the repository at this point in the history
The tests for MediaSourceEngine codec switching were written to ignore
types and suppress access controls. The were unreadable, too, with very
little whitespace, confusing one-letter variable names, and
difficult-to-follow event mocking. This made it more difficult to debug
test failures in PR #6460.

This rewrites the tests in a more readable manner with compiler
enforcement of types in the tests. Two helper functions are used to
isolate the necessary access-control suppressions.

This exposed a bug in the tests, in which one test case (preserve
SourceBuffer attributes) only passed because the original version failed
to await on an async process. I am not sure that the functionality in
that test exists at that level. For now, the test is disabled. I'll
follow up with removal after more investigation.

Related to #6458, #6460
  • Loading branch information
joeyparrish authored Apr 22, 2024
1 parent 32d7054 commit e75fcc9
Showing 1 changed file with 129 additions and 148 deletions.
277 changes: 129 additions & 148 deletions test/media/media_source_engine_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let MockTimeRanges;
* removeEventListener: jasmine.Spy,
* buffered: (MockTimeRanges|TimeRanges),
* timestampOffset: number,
* appendWindowStart: number,
* appendWindowEnd: number,
* updateend: function(),
* error: function(),
Expand Down Expand Up @@ -166,11 +167,18 @@ describe('MediaSourceEngine', () => {
mockMediaSource.addSourceBuffer.and.callFake((mimeType) => {
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();
Expand Down Expand Up @@ -223,11 +231,14 @@ describe('MediaSourceEngine', () => {
mockVideo.src = '';
},
addEventListener: jasmine.createSpy('addVideoEventListener'),
removeEventListener: jasmine.createSpy('removeVideoEventListener'),
load: /** @this {HTMLVideoElement} */ () => {
// This assertion alerts us if the requirements for this mock change.
goog.asserts.assert(mockVideo.src == '', 'Unexpected load() call');
},
play: jasmine.createSpy('play'),
paused: true,
autoplay: false,
};
video = /** @type {HTMLMediaElement} */(mockVideo);
mockClosedCaptionParser = new shaka.test.FakeClosedCaptionParser();
Expand Down Expand Up @@ -1227,158 +1238,127 @@ describe('MediaSourceEngine', () => {
});

describe('reload codec switching', () => {
beforeEach(
/** @suppress {visibility, checkTypes} */
() => {
mediaSourceEngine.eventManager_.listenOnce =
jasmine.createSpy('listener');
mediaSourceEngine.eventManager_.listen =
jasmine.createSpy('eventListener');
});
const initObject = new Map();
initObject.set(ContentType.VIDEO, fakeVideoStream);
initObject.set(ContentType.AUDIO, fakeAudioStream);

it('should re-create a new MediaSource',
/** @suppress {visibility} */ async () => {
await mediaSourceEngine.init(initObject, false);
mediaSourceEngine.reset_(initObject);
expect(createMediaSourceSpy).toHaveBeenCalled();
});

it('should re-create the audio & video source buffers',
/** @suppress {invalidCasts, visibility, checkTypes} */ async () => {
await mediaSourceEngine.init(initObject, false);
mediaSourceEngine.reset_(initObject);
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledTimes(2);
});

it('should persist the previous source buffer parameters',
/** @suppress {invalidCasts, visibility, checkTypes} */async () => {
await mediaSourceEngine.init(initObject, false);

audioSourceBuffer.timestampOffset = 10;
audioSourceBuffer.appendWindowStart = 5;
audioSourceBuffer.appendWindowEnd = 20;

videoSourceBuffer.timestampOffset = 20;
videoSourceBuffer.appendWindowStart = 15;
videoSourceBuffer.appendWindowEnd = 30;

mediaSourceEngine.reset_(initObject);

expect(audioSourceBuffer.timestampOffset).toBe(10);
expect(audioSourceBuffer.appendWindowStart).toBe(5);
expect(audioSourceBuffer.appendWindowEnd).toBe(20);

expect(videoSourceBuffer.timestampOffset).toBe(20);
expect(videoSourceBuffer.appendWindowStart).toBe(15);
expect(videoSourceBuffer.appendWindowEnd).toBe(30);
});

it('should preserve autoplay state',
/** @suppress {invalidCasts, visibility, checkTypes} */
async () => {
const originalInitSourceBuffer = mediaSourceEngine.initSourceBuffer_;
try {
await mediaSourceEngine.init(initObject, false);
video.autoplay = true;
video.paused = true;
const playSpy = /** @type {jasmine.Spy} */ (video.play);
const addListenOnceSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.eventManager_.listenOnce);
const addEventListenerSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.eventManager_.listen);
mediaSourceEngine.playbackHasBegun_ = true;
mediaSourceEngine.initSourceBuffer_ =
jasmine.createSpy('initSourceBuffer');
const initSourceBufferSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.initSourceBuffer_);
addEventListenerSpy.and.callFake((o, e, c) => {
c(); // audio
c(); // video
});
await mediaSourceEngine.reset_(initObject);
const callback = addListenOnceSpy.calls.argsFor(0)[2];
callback();
expect(initSourceBufferSpy).toHaveBeenCalled();
expect(addListenOnceSpy.calls.argsFor(0)[1]).toBe('canplaythrough');
expect(video.autoplay).toBe(true);
expect(playSpy).not.toHaveBeenCalled();
} finally {
mediaSourceEngine.initSourceBuffer_ = originalInitSourceBuffer;
}
});

it('should not set autoplay to false if playback has not begun',
/** @suppress {invalidCasts, visibility, checkTypes} */
async () => {
const originalInitSourceBuffer = mediaSourceEngine.initSourceBuffer_;
try {
await mediaSourceEngine.init(initObject, false);
video.autoplay = true;
let setCount = 0;
const addEventListenerSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.eventManager_.listen);
addEventListenerSpy.and.callFake((o, e, c) => {
c(); // audio
c(); // video
});
mediaSourceEngine.initSourceBuffer_ =
jasmine.createSpy('initSourceBuffer');
Object.defineProperty(video, 'autoplay', {
get: () => true,
set: () => {
setCount++;
},
});
await mediaSourceEngine.reset_(initObject);
expect(setCount).toBe(0);
} finally {
mediaSourceEngine.initSourceBuffer_ = originalInitSourceBuffer;
}
});

it('should preserve playing state',
/** @suppress {invalidCasts, visibility, checkTypes} */
async () => {
const originalInitSourceBuffer = mediaSourceEngine.initSourceBuffer_;
try {
await mediaSourceEngine.init(initObject, false);
video.autoplay = false;
video.paused = false;
const playSpy = /** @type {jasmine.Spy} */ (video.play);
const addListenOnceSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.eventManager_.listenOnce);
const addEventListenerSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.eventManager_.listen);
mediaSourceEngine.playbackHasBegun_ = true;
mediaSourceEngine.initSourceBuffer_ =
jasmine.createSpy('initSourceBuffer');
const initSourceBufferSpy =
/** @type {jasmine.Spy} */
(mediaSourceEngine.initSourceBuffer_);
addEventListenerSpy.and.callFake((o, e, c) => {
c(); // audio
c(); // video
});
await mediaSourceEngine.reset_(initObject);
const callback = addListenOnceSpy.calls.argsFor(0)[2];
callback();
expect(initSourceBufferSpy).toHaveBeenCalled();
expect(addListenOnceSpy.calls.argsFor(0)[1]).toBe('canplaythrough');
expect(video.autoplay).toBe(false);
expect(playSpy).toHaveBeenCalled();
} finally {
mediaSourceEngine.initSourceBuffer_ = originalInitSourceBuffer;
}
});
/** @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);
});

// TODO: Fix this test/feature. This only passed before because the
// original author failed to await the reset. */
xit('should persist the previous source buffer parameters', async () => {
await mediaSourceEngine.init(initObject, false);

audioSourceBuffer.timestampOffset = 10;
audioSourceBuffer.appendWindowStart = 5;
audioSourceBuffer.appendWindowEnd = 20;

videoSourceBuffer.timestampOffset = 20;
videoSourceBuffer.appendWindowStart = 15;
videoSourceBuffer.appendWindowEnd = 30;

await resetMSE(initObject);

expect(audioSourceBuffer.timestampOffset).toBe(10);
expect(audioSourceBuffer.appendWindowStart).toBe(5);
expect(audioSourceBuffer.appendWindowEnd).toBe(20);

expect(videoSourceBuffer.timestampOffset).toBe(20);
expect(videoSourceBuffer.appendWindowStart).toBe(15);
expect(videoSourceBuffer.appendWindowEnd).toBe(30);
});

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', () => {
Expand Down Expand Up @@ -1534,6 +1514,7 @@ describe('MediaSourceEngine', () => {
end: jasmine.createSpy('buffered.end'),
},
timestampOffset: 0,
appendWindowStart: 0,
appendWindowEnd: Infinity,
updateend: () => {},
error: () => {},
Expand Down

0 comments on commit e75fcc9

Please sign in to comment.