246 lines
8.8 KiB
JavaScript
246 lines
8.8 KiB
JavaScript
// 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);
|
|
}
|
|
}
|