| // META: global=window,dedicatedworker | |
| // META: script=/webcodecs/video-encoder-utils.js | |
| // META: variant=?av1 | |
| // META: variant=?vp9_p0 | |
| // META: variant=?vp9_p2 | |
| function get_config() { | |
| const config = { | |
| '?av1': {codec: 'av01.0.04M.08'}, | |
| '?vp8': {codec: 'vp8'}, | |
| '?vp9_p0': {codec: 'vp09.00.10.08'}, | |
| '?vp9_p2': {codec: 'vp09.02.10.10'}, | |
| '?h264': {codec: 'avc1.42001E', avc: {format: 'annexb'}}, | |
| '?h265': {codec: 'hev1.1.6.L93.90', hevc: {format: 'annexb'}} | |
| }[location.search]; | |
| config.width = 320; | |
| config.height = 200; | |
| config.bitrate = 1000000; | |
| config.bitrateMode = 'quantizer'; | |
| config.framerate = 30; | |
| return config; | |
| } | |
| function get_qp_range() { | |
| switch (location.search) { | |
| case '?av1': | |
| return {min: 1, max: 255}; | |
| case '?vp9_p0': | |
| return {min: 1, max: 255}; | |
| case '?vp9_p2': | |
| return {min: 1, max: 255}; | |
| case '?h264': | |
| return {min: 1, max: 51}; | |
| case '?h265': | |
| return {min: 1, max: 51}; | |
| } | |
| return null; | |
| } | |
| function set_qp(options, value) { | |
| switch (location.search) { | |
| case '?av1': | |
| options.av1 = {quantizer: value}; | |
| return; | |
| case '?vp9_p0': | |
| options.vp9 = {quantizer: value}; | |
| return; | |
| case '?vp9_p2': | |
| options.vp9 = {quantizer: value}; | |
| return; | |
| case '?h264': | |
| options.avc = {quantizer: value}; | |
| return; | |
| case '?h265': | |
| options.hevc = {quantizer: value}; | |
| } | |
| } | |
| async function per_frame_qp_test(t, encoder_config, qp_range, validate_result) { | |
| const w = encoder_config.width; | |
| const h = encoder_config.height; | |
| await checkEncoderSupport(t, encoder_config); | |
| const frames_to_encode = 12; | |
| let frames_decoded = 0; | |
| let frames_encoded = 0; | |
| let chunks = []; | |
| let corrupted_frames = []; | |
| const encoder_init = { | |
| output(chunk, metadata) { | |
| frames_encoded++; | |
| chunks.push(chunk); | |
| }, | |
| error(e) { | |
| assert_unreached(e.message); | |
| } | |
| }; | |
| let encoder = new VideoEncoder(encoder_init); | |
| encoder.configure(encoder_config); | |
| let qp = qp_range.min; | |
| for (let i = 0; i < frames_to_encode; i++) { | |
| let frame = createDottedFrame(w, h, i); | |
| let encode_options = {keyFrame: false}; | |
| set_qp(encode_options, qp); | |
| encoder.encode(frame, encode_options); | |
| frame.close(); | |
| qp += 3; | |
| if (qp > qp_range.max) { | |
| qp = qp_range.min | |
| } | |
| } | |
| await encoder.flush(); | |
| let decoder = new VideoDecoder({ | |
| output(frame) { | |
| frames_decoded++; | |
| // Check that we have intended number of dots and no more. | |
| // Completely black frame shouldn't pass the test. | |
| if (validate_result && !validateBlackDots(frame, frame.timestamp) || | |
| validateBlackDots(frame, frame.timestamp + 1)) { | |
| corrupted_frames.push(frame.timestamp) | |
| } | |
| frame.close(); | |
| }, | |
| error(e) { | |
| assert_unreached(e.message); | |
| } | |
| }); | |
| let decoder_config = { | |
| codec: encoder_config.codec, | |
| codedWidth: w, | |
| codedHeight: h, | |
| }; | |
| decoder.configure(decoder_config); | |
| for (let chunk of chunks) { | |
| decoder.decode(chunk); | |
| } | |
| await decoder.flush(); | |
| encoder.close(); | |
| decoder.close(); | |
| assert_equals(frames_encoded, frames_to_encode); | |
| assert_equals(chunks.length, frames_to_encode); | |
| assert_equals(frames_decoded, frames_to_encode); | |
| assert_equals( | |
| corrupted_frames.length, 0, `corrupted_frames: ${corrupted_frames}`); | |
| } | |
| promise_test(async t => { | |
| let config = get_config(); | |
| let range = get_qp_range(); | |
| return per_frame_qp_test(t, config, range, false); | |
| }, 'Frame QP encoding, full range'); | |
| promise_test(async t => { | |
| let config = get_config(); | |
| return per_frame_qp_test(t, config, {min: 1, max: 20}, true); | |
| }, 'Frame QP encoding, good range with validation'); |