tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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');