eme.js (11930B)
1 /* import-globals-from manifest.js */ 2 3 const CLEARKEY_KEYSYSTEM = "org.w3.clearkey"; 4 5 const gCencMediaKeySystemConfig = [ 6 { 7 initDataTypes: ["cenc"], 8 videoCapabilities: [{ contentType: "video/mp4" }], 9 audioCapabilities: [{ contentType: "audio/mp4" }], 10 }, 11 ]; 12 13 function bail(message) { 14 return function (err) { 15 if (err) { 16 message += "; " + String(err); 17 } 18 ok(false, message); 19 if (err) { 20 info(String(err)); 21 } 22 SimpleTest.finish(); 23 }; 24 } 25 26 function ArrayBufferToString(arr) { 27 var str = ""; 28 var view = new Uint8Array(arr); 29 for (var i = 0; i < view.length; i++) { 30 str += String.fromCharCode(view[i]); 31 } 32 return str; 33 } 34 35 function StringToArrayBuffer(str) { 36 var arr = new ArrayBuffer(str.length); 37 var view = new Uint8Array(arr); 38 for (var i = 0; i < str.length; i++) { 39 view[i] = str.charCodeAt(i); 40 } 41 return arr; 42 } 43 44 function StringToHex(str) { 45 var res = ""; 46 for (var i = 0; i < str.length; ++i) { 47 res += ("0" + str.charCodeAt(i).toString(16)).slice(-2); 48 } 49 return res; 50 } 51 52 function Base64ToHex(str) { 53 var bin = window.atob(str.replace(/-/g, "+").replace(/_/g, "/")); 54 var res = ""; 55 for (var i = 0; i < bin.length; i++) { 56 res += ("0" + bin.charCodeAt(i).toString(16)).substr(-2); 57 } 58 return res; 59 } 60 61 function HexToBase64(hex) { 62 var bin = ""; 63 for (var i = 0; i < hex.length; i += 2) { 64 bin += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 65 } 66 return window 67 .btoa(bin) 68 .replace(/=/g, "") 69 .replace(/\+/g, "-") 70 .replace(/\//g, "_"); 71 } 72 73 function TimeRangesToString(trs) { 74 var l = trs.length; 75 if (l === 0) { 76 return "-"; 77 } 78 var s = ""; 79 var i = 0; 80 for (;;) { 81 s += trs.start(i) + "-" + trs.end(i); 82 if (++i === l) { 83 return s; 84 } 85 s += ","; 86 } 87 } 88 89 function SourceBufferToString(sb) { 90 return ( 91 "SourceBuffer{" + 92 "AppendMode=" + 93 (sb.AppendMode || "-") + 94 ", updating=" + 95 (sb.updating ? "true" : "false") + 96 ", buffered=" + 97 TimeRangesToString(sb.buffered) + 98 ", audioTracks=" + 99 (sb.audioTracks ? sb.audioTracks.length : "-") + 100 ", videoTracks=" + 101 (sb.videoTracks ? sb.videoTracks.length : "-") + 102 "}" 103 ); 104 } 105 106 function SourceBufferListToString(sbl) { 107 return "SourceBufferList[" + sbl.map(SourceBufferToString).join(", ") + "]"; 108 } 109 110 function GenerateClearKeyLicense(licenseRequest, keyStore) { 111 var msgStr = ArrayBufferToString(licenseRequest); 112 var msg = JSON.parse(msgStr); 113 114 var keys = []; 115 for (var i = 0; i < msg.kids.length; i++) { 116 var id64 = msg.kids[i]; 117 var idHex = Base64ToHex(msg.kids[i]).toLowerCase(); 118 var key = keyStore[idHex]; 119 120 if (key) { 121 keys.push({ 122 kty: "oct", 123 kid: id64, 124 k: HexToBase64(key), 125 }); 126 } 127 } 128 129 return new TextEncoder().encode( 130 JSON.stringify({ 131 keys, 132 type: msg.type || "temporary", 133 }) 134 ); 135 } 136 137 function UpdateSessionFunc(test, token, sessionType, resolve, reject) { 138 return function (ev) { 139 var license = GenerateClearKeyLicense(ev.message, test.keys); 140 Log( 141 token, 142 "sending update message to CDM: " + new TextDecoder().decode(license) 143 ); 144 ev.target 145 .update(license) 146 .then(function () { 147 Log(token, "MediaKeySession update ok!"); 148 resolve(ev.target); 149 }) 150 .catch(function (reason) { 151 reject(`${token} MediaKeySession update failed: ${reason}`); 152 }); 153 }; 154 } 155 156 function MaybeCrossOriginURI(test, uri) { 157 if (test.crossOrigin) { 158 return "https://example.com:443/tests/dom/media/test/allowed.sjs?" + uri; 159 } 160 return uri; 161 } 162 163 function AppendTrack(test, ms, track, token) { 164 return new Promise(function (resolve, reject) { 165 var sb; 166 var curFragment = 0; 167 var fragments = track.fragments; 168 var fragmentFile; 169 170 function addNextFragment() { 171 if (curFragment >= fragments.length) { 172 Log(token, track.name + ": end of track"); 173 resolve(); 174 return; 175 } 176 177 fragmentFile = MaybeCrossOriginURI(test, fragments[curFragment++]); 178 179 var req = new XMLHttpRequest(); 180 req.open("GET", fragmentFile); 181 req.responseType = "arraybuffer"; 182 183 req.addEventListener("load", function () { 184 Log( 185 token, 186 track.name + ": fetch of " + fragmentFile + " complete, appending" 187 ); 188 sb.appendBuffer(new Uint8Array(req.response)); 189 }); 190 191 req.addEventListener("error", function () { 192 reject(`${token} - ${track.name}: error fetching ${fragmentFile}`); 193 }); 194 req.addEventListener("abort", function () { 195 reject(`${token} - ${track.name}: aborted fetching ${fragmentFile}`); 196 }); 197 198 Log( 199 token, 200 track.name + 201 ": addNextFragment() fetching next fragment " + 202 fragmentFile 203 ); 204 req.send(null); 205 } 206 207 Log(token, track.name + ": addSourceBuffer(" + track.type + ")"); 208 sb = ms.addSourceBuffer(track.type); 209 sb.addEventListener("updateend", function () { 210 Log( 211 token, 212 track.name + 213 ": updateend for " + 214 fragmentFile + 215 ", " + 216 SourceBufferToString(sb) 217 ); 218 addNextFragment(); 219 }); 220 221 addNextFragment(); 222 }); 223 } 224 225 //Returns a promise that is resolved when the media element is ready to have 226 //its play() function called; when it's loaded MSE fragments. 227 function LoadTest(test, elem, token, endOfStream = true) { 228 if (!test.tracks) { 229 ok(false, token + " test does not have a tracks list"); 230 return Promise.reject(); 231 } 232 233 var ms = new MediaSource(); 234 elem.src = URL.createObjectURL(ms); 235 elem.crossOrigin = test.crossOrigin || false; 236 237 return new Promise(function (resolve, reject) { 238 ms.addEventListener( 239 "sourceopen", 240 function () { 241 Log(token, "sourceopen"); 242 Promise.all( 243 test.tracks.map(function (track) { 244 return AppendTrack(test, ms, track, token); 245 }) 246 ) 247 .then(function () { 248 Log(token, "Tracks loaded, calling MediaSource.endOfStream()"); 249 if (endOfStream) { 250 ms.endOfStream(); 251 } 252 resolve(); 253 }) 254 .catch(reject); 255 }, 256 { once: true } 257 ); 258 }); 259 } 260 261 function EMEPromise() { 262 var self = this; 263 self.promise = new Promise(function (resolve, reject) { 264 self.resolve = resolve; 265 self.reject = reject; 266 }); 267 } 268 269 /* 270 * Create a new MediaKeys object. 271 * Return a promise which will be resolved with a new MediaKeys object, 272 * or will be rejected with a string that describes the failure. 273 */ 274 function CreateMediaKeys(v, test, token) { 275 let p = new EMEPromise(); 276 277 function streamType(type) { 278 var x = test.tracks.find(o => o.name == type); 279 return x ? x.type : undefined; 280 } 281 282 function onencrypted(ev) { 283 var options = { initDataTypes: [ev.initDataType] }; 284 if (streamType("video")) { 285 options.videoCapabilities = [{ contentType: streamType("video") }]; 286 } 287 if (streamType("audio")) { 288 options.audioCapabilities = [{ contentType: streamType("audio") }]; 289 } 290 navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, [options]).then( 291 keySystemAccess => { 292 keySystemAccess 293 .createMediaKeys() 294 .then(p.resolve, () => 295 p.reject(`${token} Failed to create MediaKeys object.`) 296 ); 297 }, 298 () => p.reject(`${token} Failed to request key system access.`) 299 ); 300 } 301 302 v.addEventListener("encrypted", onencrypted, { once: true }); 303 return p.promise; 304 } 305 306 /* 307 * Create a new MediaKeys object and provide it to the media element. 308 * Return a promise which will be resolved if succeeded, or will be rejected 309 * with a string that describes the failure. 310 */ 311 function CreateAndSetMediaKeys(v, test, token) { 312 let p = new EMEPromise(); 313 314 CreateMediaKeys(v, test, token).then(mediaKeys => { 315 v.setMediaKeys(mediaKeys).then(p.resolve, () => 316 p.reject(`${token} Failed to set MediaKeys on <video> element.`) 317 ); 318 }, p.reject); 319 320 return p.promise; 321 } 322 323 /* 324 * Collect the init data from 'encrypted' events. 325 * Return a promise which will be resolved with the init data when collection 326 * is completed (specified by test.sessionCount). 327 */ 328 function LoadInitData(v, test, token) { 329 let p = new EMEPromise(); 330 let initDataQueue = []; 331 332 // Call SimpleTest._originalSetTimeout() to bypass the flaky timeout checker. 333 let timer = SimpleTest._originalSetTimeout.call( 334 window, 335 () => { 336 p.reject(`${token} Timed out in waiting for the init data.`); 337 }, 338 60000 339 ); 340 341 function onencrypted(ev) { 342 initDataQueue.push(ev); 343 Log( 344 token, 345 `got encrypted(${ev.initDataType}, ` + 346 `${StringToHex(ArrayBufferToString(ev.initData))}) event.` 347 ); 348 if (test.sessionCount == initDataQueue.length) { 349 p.resolve(initDataQueue); 350 clearTimeout(timer); 351 } 352 } 353 354 v.addEventListener("encrypted", onencrypted); 355 return p.promise; 356 } 357 358 /* 359 * Generate a license request and update the session. 360 * Return a promsise which will be resolved with the updated session 361 * or rejected with a string that describes the failure. 362 */ 363 function MakeRequest(test, token, ev, session, sessionType) { 364 sessionType = sessionType || "temporary"; 365 let p = new EMEPromise(); 366 let str = 367 `session[${session.sessionId}].generateRequest(` + 368 `${ev.initDataType}, ${StringToHex(ArrayBufferToString(ev.initData))})`; 369 370 session.addEventListener( 371 "message", 372 UpdateSessionFunc(test, token, sessionType, p.resolve, p.reject) 373 ); 374 375 Log(token, str); 376 session.generateRequest(ev.initDataType, ev.initData).catch(reason => { 377 // Reject the promise if generateRequest() failed. 378 // Otherwise it will be resolved in UpdateSessionFunc(). 379 p.reject(`${token}: ${str} failed; ${reason}`); 380 }); 381 382 return p.promise; 383 } 384 385 /* 386 * Process the init data by calling MakeRequest(). 387 * Return a promise which will be resolved with the updated sessions 388 * when all init data are processed or rejected if any failure. 389 */ 390 function ProcessInitData(v, test, token, initData, sessionType) { 391 return Promise.all( 392 initData.map(ev => { 393 let session = v.mediaKeys.createSession(sessionType); 394 return MakeRequest(test, token, ev, session, sessionType); 395 }) 396 ); 397 } 398 399 /* 400 * Clean up the |v| element. 401 */ 402 function CleanUpMedia(v) { 403 v.setMediaKeys(null); 404 v.remove(); 405 v.removeAttribute("src"); 406 v.load(); 407 } 408 409 /* 410 * Close all sessions and clean up the |v| element. 411 */ 412 function CloseSessions(v, sessions) { 413 return Promise.all(sessions.map(s => s.close())).then(CleanUpMedia(v)); 414 } 415 416 /* 417 * Set up media keys and source buffers for the media element. 418 * Return a promise resolved when all key sessions are updated or rejected 419 * if any failure. 420 */ 421 function SetupEME(v, test, token) { 422 let p = new EMEPromise(); 423 424 v.onerror = function () { 425 p.reject(`${token} got an error event.`); 426 }; 427 428 Promise.all([ 429 LoadInitData(v, test, token), 430 CreateAndSetMediaKeys(v, test, token), 431 LoadTest(test, v, token), 432 ]) 433 .then(values => { 434 let initData = values[0]; 435 return ProcessInitData(v, test, token, initData); 436 }) 437 .then(p.resolve, p.reject); 438 439 return p.promise; 440 } 441 442 function fetchWithXHR(uri, onLoadFunction) { 443 var p = new Promise(function (resolve) { 444 var xhr = new XMLHttpRequest(); 445 xhr.open("GET", uri, true); 446 xhr.responseType = "arraybuffer"; 447 xhr.addEventListener("load", function () { 448 is( 449 xhr.status, 450 200, 451 "fetchWithXHR load uri='" + uri + "' status=" + xhr.status 452 ); 453 resolve(xhr.response); 454 }); 455 xhr.send(); 456 }); 457 458 if (onLoadFunction) { 459 p.then(onLoadFunction); 460 } 461 462 return p; 463 } 464 465 function once(target, name, cb) { 466 var p = new Promise(function (resolve) { 467 target.addEventListener( 468 name, 469 function (arg) { 470 resolve(arg); 471 }, 472 { once: true } 473 ); 474 }); 475 if (cb) { 476 p.then(cb); 477 } 478 return p; 479 }