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>