function make_audio_data(timestamp, channels, sampleRate, frames) { let data = new Float32Array(frames*channels); // This generates samples in a planar format. for (var channel = 0; channel < channels; channel++) { let hz = 100 + channel * 50; // sound frequency let base_index = channel * frames; for (var i = 0; i < frames; i++) { let t = (i / sampleRate) * hz * (Math.PI * 2); data[base_index + i] = Math.sin(t); } } return new AudioData({ timestamp: timestamp, data: data, numberOfChannels: channels, numberOfFrames: frames, sampleRate: sampleRate, format: "f32-planar", }); } function makeOffscreenCanvas(width, height, options) { let canvas = new OffscreenCanvas(width, height); let ctx = canvas.getContext('2d', options); ctx.fillStyle = 'rgba(50, 100, 150, 255)'; ctx.fillRect(0, 0, width, height); return canvas; } function makeImageBitmap(width, height) { return makeOffscreenCanvas(width, height).transferToImageBitmap(); } // Gives a chance to pending output and error callbacks to complete before // resolving. function endAfterEventLoopTurn() { return new Promise(resolve => step_timeout(resolve, 0)); } // Returns a codec initialization with callbacks that expected to not be called. function getDefaultCodecInit(test) { return { output: test.unreached_func("unexpected output"), error: test.unreached_func("unexpected error"), } } // Checks that codec can be configured, reset, reconfigured, and that incomplete // or invalid configs throw errors immediately. function testConfigurations(codec, validCondig, invalidCodecs) { assert_equals(codec.state, "unconfigured"); const requiredConfigPairs = validCondig; let incrementalConfig = {}; for (let key in requiredConfigPairs) { // Configure should fail while required keys are missing. assert_throws_js(TypeError, () => { codec.configure(incrementalConfig); }); incrementalConfig[key] = requiredConfigPairs[key]; assert_equals(codec.state, "unconfigured"); } // Configure should pass once incrementalConfig meets all requirements. codec.configure(incrementalConfig); assert_equals(codec.state, "configured"); // We should be able to reconfigure the codec. codec.configure(incrementalConfig); assert_equals(codec.state, "configured"); let config = incrementalConfig; invalidCodecs.forEach(badCodec => { // Invalid codecs should fail. config.codec = badCodec; assert_throws_js(TypeError, () => { codec.configure(config); }, badCodec); }) // The failed configures should not affect the current config. assert_equals(codec.state, "configured"); // Test we can configure after a reset. codec.reset() assert_equals(codec.state, "unconfigured"); codec.configure(validCondig); assert_equals(codec.state, "configured"); } // Performs an encode or decode with the provided input, depending on whether // the passed codec is an encoder or a decoder. function encodeOrDecodeShouldThrow(codec, input) { // We are testing encode/decode on codecs in invalid states. assert_not_equals(codec.state, "configured"); if (codec.decode) { assert_throws_dom("InvalidStateError", () => codec.decode(input), "decode"); } else if (codec.encode) { // Encoders consume frames, so clone it to be safe. assert_throws_dom("InvalidStateError", () => codec.encode(input.clone()), "encode"); } else { assert_unreached("Codec should have encode or decode function"); } } // Makes sure that we cannot close, configure, reset, flush, decode or encode a // closed codec. function testClosedCodec(test, codec, validconfig, codecInput) { assert_equals(codec.state, "unconfigured"); codec.close(); assert_equals(codec.state, "closed"); assert_throws_dom("InvalidStateError", () => codec.configure(validconfig), "configure"); assert_throws_dom("InvalidStateError", () => codec.reset(), "reset"); assert_throws_dom("InvalidStateError", () => codec.close(), "close"); encodeOrDecodeShouldThrow(codec, codecInput); return promise_rejects_dom(test, 'InvalidStateError', codec.flush(), 'flush'); } // Makes sure we cannot flush, encode or decode with an unconfigured coded, and // that reset is a valid no-op. function testUnconfiguredCodec(test, codec, codecInput) { assert_equals(codec.state, "unconfigured"); // Configure() and Close() are valid operations that would transition us into // a different state. // Resetting an unconfigured encoder is a no-op. codec.reset(); assert_equals(codec.state, "unconfigured"); encodeOrDecodeShouldThrow(codec, codecInput); return promise_rejects_dom(test, 'InvalidStateError', codec.flush(), 'flush'); } // Reference values generated by: // https://fiddle.skia.org/c/f100d4d5f085a9e09896aabcbc463868 const kSRGBPixel = [50, 100, 150, 255]; const kP3Pixel = [62, 99, 146, 255]; const kRec2020Pixel = [87, 106, 151, 255]; const kCanvasOptionsP3Uint8 = { colorSpace: 'display-p3', pixelFormat: 'uint8' }; const kImageSettingOptionsP3Uint8 = { colorSpace: 'display-p3', storageFormat: 'uint8' }; const kCanvasOptionsRec2020Uint8 = { colorSpace: 'rec2020', pixelFormat: 'uint8' }; const kImageSettingOptionsRec2020Uint8 = { colorSpace: 'rec2020', storageFormat: 'uint8' }; function testCanvas(ctx, width, height, expected_pixel, imageSetting, assert_compares) { // The dup getImageData is to workaournd crbug.com/1100233 let imageData = ctx.getImageData(0, 0, width, height, imageSetting); let colorData = ctx.getImageData(0, 0, width, height, imageSetting).data; const kMaxPixelToCheck = 128 * 96; let step = width * height / kMaxPixelToCheck; step = Math.round(step); step = (step < 1) ? 1 : step; for (let i = 0; i < 4 * width * height; i += (4 * step)) { assert_compares(colorData[i], expected_pixel[0]); assert_compares(colorData[i + 1], expected_pixel[1]); assert_compares(colorData[i + 2], expected_pixel[2]); assert_compares(colorData[i + 3], expected_pixel[3]); } } function makeDetachedArrayBuffer() { const buffer = new ArrayBuffer(10); const view = new Uint8Array(buffer); new MessageChannel().port1.postMessage(buffer, [buffer]); return view; } function isFrameClosed(frame) { return frame.format == null && frame.codedWidth == 0 && frame.codedHeight == 0 && frame.displayWidth == 0 && frame.displayHeight == 0 && frame.codedRect == null && frame.visibleRect == null; } function testImageBitmapToAndFromVideoFrame( width, height, expectedPixel, canvasOptions, imageBitmapOptions, imageSetting) { let canvas = new OffscreenCanvas(width, height); let ctx = canvas.getContext('2d', canvasOptions); ctx.fillStyle = 'rgb(50, 100, 150)'; ctx.fillRect(0, 0, width, height); testCanvas(ctx, width, height, expectedPixel, imageSetting, assert_equals); return createImageBitmap(canvas, imageBitmapOptions) .then((fromImageBitmap) => { let videoFrame = new VideoFrame(fromImageBitmap, {timestamp: 0}); return createImageBitmap(videoFrame, imageBitmapOptions); }) .then((toImageBitmap) => { let myCanvas = new OffscreenCanvas(width, height); let myCtx = myCanvas.getContext('2d', canvasOptions); myCtx.drawImage(toImageBitmap, 0, 0); let tolerance = 2; testCanvas( myCtx, width, height, expectedPixel, imageSetting, (actual, expected) => { assert_approx_equals(actual, expected, tolerance); }); }); }