diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts new file mode 100644 index 0000000000..da986b6c12 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/util.ts @@ -0,0 +1,185 @@ +import { Fixture, SkipTestCase } from '../../common/framework/fixture.js'; +import { + assert, + ErrorWithExtra, + raceWithRejectOnTimeout, + unreachable, +} from '../../common/util/util.js'; + +declare global { + interface HTMLMediaElement { + // Add captureStream() support for HTMLMediaElement from + // https://w3c.github.io/mediacapture-fromelement/#dom-htmlmediaelement-capturestream + captureStream(): MediaStream; + } +} + +/** + * Starts playing a video and waits for it to be consumable. + * Returns a promise which resolves after `callback` (which may be async) completes. + * + * @param video An HTML5 Video element. + * @param callback Function to call when video is ready. + * + * Adapted from https://github.com/KhronosGroup/WebGL/blob/main/sdk/tests/js/webgl-test-utils.js + */ +export function startPlayingAndWaitForVideo( + video: HTMLVideoElement, + callback: () => unknown | Promise<unknown> +): Promise<void> { + return raceWithRejectOnTimeout( + new Promise((resolve, reject) => { + const callbackAndResolve = () => + void (async () => { + try { + await callback(); + resolve(); + } catch (ex) { + reject(); + } + })(); + if (video.error) { + reject( + new ErrorWithExtra('Video.error: ' + video.error.message, () => ({ error: video.error })) + ); + return; + } + + video.addEventListener( + 'error', + event => reject(new ErrorWithExtra('Video received "error" event', () => ({ event }))), + true + ); + + if ('requestVideoFrameCallback' in video) { + video.requestVideoFrameCallback(() => { + callbackAndResolve(); + }); + } else { + // If requestVideoFrameCallback isn't available, check each frame if the video has advanced. + const timeWatcher = () => { + if (video.currentTime > 0) { + callbackAndResolve(); + } else { + requestAnimationFrame(timeWatcher); + } + }; + timeWatcher(); + } + + video.loop = true; + video.muted = true; + video.preload = 'auto'; + video.play().catch(reject); + }), + 2000, + 'Video never became ready' + ); +} + +/** + * Fire a `callback` when the video reaches a new frame. + * Returns a promise which resolves after `callback` (which may be async) completes. + * + * MAINTENANCE_TODO: Find a way to implement this for browsers without requestVideoFrameCallback as + * well, similar to the timeWatcher path in startPlayingAndWaitForVideo. If that path is proven to + * work well, we can consider getting rid of the requestVideoFrameCallback path. + */ +export function waitForNextFrame( + video: HTMLVideoElement, + callback: () => unknown | Promise<unknown> +): Promise<void> { + const { promise, callbackAndResolve } = videoCallbackHelper( + callback, + 'waitForNextFrame timed out' + ); + + if ('requestVideoFrameCallback' in video) { + video.requestVideoFrameCallback(() => { + callbackAndResolve(); + }); + } else { + throw new SkipTestCase('waitForNextFrame currently requires requestVideoFrameCallback'); + } + + return promise; +} + +type VideoColorSpaceName = 'REC601' | 'REC709' | 'REC2020'; + +export function getVideoColorSpaceInit(colorSpaceName: VideoColorSpaceName): VideoColorSpaceInit { + switch (colorSpaceName) { + case 'REC601': + return { + primaries: 'smpte170m', + transfer: 'smpte170m', + matrix: 'smpte170m', + fullRange: false, + }; + case 'REC709': + return { primaries: 'bt709', transfer: 'bt709', matrix: 'bt709', fullRange: false }; + case 'REC2020': + return { primaries: 'bt709', transfer: 'iec61966-2-1', matrix: 'rgb', fullRange: true }; + default: + unreachable(); + } +} + +export async function getVideoFrameFromVideoElement( + test: Fixture, + video: HTMLVideoElement, + colorSpace: VideoColorSpaceInit = getVideoColorSpaceInit('REC709') +): Promise<VideoFrame> { + if (video.captureStream === undefined) { + test.skip('HTMLVideoElement.captureStream is not supported'); + } + + const track: MediaStreamVideoTrack = video.captureStream().getVideoTracks()[0]; + const reader = new MediaStreamTrackProcessor({ track }).readable.getReader(); + const videoFrame = (await reader.read()).value; + assert(videoFrame !== undefined, 'unable to get a VideoFrame from track 0'); + assert( + videoFrame.format !== null && videoFrame.timestamp !== null, + 'unable to get a valid VideoFrame from track 0' + ); + // Apply color space info because the VideoFrame generated from captured stream + // doesn't have it. + const bufferSize = videoFrame.allocationSize(); + const buffer = new ArrayBuffer(bufferSize); + const frameLayout = await videoFrame.copyTo(buffer); + const frameInit: VideoFrameBufferInit = { + format: videoFrame.format, + timestamp: videoFrame.timestamp, + codedWidth: videoFrame.codedWidth, + codedHeight: videoFrame.codedHeight, + colorSpace, + layout: frameLayout, + }; + return new VideoFrame(buffer, frameInit); +} + +/** + * Helper for doing something inside of a (possibly async) callback (directly, not in a following + * microtask), and returning a promise when the callback is done. + * MAINTENANCE_TODO: Use this in startPlayingAndWaitForVideo (and make sure it works). + */ +function videoCallbackHelper( + callback: () => unknown | Promise<unknown>, + timeoutMessage: string +): { promise: Promise<void>; callbackAndResolve: () => void } { + let callbackAndResolve: () => void; + + const promiseWithoutTimeout = new Promise<void>((resolve, reject) => { + callbackAndResolve = () => + void (async () => { + try { + await callback(); // catches both exceptions and rejections + resolve(); + } catch (ex) { + reject(ex); + } + })(); + }); + const promise = raceWithRejectOnTimeout(promiseWithoutTimeout, 2000, timeoutMessage); + return { promise, callbackAndResolve: callbackAndResolve! }; +} |