tor-browser

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

waiting-for-audio.html (6890B)


      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>Test playback after waiting for audio</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="mediasource-util.js"></script>
      8 </head>
      9 <body>
     10 </body>
     11 <script>
     12 // This test was designed to reproduce a bug in Gecko that occurred when a
     13 // queue of decoded buffered video data was drained quickly and buffered audio
     14 // was considered insufficient after playback was resumed.  The frame
     15 // durations are set very short to support this.
     16 // https://bugzilla.mozilla.org/show_bug.cgi?id=1915045
     17 'use strict';
     18 
     19 // Overwrite the timescales of a single segment resource to adjust frame
     20 // durations.
     21 function adjust_resource_for_timescale(resource) {
     22  MediaSourceUtil.WriteBigEndianInteger32ToUint8Array(
     23    resource.timescale,
     24    resource.data.subarray(resource.media_timescale_start));
     25  MediaSourceUtil.WriteBigEndianInteger32ToUint8Array(
     26    resource.timescale,
     27    resource.data.subarray(resource.segment_index_timescale_start));
     28 }
     29 
     30 async function append_resource_to_source_buffer(resource) {
     31  const source_buffer = resource.buffer;
     32  // Adjust so that the first video frame aligns with the end of the previous
     33  // append, or with zero if there has been no previous append.
     34  source_buffer.timestampOffset -= resource.initial_offset;
     35 
     36  source_buffer.appendBuffer(resource.data);
     37  await source_buffer.watcher.wait_for('updateend');
     38  assert_approx_equals(
     39    source_buffer.buffered.end(0),
     40    source_buffer.timestampOffset + resource.initial_offset + resource.duration,
     41    2e-6,
     42    `${resource.type} source_buffer.buffered.end()`);
     43  source_buffer.timestampOffset = source_buffer.buffered.end(0);
     44 }
     45 
     46 promise_test(async t => {
     47  const frames_per_keyframe = 8;
     48  const video = await new Promise(
     49    r => MediaSourceUtil.fetchManifestAndData(
     50      t,
     51      `mp4/test-v-128k-320x240-24fps-${frames_per_keyframe}kfr-manifest.json`,
     52      (type, data) => r({type, data})));
     53  {
     54    // Truncate at the end of the first segment, which is also the end of 8
     55    // frames.  At least 11 frames need to be available for decoding to
     56    // reproduce the Gecko bug.
     57    const first_segment_end = 0x1b1a;
     58    video.data = video.data.subarray(0, first_segment_end);
     59    // Video frame duration is 100 microseconds, short so that buffered frames
     60    // are drained quickly.  The audio and video timescales are easily
     61    // representable with unsigned 32-bit integers.
     62    const video_fps = 10e3;
     63    const default_sample_duration = 512;
     64    video.timescale = default_sample_duration * video_fps;
     65    video.duration = frames_per_keyframe / video_fps;
     66    const earliest_presentation_time = 1024;
     67    video.initial_offset =
     68      earliest_presentation_time / video.timescale;
     69    // Overwrite timescale to adjust frame durations.
     70    video.media_timescale_start = 0x182;
     71    video.segment_index_timescale_start = 0x353;
     72    adjust_resource_for_timescale(video);
     73  }
     74  const audio = await new Promise(
     75    r => MediaSourceUtil.fetchManifestAndData(
     76      t,
     77      `mp4/test-a-128k-44100Hz-1ch-manifest.json`,
     78      (type, data) => r({type, data})));
     79  {
     80    // Truncate at end of first segment, which is also the end of 10240 samples.
     81    const first_segment_end = 0x0830;
     82    audio.data = audio.data.subarray(0, first_segment_end);
     83 
     84    // The audio sample rate is increased so that Gecko considers a single
     85    // audio segment to be not enough, which is necessary to trigger the bug.
     86    audio.duration = video.duration;
     87    const subsegment_duration = 10240;
     88    audio.timescale = subsegment_duration / audio.duration;
     89    assert_equals(audio.timescale, Math.round(audio.timescale),
     90                  'integer timescale');
     91    audio.initial_offset = 0;
     92    // Overwrite timescale to adjust segment duration.
     93    audio.media_timescale_start = 0x17e;
     94    audio.segment_index_timescale_start = 0x30b;
     95    adjust_resource_for_timescale(audio);
     96  }
     97 
     98  const v = document.createElement('video');
     99  // Muting the audio output allows Gecko's playback position to advance a
    100  // little beyond the decoded audio, making the bug more likely to reproduce.
    101  v.volume = 0;
    102  v.watcher = new EventWatcher(t, v, ['waiting', 'error', 'ended']);
    103  document.body.appendChild(v);
    104  const media_source = new MediaSource();
    105  media_source.watcher = new EventWatcher(t, media_source, ['sourceopen']);
    106  v.src = URL.createObjectURL(media_source);
    107  await media_source.watcher.wait_for('sourceopen');
    108 
    109  function add_source_buffer(resource) {
    110    assert_implements_optional(MediaSource.isTypeSupported(resource.type),
    111                             `${resource.type} supported`);
    112 
    113    resource.buffer = media_source.addSourceBuffer(resource.type);
    114    assert_equals(resource.buffer.mode, 'segments',
    115                  `${resource.type} buffer.mode`);
    116    resource.buffer.watcher =
    117      new EventWatcher(t, resource.buffer, ['updateend']);
    118  }
    119  add_source_buffer(video);
    120  add_source_buffer(audio);
    121 
    122  async function append_until_canplay() {
    123    // Ensure 2 video segments to make available at least the 11 frames to
    124    // reproduce the Gecko bug.
    125    while (video.buffer.buffered.length == 0 ||
    126           video.buffer.buffered.end(0) <
    127           v.currentTime + 2 * video.duration) {
    128      await append_resource_to_source_buffer(video);
    129    }
    130 
    131    while (true) {
    132      if (audio.buffer.buffered.length == 0 ||
    133          audio.buffer.buffered.end(0) <
    134          video.buffer.buffered.end(0)) {
    135        await append_resource_to_source_buffer(audio);
    136      } else {
    137        await append_resource_to_source_buffer(video);
    138      }
    139 
    140      if (v.readyState >= v.HAVE_FUTURE_DATA) {
    141        return;
    142      }
    143      // A single append might not be sufficient because either
    144      // 1. the playback position had already advanced beyond the end of the
    145      //    newly appended data, or
    146      // 2. Chrome (as of version 131.0.6778.24) does not transition to
    147      //    >= HAVE_FUTURE_DATA / canplay on the first frame beyond
    148      //    currentTime, but on some additional number of extra frames.
    149      //
    150      // Or the v.readyState change might still be pending while the browser
    151      // is processing the newly appended data.  Instead of waiting an
    152      // arbitrary length of time to find out, append more data and try again.
    153    }
    154  }
    155 
    156  // Three iterations checks that playback resumes after the Gecko bug would
    157  // have occurred.
    158  for (const i of Array(3).keys()) {
    159    await append_until_canplay();
    160 
    161    audio.buffer.remove(0, Number.POSITIVE_INFINITY);
    162    await audio.buffer.watcher.wait_for('updateend');
    163    audio.buffer.timestampOffset = 0;
    164 
    165    v.play().catch(e => {});
    166    await v.watcher.wait_for('waiting');
    167    assert_less_than(v.readyState, v.HAVE_FUTURE_DATA,
    168                     `waiting ${i} at ${v.currentTime}`);
    169 
    170    v.pause();
    171  }
    172 }, 'playback after waiting for audio');
    173 </script>
    174 </html>