audioDecoder-codec-specific.https.any.js (9119B)
1 // META: global=window,dedicatedworker 2 // META: script=/webcodecs/utils.js 3 // META: variant=?mp4_raw_aac_no_desc 4 5 // By spec, if the description is absent, the bitstream defaults to ADTS format. 6 // However, this is added to ensure compatibility and handle potential misuse cases. 7 const MP4_AAC_DATA_NO_DESCRIPTION = { 8 src: 'sfx-aac.mp4', 9 config: { 10 codec: 'mp4a.40.2', 11 sampleRate: 48000, 12 numberOfChannels: 1, 13 }, 14 chunks: [ 15 {offset: 44, size: 241}, 16 {offset: 285, size: 273}, 17 {offset: 558, size: 251}, 18 {offset: 809, size: 118}, 19 {offset: 927, size: 223}, 20 {offset: 1150, size: 141}, 21 {offset: 1291, size: 217}, 22 {offset: 1508, size: 159}, 23 {offset: 1667, size: 209}, 24 {offset: 1876, size: 176}, 25 ], 26 duration: 21333 27 }; 28 29 // Allows mutating `callbacks` after constructing the AudioDecoder, wraps calls 30 // in t.step(). 31 function createAudioDecoder(t, callbacks) { 32 return new AudioDecoder({ 33 output(frame) { 34 if (callbacks && callbacks.output) { 35 t.step(() => callbacks.output(frame)); 36 } else { 37 t.unreached_func('unexpected output()'); 38 } 39 }, 40 error(e) { 41 if (callbacks && callbacks.error) { 42 t.step(() => callbacks.error(e)); 43 } else { 44 t.unreached_func('unexpected error()'); 45 } 46 } 47 }); 48 } 49 50 // Create a view of an ArrayBuffer. 51 function view(buffer, {offset, size}) { 52 return new Uint8Array(buffer, offset, size); 53 } 54 55 let CONFIG = null; 56 let CHUNK_DATA = null; 57 let CHUNKS = null; 58 promise_setup(async () => { 59 const data = { 60 '?mp4_raw_aac_no_desc': MP4_AAC_DATA_NO_DESCRIPTION, 61 }[location.search]; 62 63 // Don't run any tests if the codec is not supported. 64 assert_equals("function", typeof AudioDecoder.isConfigSupported); 65 let supported = false; 66 try { 67 const support = await AudioDecoder.isConfigSupported({ 68 codec: data.config.codec, 69 sampleRate: data.config.sampleRate, 70 numberOfChannels: data.config.numberOfChannels 71 }); 72 supported = support.supported; 73 } catch (e) { 74 } 75 assert_implements_optional(supported, data.config.codec + ' unsupported'); 76 77 // Fetch the media data and prepare buffers. 78 const response = await fetch(data.src); 79 const buf = await response.arrayBuffer(); 80 81 CONFIG = {...data.config}; 82 if (data.config.description) { 83 CONFIG.description = view(buf, data.config.description); 84 } 85 86 CHUNK_DATA = []; 87 // For PCM, split in chunks of 1200 bytes and compute the rest 88 if (data.chunks.length == 0) { 89 let offset = data.offset; 90 // 1200 is divisible by 2 and 3 and is a plausible packet length 91 // for PCM: this means that there won't be samples split in two packet 92 let PACKET_LENGTH = 1200; 93 let bytesPerSample = 0; 94 switch (data.config.codec) { 95 case "pcm-s16": bytesPerSample = 2; break; 96 case "pcm-s24": bytesPerSample = 3; break; 97 case "pcm-s32": bytesPerSample = 4; break; 98 case "pcm-f32": bytesPerSample = 4; break; 99 default: bytesPerSample = 1; break; 100 } 101 while (offset < buf.byteLength) { 102 let size = Math.min(buf.byteLength - offset, PACKET_LENGTH); 103 assert_equals(size % bytesPerSample, 0); 104 CHUNK_DATA.push(view(buf, {offset, size})); 105 offset += size; 106 } 107 data.duration = 1000 * 1000 * PACKET_LENGTH / data.config.sampleRate / bytesPerSample; 108 } else { 109 CHUNK_DATA = data.chunks.map((chunk, i) => view(buf, chunk)); 110 } 111 112 CHUNKS = CHUNK_DATA.map((encodedData, i) => new EncodedAudioChunk({ 113 type: 'key', 114 timestamp: i * data.duration, 115 duration: data.duration, 116 data: encodedData 117 })); 118 }); 119 120 promise_test(t => { 121 return AudioDecoder.isConfigSupported(CONFIG); 122 }, 'Test isConfigSupported()'); 123 124 promise_test(t => { 125 // Define a valid config that includes a hypothetical 'futureConfigFeature', 126 // which is not yet recognized by the User Agent. 127 const validConfig = { 128 ...CONFIG, 129 futureConfigFeature: 'foo', 130 }; 131 132 // The UA will evaluate validConfig as being "valid", ignoring the 133 // `futureConfigFeature` it doesn't recognize. 134 return AudioDecoder.isConfigSupported(validConfig).then((decoderSupport) => { 135 // AudioDecoderSupport must contain the following properites. 136 assert_true(decoderSupport.hasOwnProperty('supported')); 137 assert_true(decoderSupport.hasOwnProperty('config')); 138 139 // AudioDecoderSupport.config must not contain unrecognized properties. 140 assert_false(decoderSupport.config.hasOwnProperty('futureConfigFeature')); 141 142 // AudioDecoderSupport.config must contiain the recognized properties. 143 assert_equals(decoderSupport.config.codec, validConfig.codec); 144 assert_equals(decoderSupport.config.sampleRate, validConfig.sampleRate); 145 assert_equals( 146 decoderSupport.config.numberOfChannels, validConfig.numberOfChannels); 147 148 if (validConfig.description) { 149 // The description must be copied. 150 assert_false( 151 decoderSupport.config.description === validConfig.description, 152 'description is unique'); 153 assert_array_equals( 154 new Uint8Array(decoderSupport.config.description, 0), 155 new Uint8Array(validConfig.description, 0), 'description'); 156 } else { 157 assert_false( 158 decoderSupport.config.hasOwnProperty('description'), 'description'); 159 } 160 }); 161 }, 'Test that AudioDecoder.isConfigSupported() returns a parsed configuration'); 162 163 promise_test(async t => { 164 const decoder = createAudioDecoder(t); 165 decoder.configure(CONFIG); 166 assert_equals(decoder.state, 'configured', 'state'); 167 }, 'Test configure()'); 168 169 promise_test(t => { 170 const decoder = createAudioDecoder(t); 171 return testClosedCodec(t, decoder, CONFIG, CHUNKS[0]); 172 }, 'Verify closed AudioDecoder operations'); 173 174 promise_test(async t => { 175 const callbacks = {}; 176 const decoder = createAudioDecoder(t, callbacks); 177 178 let outputs = 0; 179 callbacks.output = frame => { 180 outputs++; 181 frame.close(); 182 }; 183 184 decoder.configure(CONFIG); 185 CHUNKS.forEach(chunk => { 186 decoder.decode(chunk); 187 }); 188 189 await decoder.flush(); 190 assert_equals(outputs, CHUNKS.length, 'outputs'); 191 }, 'Test decoding'); 192 193 promise_test(async t => { 194 const callbacks = {}; 195 const decoder = createAudioDecoder(t, callbacks); 196 197 let outputs = 0; 198 callbacks.output = frame => { 199 outputs++; 200 frame.close(); 201 }; 202 203 decoder.configure(CONFIG); 204 decoder.decode(new EncodedAudioChunk( 205 {type: 'key', timestamp: -42, data: CHUNK_DATA[0]})); 206 207 await decoder.flush(); 208 assert_equals(outputs, 1, 'outputs'); 209 }, 'Test decoding a with negative timestamp'); 210 211 promise_test(async t => { 212 const callbacks = {}; 213 const decoder = createAudioDecoder(t, callbacks); 214 215 let outputs = 0; 216 callbacks.output = frame => { 217 outputs++; 218 frame.close(); 219 }; 220 221 decoder.configure(CONFIG); 222 decoder.decode(CHUNKS[0]); 223 224 await decoder.flush(); 225 assert_equals(outputs, 1, 'outputs'); 226 227 decoder.decode(CHUNKS[0]); 228 await decoder.flush(); 229 assert_equals(outputs, 2, 'outputs'); 230 }, 'Test decoding after flush'); 231 232 promise_test(async t => { 233 const callbacks = {}; 234 const decoder = createAudioDecoder(t, callbacks); 235 236 decoder.configure(CONFIG); 237 decoder.decode(CHUNKS[0]); 238 decoder.decode(CHUNKS[1]); 239 const flushDone = decoder.flush(); 240 241 // Wait for the first output, then reset. 242 let outputs = 0; 243 await new Promise(resolve => { 244 callbacks.output = frame => { 245 outputs++; 246 assert_equals(outputs, 1, 'outputs'); 247 decoder.reset(); 248 frame.close(); 249 resolve(); 250 }; 251 }); 252 253 // Flush should have been synchronously rejected. 254 await promise_rejects_dom(t, 'AbortError', flushDone); 255 256 assert_equals(outputs, 1, 'outputs'); 257 }, 'Test reset during flush'); 258 259 promise_test(async t => { 260 const callbacks = {}; 261 const decoder = createAudioDecoder(t, callbacks); 262 263 // No decodes yet. 264 assert_equals(decoder.decodeQueueSize, 0); 265 266 decoder.configure(CONFIG); 267 268 // Still no decodes. 269 assert_equals(decoder.decodeQueueSize, 0); 270 271 let lastDequeueSize = Infinity; 272 decoder.ondequeue = () => { 273 assert_greater_than(lastDequeueSize, 0, "Dequeue event after queue empty"); 274 assert_greater_than(lastDequeueSize, decoder.decodeQueueSize, 275 "Dequeue event without decreased queue size"); 276 lastDequeueSize = decoder.decodeQueueSize; 277 }; 278 279 for (let chunk of CHUNKS) 280 decoder.decode(chunk); 281 282 assert_greater_than_equal(decoder.decodeQueueSize, 0); 283 assert_less_than_equal(decoder.decodeQueueSize, CHUNKS.length); 284 285 await decoder.flush(); 286 // We can guarantee that all decodes are processed after a flush. 287 assert_equals(decoder.decodeQueueSize, 0); 288 // Last dequeue event should fire when the queue is empty. 289 assert_equals(lastDequeueSize, 0); 290 291 // Reset this to Infinity to track the decline of queue size for this next 292 // batch of decodes. 293 lastDequeueSize = Infinity; 294 295 for (let chunk of CHUNKS) 296 decoder.decode(chunk); 297 298 assert_greater_than_equal(decoder.decodeQueueSize, 0); 299 decoder.reset(); 300 assert_equals(decoder.decodeQueueSize, 0); 301 }, 'AudioDecoder decodeQueueSize test');