tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };