audioDecoder-codec-specific.https.any.js (14192B)
1 // META: global=window,dedicatedworker 2 // META: script=/webcodecs/utils.js 3 // META: variant=?adts_aac 4 // META: variant=?mp4_aac 5 // META: variant=?mp3 6 // META: variant=?opus 7 // META: variant=?pcm_alaw 8 // META: variant=?pcm_ulaw 9 // META: variant=?pcm_u8 10 // META: variant=?pcm_s16 11 // META: variant=?pcm_s24 12 // META: variant=?pcm_s32 13 // META: variant=?pcm_f32 14 // META: variant=?flac 15 // META: variant=?vorbis 16 17 const ADTS_AAC_DATA = { 18 src: 'sfx.adts', 19 config: { 20 codec: 'mp4a.40.2', 21 sampleRate: 48000, 22 numberOfChannels: 1, 23 }, 24 chunks: [ 25 {offset: 0, size: 248}, {offset: 248, size: 280}, {offset: 528, size: 258}, 26 {offset: 786, size: 125}, {offset: 911, size: 230}, 27 {offset: 1141, size: 148}, {offset: 1289, size: 224}, 28 {offset: 1513, size: 166}, {offset: 1679, size: 216}, 29 {offset: 1895, size: 183} 30 ], 31 duration: 24000 32 }; 33 34 const MP3_DATA = { 35 src: 'sfx.mp3', 36 config: { 37 codec: 'mp3', 38 sampleRate: 48000, 39 numberOfChannels: 1, 40 }, 41 chunks: [ 42 {offset: 333, size: 288}, {offset: 621, size: 288}, 43 {offset: 909, size: 288}, {offset: 1197, size: 288}, 44 {offset: 1485, size: 288}, {offset: 1773, size: 288}, 45 {offset: 2061, size: 288}, {offset: 2349, size: 288}, 46 {offset: 2637, size: 288}, {offset: 2925, size: 288} 47 ], 48 duration: 24000 49 }; 50 51 const MP4_AAC_DATA = { 52 src: 'sfx-aac.mp4', 53 config: { 54 codec: 'mp4a.40.2', 55 sampleRate: 48000, 56 numberOfChannels: 1, 57 description: {offset: 2552, size: 5}, 58 }, 59 chunks: [ 60 {offset: 44, size: 241}, 61 {offset: 285, size: 273}, 62 {offset: 558, size: 251}, 63 {offset: 809, size: 118}, 64 {offset: 927, size: 223}, 65 {offset: 1150, size: 141}, 66 {offset: 1291, size: 217}, 67 {offset: 1508, size: 159}, 68 {offset: 1667, size: 209}, 69 {offset: 1876, size: 176}, 70 ], 71 duration: 21333 72 }; 73 74 const OPUS_DATA = { 75 src: 'sfx-opus.ogg', 76 config: { 77 codec: 'opus', 78 sampleRate: 48000, 79 numberOfChannels: 1, 80 description: {offset: 28, size: 19}, 81 }, 82 chunks: [ 83 {offset: 185, size: 450}, {offset: 635, size: 268}, 84 {offset: 903, size: 285}, {offset: 1188, size: 296}, 85 {offset: 1484, size: 287}, {offset: 1771, size: 308}, 86 {offset: 2079, size: 289}, {offset: 2368, size: 286}, 87 {offset: 2654, size: 296}, {offset: 2950, size: 294} 88 ], 89 duration: 20000 90 }; 91 92 const FLAC_DATA = { 93 src: 'sfx.flac', 94 config: { 95 codec: 'flac', 96 sampleRate: 48000, 97 numberOfChannels: 1, 98 description: { offset: 0, size: 8287 } 99 }, 100 chunks: [ 101 { offset: 8288, size: 2276 }, 102 { offset: 10564, size: 2038 }, 103 { offset: 12602, size: 521 }, 104 ], 105 duration: 20000 106 }; 107 108 function pcm(codec, dataOffset) { 109 return { 110 src: `sfx-${codec}.wav`, 111 config: { 112 codec: codec, 113 sampleRate: 48000, 114 numberOfChannels: 1, 115 }, 116 117 // Chunk are arbitrary and will be generated lazily 118 chunks: [], 119 offset: dataOffset, 120 duration: 0 121 } 122 } 123 124 const PCM_ULAW_DATA = pcm("ulaw", 0x5c); 125 const PCM_ALAW_DATA = pcm("alaw", 0x5c); 126 const PCM_U8_DATA = pcm("pcm-u8", 0x4e); 127 const PCM_S16_DATA = pcm("pcm-s16", 0x4e); 128 const PCM_S24_DATA = pcm("pcm-s24", 0x66); 129 const PCM_S32_DATA = pcm("pcm-s32", 0x66); 130 const PCM_F32_DATA = pcm("pcm-f32", 0x72); 131 132 const VORBIS_DATA = { 133 src: 'sfx-vorbis.ogg', 134 config: { 135 codec: 'vorbis', 136 description: [ 137 2, 138 30, 139 62, 140 {offset: 28, size: 30}, 141 {offset: 101, size: 62}, 142 {offset: 163, size: 3771} 143 ], 144 numberOfChannels: 1, 145 sampleRate: 48000, 146 }, 147 chunks: [ 148 {offset: 3968, size: 44}, {offset: 4012, size: 21}, 149 {offset: 4033, size: 57}, {offset: 4090, size: 37}, 150 {offset: 4127, size: 37}, {offset: 4164, size: 107}, 151 {offset: 4271, size: 172} 152 ], 153 duration: 21333 154 }; 155 156 // Allows mutating `callbacks` after constructing the AudioDecoder, wraps calls 157 // in t.step(). 158 function createAudioDecoder(t, callbacks) { 159 return new AudioDecoder({ 160 output(frame) { 161 if (callbacks && callbacks.output) { 162 t.step(() => callbacks.output(frame)); 163 } else { 164 t.unreached_func('unexpected output()'); 165 } 166 }, 167 error(e) { 168 if (callbacks && callbacks.error) { 169 t.step(() => callbacks.error(e)); 170 } else { 171 t.unreached_func('unexpected error()'); 172 } 173 } 174 }); 175 } 176 177 // Create a view of an ArrayBuffer. 178 function view(buffer, {offset, size}) { 179 return new Uint8Array(buffer, offset, size); 180 } 181 182 let CONFIG = null; 183 let CHUNK_DATA = null; 184 let CHUNKS = null; 185 promise_setup(async () => { 186 const data = { 187 '?adts_aac': ADTS_AAC_DATA, 188 '?mp3': MP3_DATA, 189 '?mp4_aac': MP4_AAC_DATA, 190 '?opus': OPUS_DATA, 191 '?pcm_alaw': PCM_ALAW_DATA, 192 '?pcm_ulaw': PCM_ULAW_DATA, 193 '?pcm_u8': PCM_U8_DATA, 194 '?pcm_s16': PCM_S16_DATA, 195 '?pcm_s24': PCM_S24_DATA, 196 '?pcm_s32': PCM_S32_DATA, 197 '?pcm_f32': PCM_F32_DATA, 198 '?flac': FLAC_DATA, 199 '?vorbis': VORBIS_DATA, 200 }[location.search]; 201 202 // Don't run any tests if the codec is not supported. 203 assert_equals("function", typeof AudioDecoder.isConfigSupported); 204 let supported = false; 205 try { 206 const support = await AudioDecoder.isConfigSupported({ 207 codec: data.config.codec, 208 sampleRate: data.config.sampleRate, 209 numberOfChannels: data.config.numberOfChannels 210 }); 211 supported = support.supported; 212 } catch (e) { 213 } 214 assert_implements_optional(supported, data.config.codec + ' unsupported'); 215 216 // Fetch the media data and prepare buffers. 217 const response = await fetch(data.src); 218 const buf = await response.arrayBuffer(); 219 220 CONFIG = {...data.config}; 221 if (data.config.description) { 222 // The description for decoding vorbis is expected to be in Xiph extradata format. 223 // https://w3c.github.io/webcodecs/vorbis_codec_registration.html#audiodecoderconfig-description 224 if (Array.isArray(data.config.description)) { 225 const length = data.config.description.reduce((sum, value) => sum + ((typeof value === 'number') ? 1 : value.size), 0); 226 const description = new Uint8Array(length); 227 228 data.config.description.reduce((offset, value) => { 229 if (typeof value === 'number') { 230 description[offset] = value; 231 232 return offset + 1; 233 } 234 235 description.set(view(buf, value), offset); 236 237 return offset + value.size; 238 }, 0); 239 240 CONFIG.description = description; 241 } else { 242 CONFIG.description = view(buf, data.config.description); 243 } 244 } 245 246 CHUNK_DATA = []; 247 // For PCM, split in chunks of 1200 bytes and compute the rest 248 if (data.chunks.length == 0) { 249 let offset = data.offset; 250 // 1200 is divisible by 2 and 3 and is a plausible packet length 251 // for PCM: this means that there won't be samples split in two packet 252 let PACKET_LENGTH = 1200; 253 let bytesPerSample = 0; 254 switch (data.config.codec) { 255 case "pcm-s16": bytesPerSample = 2; break; 256 case "pcm-s24": bytesPerSample = 3; break; 257 case "pcm-s32": bytesPerSample = 4; break; 258 case "pcm-f32": bytesPerSample = 4; break; 259 default: bytesPerSample = 1; break; 260 } 261 while (offset < buf.byteLength) { 262 let size = Math.min(buf.byteLength - offset, PACKET_LENGTH); 263 assert_equals(size % bytesPerSample, 0); 264 CHUNK_DATA.push(view(buf, {offset, size})); 265 offset += size; 266 } 267 data.duration = 1000 * 1000 * PACKET_LENGTH / data.config.sampleRate / bytesPerSample; 268 } else { 269 CHUNK_DATA = data.chunks.map((chunk, i) => view(buf, chunk)); 270 } 271 272 CHUNKS = CHUNK_DATA.map((encodedData, i) => new EncodedAudioChunk({ 273 type: 'key', 274 timestamp: i * data.duration, 275 duration: data.duration, 276 data: encodedData 277 })); 278 }); 279 280 promise_test(t => { 281 return AudioDecoder.isConfigSupported(CONFIG); 282 }, 'Test isConfigSupported()'); 283 284 promise_test(t => { 285 // Define a valid config that includes a hypothetical 'futureConfigFeature', 286 // which is not yet recognized by the User Agent. 287 const validConfig = { 288 ...CONFIG, 289 futureConfigFeature: 'foo', 290 }; 291 292 // The UA will evaluate validConfig as being "valid", ignoring the 293 // `futureConfigFeature` it doesn't recognize. 294 return AudioDecoder.isConfigSupported(validConfig).then((decoderSupport) => { 295 // AudioDecoderSupport must contain the following properites. 296 assert_true(decoderSupport.hasOwnProperty('supported')); 297 assert_true(decoderSupport.hasOwnProperty('config')); 298 299 // AudioDecoderSupport.config must not contain unrecognized properties. 300 assert_false(decoderSupport.config.hasOwnProperty('futureConfigFeature')); 301 302 // AudioDecoderSupport.config must contiain the recognized properties. 303 assert_equals(decoderSupport.config.codec, validConfig.codec); 304 assert_equals(decoderSupport.config.sampleRate, validConfig.sampleRate); 305 assert_equals( 306 decoderSupport.config.numberOfChannels, validConfig.numberOfChannels); 307 308 if (validConfig.description) { 309 // The description must be copied. 310 assert_false( 311 decoderSupport.config.description === validConfig.description, 312 'description is unique'); 313 assert_array_equals( 314 new Uint8Array(decoderSupport.config.description, 0), 315 new Uint8Array(validConfig.description, 0), 'description'); 316 } else { 317 assert_false( 318 decoderSupport.config.hasOwnProperty('description'), 'description'); 319 } 320 }); 321 }, 'Test that AudioDecoder.isConfigSupported() returns a parsed configuration'); 322 323 promise_test(async t => { 324 const decoder = createAudioDecoder(t); 325 decoder.configure(CONFIG); 326 assert_equals(decoder.state, 'configured', 'state'); 327 }, 'Test configure()'); 328 329 promise_test(t => { 330 const decoder = createAudioDecoder(t); 331 return testClosedCodec(t, decoder, CONFIG, CHUNKS[0]); 332 }, 'Verify closed AudioDecoder operations'); 333 334 promise_test(async t => { 335 const callbacks = {}; 336 const decoder = createAudioDecoder(t, callbacks); 337 338 let outputs = 0; 339 callbacks.output = frame => { 340 outputs++; 341 frame.close(); 342 }; 343 344 decoder.configure(CONFIG); 345 CHUNKS.forEach(chunk => { 346 decoder.decode(chunk); 347 }); 348 349 await decoder.flush(); 350 assert_equals(outputs, CONFIG.codec === 'vorbis' ? CHUNKS.length - 1 : CHUNKS.length, 'outputs'); 351 }, 'Test decoding'); 352 353 promise_test(async t => { 354 const callbacks = {}; 355 const decoder = createAudioDecoder(t, callbacks); 356 357 let outputs = 0; 358 callbacks.output = frame => { 359 if (outputs === 0) { 360 assert_equals(frame.timestamp, -42); 361 } 362 outputs++; 363 frame.close(); 364 }; 365 366 decoder.configure(CONFIG); 367 decoder.decode(new EncodedAudioChunk( 368 {type: 'key', timestamp: -42, data: CHUNK_DATA[0]})); 369 decoder.decode(new EncodedAudioChunk( 370 {type: 'key', timestamp: CHUNKS[0].duration - 42, data: CHUNK_DATA[1]})); 371 372 await decoder.flush(); 373 assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); 374 }, 'Test decoding a with a negative timestamp'); 375 376 promise_test(async t => { 377 const callbacks = {}; 378 const decoder = createAudioDecoder(t, callbacks); 379 380 let outputs = 0; 381 callbacks.output = frame => { 382 if (outputs === 0) { 383 assert_equals(frame.timestamp, 42); 384 } 385 outputs++; 386 frame.close(); 387 }; 388 389 decoder.configure(CONFIG); 390 decoder.decode(new EncodedAudioChunk( 391 {type: 'key', timestamp: 42, data: CHUNK_DATA[0]})); 392 decoder.decode(new EncodedAudioChunk( 393 {type: 'key', timestamp: CHUNKS[0].duration + 42, data: CHUNK_DATA[1]})); 394 395 await decoder.flush(); 396 assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); 397 }, 'Test decoding a with a positive timestamp'); 398 399 promise_test(async t => { 400 const callbacks = {}; 401 const decoder = createAudioDecoder(t, callbacks); 402 403 let outputs = 0; 404 callbacks.output = frame => { 405 outputs++; 406 frame.close(); 407 }; 408 409 decoder.configure(CONFIG); 410 decoder.decode(CHUNKS[0]); 411 decoder.decode(CHUNKS[1]); 412 413 await decoder.flush(); 414 assert_equals(outputs, CONFIG.codec === 'vorbis' ? 1 : 2, 'outputs'); 415 416 decoder.decode(CHUNKS[2]); 417 await decoder.flush(); 418 assert_equals(outputs, CONFIG.codec === 'vorbis' ? 2 : 3, 'outputs'); 419 }, 'Test decoding after flush'); 420 421 promise_test(async t => { 422 const callbacks = {}; 423 const decoder = createAudioDecoder(t, callbacks); 424 425 decoder.configure(CONFIG); 426 decoder.decode(CHUNKS[0]); 427 decoder.decode(CHUNKS[1]); 428 const flushDone = decoder.flush(); 429 430 // Wait for the first output, then reset. 431 let outputs = 0; 432 await new Promise(resolve => { 433 callbacks.output = frame => { 434 outputs++; 435 assert_equals(outputs, 1, 'outputs'); 436 decoder.reset(); 437 frame.close(); 438 resolve(); 439 }; 440 }); 441 442 // Flush should have been synchronously rejected. 443 await promise_rejects_dom(t, 'AbortError', flushDone); 444 445 assert_equals(outputs, 1, 'outputs'); 446 }, 'Test reset during flush'); 447 448 promise_test(async t => { 449 const callbacks = {}; 450 const decoder = createAudioDecoder(t, callbacks); 451 452 // No decodes yet. 453 assert_equals(decoder.decodeQueueSize, 0); 454 455 decoder.configure(CONFIG); 456 457 // Still no decodes. 458 assert_equals(decoder.decodeQueueSize, 0); 459 460 let lastDequeueSize = Infinity; 461 decoder.ondequeue = () => { 462 assert_greater_than(lastDequeueSize, 0, "Dequeue event after queue empty"); 463 assert_greater_than(lastDequeueSize, decoder.decodeQueueSize, 464 "Dequeue event without decreased queue size"); 465 lastDequeueSize = decoder.decodeQueueSize; 466 }; 467 468 for (let chunk of CHUNKS) 469 decoder.decode(chunk); 470 471 assert_greater_than_equal(decoder.decodeQueueSize, 0); 472 assert_less_than_equal(decoder.decodeQueueSize, CHUNKS.length); 473 474 await decoder.flush(); 475 // We can guarantee that all decodes are processed after a flush. 476 assert_equals(decoder.decodeQueueSize, 0); 477 // Last dequeue event should fire when the queue is empty. 478 assert_equals(lastDequeueSize, 0); 479 480 // Reset this to Infinity to track the decline of queue size for this next 481 // batch of decodes. 482 lastDequeueSize = Infinity; 483 484 for (let chunk of CHUNKS) 485 decoder.decode(chunk); 486 487 assert_greater_than_equal(decoder.decodeQueueSize, 0); 488 decoder.reset(); 489 assert_equals(decoder.decodeQueueSize, 0); 490 }, 'AudioDecoder decodeQueueSize test');