videoFrame-copyTo-rgb.any.js (7928B)
1 // META: global=window,dedicatedworker 2 // META: script=/webcodecs/videoFrame-utils.js 3 // META: script=/webcodecs/video-encoder-utils.js 4 5 const smpte170m = { 6 matrix: 'smpte170m', 7 primaries: 'smpte170m', 8 transfer: 'smpte170m', 9 fullRange: false 10 }; 11 const bt709 = { 12 matrix: 'bt709', 13 primaries: 'bt709', 14 transfer: 'bt709', 15 fullRange: false 16 }; 17 18 function compareColors(actual, expected, tolerance, msg) { 19 let channel = ['R', 'G', 'B', 'A']; 20 for (let i = 0; i < 4; i++) { 21 assert_approx_equals( 22 actual[i], expected[i], tolerance, 23 `${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`); 24 } 25 } 26 27 function rgb2yuv(r, g, b) { 28 let y = r * .299000 + g * .587000 + b * .114000 29 let u = r * -.168736 + g * -.331264 + b * .500000 + 128 30 let v = r * .500000 + g * -.418688 + b * -.081312 + 128 31 32 y = Math.round(y); 33 u = Math.round(u); 34 v = Math.round(v); 35 return { 36 y, u, v 37 } 38 } 39 40 function makeI420Frames(colorSpace) { 41 const kYellow = {r: 0xFF, g: 0xFF, b: 0x00}; 42 const kRed = {r: 0xFF, g: 0x00, b: 0x00}; 43 const kBlue = {r: 0x00, g: 0x00, b: 0xFF}; 44 const kGreen = {r: 0x00, g: 0xFF, b: 0x00}; 45 const kPink = {r: 0xFF, g: 0x78, b: 0xFF}; 46 const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF}; 47 const kBlack = {r: 0x00, g: 0x00, b: 0x00}; 48 const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF}; 49 50 const result = []; 51 const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4}; 52 const colors = 53 [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink]; 54 const data = new Uint8Array(24); 55 init.colorSpace = colorSpace; 56 for (let color of colors) { 57 color = rgb2yuv(color.r, color.g, color.b); 58 data.fill(color.y, 0, 16); 59 data.fill(color.u, 16, 20); 60 data.fill(color.v, 20, 24); 61 result.push(new VideoFrame(data, init)); 62 } 63 return result; 64 } 65 66 function makeRGBXFrames(colorSpace) { 67 const kYellow = 0xFFFF00; 68 const kRed = 0xFF0000; 69 const kBlue = 0x0000FF; 70 const kGreen = 0x00FF00; 71 const kBlack = 0x000000; 72 const kWhite = 0xFFFFFF; 73 74 const result = []; 75 const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; 76 const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite]; 77 const data = new Uint32Array(16); 78 init.colorSpace = colorSpace; 79 for (let color of colors) { 80 data.fill(color, 0, 16); 81 result.push(new VideoFrame(data, init)); 82 } 83 return result; 84 } 85 86 async function testFrame(frame, colorSpace, pixelFormat) { 87 const width = frame.visibleRect.width; 88 const height = frame.visibleRect.height; 89 let frame_message = 'Frame: ' + JSON.stringify({ 90 format: frame.format, 91 width: width, 92 height: height, 93 matrix: frame.colorSpace?.matrix, 94 primaries: frame.colorSpace?.primaries, 95 transfer: frame.colorSpace?.transfer, 96 }); 97 const cnv = new OffscreenCanvas(width, height); 98 const ctx = 99 cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true}); 100 101 // Read VideoFrame pixels via copyTo() 102 let imageData = ctx.createImageData(width, height); 103 let copy_to_buf = imageData.data.buffer; 104 let layout = null; 105 try { 106 const options = { 107 rect: {x: 0, y: 0, width: width, height: height}, 108 format: pixelFormat, 109 colorSpace: colorSpace 110 }; 111 assert_equals(frame.allocationSize(options), copy_to_buf.byteLength); 112 layout = await frame.copyTo(copy_to_buf, options); 113 } catch (e) { 114 assert_unreached(`copyTo() failure: ${e}`); 115 return; 116 } 117 if (layout.length != 1) { 118 assert_unreached('Conversion to RGB is not supported by the browser'); 119 return; 120 } 121 122 // Read VideoFrame pixels via drawImage() 123 ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height); 124 imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace}); 125 let get_image_buf = imageData.data.buffer; 126 127 // Compare! 128 const tolerance = 1; 129 for (let i = 0; i < copy_to_buf.byteLength; i += 4) { 130 if (pixelFormat.startsWith('BGR')) { 131 // getImageData() always gives us RGB, we need to swap bytes before 132 // comparing them with BGR. 133 new Uint8Array(get_image_buf, i, 3).reverse(); 134 } 135 compareColors( 136 new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4), 137 tolerance, frame_message + ` Mismatch at offset ${i}`); 138 } 139 } 140 141 function test_4x4_I420_frames() { 142 for (let colorSpace of ['srgb', 'display-p3']) { 143 for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { 144 for (let frameColorSpace of [null, smpte170m, bt709]) { 145 const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null"; 146 promise_test(async t => { 147 for (let frame of makeI420Frames(frameColorSpace)) { 148 await testFrame(frame, colorSpace, pixelFormat); 149 frame.close(); 150 } 151 }, `Convert 4x4 ${frameColorSpaceName} I420 frames to ${pixelFormat} / ${colorSpace}`); 152 } 153 } 154 } 155 } 156 test_4x4_I420_frames(); 157 158 function test_4x4_RGB_frames() { 159 for (let colorSpace of ['srgb', 'display-p3']) { 160 for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { 161 for (let frameColorSpace of [null, smpte170m, bt709]) { 162 const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null"; 163 promise_test(async t => { 164 for (let frame of makeRGBXFrames(frameColorSpace)) { 165 await testFrame(frame, colorSpace, pixelFormat); 166 frame.close(); 167 } 168 }, `Convert 4x4 ${frameColorSpaceName} RGBX frames to ${pixelFormat} / ${colorSpace}`); 169 } 170 } 171 } 172 } 173 test_4x4_RGB_frames(); 174 175 176 function test_4color_canvas_frames() { 177 for (let colorSpace of ['srgb', 'display-p3']) { 178 for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { 179 promise_test(async t => { 180 const frame = createFrame(32, 16); 181 await testFrame(frame, colorSpace, pixelFormat); 182 frame.close(); 183 }, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`); 184 } 185 } 186 } 187 test_4color_canvas_frames(); 188 189 promise_test(async t => { 190 let pixelFormat = 'RGBA' 191 const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4}; 192 const src_data = new Uint32Array(init.codedWidth * init.codedHeight); 193 src_data.fill(0xFFFFFFFF); 194 const offset = 5; 195 const stride = 40; 196 const dst_data = new Uint8Array(offset + stride * init.codedHeight); 197 const options = { 198 format: pixelFormat, 199 layout: [ 200 {offset: offset, stride: stride}, 201 ] 202 }; 203 const frame = new VideoFrame(src_data, init); 204 await frame.copyTo(dst_data, options) 205 assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset'); 206 for (let row = 0; row < init.codedHeight; ++row) { 207 let width = init.codedWidth * 4; 208 const row_data = 209 dst_data.slice(offset + stride * row, offset + stride * row + width); 210 const margin_data = dst_data.slice( 211 offset + stride * row + width, offset + stride * (row + 1)); 212 213 assert_false( 214 row_data.some(e => e != 0xFF), 215 `unexpected data in row ${row} [${row_data}]`); 216 assert_false( 217 margin_data.some(e => e != 0), 218 `unexpected margin in row ${row} [${margin_data}]`); 219 } 220 221 frame.close(); 222 }, `copyTo() with layout`); 223 224 function test_unsupported_pixel_formats() { 225 const kUnsupportedFormats = [ 226 'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A', 227 'NV12' 228 ]; 229 230 for (let pixelFormat of kUnsupportedFormats) { 231 promise_test(async t => { 232 const init = 233 {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; 234 const data = new Uint32Array(16); 235 const options = {format: pixelFormat}; 236 const frame = new VideoFrame(data, init); 237 assert_throws_dom( 238 'NotSupportedError', () => frame.allocationSize(options)); 239 await promise_rejects_dom( 240 t, 'NotSupportedError', frame.copyTo(data, options)) 241 frame.close(); 242 }, `Unsupported format ${pixelFormat}`); 243 } 244 } 245 test_unsupported_pixel_formats();