tor-browser

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

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 }