// META: global=window,dedicatedworker // META: variant=?av1 // META: variant=?vp8 // META: variant=?vp9_p0 // META: variant=?h264_avc // META: variant=?h264_annexb let BASECONFIG = null; promise_setup(async () => { const config = { '?av1': { codec: 'av01.0.04M.08' }, '?vp8': { codec: 'vp8' }, '?vp9_p0': { codec: 'vp09.00.10.08' }, '?h264_avc': { codec: 'avc1.42001E', avc: { format: 'avc' } }, '?h264_annexb': { codec: 'avc1.42001E', avc: { format: 'annexb' } }, }[location.search]; BASECONFIG = config; BASECONFIG.framerate = 30; BASECONFIG.bitrate = 3000000; }); function scaleFrame(oneFrame, scaleSize) { const { w: width, h: height } = scaleSize; return new Promise(async (resolve, reject) => { let encodedResult; const encoder = new VideoEncoder({ output: (chunk, metadata) => { encodedResult = { chunk, metadata }; }, error: (error) => { reject(error); }, }); const encoderConfig = { ...BASECONFIG, width, height, }; encoder.configure(encoderConfig); encoder.encode(oneFrame); await encoder.flush(); let decodedResult; const decoder = new VideoDecoder({ output(frame) { decodedResult = frame; }, error: (error) => { reject(error); }, }); decoder.configure(encodedResult.metadata.decoderConfig); decoder.decode(encodedResult.chunk); await decoder.flush(); encoder.close(); decoder.close(); resolve(decodedResult); }); } // This function determines which quadrant of a rectangle (width * height) // a point (x, y) falls into, and returns the corresponding color for that // quadrant. The rectangle is divided into four quadrants: // < w > // ^ +--------+--------+ // | (0, 0) | (1, 0) | // h +--------+--------+ // | (0, 1) | (1, 1) | // v +--------+--------+ // // The colors array must contain at least four colors, each corresponding // to one of the quadrants: // - colors[0] : top-left (0, 0) // - colors[1] : top-right (1, 0) // - colors[2] : bottom-left (0, 1) // - colors[3] : bottom-right (1, 1) function getColor(x, y, width, height, colors, channel) { // Determine which quadrant (x, y) belongs to. const xIndex = x * 2 >= width ? 1 : 0; const yIndex = y * 2 >= height ? 1 : 0; const index = yIndex * 2 + xIndex; return colors[index][channel]; } // All channel paramaters are arrays with the index being the channel // channelOffset: The offset for each channel in allocated data array. // channelWidth: The width of ecah channel in pixels // channelPlaneWidths: the width of the channel used to calculate the image's memory size. // For interleaved data, only the first width is set to the width of the full data in bytes; see RGBX for an example. // channelStrides: The stride (in bytes) for each channel. // channelSteps: The step size in bytes to move from one pixel to the next horizontally within the same row // channelHeights: The height (in bytes) for each channel. // channelFourColor: The four colors encoded in the color format of the channels // function createImageData({ channelOffsets, channelWidths, channelPlaneWidths, channelStrides, channelSteps, channelHeights, channelFourColors }) { let memSize = 0; for (let chan = 0; chan < 3; chan++) { memSize += channelHeights[chan] * channelPlaneWidths[chan]; } let data = new Uint8Array(memSize); for (let chan = 0; chan < 3; chan++) { for (let y = 0; y < channelHeights[chan]; y++) { for (let x = 0; x < channelWidths[chan]; x++) { data[channelOffsets[chan] + Math.floor(channelStrides[chan] * y) + Math.floor(channelSteps[chan] * x)] = getColor(x, y, channelWidths[chan], channelHeights[chan], channelFourColors, chan); } } } return data; } function testImageData(data, { channelOffsets, channelWidths, channelStrides, channelSteps, channelHeights, channelFourColors }) { let err = 0.; for (let chan = 0; chan < 3; chan++) { for (let y = 0; y < channelHeights[chan]; y++) { for (let x = 0; x < channelWidths[chan]; x++) { const curdata = data[channelOffsets[chan] + Math.floor(channelStrides[chan] * y) + Math.floor(channelSteps[chan] * x)]; const diff = curdata - getColor(x, y, channelWidths[chan], channelHeights[chan], channelFourColors, chan); err += Math.abs(diff); } } } return err / data.length / 3 / 255 * 4; } function rgb2yuv(rgb) { let y = rgb[0] * .299000 + rgb[1] * .587000 + rgb[2] * .114000 let u = rgb[0] * -.168736 + rgb[1] * -.331264 + rgb[2] * .500000 + 128 let v = rgb[0] * .500000 + rgb[1] * -.418688 + rgb[2] * -.081312 + 128 y = Math.floor(y); u = Math.floor(u); v = Math.floor(v); return [ y, u, v ] } function createChannelParameters(channelParams, x, y) { return { channelOffsets: channelParams.channelOffsetsConstant.map( (cont, index) => cont + channelParams.channelOffsetsSize[index] * x * y), channelWidths: channelParams.channelWidths.map((width) => Math.floor(width * x)), channelPlaneWidths: channelParams.channelPlaneWidths.map((width) => Math.floor(width * x)), channelStrides: channelParams.channelStrides.map((width) => Math.floor(width * x)), channelSteps: channelParams.channelSteps.map((height) => height), channelHeights: channelParams.channelHeights.map((height) => Math.floor(height * y)), channelFourColors: channelParams.channelFourColors } } const scaleTests = [ { from: { w: 64, h: 64 }, to: { w: 128, h: 128 } }, // Factor 2 { from: { w: 128, h: 128 }, to: { w: 128, h: 128 } }, // Factor 1 { from: { w: 128, h: 128 }, to: { w: 64, h: 64 } }, // Factor 0.5 { from: { w: 32, h: 32 }, to: { w: 96, h: 96 } }, // Factor 3 { from: { w: 192, h: 192 }, to: { w: 64, h: 64 } }, // Factor 1/3 { from: { w: 64, h: 32 }, to: { w: 128, h: 64 } }, // Factor 2 { from: { w: 128, h: 256 }, to: { w: 64, h: 128 } }, // Factor 0.5 { from: { w: 64, h: 64 }, to: { w: 128, h: 192 } }, // Factor 2 (w) and 3 (h) { from: { w: 128, h: 192 }, to: { w: 64, h: 64 } }, // Factor 0.5 (w) and 1/3 (h) ] const fourColors = [[255, 255, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255]]; const pixelFormatChannelParameters = [ { // RGBX channelOffsetsConstant: [0, 1, 2], channelOffsetsSize: [0, 0, 0], channelPlaneWidths: [4, 0, 0], // only used for allocation channelWidths: [1, 1, 1], channelStrides: [4, 4, 4], // scaled by width channelSteps: [4, 4, 4], channelHeights: [1, 1, 1], // scaled by height channelFourColors: fourColors.map((col) => col), // just clone, format: 'RGBX' }, { // I420 channelOffsetsConstant: [0, 0, 0], channelOffsetsSize: [0, 1, 1.25], channelPlaneWidths: [1, 0.5, 0.5], channelWidths: [1, 0.5, 0.5], channelStrides: [1, 0.5, 0.5], // scaled by width channelSteps: [1, 1, 1], channelHeights: [1, 0.5, 0.5], // scaled by height channelFourColors: fourColors.map((col) => rgb2yuv(col)), // just clone format: 'I420' } ] for (const scale of scaleTests) { for (const channelParams of pixelFormatChannelParameters) { promise_test(async t => { const inputChannelParameters = createChannelParameters(channelParams, scale.from.w, scale.from.h); const inputData = createImageData(inputChannelParameters); const inputFrame = new VideoFrame(inputData, { timestamp: 0, displayWidth: scale.from.w, displayHeight: scale.from.h, codedWidth: scale.from.w, codedHeight: scale.from.h, format: channelParams.format }); const outputFrame = await scaleFrame(inputFrame, scale.to); const outputArrayBuffer = new Uint8Array(outputFrame.allocationSize({ format: 'RGBX' })); const layout = await outputFrame.copyTo(outputArrayBuffer, { format: 'RGBX' }); const stride = layout[0].stride const offset = layout[0].offset const error = testImageData(outputArrayBuffer, { channelOffsets: [offset, offset + 1, offset + 2], channelWidths: [ outputFrame.visibleRect.width, outputFrame.visibleRect.width, outputFrame.visibleRect.width ], channelStrides: [stride, stride, stride], channelSteps: [4, 4, 4], channelHeights: [ outputFrame.visibleRect.height, outputFrame.visibleRect.height, outputFrame.visibleRect.height ], channelFourColors: fourColors.map((col) => col) }); outputFrame.close(); assert_approx_equals(error, 0, 0.05, 'Scaled Image differs too much! Scaling from ' + scale.from.w + ' x ' + scale.from.h + ' to ' + scale.to.w + ' x ' + scale.to.h + ' Format:' + channelParams.format ); }, 'Scaling Image in Encoding from ' + scale.from.w + ' x ' + scale.from.h + ' to ' + scale.to.w + ' x ' + scale.to.h + ' Format: ' + channelParams.format); } }