// META: global=window,dedicatedworker // META: script=/webcodecs/utils.js // META: variant=?adts_aac // META: variant=?mp4_aac // META: variant=?mp3 // META: variant=?opus // META: variant=?pcm_alaw // META: variant=?pcm_ulaw // META: variant=?pcm_u8 // META: variant=?pcm_s16 // META: variant=?pcm_s24 // META: variant=?pcm_s32 // META: variant=?pcm_f32 // META: variant=?flac // META: variant=?vorbis const ADTS_AAC_DATA = { src: 'sfx.adts', config: { codec: 'mp4a.40.2', sampleRate: 48000, numberOfChannels: 1, }, chunks: [ {offset: 0, size: 248}, {offset: 248, size: 280}, {offset: 528, size: 258}, {offset: 786, size: 125}, {offset: 911, size: 230}, {offset: 1141, size: 148}, {offset: 1289, size: 224}, {offset: 1513, size: 166}, {offset: 1679, size: 216}, {offset: 1895, size: 183} ], duration: 24000 }; const MP3_DATA = { src: 'sfx.mp3', config: { codec: 'mp3', sampleRate: 48000, numberOfChannels: 1, }, chunks: [ {offset: 333, size: 288}, {offset: 621, size: 288}, {offset: 909, size: 288}, {offset: 1197, size: 288}, {offset: 1485, size: 288}, {offset: 1773, size: 288}, {offset: 2061, size: 288}, {offset: 2349, size: 288}, {offset: 2637, size: 288}, {offset: 2925, size: 288} ], duration: 24000 }; const MP4_AAC_DATA = { src: 'sfx-aac.mp4', config: { codec: 'mp4a.40.2', sampleRate: 48000, numberOfChannels: 1, description: {offset: 2552, size: 5}, }, chunks: [ {offset: 44, size: 241}, {offset: 285, size: 273}, {offset: 558, size: 251}, {offset: 809, size: 118}, {offset: 927, size: 223}, {offset: 1150, size: 141}, {offset: 1291, size: 217}, {offset: 1508, size: 159}, {offset: 1667, size: 209}, {offset: 1876, size: 176}, ], duration: 21333 }; const OPUS_DATA = { src: 'sfx-opus.ogg', config: { codec: 'opus', sampleRate: 48000, numberOfChannels: 1, description: {offset: 28, size: 19}, }, chunks: [ {offset: 185, size: 450}, {offset: 635, size: 268}, {offset: 903, size: 285}, {offset: 1188, size: 296}, {offset: 1484, size: 287}, {offset: 1771, size: 308}, {offset: 2079, size: 289}, {offset: 2368, size: 286}, {offset: 2654, size: 296}, {offset: 2950, size: 294} ], duration: 20000 }; const FLAC_DATA = { src: 'sfx.flac', config: { codec: 'flac', sampleRate: 48000, numberOfChannels: 1, description: { offset: 0, size: 8287 } }, chunks: [ { offset: 8288, size: 2276 }, { offset: 10564, size: 2038 }, { offset: 12602, size: 521 }, ], duration: 20000 }; function pcm(codec, dataOffset) { return { src: `sfx-${codec}.wav`, config: { codec: codec, sampleRate: 48000, numberOfChannels: 1, }, // Chunk are arbitrary and will be generated lazily chunks: [], offset: dataOffset, duration: 0 } } const PCM_ULAW_DATA = pcm("ulaw", 0x5c); const PCM_ALAW_DATA = pcm("alaw", 0x5c); const PCM_U8_DATA = pcm("pcm-u8", 0x4e); const PCM_S16_DATA = pcm("pcm-s16", 0x4e); const PCM_S24_DATA = pcm("pcm-s24", 0x66); const PCM_S32_DATA = pcm("pcm-s32", 0x66); const PCM_F32_DATA = pcm("pcm-f32", 0x72); const VORBIS_DATA = { src: 'sfx-vorbis.ogg', config: { codec: 'vorbis', description: [ 2, 30, 62, {offset: 28, size: 30}, {offset: 101, size: 62}, {offset: 163, size: 3771} ], numberOfChannels: 1, sampleRate: 48000, }, chunks: [ {offset: 3968, size: 44}, {offset: 4012, size: 21}, {offset: 4033, size: 57}, {offset: 4090, size: 37}, {offset: 4127, size: 37}, {offset: 4164, size: 107}, {offset: 4271, size: 172} ], duration: 21333 }; // Allows mutating `callbacks` after constructing the AudioDecoder, wraps calls // in t.step(). function createAudioDecoder(t, callbacks) { return new AudioDecoder({ output(frame) { if (callbacks && callbacks.output) { t.step(() => callbacks.output(frame)); } else { t.unreached_func('unexpected output()'); } }, error(e) { if (callbacks && callbacks.error) { t.step(() => callbacks.error(e)); } else { t.unreached_func('unexpected error()'); } } }); } // Create a view of an ArrayBuffer. function view(buffer, {offset, size}) { return new Uint8Array(buffer, offset, size); } let CONFIG = null; let CHUNK_DATA = null; let CHUNKS = null; promise_setup(async () => { const data = { '?adts_aac': ADTS_AAC_DATA, '?mp3': MP3_DATA, '?mp4_aac': MP4_AAC_DATA, '?opus': OPUS_DATA, '?pcm_alaw': PCM_ALAW_DATA, '?pcm_ulaw': PCM_ULAW_DATA, '?pcm_u8': PCM_U8_DATA, '?pcm_s16': PCM_S16_DATA, '?pcm_s24': PCM_S24_DATA, '?pcm_s32': PCM_S32_DATA, '?pcm_f32': PCM_F32_DATA, '?flac': FLAC_DATA, '?vorbis': VORBIS_DATA, }[location.search]; // Don't run any tests if the codec is not supported. assert_equals("function", typeof AudioDecoder.isConfigSupported); let supported = false; try { const support = await AudioDecoder.isConfigSupported({ codec: data.config.codec, sampleRate: data.config.sampleRate, numberOfChannels: data.config.numberOfChannels }); supported = support.supported; } catch (e) { } assert_implements_optional(supported, data.config.codec + ' unsupported'); // Fetch the media data and prepare buffers. const response = await fetch(data.src); const buf = await response.arrayBuffer(); CONFIG = {...data.config}; if (data.config.description) { // The description for decoding vorbis is expected to be in Xiph extradata format. // https://w3c.github.io/webcodecs/vorbis_codec_registration.html#audiodecoderconfig-description if (Array.isArray(data.config.description)) { const length = data.config.description.reduce((sum, value) => sum + ((typeof value === 'number') ? 1 : value.size), 0); const description = new Uint8Array(length); data.config.description.reduce((offset, value) => { if (typeof value === 'number') { description[offset] = value; return offset + 1; } description.set(view(buf, value), offset); return offset + value.size; }, 0); CONFIG.description = description; } else { CONFIG.description = view(buf, data.config.description); } } CHUNK_DATA = []; // For PCM, split in chunks of 1200 bytes and compute the rest if (data.chunks.length == 0) { let offset = data.offset; // 1200 is divisible by 2 and 3 and is a plausible packet length // for PCM: this means that there won't be samples split in two packet let PACKET_LENGTH = 1200; let bytesPerSample = 0; switch (data.config.codec) { case "pcm-s16": bytesPerSample = 2; break; case "pcm-s24": bytesPerSample = 3; break; case "pcm-s32": bytesPerSample = 4; break; case "pcm-f32": bytesPerSample = 4; break; default: bytesPerSample = 1; break; } while (offset < buf.byteLength) { let size = Math.min(buf.byteLength - offset, PACKET_LENGTH); assert_equals(size % bytesPerSample, 0); CHUNK_DATA.push(view(buf, {offset, size})); offset += size; } data.duration = 1000 * 1000 * PACKET_LENGTH / data.config.sampleRate / bytesPerSample; } else { CHUNK_DATA = data.chunks.map((chunk, i) => view(buf, chunk)); } CHUNKS = CHUNK_DATA.map((encodedData, i) => new EncodedAudioChunk({ type: 'key', timestamp: i * data.duration, duration: data.duration, data: encodedData })); }); promise_test(t => { return AudioDecoder.isConfigSupported(CONFIG); }, 'Test isConfigSupported()'); promise_test(t => { // Define a valid config that includes a hypothetical 'futureConfigFeature', // which is not yet recognized by the User Agent. const validConfig = { ...CONFIG, futureConfigFeature: 'foo', }; // The UA will evaluate validConfig as being "valid", ignoring the // `futureConfigFeature` it doesn't recognize. return AudioDecoder.isConfigSupported(validConfig).then((decoderSupport) => { // AudioDecoderSupport must contain the following properites. assert_true(decoderSupport.hasOwnProperty('supported')); assert_true(decoderSupport.hasOwnProperty('config')); // AudioDecoderSupport.config must not contain unrecognized properties. assert_false(decoderSupport.config.hasOwnProperty('futureConfigFeature')); // AudioDecoderSupport.config must contiain the recognized properties. assert_equals(decoderSupport.config.codec, validConfig.codec); assert_equals(decoderSupport.config.sampleRate, validConfig.sampleRate); assert_equals( decoderSupport.config.numberOfChannels, validConfig.numberOfChannels); if (validConfig.description) { // The description must be copied. assert_false( decoderSupport.config.description === validConfig.description, 'description is unique'); assert_array_equals( new Uint8Array(decoderSupport.config.description, 0), new Uint8Array(validConfig.description, 0), 'description'); } else { assert_false( decoderSupport.config.hasOwnProperty('description'), 'description'); } }); }, 'Test that AudioDecoder.isConfigSupported() returns a parsed configuration'); promise_test(async t => { const decoder = createAudioDecoder(t); decoder.configure(CONFIG); assert_equals(decoder.state, 'configured', 'state'); }, 'Test configure()'); promise_test(t => { const decoder = createAudioDecoder(t); return testClosedCodec(t, decoder, CONFIG, CHUNKS[0]); }, 'Verify closed AudioDecoder operations'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); let outputs = 0; callbacks.output = frame => { outputs++; frame.close(); }; decoder.configure(CONFIG); CHUNKS.forEach(chunk => { decoder.decode(chunk); }); await decoder.flush(); assert_equals(outputs, CONFIG.codec === 'vorbis' ? CHUNKS.length - 1 : CHUNKS.length, 'outputs'); }, 'Test decoding'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); let outputs = 0; callbacks.output = frame => { if (outputs === 0) { assert_equals(frame.timestamp, -42); } outputs++; frame.close(); }; decoder.configure(CONFIG); decoder.decode(new EncodedAudioChunk( {type: 'key', timestamp: -42, data: CHUNK_DATA[0]})); decoder.decode(new EncodedAudioChunk( {type: 'key', timestamp: CHUNKS[0].duration - 42, data: CHUNK_DATA[1]})); await decoder.flush(); assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); }, 'Test decoding a with a negative timestamp'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); let outputs = 0; callbacks.output = frame => { if (outputs === 0) { assert_equals(frame.timestamp, 42); } outputs++; frame.close(); }; decoder.configure(CONFIG); decoder.decode(new EncodedAudioChunk( {type: 'key', timestamp: 42, data: CHUNK_DATA[0]})); decoder.decode(new EncodedAudioChunk( {type: 'key', timestamp: CHUNKS[0].duration + 42, data: CHUNK_DATA[1]})); await decoder.flush(); assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); }, 'Test decoding a with a positive timestamp'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); let outputs = 0; callbacks.output = frame => { outputs++; frame.close(); }; decoder.configure(CONFIG); decoder.decode(CHUNKS[0]); decoder.decode(CHUNKS[1]); await decoder.flush(); assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); decoder.decode(CHUNKS[2]); await decoder.flush(); assert_equals(outputs, CONFIG.codec === 'vorbis' ? 2 : 3, 'outputs'); }, 'Test decoding after flush'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); decoder.configure(CONFIG); decoder.decode(CHUNKS[0]); decoder.decode(CHUNKS[1]); const flushDone = decoder.flush(); // Wait for the first output, then reset. let outputs = 0; await new Promise(resolve => { callbacks.output = frame => { outputs++; assert_equals(outputs, 1, 'outputs'); decoder.reset(); frame.close(); resolve(); }; }); // Flush should have been synchronously rejected. await promise_rejects_dom(t, 'AbortError', flushDone); assert_equals(outputs, 1, 'outputs'); }, 'Test reset during flush'); promise_test(async t => { const callbacks = {}; const decoder = createAudioDecoder(t, callbacks); // No decodes yet. assert_equals(decoder.decodeQueueSize, 0); decoder.configure(CONFIG); // Still no decodes. assert_equals(decoder.decodeQueueSize, 0); let lastDequeueSize = Infinity; decoder.ondequeue = () => { assert_greater_than(lastDequeueSize, 0, "Dequeue event after queue empty"); assert_greater_than(lastDequeueSize, decoder.decodeQueueSize, "Dequeue event without decreased queue size"); lastDequeueSize = decoder.decodeQueueSize; }; for (let chunk of CHUNKS) decoder.decode(chunk); assert_greater_than_equal(decoder.decodeQueueSize, 0); assert_less_than_equal(decoder.decodeQueueSize, CHUNKS.length); await decoder.flush(); // We can guarantee that all decodes are processed after a flush. assert_equals(decoder.decodeQueueSize, 0); // Last dequeue event should fire when the queue is empty. assert_equals(lastDequeueSize, 0); // Reset this to Infinity to track the decline of queue size for this next // batch of decodes. lastDequeueSize = Infinity; for (let chunk of CHUNKS) decoder.decode(chunk); assert_greater_than_equal(decoder.decodeQueueSize, 0); decoder.reset(); assert_equals(decoder.decodeQueueSize, 0); }, 'AudioDecoder decodeQueueSize test');