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