video-encoder-rescaling.https.any.js (8996B)
1 // META: global=window,dedicatedworker 2 // META: variant=?av1 3 // META: variant=?vp8 4 // META: variant=?vp9_p0 5 // META: variant=?h264_avc 6 // META: variant=?h264_annexb 7 8 let BASECONFIG = null; 9 promise_setup(async () => { 10 const config = { 11 '?av1': { codec: 'av01.0.04M.08' }, 12 '?vp8': { codec: 'vp8' }, 13 '?vp9_p0': { codec: 'vp09.00.10.08' }, 14 '?h264_avc': { codec: 'avc1.42001E', avc: { format: 'avc' } }, 15 '?h264_annexb': { codec: 'avc1.42001E', avc: { format: 'annexb' } }, 16 }[location.search]; 17 BASECONFIG = config; 18 BASECONFIG.framerate = 30; 19 BASECONFIG.bitrate = 3000000; 20 }); 21 22 function scaleFrame(oneFrame, scaleSize) { 23 const { w: width, h: height } = scaleSize; 24 return new Promise(async (resolve, reject) => { 25 let encodedResult; 26 const encoder = new VideoEncoder({ 27 output: (chunk, metadata) => { 28 encodedResult = { chunk, metadata }; 29 }, 30 error: (error) => { 31 reject(error); 32 }, 33 }); 34 35 const encoderConfig = { 36 ...BASECONFIG, 37 width, 38 height, 39 }; 40 encoder.configure(encoderConfig); 41 42 encoder.encode(oneFrame); 43 await encoder.flush(); 44 45 let decodedResult; 46 const decoder = new VideoDecoder({ 47 output(frame) { 48 decodedResult = frame; 49 }, 50 error: (error) => { 51 reject(error); 52 }, 53 }); 54 55 decoder.configure(encodedResult.metadata.decoderConfig); 56 decoder.decode(encodedResult.chunk); 57 await decoder.flush(); 58 59 encoder.close(); 60 decoder.close(); 61 62 resolve(decodedResult); 63 }); 64 } 65 66 // This function determines which quadrant of a rectangle (width * height) 67 // a point (x, y) falls into, and returns the corresponding color for that 68 // quadrant. The rectangle is divided into four quadrants: 69 // < w > 70 // ^ +--------+--------+ 71 // | (0, 0) | (1, 0) | 72 // h +--------+--------+ 73 // | (0, 1) | (1, 1) | 74 // v +--------+--------+ 75 // 76 // The colors array must contain at least four colors, each corresponding 77 // to one of the quadrants: 78 // - colors[0] : top-left (0, 0) 79 // - colors[1] : top-right (1, 0) 80 // - colors[2] : bottom-left (0, 1) 81 // - colors[3] : bottom-right (1, 1) 82 function getColor(x, y, width, height, colors, channel) { 83 // Determine which quadrant (x, y) belongs to. 84 const xIndex = x * 2 >= width ? 1 : 0; 85 const yIndex = y * 2 >= height ? 1 : 0; 86 87 const index = yIndex * 2 + xIndex; 88 return colors[index][channel]; 89 } 90 91 92 // All channel paramaters are arrays with the index being the channel 93 // channelOffset: The offset for each channel in allocated data array. 94 // channelWidth: The width of ecah channel in pixels 95 // channelPlaneWidths: the width of the channel used to calculate the image's memory size. 96 // For interleaved data, only the first width is set to the width of the full data in bytes; see RGBX for an example. 97 // channelStrides: The stride (in bytes) for each channel. 98 // channelSteps: The step size in bytes to move from one pixel to the next horizontally within the same row 99 // channelHeights: The height (in bytes) for each channel. 100 // channelFourColor: The four colors encoded in the color format of the channels 101 // 102 function createImageData({ channelOffsets, channelWidths, channelPlaneWidths, channelStrides, channelSteps, channelHeights, channelFourColors }) { 103 let memSize = 0; 104 for (let chan = 0; chan < 3; chan++) { 105 memSize += channelHeights[chan] * channelPlaneWidths[chan]; 106 } 107 let data = new Uint8Array(memSize); 108 for (let chan = 0; chan < 3; chan++) { 109 for (let y = 0; y < channelHeights[chan]; y++) { 110 for (let x = 0; x < channelWidths[chan]; x++) { 111 data[channelOffsets[chan] + Math.floor(channelStrides[chan] * y) + Math.floor(channelSteps[chan] * x)] = 112 getColor(x, y, channelWidths[chan], channelHeights[chan], channelFourColors, chan); 113 } 114 } 115 } 116 return data; 117 } 118 119 function testImageData(data, { channelOffsets, channelWidths, channelStrides, channelSteps, channelHeights, channelFourColors }) { 120 let err = 0.; 121 for (let chan = 0; chan < 3; chan++) { 122 for (let y = 0; y < channelHeights[chan]; y++) { 123 for (let x = 0; x < channelWidths[chan]; x++) { 124 const curdata = data[channelOffsets[chan] + Math.floor(channelStrides[chan] * y) + Math.floor(channelSteps[chan] * x)]; 125 const diff = curdata - getColor(x, y, channelWidths[chan], channelHeights[chan], channelFourColors, chan); 126 err += Math.abs(diff); 127 } 128 } 129 } 130 return err / data.length / 3 / 255 * 4; 131 } 132 133 function rgb2yuv(rgb) { 134 let y = rgb[0] * .299000 + rgb[1] * .587000 + rgb[2] * .114000 135 let u = rgb[0] * -.168736 + rgb[1] * -.331264 + rgb[2] * .500000 + 128 136 let v = rgb[0] * .500000 + rgb[1] * -.418688 + rgb[2] * -.081312 + 128 137 138 y = Math.floor(y); 139 u = Math.floor(u); 140 v = Math.floor(v); 141 return [ 142 y, u, v 143 ] 144 } 145 146 function createChannelParameters(channelParams, x, y) { 147 return { 148 channelOffsets: channelParams.channelOffsetsConstant.map( 149 (cont, index) => cont + channelParams.channelOffsetsSize[index] * 150 x * y), 151 channelWidths: channelParams.channelWidths.map((width) => Math.floor(width * x)), 152 channelPlaneWidths: channelParams.channelPlaneWidths.map((width) => Math.floor(width * x)), 153 channelStrides: channelParams.channelStrides.map((width) => Math.floor(width * x)), 154 channelSteps: channelParams.channelSteps.map((height) => height), 155 channelHeights: channelParams.channelHeights.map((height) => Math.floor(height * y)), 156 channelFourColors: channelParams.channelFourColors 157 } 158 } 159 160 161 const scaleTests = [ 162 { from: { w: 64, h: 64 }, to: { w: 128, h: 128 } }, // Factor 2 163 { from: { w: 128, h: 128 }, to: { w: 128, h: 128 } }, // Factor 1 164 { from: { w: 128, h: 128 }, to: { w: 64, h: 64 } }, // Factor 0.5 165 { from: { w: 32, h: 32 }, to: { w: 96, h: 96 } }, // Factor 3 166 { from: { w: 192, h: 192 }, to: { w: 64, h: 64 } }, // Factor 1/3 167 { from: { w: 64, h: 32 }, to: { w: 128, h: 64 } }, // Factor 2 168 { from: { w: 128, h: 256 }, to: { w: 64, h: 128 } }, // Factor 0.5 169 { from: { w: 64, h: 64 }, to: { w: 128, h: 192 } }, // Factor 2 (w) and 3 (h) 170 { from: { w: 128, h: 192 }, to: { w: 64, h: 64 } }, // Factor 0.5 (w) and 1/3 (h) 171 ] 172 const fourColors = [[255, 255, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255]]; 173 const pixelFormatChannelParameters = [ 174 { // RGBX 175 channelOffsetsConstant: [0, 1, 2], 176 channelOffsetsSize: [0, 0, 0], 177 channelPlaneWidths: [4, 0, 0], // only used for allocation 178 channelWidths: [1, 1, 1], 179 channelStrides: [4, 4, 4], // scaled by width 180 channelSteps: [4, 4, 4], 181 channelHeights: [1, 1, 1], // scaled by height 182 channelFourColors: fourColors.map((col) => col), // just clone, 183 format: 'RGBX' 184 }, 185 { // I420 186 channelOffsetsConstant: [0, 0, 0], 187 channelOffsetsSize: [0, 1, 1.25], 188 channelPlaneWidths: [1, 0.5, 0.5], 189 channelWidths: [1, 0.5, 0.5], 190 channelStrides: [1, 0.5, 0.5], // scaled by width 191 channelSteps: [1, 1, 1], 192 channelHeights: [1, 0.5, 0.5], // scaled by height 193 channelFourColors: fourColors.map((col) => rgb2yuv(col)), // just clone 194 format: 'I420' 195 } 196 ] 197 198 for (const scale of scaleTests) { 199 for (const channelParams of pixelFormatChannelParameters) { 200 promise_test(async t => { 201 const inputChannelParameters = createChannelParameters(channelParams, scale.from.w, scale.from.h); 202 const inputData = createImageData(inputChannelParameters); 203 const inputFrame = new VideoFrame(inputData, { 204 timestamp: 0, 205 displayWidth: scale.from.w, 206 displayHeight: scale.from.h, 207 codedWidth: scale.from.w, 208 codedHeight: scale.from.h, 209 format: channelParams.format 210 }); 211 const outputFrame = await scaleFrame(inputFrame, scale.to); 212 const outputArrayBuffer = new Uint8Array(outputFrame.allocationSize({ format: 'RGBX' })); 213 const layout = await outputFrame.copyTo(outputArrayBuffer, { format: 'RGBX' }); 214 const stride = layout[0].stride 215 const offset = layout[0].offset 216 217 const error = testImageData(outputArrayBuffer, { 218 channelOffsets: [offset, offset + 1, offset + 2], 219 channelWidths: [ 220 outputFrame.visibleRect.width, outputFrame.visibleRect.width, 221 outputFrame.visibleRect.width 222 ], 223 channelStrides: [stride, stride, stride], 224 channelSteps: [4, 4, 4], 225 channelHeights: [ 226 outputFrame.visibleRect.height, outputFrame.visibleRect.height, 227 outputFrame.visibleRect.height 228 ], 229 channelFourColors: fourColors.map((col) => col) 230 }); 231 outputFrame.close(); 232 assert_approx_equals(error, 0, 0.05, 'Scaled Image differs too much! Scaling from ' 233 + scale.from.w + ' x ' + scale.from.h 234 + ' to ' 235 + scale.to.w + ' x ' + scale.to.h 236 + ' Format:' + 237 channelParams.format 238 ); 239 }, 'Scaling Image in Encoding from ' 240 + scale.from.w + ' x ' + scale.from.h 241 + ' to ' 242 + scale.to.w + ' x ' + scale.to.h 243 + ' Format: ' + 244 channelParams.format); 245 } 246 }