mediasource-worker-handle-transfer.html (12426B)
1 <!DOCTYPE html> 2 <html> 3 <title>Test MediaSourceHandle transfer characteristics</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="mediasource-message-util.js"></script> 7 <body> 8 <script> 9 10 function assert_mseiw_supported() { 11 // Fail fast if MSE-in-Workers is not supported. 12 assert_true( 13 MediaSource.hasOwnProperty('canConstructInDedicatedWorker'), 14 'MediaSource hasOwnProperty \'canConstructInDedicatedWorker\''); 15 assert_true( 16 MediaSource.canConstructInDedicatedWorker, 17 'MediaSource.canConstructInDedicatedWorker'); 18 assert_true( 19 window.hasOwnProperty('MediaSourceHandle'), 20 'window must have MediaSourceHandle visibility'); 21 } 22 23 function get_handle_from_new_worker( 24 t, script = 'mediasource-worker-handle-transfer-to-main.js') { 25 return new Promise((r) => { 26 let worker = new Worker(script); 27 worker.addEventListener('message', t.step_func(e => { 28 let subject = e.data.subject; 29 assert_true(subject != undefined, 'message must have a subject field'); 30 switch (subject) { 31 case messageSubject.ERROR: 32 assert_unreached('Worker error: ' + e.data.info); 33 break; 34 case messageSubject.HANDLE: 35 const handle = e.data.info; 36 assert_not_equals( 37 handle, null, 'must have a non-null MediaSourceHandle'); 38 r({worker, handle}); 39 break; 40 default: 41 assert_unreached('Unexpected message subject: ' + subject); 42 } 43 })); 44 }); 45 } 46 47 promise_test(async t => { 48 assert_mseiw_supported(); 49 let {worker, handle} = await get_handle_from_new_worker(t); 50 assert_true( 51 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 52 assert_throws_dom('DataCloneError', function() { 53 worker.postMessage(handle); 54 }, 'serializing handle without transfer'); 55 }, 'MediaSourceHandle serialization without transfer must fail, tested in window context'); 56 57 promise_test(async t => { 58 assert_mseiw_supported(); 59 let {worker, handle} = await get_handle_from_new_worker(t); 60 assert_true( 61 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 62 assert_throws_dom('DataCloneError', function() { 63 worker.postMessage(handle, [handle, handle]); 64 }, 'transferring same handle more than once in same postMessage'); 65 }, 'Same MediaSourceHandle transferred multiple times in single postMessage must fail, tested in window context'); 66 67 promise_test(async t => { 68 assert_mseiw_supported(); 69 let {worker, handle} = await get_handle_from_new_worker(t); 70 assert_true( 71 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 72 73 // Transferring handle to worker without including it in the message is still 74 // a valid transfer, though the recipient will not be able to obtain the 75 // handle itself. Regardless, the handle in this sender's context will be 76 // detached. 77 worker.postMessage(null, [handle]); 78 79 assert_throws_dom('DataCloneError', function() { 80 worker.postMessage(null, [handle]); 81 }, 'transferring handle that was already detached should fail'); 82 83 assert_throws_dom('DataCloneError', function() { 84 worker.postMessage(handle, [handle]); 85 }, 'transferring handle that was already detached should fail, even if this time it\'s included in the message'); 86 }, 'Attempt to transfer detached MediaSourceHandle must fail, tested in window context'); 87 88 promise_test(async t => { 89 assert_mseiw_supported(); 90 let {worker, handle} = await get_handle_from_new_worker(t); 91 assert_true( 92 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 93 94 let video = document.createElement('video'); 95 document.body.appendChild(video); 96 video.srcObject = handle; 97 98 assert_throws_dom('DataCloneError', function() { 99 worker.postMessage(handle, [handle]); 100 }, 'transferring handle that is currently srcObject fails'); 101 assert_equals(video.srcObject, handle); 102 103 // Clear |handle| from being the srcObject value. 104 video.srcObject = null; 105 106 assert_throws_dom('DataCloneError', function() { 107 worker.postMessage(handle, [handle]); 108 }, 'transferring handle that was briefly srcObject before srcObject was reset to null should also fail'); 109 assert_equals(video.srcObject, null); 110 }, 'MediaSourceHandle cannot be transferred, immediately after set as srcObject, even if srcObject immediately reset to null'); 111 112 promise_test(async t => { 113 assert_mseiw_supported(); 114 let {worker, handle} = await get_handle_from_new_worker(t); 115 assert_true( 116 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 117 118 let video = document.createElement('video'); 119 document.body.appendChild(video); 120 video.srcObject = handle; 121 assert_not_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING); 122 // Initial step of resource selection algorithm sets networkState to 123 // NETWORK_NO_SOURCE. networkState only becomes NETWORK_LOADING after stable 124 // state awaited and resource selection algorithm continues with, in this 125 // case, an assigned media provider object (which is the MediaSource 126 // underlying the handle). 127 assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE); 128 129 // Wait until 'loadstart' media element event is dispatched. 130 await new Promise((r) => { 131 video.addEventListener( 132 'loadstart', t.step_func(e => { 133 r(); 134 }), 135 {once: true}); 136 }); 137 assert_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING); 138 139 assert_throws_dom('DataCloneError', function() { 140 worker.postMessage(handle, [handle]); 141 }, 'transferring handle that is currently srcObject, after loadstart, fails'); 142 assert_equals(video.srcObject, handle); 143 144 // Clear |handle| from being the srcObject value. 145 video.srcObject = null; 146 147 assert_throws_dom('DataCloneError', function() { 148 worker.postMessage(handle, [handle]); 149 }, 'transferring handle that was srcObject until \'loadstart\' when srcObject was reset to null should also fail'); 150 assert_equals(video.srcObject, null); 151 }, 'MediaSourceHandle cannot be transferred, if it was srcObject when asynchronous load starts (loadstart), even if srcObject is then immediately reset to null'); 152 153 promise_test(async t => { 154 assert_mseiw_supported(); 155 let {worker, handle} = await get_handle_from_new_worker(t); 156 assert_true( 157 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 158 159 let video = document.createElement('video'); 160 document.body.appendChild(video); 161 162 // Transfer the handle away so that our instance of it is detached. 163 worker.postMessage(null, [handle]); 164 165 // Now assign handle to srcObject to attempt load. 'loadstart' event should 166 // occur, but then media element error should occur due to failure to attach 167 // to the underlying MediaSource of a detached MediaSourceHandle. 168 169 video.srcObject = handle; 170 assert_equals( 171 video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE, 172 'before async load start, networkState should be NETWORK_NO_SOURCE'); 173 174 // Before 'loadstart' dispatch, we don't expect the media element error. 175 video.onerror = t.unreached_func( 176 'Error is unexpected before \'loadstart\' event dispatch'); 177 178 // Wait until 'loadstart' media element event is dispatched. 179 await new Promise((r) => { 180 video.addEventListener( 181 'loadstart', t.step_func(e => { 182 r(); 183 }), 184 {once: true}); 185 }); 186 187 // Now wait until 'error' media element event is dispatched. 188 video.onerror = null; 189 await new Promise((r) => { 190 video.addEventListener( 191 'error', t.step_func(e => { 192 r(); 193 }), 194 {once: true}); 195 }); 196 197 // Confirm expected error and states resulting from the "dedicated media 198 // source failure steps": 199 // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps 200 let e = video.error; 201 assert_true(e instanceof MediaError); 202 assert_equals(e.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); 203 assert_equals( 204 video.readyState, HTMLMediaElement.HAVE_NOTHING, 205 'load failure should occur long before parsing any appended metadata.'); 206 assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE); 207 208 // Even if the handle is detached and attempt to load it failed, the handle is 209 // still detached, and as well, has also been used as srcObject now. Re-verify 210 // that such a handle instance must fail transfer attempt. 211 assert_throws_dom('DataCloneError', function() { 212 worker.postMessage(handle, [handle]); 213 }, 'transferring detached handle that is currently srcObject, after loadstart and load failure, fails'); 214 assert_equals(video.srcObject, handle); 215 216 // Clear |handle| from being the srcObject value. 217 video.srcObject = null; 218 219 assert_throws_dom('DataCloneError', function() { 220 worker.postMessage(handle, [handle]); 221 }, 'transferring detached handle that was srcObject until \'loadstart\' and load failure when srcObject was reset to null should also fail'); 222 assert_equals(video.srcObject, null); 223 }, 'A detached (already transferred away) MediaSourceHandle cannot successfully load when assigned to srcObject'); 224 225 promise_test(async t => { 226 assert_mseiw_supported(); 227 // Get a handle from a worker that is prepared to buffer real media once its 228 // MediaSource instance attaches and 'sourceopen' is dispatched. Unlike 229 // earlier cases in this file, we need positive indication from precisely one 230 // of multiple media elements that the attachment and playback succeeded. 231 let {worker, handle} = 232 await get_handle_from_new_worker(t, 'mediasource-worker-play.js'); 233 assert_true( 234 handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle'); 235 236 let videos = []; 237 const NUM_ELEMENTS = 5; 238 for (let i = 0; i < NUM_ELEMENTS; ++i) { 239 let v = document.createElement('video'); 240 videos.push(v); 241 document.body.appendChild(v); 242 } 243 244 await new Promise((r) => { 245 let errors = 0; 246 let endeds = 0; 247 248 // Setup handlers to expect precisely 1 ended and N-1 errors. 249 videos.forEach((v) => { 250 v.addEventListener( 251 'error', t.step_func(e => { 252 // Confirm expected error and states resulting from the "dedicated 253 // media source failure steps": 254 // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps 255 let err = v.error; 256 assert_true(err instanceof MediaError); 257 assert_equals(err.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); 258 assert_equals( 259 v.readyState, HTMLMediaElement.HAVE_NOTHING, 260 'load failure should occur long before parsing any appended metadata.'); 261 assert_equals(v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE); 262 263 errors++; 264 if (errors + endeds == videos.length && endeds == 1) 265 r(); 266 }), 267 {once: true}); 268 v.addEventListener( 269 'ended', t.step_func(e => { 270 endeds++; 271 if (errors + endeds == videos.length && endeds == 1) 272 r(); 273 }), 274 {once: true}); 275 v.srcObject = handle; 276 assert_equals( 277 v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE, 278 'before async load start, networkState should be NETWORK_NO_SOURCE'); 279 }); 280 281 let playPromises = []; 282 videos.forEach((v) => { 283 playPromises.push(v.play()); 284 }); 285 286 // Ignore playPromise success/rejection, if any. 287 playPromises.forEach((p) => { 288 if (p !== undefined) { 289 p.then(_ => {}).catch(_ => {}); 290 } 291 }); 292 }); 293 294 // Once the handle has been assigned as srcObject, it must fail transfer 295 // steps. 296 assert_throws_dom('DataCloneError', function() { 297 worker.postMessage(handle, [handle]); 298 }, 'transferring handle that is currently srcObject on multiple elements, fails'); 299 videos.forEach((v) => { 300 assert_equals(v.srcObject, handle); 301 v.srcObject = null; 302 }); 303 304 assert_throws_dom('DataCloneError', function() { 305 worker.postMessage(handle, [handle]); 306 }, 'transferring handle that was srcObject on multiple elements, then was unset on them, should also fail'); 307 videos.forEach((v) => { 308 assert_equals(v.srcObject, null); 309 }); 310 }, 'Precisely one load of the same MediaSourceHandle assigned synchronously to multiple media element srcObjects succeeds'); 311 312 fetch_tests_from_worker(new Worker('mediasource-worker-handle-transfer.js')); 313 314 </script> 315 </body> 316 </html>