245 lines
7.7 KiB
JavaScript
245 lines
7.7 KiB
JavaScript
// META: global=window,dedicatedworker
|
|
// META: script=/webcodecs/videoFrame-utils.js
|
|
// META: script=/webcodecs/video-encoder-utils.js
|
|
|
|
const smpte170m = {
|
|
matrix: 'smpte170m',
|
|
primaries: 'smpte170m',
|
|
transfer: 'smpte170m',
|
|
fullRange: false
|
|
};
|
|
const bt709 = {
|
|
matrix: 'bt709',
|
|
primaries: 'bt709',
|
|
transfer: 'bt709',
|
|
fullRange: false
|
|
};
|
|
|
|
function compareColors(actual, expected, tolerance, msg) {
|
|
let channel = ['R', 'G', 'B', 'A'];
|
|
for (let i = 0; i < 4; i++) {
|
|
assert_approx_equals(
|
|
actual[i], expected[i], tolerance,
|
|
`${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`);
|
|
}
|
|
}
|
|
|
|
function rgb2yuv(r, g, b) {
|
|
let y = r * .299000 + g * .587000 + b * .114000
|
|
let u = r * -.168736 + g * -.331264 + b * .500000 + 128
|
|
let v = r * .500000 + g * -.418688 + b * -.081312 + 128
|
|
|
|
y = Math.round(y);
|
|
u = Math.round(u);
|
|
v = Math.round(v);
|
|
return {
|
|
y, u, v
|
|
}
|
|
}
|
|
|
|
function makeI420Frames(colorSpace) {
|
|
const kYellow = {r: 0xFF, g: 0xFF, b: 0x00};
|
|
const kRed = {r: 0xFF, g: 0x00, b: 0x00};
|
|
const kBlue = {r: 0x00, g: 0x00, b: 0xFF};
|
|
const kGreen = {r: 0x00, g: 0xFF, b: 0x00};
|
|
const kPink = {r: 0xFF, g: 0x78, b: 0xFF};
|
|
const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF};
|
|
const kBlack = {r: 0x00, g: 0x00, b: 0x00};
|
|
const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF};
|
|
|
|
const result = [];
|
|
const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4};
|
|
const colors =
|
|
[kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink];
|
|
const data = new Uint8Array(24);
|
|
init.colorSpace = colorSpace;
|
|
for (let color of colors) {
|
|
color = rgb2yuv(color.r, color.g, color.b);
|
|
data.fill(color.y, 0, 16);
|
|
data.fill(color.u, 16, 20);
|
|
data.fill(color.v, 20, 24);
|
|
result.push(new VideoFrame(data, init));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function makeRGBXFrames(colorSpace) {
|
|
const kYellow = 0xFFFF00;
|
|
const kRed = 0xFF0000;
|
|
const kBlue = 0x0000FF;
|
|
const kGreen = 0x00FF00;
|
|
const kBlack = 0x000000;
|
|
const kWhite = 0xFFFFFF;
|
|
|
|
const result = [];
|
|
const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
|
|
const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite];
|
|
const data = new Uint32Array(16);
|
|
init.colorSpace = colorSpace;
|
|
for (let color of colors) {
|
|
data.fill(color, 0, 16);
|
|
result.push(new VideoFrame(data, init));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function testFrame(frame, colorSpace, pixelFormat) {
|
|
const width = frame.visibleRect.width;
|
|
const height = frame.visibleRect.height;
|
|
let frame_message = 'Frame: ' + JSON.stringify({
|
|
format: frame.format,
|
|
width: width,
|
|
height: height,
|
|
matrix: frame.colorSpace?.matrix,
|
|
primaries: frame.colorSpace?.primaries,
|
|
transfer: frame.colorSpace?.transfer,
|
|
});
|
|
const cnv = new OffscreenCanvas(width, height);
|
|
const ctx =
|
|
cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true});
|
|
|
|
// Read VideoFrame pixels via copyTo()
|
|
let imageData = ctx.createImageData(width, height);
|
|
let copy_to_buf = imageData.data.buffer;
|
|
let layout = null;
|
|
try {
|
|
const options = {
|
|
rect: {x: 0, y: 0, width: width, height: height},
|
|
format: pixelFormat,
|
|
colorSpace: colorSpace
|
|
};
|
|
assert_equals(frame.allocationSize(options), copy_to_buf.byteLength);
|
|
layout = await frame.copyTo(copy_to_buf, options);
|
|
} catch (e) {
|
|
assert_unreached(`copyTo() failure: ${e}`);
|
|
return;
|
|
}
|
|
if (layout.length != 1) {
|
|
assert_unreached('Conversion to RGB is not supported by the browser');
|
|
return;
|
|
}
|
|
|
|
// Read VideoFrame pixels via drawImage()
|
|
ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height);
|
|
imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace});
|
|
let get_image_buf = imageData.data.buffer;
|
|
|
|
// Compare!
|
|
const tolerance = 1;
|
|
for (let i = 0; i < copy_to_buf.byteLength; i += 4) {
|
|
if (pixelFormat.startsWith('BGR')) {
|
|
// getImageData() always gives us RGB, we need to swap bytes before
|
|
// comparing them with BGR.
|
|
new Uint8Array(get_image_buf, i, 3).reverse();
|
|
}
|
|
compareColors(
|
|
new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4),
|
|
tolerance, frame_message + ` Mismatch at offset ${i}`);
|
|
}
|
|
}
|
|
|
|
function test_4x4_I420_frames() {
|
|
for (let colorSpace of ['srgb', 'display-p3']) {
|
|
for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
|
|
for (let frameColorSpace of [null, smpte170m, bt709]) {
|
|
const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null";
|
|
promise_test(async t => {
|
|
for (let frame of makeI420Frames(frameColorSpace)) {
|
|
await testFrame(frame, colorSpace, pixelFormat);
|
|
frame.close();
|
|
}
|
|
}, `Convert 4x4 ${frameColorSpaceName} I420 frames to ${pixelFormat} / ${colorSpace}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
test_4x4_I420_frames();
|
|
|
|
function test_4x4_RGB_frames() {
|
|
for (let colorSpace of ['srgb', 'display-p3']) {
|
|
for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
|
|
for (let frameColorSpace of [null, smpte170m, bt709]) {
|
|
const frameColorSpaceName = frameColorSpace? frameColorSpace.primaries : "null";
|
|
promise_test(async t => {
|
|
for (let frame of makeRGBXFrames(frameColorSpace)) {
|
|
await testFrame(frame, colorSpace, pixelFormat);
|
|
frame.close();
|
|
}
|
|
}, `Convert 4x4 ${frameColorSpaceName} RGBX frames to ${pixelFormat} / ${colorSpace}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
test_4x4_RGB_frames();
|
|
|
|
|
|
function test_4color_canvas_frames() {
|
|
for (let colorSpace of ['srgb', 'display-p3']) {
|
|
for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) {
|
|
promise_test(async t => {
|
|
const frame = createFrame(32, 16);
|
|
await testFrame(frame, colorSpace, pixelFormat);
|
|
frame.close();
|
|
}, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`);
|
|
}
|
|
}
|
|
}
|
|
test_4color_canvas_frames();
|
|
|
|
promise_test(async t => {
|
|
let pixelFormat = 'RGBA'
|
|
const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4};
|
|
const src_data = new Uint32Array(init.codedWidth * init.codedHeight);
|
|
src_data.fill(0xFFFFFFFF);
|
|
const offset = 5;
|
|
const stride = 40;
|
|
const dst_data = new Uint8Array(offset + stride * init.codedHeight);
|
|
const options = {
|
|
format: pixelFormat,
|
|
layout: [
|
|
{offset: offset, stride: stride},
|
|
]
|
|
};
|
|
const frame = new VideoFrame(src_data, init);
|
|
await frame.copyTo(dst_data, options)
|
|
assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset');
|
|
for (let row = 0; row < init.codedHeight; ++row) {
|
|
let width = init.codedWidth * 4;
|
|
const row_data =
|
|
dst_data.slice(offset + stride * row, offset + stride * row + width);
|
|
const margin_data = dst_data.slice(
|
|
offset + stride * row + width, offset + stride * (row + 1));
|
|
|
|
assert_false(
|
|
row_data.some(e => e != 0xFF),
|
|
`unexpected data in row ${row} [${row_data}]`);
|
|
assert_false(
|
|
margin_data.some(e => e != 0),
|
|
`unexpected margin in row ${row} [${margin_data}]`);
|
|
}
|
|
|
|
frame.close();
|
|
}, `copyTo() with layout`);
|
|
|
|
function test_unsupported_pixel_formats() {
|
|
const kUnsupportedFormats = [
|
|
'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A',
|
|
'NV12'
|
|
];
|
|
|
|
for (let pixelFormat of kUnsupportedFormats) {
|
|
promise_test(async t => {
|
|
const init =
|
|
{format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4};
|
|
const data = new Uint32Array(16);
|
|
const options = {format: pixelFormat};
|
|
const frame = new VideoFrame(data, init);
|
|
assert_throws_dom(
|
|
'NotSupportedError', () => frame.allocationSize(options));
|
|
await promise_rejects_dom(
|
|
t, 'NotSupportedError', frame.copyTo(data, options))
|
|
frame.close();
|
|
}, `Unsupported format ${pixelFormat}`);
|
|
}
|
|
}
|
|
test_unsupported_pixel_formats();
|