error-sequence.html (13554B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Test sequence of effects of errors 5 </title> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="/media-source/mediasource-util.js"></script> 9 </head> 10 <body> 11 </body> 12 <script> 13 'use strict'; 14 15 function create_audio(t) { 16 const audio = document.createElement('audio'); 17 audio.controls = true; 18 audio.watcher = new EventWatcher( 19 t, audio, 20 [ 21 'loadstart', 22 'waiting', 23 'error', 24 'ended', 25 'loadedmetadata', 26 'canplay', 27 'volumechange', 28 'playing', 29 'pause', 30 ]); 31 document.body.appendChild(audio); 32 return audio; 33 } 34 35 promise_test(async t => { 36 const audio = create_audio(t); 37 audio.src = ''; 38 assert_equals(audio.error, null, 'initial error attribute'); 39 // Queue a volumechange event on the media element task source. 40 audio.volume = 0; 41 // The dedicated media source failure steps are described as queued, but 42 // browsers do not make state changes asynchronously. 43 // https://github.com/whatwg/html/issues/11155 44 audio.onvolumechange = t.step_func(() => { 45 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 46 'error code'); 47 // Queue a second volumechange. This arrives after the error event 48 // because the error event is queued immediately after the resource 49 // selection algorithm synchronous steps. 50 audio.volume = 1; 51 }); 52 await audio.watcher.wait_for( 53 ['volumechange', 'loadstart', 'error', 'volumechange']); 54 }, 'empty src attribute'); 55 56 promise_test(async t => { 57 const audio = create_audio(t); 58 // src is such that "the result of encoding-parsing a URL" is failure. 59 audio.src = 'https://#fragment'; 60 assert_equals(audio.error, null, 'initial error attribute'); 61 // Queue a volumechange event on the media element task source. 62 audio.volume = 0; 63 // The dedicated media source failure steps are described as queued from 64 // parallel steps in the resource selection algorithm, but browsers do not 65 // make state changes asynchronously, and they queue the error event 66 // immediately after the resource selection algorithm synchronous steps. 67 // https://github.com/whatwg/html/issues/11155 68 audio.onvolumechange = t.step_func(() => { 69 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 70 'error code'); 71 audio.volume = 1; 72 }); 73 await audio.watcher.wait_for( 74 ['volumechange', 'loadstart', 'error', 'volumechange']); 75 }, 'urlRecord failure'); 76 77 let resource; 78 promise_test(async t => { 79 resource = await MediaSourceUtil.fetchResourceOfManifest( 80 t, 81 '/media-source/webm/test-a-128k-44100Hz-1ch-manifest.json'); 82 }, 'fetch resource'); 83 84 async function create_audio_with_source_buffer(t) { 85 const audio = create_audio(t); 86 87 audio.source = new MediaSource(); 88 audio.source.watcher = new EventWatcher(t, audio.source, ['sourceopen']); 89 audio.src = URL.createObjectURL(audio.source); 90 await audio.watcher.wait_for('loadstart'); 91 await audio.source.watcher.wait_for('sourceopen'); 92 93 assert_implements_optional(MediaSource.isTypeSupported(resource.type), 94 `${resource.type} supported`); 95 96 audio.buffer = audio.source.addSourceBuffer(resource.type); 97 assert_equals(audio.buffer.mode, 'segments', 98 `${resource.type} buffer.mode`); 99 audio.buffer.watcher = 100 new EventWatcher(t, audio.buffer, ['updateend']); 101 return audio; 102 } 103 104 // While different browsers pass different HAVE_NOTHING subtests, the four 105 // subtests are helpful to identify the different interactions. 106 107 promise_test(async t => { 108 const audio = await create_audio_with_source_buffer(t); 109 assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState'); 110 111 // Queue a volumechange event on the media element task source to check that 112 // the event named 'error' is fired from the same task source. 113 audio.volume = 0; 114 audio.source.endOfStream("decode"); 115 audio.volume = 1; 116 await audio.watcher.wait_for(['volumechange', 'error', 'volumechange']); 117 }, 'error event while HAVE_NOTHING'); 118 119 // This subtest is arranged to demonstrate that the specification does not 120 // describe what browsers do. Please do not adjust implementations to make 121 // this pass as https://github.com/whatwg/html/issues/11155 proposes changing 122 // the spec. 123 promise_test(async t => { 124 const audio = await create_audio_with_source_buffer(t); 125 assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState'); 126 127 // Queue a volumechange event on the media element task source 128 audio.volume = 0; 129 audio.source.endOfStream("decode"); 130 // The dedicated media source failure steps are described as queued so state 131 // would not change until the task runs. 132 await audio.watcher.wait_for('volumechange'); 133 assert_equals(audio.error, null, 'error attribute'); 134 await audio.watcher.wait_for('error'); 135 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 136 'error code'); 137 }, 'error attribute while HAVE_NOTHING'); 138 139 // This subtest is arranged to demonstrate that the specification does not 140 // describe what browsers do. Please do not adjust implementations to make 141 // this pass as https://github.com/whatwg/html/issues/11155 proposes changing 142 // the spec. 143 promise_test(async t => { 144 const audio = await create_audio_with_source_buffer(t); 145 assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState'); 146 147 const play_promise = audio.play(); 148 await audio.watcher.wait_for('waiting'); 149 assert_false(audio.paused, 'paused attribute'); 150 151 // 'error event while HAVE_NOTHING' checks the order of events. 152 audio.watcher.stop_watching(); 153 154 // Queue a volumechange event on the media element task source to see 155 // whether the play promise is rejected from a task on same task source. 156 audio.volume = 0; 157 audio.source.endOfStream("decode"); 158 audio.volume = 1; 159 const sequence = []; 160 const events_promise = new Promise(resolve => { 161 audio.onvolumechange = t.step_func(() => { 162 sequence.push('volumechange'); 163 if (sequence.filter(_ => _ == 'volumechange').length == 2) { 164 resolve(); 165 } 166 }); 167 }); 168 try { 169 await play_promise; 170 assert_unreached('promise should reject'); 171 } catch { 172 sequence.push('rejection'); 173 } 174 await events_promise; 175 assert_array_equals(sequence, ['volumechange', 'rejection', 'volumechange'], 176 'sequence'); 177 }, 'play() promise while HAVE_NOTHING'); 178 179 // This subtest is arranged to demonstrate inconsistencies between 180 // implementations. Please do not adjust implementations to make this pass as 181 // https://github.com/whatwg/html/issues/11155 proposes changing the spec. 182 promise_test(async t => { 183 const audio = await create_audio_with_source_buffer(t); 184 assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState'); 185 186 const play_promise = audio.play(); 187 await audio.watcher.wait_for('waiting'); 188 assert_false(audio.paused, 'paused attribute'); 189 190 // 'error event while HAVE_NOTHING' checks the order of events. 191 audio.watcher.stop_watching(); 192 193 // The resource selection algorithm describes the dedicated media source 194 // failure steps as queued and the event named "error" as fired 195 // synchronously from those steps. 196 // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-algorithm 197 // That is not what browsers do, but, as described, the error event would 198 // arrive before the pending play promise rejection. 199 audio.source.endOfStream("decode"); 200 const sequence = []; 201 const event_promise = new Promise(resolve => { 202 audio.onerror = t.step_func(() => { 203 sequence.push('event'); 204 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 205 'error code on event'); 206 resolve(); 207 }); 208 }); 209 try { 210 await play_promise; 211 assert_unreached('promise should reject'); 212 } catch { 213 sequence.push('rejection'); 214 } 215 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 216 'error code on rejection'); 217 await event_promise; 218 assert_array_equals(sequence, ['event', 'rejection'], 'sequence'); 219 }, 'play() promise after error event while HAVE_NOTHING'); 220 221 promise_test(async t => { 222 const audio = await create_audio_with_source_buffer(t); 223 // Truncate at the end of the metadata. 224 audio.buffer.appendBuffer( 225 resource.data.subarray(0, resource.cluster_start[0])); 226 await Promise.all([ 227 audio.watcher.wait_for('loadedmetadata'), 228 audio.buffer.watcher.wait_for('updateend'), 229 ]); 230 assert_equals(audio.readyState, audio.HAVE_METADATA, 'loadedmetadata'); 231 232 const play_promise1 = audio.play(); 233 await audio.watcher.wait_for('waiting'); 234 assert_false(audio.paused, 'paused attribute'); 235 236 let settled = 'NOT SETTLED'; 237 play_promise1.catch(_ => _).then(_ => settled = _); 238 239 assert_equals(audio.error, null, 'error attribute'); 240 // Trigger "If the media data is corrupted" in the media data processing 241 // steps list. 242 // https://html.spec.whatwg.org/multipage/media.html#media-data-processing-steps-list 243 audio.source.endOfStream("decode"); 244 // The error event is described as firing synchronously during endOfStream(), 245 // but no browsers do this. https://github.com/whatwg/html/issues/11155 246 await audio.watcher.wait_for('error'); 247 // The error attribute should be set synchronously, but this checked late 248 // just for Blink. 249 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code'); 250 // The end of stream algorithm does not change duration on error 251 // https://w3c.github.io/media-source/#dfn-end-of-stream 252 assert_equals(audio.duration, 2.023, 'duration'); 253 // MEDIA_ERR_DECODE does not reject the pending play promise 254 // https://github.com/whatwg/html/issues/505#issuecomment-178046408 255 // because playback is sometimes possible after such errors, 256 // https://github.com/whatwg/html/pull/509#issuecomment-174967812 257 // as in the 'error after HAVE_FUTURE_DATA' subtest below. 258 // Trigger volumechange for media element task source tasks. 259 // Await 2 tasks to check that the play() promise is not about to be 260 // rejected. 261 // 2 is the number of tasks necessary to wait for the spurious promise 262 // rejection with Blink. 263 for (const i of Array(2).keys()) { 264 audio.volume = i % 2; 265 await audio.watcher.wait_for('volumechange'); 266 } 267 assert_equals(settled, 'NOT SETTLED', 'play(() promise should not settle'); 268 269 // Check that the promise is rejected when appropriate. 270 audio.pause(); 271 const sequence = []; 272 const play_promise2 = new Promise(resolve => { 273 audio.onpause = () => { 274 sequence.push('pause'); 275 if (sequence.filter(_ => _ == 'pause').length == 2) { 276 audio.onpause = null; 277 return; 278 } 279 resolve(audio.play()); 280 audio.pause(); 281 } 282 }); 283 audio.onwaiting = () => { 284 sequence.push('waiting'); 285 } 286 assert_true(audio.paused, 'paused attribute'); 287 await Promise.all([ 288 audio.watcher.wait_for(['pause', 'waiting', 'pause']), 289 play_promise1.catch(() => sequence.push('promise1')), 290 play_promise2.catch(() => sequence.push('promise2')), 291 ]); 292 assert_array_equals(sequence, 293 ['pause', 'promise1', 'waiting', 'pause', 'promise2'], 294 'sequence'); 295 promise_rejects_dom( 296 t, 'AbortError', play_promise1, 'play promise rejection'); 297 }, 'error event while HAVE_METADATA'); 298 299 async function create_audio_with_full_resource(t) { 300 const audio = await create_audio_with_source_buffer(t); 301 // Just to reduce sound impacts 302 audio.volume = 0; 303 audio.buffer.appendBuffer(resource.data); 304 await Promise.all([ 305 audio.watcher.wait_for(['volumechange', 'loadedmetadata', 'canplay']), 306 audio.buffer.watcher.wait_for('updateend'), 307 ]); 308 assert_greater_than(audio.readyState, audio.HAVE_CURRENT_DATA, 309 'readyState'); 310 311 assert_equals(audio.error, null, 'error attribute'); 312 return audio; 313 } 314 315 promise_test(async t => { 316 const audio = await create_audio_with_full_resource(t); 317 audio.source.endOfStream("decode"); 318 // The error event is specified to fire synchronously during endOfStream(), 319 // but no browsers do this. https://github.com/whatwg/html/issues/11155 320 await audio.watcher.wait_for('error'); 321 // The error attribute should be set synchronously, but this is checked late 322 // just for Blink. It is checked synchronously in the 'error attribute 323 // after DECODE_ERROR' subtest below. 324 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code'); 325 326 const sequence = []; 327 const play_promise1 = audio.play().then(() => sequence.push('promise1')); 328 const play_promise2 = new Promise(resolve => { 329 audio.onplaying = () => { 330 sequence.push('event'); 331 resolve(audio.play().then(() => sequence.push('promise2'))); 332 }; 333 }); 334 assert_false(audio.paused, 'paused attribute'); 335 await Promise.all([ 336 audio.watcher.wait_for('playing'), 337 play_promise1, 338 play_promise2, 339 ]); 340 assert_array_equals(sequence, ['event', 'promise1', 'promise2'], 'sequence'); 341 }, 'error event after HAVE_FUTURE_DATA'); 342 343 // This subtest could be merged into 'error event after HAVE_FUTURE_DATA' and 344 // 'error event while HAVE_METADATA' if/when Blink conforms with the synchronous 345 // attribute change. 346 promise_test(async t => { 347 const audio = await create_audio_with_full_resource(t); 348 audio.source.endOfStream("decode"); 349 // The error attribute is set synchronously. 350 assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code'); 351 }, 'error attribute after DECODE_ERROR'); 352 </script> 353 </html>