mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
406729873b
It encapsulates and simplifies the use of requestVideoFrameCallback and cancelVideoFrameCallback --------- Co-authored-by: Wojciech Tyczyński <tykus160@gmail.com>
230 lines
7.0 KiB
JavaScript
230 lines
7.0 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
describe('VideoFrameCallbackHandler', () => {
|
|
const VideoFrameCallbackHandler = shaka.util.VideoFrameCallbackHandler;
|
|
|
|
/** @type {!HTMLVideoElement} */
|
|
let mockVideo;
|
|
|
|
/** @type {shaka.util.VideoFrameCallbackHandler} */
|
|
let handler;
|
|
|
|
/**
|
|
* Holds every VideoFrameRequestCallback registered via
|
|
* requestVideoFrameCallback on the current mockVideo. Populated by
|
|
* createMockVideoWithRVFC so that tests can trigger callbacks without
|
|
* accessing spy internals.
|
|
*
|
|
* @type {!Array<!VideoFrameRequestCallback>}
|
|
*/
|
|
let registeredCallbacks;
|
|
|
|
beforeEach(() => {
|
|
registeredCallbacks = [];
|
|
});
|
|
|
|
afterEach(() => {
|
|
handler?.release();
|
|
handler = null;
|
|
});
|
|
|
|
/**
|
|
* Creates a mock video element that supports requestVideoFrameCallback
|
|
* and cancelVideoFrameCallback.
|
|
*
|
|
* @return {!HTMLVideoElement}
|
|
*/
|
|
function createMockVideoWithRVFC() {
|
|
const video = jasmine.createSpyObj('video', [
|
|
'requestVideoFrameCallback',
|
|
'cancelVideoFrameCallback',
|
|
]);
|
|
let handleCounter = 1;
|
|
video.requestVideoFrameCallback.and.callFake(
|
|
(/** !VideoFrameRequestCallback */ cb) => {
|
|
registeredCallbacks.push(cb);
|
|
return handleCounter++;
|
|
});
|
|
return /** @type {!HTMLVideoElement} */ (video);
|
|
}
|
|
|
|
/**
|
|
* Creates a mock video element that does NOT support
|
|
* requestVideoFrameCallback, simulating older devices.
|
|
*
|
|
* @return {!HTMLVideoElement}
|
|
*/
|
|
function createMockVideoWithoutRVFC() {
|
|
return /** @type {!HTMLVideoElement} */ ({});
|
|
}
|
|
|
|
/**
|
|
* Fires the most recently registered video frame callback.
|
|
*
|
|
* @param {number=} now
|
|
* @param {?VideoFrameMetadata=} metadata
|
|
*/
|
|
function fireLastRegisteredCallback(now = 100, metadata = null) {
|
|
registeredCallbacks[registeredCallbacks.length - 1](now, metadata);
|
|
}
|
|
|
|
describe('start', () => {
|
|
it('returns false if requestVideoFrameCallback is not supported', () => {
|
|
handler = new VideoFrameCallbackHandler(createMockVideoWithoutRVFC());
|
|
|
|
expect(handler.start(() => {})).toBe(false);
|
|
});
|
|
|
|
it('returns true if requestVideoFrameCallback is supported', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
expect(handler.start(() => {})).toBe(true);
|
|
});
|
|
|
|
it('registers a video frame callback on the video element', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
|
|
expect(mockVideo.requestVideoFrameCallback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('cancels the pending callback before registering a new one', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
handler.start(() => {});
|
|
|
|
expect(mockVideo.cancelVideoFrameCallback).toHaveBeenCalledTimes(1);
|
|
expect(mockVideo.requestVideoFrameCallback).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('does not call cancelVideoFrameCallback on the first start', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
|
|
expect(mockVideo.cancelVideoFrameCallback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('invokes the user callback when a video frame fires', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
const callbackSpy = jasmine.createSpy('userCallback');
|
|
|
|
handler.start(shaka.test.Util.spyFunc(callbackSpy));
|
|
fireLastRegisteredCallback(200, null);
|
|
|
|
expect(callbackSpy).toHaveBeenCalledOnceWith(200, null);
|
|
});
|
|
|
|
it('re-registers the callback after each frame to loop continuously',
|
|
() => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
fireLastRegisteredCallback();
|
|
fireLastRegisteredCallback();
|
|
|
|
// 1 initial + 2 re-registrations after each fired frame
|
|
expect(mockVideo.requestVideoFrameCallback)
|
|
.toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('does not re-register if start is called again mid-loop', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
const firstCallbackSpy = jasmine.createSpy('firstCallback');
|
|
const secondCallbackSpy = jasmine.createSpy('secondCallback');
|
|
|
|
handler.start(shaka.test.Util.spyFunc(firstCallbackSpy));
|
|
|
|
// Capture the first registered callback before it fires
|
|
const firstRegisteredCallback = registeredCallbacks[0];
|
|
|
|
// Replace with a new callback
|
|
handler.start(shaka.test.Util.spyFunc(secondCallbackSpy));
|
|
|
|
// The stale first callback fires (e.g. already queued by the browser)
|
|
firstRegisteredCallback(100, null);
|
|
|
|
expect(firstCallbackSpy).not.toHaveBeenCalled();
|
|
expect(secondCallbackSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('release', () => {
|
|
it('cancels a pending callback', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
handler.release();
|
|
|
|
expect(mockVideo.cancelVideoFrameCallback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not throw if called before start', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
expect(() => handler.release()).not.toThrow();
|
|
});
|
|
|
|
it('can be called multiple times without throwing', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
|
|
expect(() => {
|
|
handler.release();
|
|
handler.release();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('stops the user callback from being invoked after release', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
const callbackSpy = jasmine.createSpy('userCallback');
|
|
|
|
handler.start(shaka.test.Util.spyFunc(callbackSpy));
|
|
|
|
// Capture the registered callback before release
|
|
const registeredCallback = registeredCallbacks[0];
|
|
|
|
handler.release();
|
|
|
|
// Simulate a queued frame firing after release
|
|
registeredCallback(100, null);
|
|
|
|
expect(callbackSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('stops re-registration of callback after release', () => {
|
|
mockVideo = createMockVideoWithRVFC();
|
|
handler = new VideoFrameCallbackHandler(mockVideo);
|
|
|
|
handler.start(() => {});
|
|
|
|
const registeredCallback = registeredCallbacks[0];
|
|
|
|
handler.release();
|
|
registeredCallback(100, null);
|
|
|
|
// requestVideoFrameCallback should only have been called once (on start),
|
|
// not again after the stale frame fires post-release.
|
|
expect(mockVideo.requestVideoFrameCallback).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|