blob: dcb0d25b72cf264f8d6ec4d8ecd840529694ecd1 [file] [log] [blame]
import { Fixture, SkipTestCase } from '../../common/framework/fixture.js';
import { getResourcePath } from '../../common/framework/resources.js';
import { keysOf } from '../../common/util/data_tables.js';
import { timeout } from '../../common/util/timeout.js';
import { ErrorWithExtra, assert, raceWithRejectOnTimeout } from '../../common/util/util.js';
import { GPUTest } from '../gpu_test.js';
import { RGBA, srgbToDisplayP3 } from '../util/color_space_conversion.js';
declare global {
interface HTMLMediaElement {
// Add captureStream() support for HTMLMediaElement from
// https://w3c.github.io/mediacapture-fromelement/#dom-htmlmediaelement-capturestream
captureStream(): MediaStream;
}
}
// MAINTENANCE_TODO: Uses raw floats as expectation in external_texture related cases has some diffs.
// Remove this conversion utils and uses raw float data as expectation in external_textrue
// related cases when resolve this.
export function convertToUnorm8(expectation: Readonly<RGBA>): Uint8Array {
const rgba8Unorm = new Uint8ClampedArray(4);
rgba8Unorm[0] = Math.round(expectation.R * 255.0);
rgba8Unorm[1] = Math.round(expectation.G * 255.0);
rgba8Unorm[2] = Math.round(expectation.B * 255.0);
rgba8Unorm[3] = Math.round(expectation.A * 255.0);
return new Uint8Array(rgba8Unorm.buffer);
}
// MAINTENANCE_TODO: Add helper function for BT.601 and BT.709 to remove all magic numbers.
// Expectation values about converting video contents to sRGB color space.
// Source video color space affects expected values.
// The process to calculate these expected pixel values can be found:
// https://github.com/gpuweb/cts/pull/2242#issuecomment-1430382811
// and https://github.com/gpuweb/cts/pull/2242#issuecomment-1463273434
const kBt601PixelValue = {
srgb: {
red: { R: 0.972945567233341, G: 0.141794376683341, B: -0.0209589916711088, A: 1.0 },
green: { R: 0.248234279433399, G: 0.984810378661784, B: -0.0564701319494314, A: 1.0 },
blue: { R: 0.10159735826538, G: 0.135451122863674, B: 1.00262982899724, A: 1.0 },
yellow: { R: 0.995470750775951, G: 0.992742114518355, B: -0.0701036235167653, A: 1.0 },
},
} as const;
const kBt709PixelValue = {
srgb: {
red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 },
green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 },
yellow: { R: 1.0, G: 1.0, B: 0.0, A: 1.0 },
},
} as const;
function makeTable<Table extends { readonly [K: string]: {} }>({
table,
}: {
table: Table;
}): {
readonly [F in keyof Table]: {
readonly [K in keyof Table[F]]: Table[F][K];
};
} {
return Object.fromEntries(
Object.entries(table).map(([k, row]) => [k, { ...row }])
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
) as any;
}
// Video expected pixel value table. Finding expected pixel value
// with video color space and dst color space.
export const kVideoExpectedColors = makeTable({
table: {
bt601: {
'display-p3': {
yellow: srgbToDisplayP3(kBt601PixelValue.srgb.yellow),
red: srgbToDisplayP3(kBt601PixelValue.srgb.red),
blue: srgbToDisplayP3(kBt601PixelValue.srgb.blue),
green: srgbToDisplayP3(kBt601PixelValue.srgb.green),
},
srgb: {
yellow: kBt601PixelValue.srgb.yellow,
red: kBt601PixelValue.srgb.red,
blue: kBt601PixelValue.srgb.blue,
green: kBt601PixelValue.srgb.green,
},
},
bt709: {
'display-p3': {
yellow: srgbToDisplayP3(kBt709PixelValue.srgb.yellow),
red: srgbToDisplayP3(kBt709PixelValue.srgb.red),
blue: srgbToDisplayP3(kBt709PixelValue.srgb.blue),
green: srgbToDisplayP3(kBt709PixelValue.srgb.green),
},
srgb: {
yellow: kBt709PixelValue.srgb.yellow,
red: kBt709PixelValue.srgb.red,
blue: kBt709PixelValue.srgb.blue,
green: kBt709PixelValue.srgb.green,
},
},
},
} as const);
export const kImageExpectedColors = {
srgb: {
red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 },
green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 },
yellow: { R: 1.0, G: 1.0, B: 0.0, A: 1.0 },
},
} as const;
// MAINTENANCE_TODO: Add BT.2020 video in table.
// Video container and codec defines several transform ops to apply to raw decoded frame to display.
// Our test cases covers 'visible rect' and 'rotation'.
// 'visible rect' is associated with the
// video bitstream and should apply to the raw decoded frames before any transformation.
// 'rotation' is associated with the track or presentation and should transform
// the whole visible rect (e.g. 90-degree rotate makes visible rect of vertical video to horizontal)
// The order to apply these transformations is below:
// [raw decoded frame] ----visible rect clipping ---->[visible frame] ---rotation ---> present
// ^ ^
// | |
// coded size display size
// The table holds test videos meta infos, including mimeType to check browser compatibility
// video color space, raw frame content layout and the frame displayed layout.
export const kVideoInfo = makeTable({
table: {
'four-colors-vp8-bt601.webm': {
mimeType: 'video/webm; codecs=vp8',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-h264-bt601.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-vp9-bt601.webm': {
mimeType: 'video/webm; codecs=vp9',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-vp9-bt709.webm': {
mimeType: 'video/webm; codecs=vp9',
colorSpace: 'bt709',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
// video coded content has been rotate
'four-colors-h264-bt601-rotate-90.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'red',
topRightColor: 'green',
bottomLeftColor: 'yellow',
bottomRightColor: 'blue',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-h264-bt601-rotate-180.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'green',
topRightColor: 'blue',
bottomLeftColor: 'red',
bottomRightColor: 'yellow',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-h264-bt601-rotate-270.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'blue',
topRightColor: 'yellow',
bottomLeftColor: 'green',
bottomRightColor: 'red',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-vp9-bt601-rotate-90.mp4': {
mimeType: 'video/mp4; codecs=vp09.00.10.08',
colorSpace: 'bt601',
coded: {
topLeftColor: 'red',
topRightColor: 'green',
bottomLeftColor: 'yellow',
bottomRightColor: 'blue',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-vp9-bt601-rotate-180.mp4': {
mimeType: 'video/mp4; codecs=vp09.00.10.08',
colorSpace: 'bt601',
coded: {
topLeftColor: 'green',
topRightColor: 'blue',
bottomLeftColor: 'red',
bottomRightColor: 'yellow',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-vp9-bt601-rotate-270.mp4': {
mimeType: 'video/mp4; codecs=vp09.00.10.08',
colorSpace: 'bt601',
coded: {
topLeftColor: 'blue',
topRightColor: 'yellow',
bottomLeftColor: 'green',
bottomRightColor: 'red',
},
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
},
'four-colors-h264-bt601-hflip.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'red',
topRightColor: 'yellow',
bottomLeftColor: 'green',
bottomRightColor: 'blue',
},
},
'four-colors-h264-bt601-vflip.mp4': {
mimeType: 'video/mp4; codecs=avc1.4d400c',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'blue',
topRightColor: 'green',
bottomLeftColor: 'yellow',
bottomRightColor: 'red',
},
},
'four-colors-vp9-bt601-hflip.mp4': {
mimeType: 'video/mp4; codecs=vp09.00.10.08',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'red',
topRightColor: 'yellow',
bottomLeftColor: 'green',
bottomRightColor: 'blue',
},
},
'four-colors-vp9-bt601-vflip.mp4': {
mimeType: 'video/mp4; codecs=vp09.00.10.08',
colorSpace: 'bt601',
coded: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
display: {
topLeftColor: 'blue',
topRightColor: 'green',
bottomLeftColor: 'yellow',
bottomRightColor: 'red',
},
},
},
} as const);
type VideoName = keyof typeof kVideoInfo;
export const kVideoNames: readonly VideoName[] = keysOf(kVideoInfo);
export const kPredefinedColorSpace = ['display-p3', 'srgb'] as const;
/**
* 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(ex);
}
})();
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, message: ' + event.message, () => ({
event,
}))
),
true
);
if (video.requestVideoFrameCallback) {
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 script animation reaches a new frame.
* Returns a promise which resolves after `callback` (which may be async) completes.
*/
export function waitForNextTask(callback: () => unknown | Promise<unknown>): Promise<void> {
const { promise, callbackAndResolve } = callbackHelper(callback, 'wait for next task timed out');
timeout(() => {
callbackAndResolve();
}, 0);
return promise;
}
/**
* 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 } = callbackHelper(callback, 'waitForNextFrame timed out');
if ('requestVideoFrameCallback' in video) {
video.requestVideoFrameCallback(() => {
callbackAndResolve();
});
} else {
throw new SkipTestCase('waitForNextFrame currently requires requestVideoFrameCallback');
}
return promise;
}
export async function getVideoFrameFromVideoElement(
test: Fixture,
video: HTMLVideoElement
): Promise<VideoFrame> {
test.skipIf(video.captureStream === undefined, 'HTMLVideoElement.captureStream is not supported');
return raceWithRejectOnTimeout(
new Promise<VideoFrame>(resolve => {
const videoTrack: MediaStreamVideoTrack = video
.captureStream()
.getVideoTracks()[0] as MediaStreamVideoTrack;
const trackProcessor: MediaStreamTrackProcessor<VideoFrame> = new MediaStreamTrackProcessor({
track: videoTrack,
});
const transformer: TransformStream = new TransformStream({
transform(videoFrame, _controller) {
videoTrack.stop();
test.trackForCleanup(videoFrame);
resolve(videoFrame);
},
flush(controller) {
controller.terminate();
},
});
const trackGenerator: MediaStreamTrackGenerator<VideoFrame> = new MediaStreamTrackGenerator({
kind: 'video',
});
trackProcessor.readable
.pipeThrough(transformer)
.pipeTo(trackGenerator.writable)
.catch(() => {});
}),
2000,
'Video never became ready'
);
}
/**
* Create HTMLVideoElement based on VideoName. Check whether video is playable in current
* browser environment.
* Returns a HTMLVideoElement.
*
* @param t: GPUTest that requires getting HTMLVideoElement
* @param videoName: Required video name
*
*/
export function getVideoElement(t: GPUTest, videoName: VideoName): HTMLVideoElement {
if (typeof HTMLVideoElement === 'undefined') {
t.skip('HTMLVideoElement not available');
}
const videoElement = document.createElement('video');
const videoInfo = kVideoInfo[videoName];
if (videoElement.canPlayType(videoInfo.mimeType) === '') {
t.skip('Video codec is not supported');
}
const videoUrl = getResourcePath(videoName);
videoElement.src = videoUrl;
t.trackForCleanup(videoElement);
return videoElement;
}
/**
* 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 callbackHelper(
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! };
}
/**
* Get a MediaStream from the default webcam via `getUserMedia()`.
*/
async function getStreamFromCamera(
test: Fixture,
videoTrackConstraints: MediaTrackConstraints | true
): Promise<MediaStream> {
test.skipIf(typeof navigator === 'undefined', 'navigator does not exist in this environment');
test.skipIf(
typeof navigator.mediaDevices === 'undefined' ||
typeof navigator.mediaDevices.getUserMedia === 'undefined',
"Browser doesn't support capture frame from camera."
);
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: videoTrackConstraints,
});
test.trackForCleanup({
close() {
for (const track of stream.getTracks()) {
track.stop();
}
},
});
return stream;
}
/**
* Chrome on macOS (at least) takes a while before it switches from blank frames
* to real frames. Wait up to 50 frames for something to show up on the camera.
*/
async function waitForNonBlankFrame({
getSource,
waitForNextFrame,
}: {
getSource: () => HTMLVideoElement | VideoFrame;
waitForNextFrame: () => Promise<void>;
}) {
const cvs = document.createElement('canvas');
[cvs.width, cvs.height] = [4, 4];
const ctx = cvs.getContext('2d', { willReadFrequently: true })!;
let foundNonBlankFrame = false;
for (let i = 0; i < 50; ++i) {
ctx.drawImage(getSource(), 0, 0, cvs.width, cvs.height);
const pixels = new Uint32Array(ctx.getImageData(0, 0, cvs.width, cvs.height).data.buffer);
// Look only at RGB, ignore alpha.
if (pixels.some(p => (p & 0x00ffffff) !== 0)) {
foundNonBlankFrame = true;
break;
}
await waitForNextFrame();
}
assert(foundNonBlankFrame, 'Failed to get a non-blank video frame');
}
/**
* Uses MediaStreamTrackProcessor to capture a VideoFrame from the camera.
* Skips the test if not supported.
* @param videoTrackConstraints - MediaTrackConstraints (e.g. width/height) to pass to
* `getUserMedia()`, or `true` if none.
*/
export async function getVideoFrameFromCamera(
test: Fixture,
videoTrackConstraints: MediaTrackConstraints | true
): Promise<VideoFrame> {
test.skipIf(
typeof MediaStreamTrackProcessor === 'undefined',
'MediaStreamTrackProcessor not supported'
);
const stream = await getStreamFromCamera(test, videoTrackConstraints);
const tracks = stream.getVideoTracks();
assert(tracks.length > 0, 'no tracks found');
const track = tracks[0] as MediaStreamVideoTrack;
const trackProcessor = new MediaStreamTrackProcessor({ track });
const reader = trackProcessor.readable.getReader();
const waitForNextFrame = async () => {
const result = await reader.read();
assert(!result.done, "MediaStreamTrackProcessor: Couldn't get valid frame from stream.");
return result.value;
};
let frame: VideoFrame = await waitForNextFrame();
await waitForNonBlankFrame({
getSource: () => frame,
async waitForNextFrame() {
frame.close();
frame = await waitForNextFrame();
},
});
test.trackForCleanup(frame);
return frame;
}
/**
* Create an HTMLVideoElement from the camera stream. Skips the test if not supported.
* @param videoTrackConstraints - MediaTrackConstraints (e.g. width/height) to pass to
* `getUserMedia()`, or `true` if none.
* @param paused - whether the video should be paused before returning.
*/
export async function getVideoElementFromCamera(
test: Fixture,
videoTrackConstraints: MediaTrackConstraints | true,
paused: boolean
): Promise<HTMLVideoElement> {
const stream = await getStreamFromCamera(test, videoTrackConstraints);
// Main thread
const video = document.createElement('video');
video.loop = false;
video.muted = true;
video.setAttribute('playsinline', '');
video.srcObject = stream;
await new Promise(resolve => {
video.onloadedmetadata = resolve;
});
await startPlayingAndWaitForVideo(video, () => {});
await waitForNonBlankFrame({
getSource: () => video,
waitForNextFrame: () =>
new Promise(resolve =>
video.requestVideoFrameCallback(() => {
resolve();
})
),
});
if (paused) {
// Pause the video so we get consistent readbacks.
await new Promise(resolve => {
video.onpause = resolve;
video.pause();
});
}
return video;
}
const kFourColorsInfo = {
display: {
topLeftColor: 'yellow',
topRightColor: 'red',
bottomLeftColor: 'blue',
bottomRightColor: 'green',
},
} as const;
export const kEXIFImageInfo = makeTable({
table: {
'four-colors.jpg': kFourColorsInfo,
'four-colors-rotate-90-cw.jpg': kFourColorsInfo,
'four-colors-rotate-180-cw.jpg': kFourColorsInfo,
'four-colors-rotate-270-cw.jpg': kFourColorsInfo,
},
} as const);
export const kImageInfo = makeTable({
table: {
'four-colors.jpg': kFourColorsInfo,
'four-colors.png': kFourColorsInfo,
'four-colors.bmp': kFourColorsInfo,
'four-colors.webp': kFourColorsInfo,
'four-colors.gif': kFourColorsInfo,
'four-colors.avif': kFourColorsInfo,
'four-colors.ico': kFourColorsInfo,
'four-colors.svg': kFourColorsInfo,
},
} as const);
type ImageName = keyof typeof kImageInfo;
export const kImageNames: readonly ImageName[] = keysOf(kImageInfo);
type EXIFImageName = keyof typeof kEXIFImageInfo;
export const kEXIFImageNames: readonly EXIFImageName[] = keysOf(kEXIFImageInfo);
type ObjectTypeFromFile = (typeof kObjectTypeFromFiles)[number];
export const kObjectTypeFromFiles = [
'ImageBitmap-from-Blob',
'ImageBitmap-from-Image',
'Image',
] as const;
/**
* Load image file(e.g. *.jpg) from ImageBitmap, blob or HTMLImageElement. And
* convert the result to valid source that GPUCopyExternalImageSource supported.
*/
export async function getSourceFromEXIFImageFile(
test: GPUTest,
exifImageName: EXIFImageName,
objectTypeFromFile: ObjectTypeFromFile
): Promise<ImageBitmap | HTMLImageElement> {
const imageUrl = getResourcePath(exifImageName);
switch (objectTypeFromFile) {
case 'ImageBitmap-from-Blob': {
// MAINTENANCE_TODO: resource folder path when using service worker is not correct. Return
// the correct path to load resource in correct place.
// The wrong path: /out/webgpu/webworker/web_platform/copyToTexture/resources
test.skipIf(
globalThis.constructor.name === 'ServiceWorkerGlobalScope',
'Try to load image resource from serivce worker but the path is not correct.'
);
test.skipIf(
typeof createImageBitmap === 'undefined',
'createImageBitmap does not exist in this environment'
);
// Load image file through fetch.
const response = await fetch(imageUrl);
return createImageBitmap(await response.blob());
}
case 'ImageBitmap-from-Image':
case 'Image': {
// Skip test if HTMLImageElement is not available, e.g. in worker.
test.skipIf(
typeof HTMLImageElement === 'undefined',
'Try to use HTMLImage do image file decoding but HTMLImageElement not available.'
);
// Load image file through HTMLImageElement.
const image = new Image();
image.src = imageUrl;
await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout');
if (objectTypeFromFile === 'Image') {
return image;
}
return createImageBitmap(image);
}
}
}
/**
* Create HTMLImageElement and load image file and waits for it to be loaded.
* Returns a promise which resolves after `callback` (which may be async) completes.
*
* @param imageName An valid imageName in kkImageInfo table .
* @param callback Function to call when HTMLImageElement is loaded.
*
*/
export function loadImageFileAndRun(
test: GPUTest,
imageName: ImageName,
callback: (image: HTMLImageElement) => unknown | Promise<unknown>
): Promise<void> {
return raceWithRejectOnTimeout(
new Promise((resolve, reject) => {
const callbackAndResolve = (image: HTMLImageElement) =>
void (async () => {
try {
await callback(image);
resolve();
} catch (ex) {
reject(ex);
}
})();
// Skip test if HTMLImageElement is not available, e.g. in worker.
test.skipIf(
typeof HTMLImageElement === 'undefined',
'Try to use HTMLImage do image file decoding but HTMLImageElement not available.'
);
const image = new Image();
image.src = getResourcePath(imageName);
image.onload = () => {
callbackAndResolve(image);
};
}),
2000,
'Video never became ready'
);
}