eme_standalone.js (10230B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // This file offers standalone (no dependencies on other files) EME test 6 // helpers. The intention is that this file can be used to provide helpers 7 // while not coupling tests as tightly to `dom/media/test/manifest.js` or other 8 // files. This allows these helpers to be used in different tests across the 9 // codebase without imports becoming a mess. 10 11 // A helper class to assist in setting up EME on media. 12 // 13 // Usage 14 // 1. First configure the EME helper so it can have the information needed 15 // to setup EME correctly. This is done by setting 16 // - keySystem via `SetKeySystem`. 17 // - initDataTypes via `SetInitDataTypes`. 18 // - audioCapabilities and/or videoCapabilities via `SetAudioCapabilities` 19 // and/or `SetVideoCapabilities`. 20 // - keyIds and keys via `AddKeyIdAndKey`. 21 // - onerror should be set to a function that will handle errors from the 22 // helper. This function should take one argument, the error. 23 // 2. Use the helper to configure a media element via `ConfigureEme`. 24 // 3. One the promise from `ConfigureEme` has resolved the media element should 25 // be configured and can be played. Errors that happen after this point are 26 // reported via `onerror`. 27 var EmeHelper = class EmeHelper { 28 // Members used to configure EME. 29 _keySystem; 30 _initDataTypes; 31 _audioCapabilities = []; 32 _videoCapabilities = []; 33 34 // Map of keyIds to keys. 35 _keyMap = new Map(); 36 37 // Will be called if an error occurs during event handling. Users of the 38 // class should set a handler to be notified of errors. 39 onerror; 40 41 /** 42 * Get the clearkey key system string. 43 * 44 * @return The clearkey key system string. 45 */ 46 static GetClearkeyKeySystemString() { 47 return "org.w3.clearkey"; 48 } 49 50 // Begin conversion helpers. 51 52 /** 53 * Helper to convert Uint8Array into base64 using base64url alphabet, without 54 * padding. 55 * 56 * @param uint8Array An array of bytes to convert to base64. 57 * @return A base 64 encoded string 58 */ 59 static Uint8ArrayToBase64(uint8Array) { 60 return new TextDecoder() 61 .decode(uint8Array) 62 .replace(/\+/g, "-") // Replace chars for base64url. 63 .replace(/\//g, "_") 64 .replace(/=*$/, ""); // Remove padding for base64url. 65 } 66 67 /** 68 * Helper to convert a hex string into base64 using base64url alphabet, 69 * without padding. 70 * 71 * @param hexString A string of hex characters. 72 * @return A base 64 encoded string 73 */ 74 static HexToBase64(hexString) { 75 return btoa( 76 hexString 77 .match(/\w{2}/g) // Take chars two by two. 78 // Map to characters. 79 .map(hexByte => String.fromCharCode(parseInt(hexByte, 16))) 80 .join("") 81 ) 82 .replace(/\+/g, "-") // Replace chars for base64url. 83 .replace(/\//g, "_") 84 .replace(/=*$/, ""); // Remove padding for base64url. 85 } 86 87 /** 88 * Helper to convert a base64 string (base64 or base64url) into a hex string. 89 * 90 * @param base64String A base64 encoded string. This can be base64url. 91 * @return A hex string (lower case); 92 */ 93 static Base64ToHex(base64String) { 94 let binString = atob(base64String.replace(/-/g, "+").replace(/_/g, "/")); 95 let hexString = ""; 96 for (let i = 0; i < binString.length; i++) { 97 // Covert to hex char. The "0" + and substr code are used to ensure we 98 // always get 2 chars, even for outputs the would normally be only one. 99 // E.g. for charcode 14 we'd get output 'e', and want to buffer that 100 // to '0e'. 101 hexString += ("0" + binString.charCodeAt(i).toString(16)).substr(-2); 102 } 103 // EMCA spec says that the num -> string conversion is lower case, so our 104 // hex string should already be lower case. 105 // https://tc39.es/ecma262/#sec-number.prototype.tostring 106 return hexString; 107 } 108 109 // End conversion helpers. 110 111 // Begin setters that setup the helper. 112 // These should be used to configure the helper prior to calling 113 // `ConfigureEme`. 114 115 /** 116 * Sets the key system that will be used by the EME helper. 117 * 118 * @param keySystem The key system to use. Probably "org.w3.clearkey", which 119 * can be fetched via `GetClearkeyKeySystemString`. 120 */ 121 SetKeySystem(keySystem) { 122 this._keySystem = keySystem; 123 } 124 125 /** 126 * Sets the init data types that will be used by the EME helper. This is used 127 * when calling `navigator.requestMediaKeySystemAccess`. 128 * 129 * @param initDataTypes A list containing the init data types to be set by 130 * the helper. This will usually be ["cenc"] or ["webm"], see 131 * https://www.w3.org/TR/eme-initdata-registry/ for more info on what these 132 * mean. 133 */ 134 SetInitDataTypes(initDataTypes) { 135 this._initDataTypes = initDataTypes; 136 } 137 138 /** 139 * Sets the audio capabilities that will be used by the EME helper. These are 140 * used when calling `navigator.requestMediaKeySystemAccess`. 141 * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess 142 * for more info on these. 143 * 144 * @param audioCapabilities A list containing audio capabilities. E.g. 145 * [{ contentType: 'audio/webm; codecs="opus"' }]. 146 */ 147 SetAudioCapabilities(audioCapabilities) { 148 this._audioCapabilities = audioCapabilities; 149 } 150 151 /** 152 * Sets the video capabilities that will be used by the EME helper. These are 153 * used when calling `navigator.requestMediaKeySystemAccess`. 154 * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess 155 * for more info on these. 156 * 157 * @param videoCapabilities A list containing video capabilities. E.g. 158 * [{ contentType: 'video/webm; codecs="vp9"' }] 159 */ 160 SetVideoCapabilities(videoCapabilities) { 161 this._videoCapabilities = videoCapabilities; 162 } 163 164 /** 165 * Adds a key id and key pair to the key map. These should both be hex 166 * strings. E.g. 167 * ```js 168 * emeHelper.AddKeyIdAndKey( 169 * "2cdb0ed6119853e7850671c3e9906c3c", 170 * "808b9adac384de1e4f56140f4ad76194" 171 * ); 172 * ``` 173 * This function will store the keyId and key in lower case to ensure 174 * consistency internally. 175 * 176 * @param keyId The key id used to lookup the following key. 177 * @param key The key associated with the earlier key id. 178 */ 179 AddKeyIdAndKey(keyId, key) { 180 this._keyMap.set(keyId.toLowerCase(), key.toLowerCase()); 181 } 182 183 /** 184 * Removes a key id and its associate key from the key map. 185 * 186 * @param keyId The key id to remove. 187 */ 188 RemoveKeyIdAndKey(keyId) { 189 this._keyMap.delete(keyId); 190 } 191 192 // End setters that setup the helper. 193 194 /** 195 * Internal handler for `session.onmessage`. When calling this either do so 196 * from inside an arrow function or using `bind` to ensure `this` points to 197 * an EmeHelper instance (rather than a session). 198 * 199 * @param messageEvent The message event passed to `session.onmessage`. 200 */ 201 _SessionMessageHandler(messageEvent) { 202 // This handles a session message and generates a clearkey license based 203 // on the information in this._keyMap. This is done by populating the 204 // appropriate keys on the session based on the keyIds surfaced in the 205 // session message (a license request). 206 let request = JSON.parse(new TextDecoder().decode(messageEvent.message)); 207 208 let keys = []; 209 for (const keyId of request.kids) { 210 let id64 = keyId; 211 let idHex = EmeHelper.Base64ToHex(keyId); 212 let key = this._keyMap.get(idHex); 213 214 if (key) { 215 keys.push({ 216 kty: "oct", 217 kid: id64, 218 k: EmeHelper.HexToBase64(key), 219 }); 220 } 221 } 222 223 let license = new TextEncoder().encode( 224 JSON.stringify({ 225 keys, 226 type: request.type || "temporary", 227 }) 228 ); 229 230 let session = messageEvent.target; 231 session.update(license).catch(error => { 232 if (this.onerror) { 233 this.onerror(error); 234 } else { 235 console.log( 236 `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}` 237 ); 238 } 239 }); 240 } 241 242 /** 243 * Configures EME on a media element using the parameters already set on the 244 * instance of EmeHelper. 245 * 246 * @param htmlMediaElement - A media element to configure EME on. 247 * @return A promise that will be resolved once the media element is 248 * configured. This promise will be rejected with an error if configuration 249 * fails. 250 */ 251 async ConfigureEme(htmlMediaElement) { 252 if (!this._keySystem) { 253 throw new Error("EmeHelper needs _keySystem to configure media"); 254 } 255 if (!this._initDataTypes) { 256 throw new Error("EmeHelper needs _initDataTypes to configure media"); 257 } 258 if (!this._audioCapabilities.length && !this._videoCapabilities.length) { 259 throw new Error( 260 "EmeHelper needs _audioCapabilities or _videoCapabilities to configure media" 261 ); 262 } 263 const options = [ 264 { 265 initDataTypes: this._initDataTypes, 266 audioCapabilities: this._audioCapabilities, 267 videoCapabilities: this._videoCapabilities, 268 }, 269 ]; 270 let access = await window.navigator.requestMediaKeySystemAccess( 271 this._keySystem, 272 options 273 ); 274 let mediaKeys = await access.createMediaKeys(); 275 await htmlMediaElement.setMediaKeys(mediaKeys); 276 277 htmlMediaElement.onencrypted = async encryptedEvent => { 278 let session = htmlMediaElement.mediaKeys.createSession(); 279 // Use arrow notation so that `this` is the EmeHelper in the message 280 // handler. If we do `session.onmessage = this._SessionMessageHandler` 281 // then `this` will be the session in the callback. 282 session.onmessage = messageEvent => 283 this._SessionMessageHandler(messageEvent); 284 try { 285 await session.generateRequest( 286 encryptedEvent.initDataType, 287 encryptedEvent.initData 288 ); 289 } catch (error) { 290 if (this.onerror) { 291 this.onerror(error); 292 } else { 293 console.log( 294 `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}` 295 ); 296 } 297 } 298 }; 299 } 300 };