| // META: global=window,dedicatedworker |
| // META: script=/webcodecs/utils.js |
| |
| // TODO(sandersd): Move metadata into a helper library. |
| // TODO(sandersd): Add H.264 decode test once there is an API to query for |
| // supported codecs. |
| const h264 = { |
| async buffer() { return (await fetch('h264.mp4')).arrayBuffer(); }, |
| codec: "avc1.64000c", |
| description: {offset: 7229, size: 46}, |
| frames: [{offset: 48, size: 4007}, |
| {offset: 4055, size: 926}, |
| {offset: 4981, size: 241}, |
| {offset: 5222, size: 97}, |
| {offset: 5319, size: 98}, |
| {offset: 5417, size: 624}, |
| {offset: 6041, size: 185}, |
| {offset: 6226, size: 94}, |
| {offset: 6320, size: 109}, |
| {offset: 6429, size: 281}] |
| }; |
| |
| const vp9 = { |
| async buffer() { return (await fetch('vp9.mp4')).arrayBuffer(); }, |
| // TODO(sandersd): Verify that the file is actually level 1. |
| codec: "vp09.00.10.08", |
| frames: [{offset: 44, size: 3315}, |
| {offset: 3359, size: 203}, |
| {offset: 3562, size: 245}, |
| {offset: 3807, size: 172}, |
| {offset: 3979, size: 312}, |
| {offset: 4291, size: 170}, |
| {offset: 4461, size: 195}, |
| {offset: 4656, size: 181}, |
| {offset: 4837, size: 356}, |
| {offset: 5193, size: 159}] |
| }; |
| |
| const badCodecsList = [ |
| '', // Empty codec |
| 'bogus', // Non exsitent codec |
| 'vorbis', // Audio codec |
| 'vp9', // Ambiguous codec |
| 'video/webm; codecs="vp9"' // Codec with mime type |
| ] |
| |
| const invalidConfigs = [ |
| { |
| comment: 'Emtpy codec', |
| config: {codec: ''}, |
| }, |
| { |
| comment: 'Unrecognized codec', |
| config: {codec: 'bogus'}, |
| }, |
| { |
| comment: 'Audio codec', |
| config: {codec: 'vorbis'}, |
| }, |
| { |
| comment: 'Ambiguous codec', |
| config: {codec: 'vp9'}, |
| }, |
| { |
| comment: 'Codec with MIME type', |
| config: {codec: 'video/webm; codecs="vp8"'}, |
| }, |
| { |
| comment: 'Zero coded size', |
| config: { |
| codec: h264.codec, |
| codedWidth: 0, |
| codedHeight: 0, |
| }, |
| }, |
| { |
| comment: 'Out of bounds crop size caused by left/top offset', |
| config: { |
| codec: h264.codec, |
| codedWidth: 1920, |
| codedHeight: 1088, |
| cropLeft: 10, |
| cropTop: 10, |
| // When unspecified, these default to coded dimensions |
| // cropWidth: 1920, |
| // cropHeight: 1088 |
| }, |
| }, |
| { |
| comment: 'Out of bounds crop size', |
| config: { |
| codec: h264.codec, |
| codedWidth: 1920, |
| codedHeight: 1088, |
| cropLeft: 10, |
| cropTop: 10, |
| cropWidth: 1920, |
| cropHeight: 1088, |
| }, |
| }, |
| { |
| comment: 'Way out of bounds crop size', |
| config: { |
| codec: h264.codec, |
| codedWidth: 1920, |
| codedHeight: 1088, |
| cropWidth: 4000, |
| cropHeight: 5000, |
| }, |
| }, |
| { |
| comment: 'Invalid display size', |
| config: { |
| codec: h264.codec, |
| displayWidth: 0, |
| displayHeight: 0, |
| }, |
| }, |
| ] // invalidConfigs |
| |
| function view(buffer, {offset, size}) { |
| return new Uint8Array(buffer, offset, size); |
| } |
| |
| function getFakeChunk() { |
| return new EncodedVideoChunk({ |
| type:'key', |
| timestamp:0, |
| data:Uint8Array.of(0) |
| }); |
| } |
| |
| invalidConfigs.forEach(entry => { |
| promise_test(t => { |
| return promise_rejects_js(t, TypeError, VideoDecoder.isConfigSupported(entry.config)); |
| }, 'Test that VideoDecoder.isConfigSupported() rejects invalid config:' + entry.comment); |
| }); |
| |
| invalidConfigs.forEach(entry => { |
| async_test(t => { |
| let codec = new VideoDecoder(getDefaultCodecInit(t)); |
| assert_throws_js(TypeError, () => { codec.configure(entry.config); }); |
| t.done(); |
| }, 'Test that VideoDecoder.configure() rejects invalid config:' + entry.comment); |
| }); |
| |
| promise_test(t => { |
| return VideoDecoder.isConfigSupported({codec: vp9.codec}); |
| }, 'Test VideoDecoder.isConfigSupported() with minimal valid config'); |
| |
| promise_test(t => { |
| // This config specifies a slight crop. H264 1080p content always crops |
| // because H264 coded dimensions are a multiple of 16 (e.g. 1088). |
| return VideoDecoder.isConfigSupported({ |
| codec: h264.codec, |
| codedWidth: 1920, |
| codedHeight: 1088, |
| cropLeft: 0, |
| cropTop: 0, |
| cropWidth: 1920, |
| cropHeight: 1080, |
| displayWidth: 1920, |
| displayHeight: 1080 |
| }); |
| }, 'Test VideoDecoder.isConfigSupported() with valid expanded config'); |
| |
| promise_test(t => { |
| // Define a valid config that includes a hypothetical 'futureConfigFeature', |
| // which is not yet recognized by the User Agent. |
| const validConfig = { |
| codec: h264.codec, |
| codedWidth: 1920, |
| codedHeight: 1088, |
| cropLeft: 0, |
| cropTop: 0, |
| cropWidth: 1920, |
| cropHeight: 1080, |
| displayWidth: 1920, |
| displayHeight: 1080, |
| description: new Uint8Array([1, 2, 3]), |
| futureConfigFeature: 'foo', |
| }; |
| |
| // The UA will evaluate validConfig as being "valid", ignoring the |
| // `futureConfigFeature` it doesn't recognize. |
| return VideoDecoder.isConfigSupported(validConfig).then((decoderSupport) => { |
| // VideoDecoderSupport must contain the following properites. |
| assert_true(decoderSupport.hasOwnProperty('supported')); |
| assert_true(decoderSupport.hasOwnProperty('config')); |
| |
| // VideoDecoderSupport.config must not contain unrecognized properties. |
| assert_false(decoderSupport.config.hasOwnProperty('futureConfigFeature')); |
| |
| // VideoDecoderSupport.config must contiain the recognized properties. |
| assert_equals(decoderSupport.config.codec, validConfig.codec); |
| assert_equals(decoderSupport.config.codedWidth, validConfig.codedWidth); |
| assert_equals(decoderSupport.config.codedHeight, validConfig.codedHeight); |
| assert_equals(decoderSupport.config.cropLeft, validConfig.cropLeft); |
| assert_equals(decoderSupport.config.cropTop, validConfig.cropTop); |
| assert_equals(decoderSupport.config.cropWidth, validConfig.cropWidth); |
| assert_equals(decoderSupport.config.displayWidth, validConfig.displayWidth); |
| assert_equals(decoderSupport.config.displayHeight, validConfig.displayHeight); |
| |
| // The description BufferSource must copy the input config description. |
| assert_not_equals(decoderSupport.config.description, validConfig.description); |
| let parsedDescription = new Uint8Array(decoderSupport.config.description); |
| assert_equals(parsedDescription.length, validConfig.description.length); |
| for (let i = 0; i < parsedDescription.length; ++i) { |
| assert_equals(parsedDescription[i], validConfig.description[i]); |
| } |
| }); |
| }, 'Test that VideoDecoder.isConfigSupported() returns a parsed configuration'); |
| |
| |
| promise_test(t => { |
| // VideoDecoderInit lacks required fields. |
| assert_throws_js(TypeError, () => { new VideoDecoder({}); }); |
| |
| // VideoDecoderInit has required fields. |
| let decoder = new VideoDecoder(getDefaultCodecInit(t)); |
| |
| assert_equals(decoder.state, "unconfigured"); |
| |
| decoder.close(); |
| |
| return endAfterEventLoopTurn(); |
| }, 'Test VideoDecoder construction'); |
| |
| promise_test(t => { |
| let decoder = new VideoDecoder(getDefaultCodecInit(t)); |
| |
| // TODO(chcunningham): Remove badCodecsList testing. It's now covered more |
| // extensively by other tests. |
| testConfigurations(decoder, { codec: vp9.codec }, badCodecsList); |
| |
| return endAfterEventLoopTurn(); |
| }, 'Test VideoDecoder.configure() with various codec strings'); |
| |
| promise_test(async t => { |
| let buffer = await vp9.buffer(); |
| |
| let numOutputs = 0; |
| let decoder = new VideoDecoder({ |
| output(frame) { |
| t.step(() => { |
| assert_equals(++numOutputs, 1, "outputs"); |
| assert_equals(frame.cropWidth, 320, "cropWidth"); |
| assert_equals(frame.cropHeight, 240, "cropHeight"); |
| assert_equals(frame.timestamp, 0, "timestamp"); |
| frame.close(); |
| }); |
| }, |
| error(e) { |
| t.step(() => { throw e; }); |
| } |
| }); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| decoder.decode(new EncodedVideoChunk({ |
| type:'key', |
| timestamp:0, |
| data: view(buffer, vp9.frames[0]) |
| })); |
| |
| await decoder.flush(); |
| |
| assert_equals(numOutputs, 1, "outputs"); |
| }, 'Decode VP9'); |
| |
| promise_test(async t => { |
| let buffer = await vp9.buffer(); |
| |
| let outputs_before_reset = 0; |
| let outputs_after_reset = 0; |
| |
| let decoder = new VideoDecoder({ |
| // Pre-reset() chunks will all have timestamp=0, while post-reset() chunks |
| // will all have timestamp=1. |
| output(frame) { |
| t.step(() => { |
| if (frame.timestamp == 0) |
| outputs_before_reset++; |
| else |
| outputs_after_reset++; |
| }); |
| }, |
| error(e) { |
| t.step(() => { throw e; }); |
| } |
| }); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| for (let i = 0; i < 100; i++) { |
| decoder.decode(new EncodedVideoChunk({ |
| type:'key', |
| timestamp:0, |
| data: view(buffer, vp9.frames[0]) |
| })); |
| } |
| |
| assert_greater_than(decoder.decodeQueueSize, 0); |
| |
| // Wait for the first frame to be decoded. |
| await t.step_wait(() => outputs_before_reset > 0, |
| "Decoded outputs started coming", 10000, 1); |
| |
| let saved_outputs_before_reset = outputs_before_reset; |
| assert_greater_than(saved_outputs_before_reset, 0); |
| assert_less_than(saved_outputs_before_reset, 100); |
| |
| decoder.reset() |
| assert_equals(decoder.decodeQueueSize, 0); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| for (let i = 0; i < 5; i++) { |
| decoder.decode(new EncodedVideoChunk({ |
| type:'key', |
| timestamp:1, |
| data: view(buffer, vp9.frames[0]) |
| })); |
| } |
| await decoder.flush(); |
| assert_equals(outputs_after_reset, 5); |
| assert_equals(saved_outputs_before_reset, outputs_before_reset); |
| assert_equals(decoder.decodeQueueSize, 0); |
| |
| endAfterEventLoopTurn(); |
| }, 'Verify reset() suppresses output and rejects flush'); |
| |
| promise_test(t => { |
| let decoder = new VideoDecoder(getDefaultCodecInit(t)); |
| |
| return testClosedCodec(t, decoder, { codec: vp9.codec }, getFakeChunk()); |
| }, 'Verify closed VideoDecoder operations'); |
| |
| promise_test(t => { |
| let decoder = new VideoDecoder(getDefaultCodecInit(t)); |
| |
| return testUnconfiguredCodec(t, decoder, getFakeChunk()); |
| }, 'Verify unconfigured VideoDecoder operations'); |
| |
| promise_test(t => { |
| let numErrors = 0; |
| let codecInit = getDefaultCodecInit(t); |
| codecInit.error = _ => numErrors++; |
| |
| let decoder = new VideoDecoder(codecInit); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| let fakeChunk = getFakeChunk(); |
| decoder.decode(fakeChunk); |
| |
| return promise_rejects_exactly(t, undefined, decoder.flush()).then( |
| () => { |
| assert_equals(numErrors, 1, "errors"); |
| assert_equals(decoder.state, "closed"); |
| }); |
| }, 'Decode corrupt VP9 frame'); |
| |
| promise_test(t => { |
| let numErrors = 0; |
| let codecInit = getDefaultCodecInit(t); |
| codecInit.error = _ => numErrors++; |
| |
| let decoder = new VideoDecoder(codecInit); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| let fakeChunk = getFakeChunk(); |
| decoder.decode(fakeChunk); |
| |
| return promise_rejects_exactly(t, undefined, decoder.flush()).then( |
| () => { |
| assert_equals(numErrors, 1, "errors"); |
| assert_equals(decoder.state, "closed"); |
| }); |
| }, 'Decode empty VP9 frame'); |
| |
| promise_test(t => { |
| let decoder = new VideoDecoder(getDefaultCodecInit(t)); |
| |
| decoder.configure({codec: vp9.codec}); |
| |
| let fakeChunk = getFakeChunk(); |
| decoder.decode(fakeChunk); |
| |
| // Create the flush promise before closing, as it is invalid to do so later. |
| let flushPromise = decoder.flush(); |
| |
| // This should synchronously reject the flush() promise. |
| decoder.close(); |
| |
| // TODO(sandersd): Wait for a bit in case there is a lingering output |
| // or error coming. |
| return promise_rejects_exactly(t, undefined, flushPromise); |
| }, 'Close while decoding corrupt VP9 frame'); |