| // META: global=window,dedicatedworker | |
| // META: script=/webcodecs/utils.js | |
| // META: script=/webcodecs/video-encoder-utils.js | |
| // META: variant=?av1 | |
| // META: variant=?vp8 | |
| // META: variant=?vp9 | |
| // META: variant=?vp9_p1 | |
| // META: variant=?vp9_p2 | |
| // META: variant=?vp9_p3 | |
| // META: variant=?h264 | |
| // Tests encoding high-bit-depth (16-bit) VideoFrame inputs. | |
| // - Standard 8-bit codec profiles (AV1 Main 8-bit, VP8, VP9 Profile 0, VP9 Profile 1, H.264 Baseline) | |
| // perform 16-to-8 bit conversion prior to encoding. | |
| // - High-bit-depth codec profiles (VP9 Profile 2, VP9 Profile 3) perform 16-bit high-bit-depth encoding. | |
| const ENCODER_CONFIG = { | |
| '?av1': { codec: 'av01.0.04M.08', isHbd: false }, | |
| '?vp8': { codec: 'vp8', isHbd: false }, | |
| '?vp9': { codec: 'vp09.00.10.08', isHbd: false }, | |
| '?vp9_p1': { codec: 'vp09.01.10.08.03', isHbd: false }, | |
| '?vp9_p2': { codec: 'vp09.02.10.10', isHbd: true }, | |
| '?vp9_p3': { codec: 'vp09.03.10.10.03', isHbd: true }, | |
| '?h264': { codec: 'avc1.42001E', avc: { format: 'avc' }, isHbd: false }, | |
| }[location.search] || {}; | |
| ENCODER_CONFIG.width = 64; | |
| ENCODER_CONFIG.height = 48; | |
| ENCODER_CONFIG.bitrate = 1000000; | |
| ENCODER_CONFIG.framerate = 30; | |
| function createHbdFrame(format, width, height, ts) { | |
| let sub_w = width; | |
| let sub_h = height; | |
| if (format.includes('I420')) { | |
| sub_w = width / 2; | |
| sub_h = height / 2; | |
| } else if (format.includes('I422')) { | |
| sub_w = width / 2; | |
| } | |
| let luma_size = width * height; | |
| let chroma_size = sub_w * sub_h; | |
| let has_alpha = format.includes('I420A') || format.includes('I422A') || format.includes('I444A'); | |
| let is_12bit = format.includes('P12'); | |
| let shift = is_12bit ? 4 : 2; | |
| let total_samples = luma_size + 2 * chroma_size + (has_alpha ? luma_size : 0); | |
| let data = new Uint16Array(total_samples); | |
| let uOffset = luma_size; | |
| let vOffset = uOffset + chroma_size; | |
| let aOffset = vOffset + chroma_size; | |
| // Y=96, U=155, V=104 | |
| data.fill(96 << shift, 0, uOffset); | |
| data.fill(155 << shift, uOffset, vOffset); | |
| data.fill(104 << shift, vOffset, has_alpha ? aOffset : total_samples); | |
| if (has_alpha) { | |
| let max_alpha = is_12bit ? 4095 : 1023; | |
| data.fill(max_alpha, aOffset); | |
| } | |
| return new VideoFrame(data, { | |
| format: format, | |
| timestamp: ts, | |
| codedWidth: width, | |
| codedHeight: height, | |
| }); | |
| } | |
| const HBD_FORMATS = [ | |
| 'I420P10', | |
| 'I422P10', | |
| 'I444P10', | |
| 'I420P12', | |
| 'I422P12', | |
| 'I444P12', | |
| 'I420AP10', | |
| 'I422AP10', | |
| 'I444AP10', | |
| ]; | |
| HBD_FORMATS.forEach(format => { | |
| promise_test(async t => { | |
| let output_chunks = []; | |
| let decoder_config = null; | |
| let encoder_init = { | |
| output: (chunk, metadata) => { | |
| output_chunks.push(chunk); | |
| if (metadata && metadata.decoderConfig) { | |
| decoder_config = metadata.decoderConfig; | |
| } | |
| }, | |
| error: (e) => { | |
| assert_unreached(e.message); | |
| } | |
| }; | |
| await checkEncoderSupport(t, ENCODER_CONFIG); | |
| let encoder = new VideoEncoder(encoder_init); | |
| encoder.configure(ENCODER_CONFIG); | |
| let frame = createHbdFrame(format, ENCODER_CONFIG.width, ENCODER_CONFIG.height, 0); | |
| encoder.encode(frame); | |
| frame.close(); | |
| await encoder.flush(); | |
| encoder.close(); | |
| assert_equals(output_chunks.length, 1, 'output chunk count'); | |
| assert_greater_than(output_chunks[0].byteLength, 0, 'output chunk byte length'); | |
| assert_not_equals(decoder_config, null, 'decoderConfig'); | |
| let decoder_support = await VideoDecoder.isConfigSupported(decoder_config); | |
| assert_implements_optional( | |
| decoder_support.supported, | |
| 'Unsupported decoder config: ' + JSON.stringify(decoder_config)); | |
| let decoded_frame = null; | |
| let decoder_init = { | |
| output: (f) => { | |
| decoded_frame = f; | |
| }, | |
| error: (e) => { | |
| assert_unreached(e.message); | |
| } | |
| }; | |
| let decoder = new VideoDecoder(decoder_init); | |
| decoder.configure(decoder_config); | |
| decoder.decode(output_chunks[0]); | |
| await decoder.flush(); | |
| decoder.close(); | |
| assert_not_equals(decoded_frame, null, 'decoded frame'); | |
| // Using copyTo({ format: 'RGBA' }) guarantees unified cross-browser readback | |
| // regardless of whether the decoder outputs software YUV or hardware GPU frames. | |
| let size = decoded_frame.allocationSize({ format: 'RGBA' }); | |
| assert_greater_than(size, 0, 'allocationSize RGBA'); | |
| let buffer = new ArrayBuffer(size); | |
| await decoded_frame.copyTo(buffer, { format: 'RGBA' }); | |
| decoded_frame.close(); | |
| let view = new Uint8Array(buffer); | |
| let tolerance = 25; | |
| assert_approx_equals(view[0], kSRGBPixel[0], tolerance, 'R pixel value'); | |
| assert_approx_equals(view[1], kSRGBPixel[1], tolerance, 'G pixel value'); | |
| assert_approx_equals(view[2], kSRGBPixel[2], tolerance, 'B pixel value'); | |
| assert_approx_equals(view[3], 255, tolerance, 'A pixel value'); | |
| }, ENCODER_CONFIG.isHbd | |
| ? `Test 16-bit high-bit-depth encoding of ${format}` | |
| : `Test 16-to-8 bit conversion encoding of ${format}`); | |
| }); |