summaryrefslogtreecommitdiffstats
path: root/dom/media/test/eme_standalone.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/test/eme_standalone.js')
-rw-r--r--dom/media/test/eme_standalone.js286
1 files changed, 286 insertions, 0 deletions
diff --git a/dom/media/test/eme_standalone.js b/dom/media/test/eme_standalone.js
new file mode 100644
index 0000000000..202259b7fa
--- /dev/null
+++ b/dom/media/test/eme_standalone.js
@@ -0,0 +1,286 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file offers standalone (no dependencies on other files) EME test
+// helpers. The intention is that this file can be used to provide helpers
+// while not coupling tests as tightly to `dom/media/test/manifest.js` or other
+// files. This allows these helpers to be used in different tests across the
+// codebase without imports becoming a mess.
+
+// A helper class to assist in setting up EME on media.
+//
+// Usage
+// 1. First configure the EME helper so it can have the information needed
+// to setup EME correctly. This is done by setting
+// - keySystem via `SetKeySystem`.
+// - initDataTypes via `SetInitDataTypes`.
+// - audioCapabilities and/or videoCapabilities via `SetAudioCapabilities`
+// and/or `SetVideoCapabilities`.
+// - keyIds and keys via `AddKeyIdAndKey`.
+// - onerror should be set to a function that will handle errors from the
+// helper. This function should take one argument, the error.
+// 2. Use the helper to configure a media element via `ConfigureEme`.
+// 3. One the promise from `ConfigureEme` has resolved the media element should
+// be configured and can be played. Errors that happen after this point are
+// reported via `onerror`.
+var EmeHelper = class EmeHelper {
+ // Members used to configure EME.
+ _keySystem;
+ _initDataTypes;
+ _audioCapabilities = [];
+ _videoCapabilities = [];
+
+ // Map of keyIds to keys.
+ _keyMap = new Map();
+
+ // Will be called if an error occurs during event handling. Users of the
+ // class should set a handler to be notified of errors.
+ onerror;
+
+ /**
+ * Get the clearkey key system string.
+ * @return The clearkey key system string.
+ */
+ static GetClearkeyKeySystemString() {
+ return "org.w3.clearkey";
+ }
+
+ // Begin conversion helpers.
+
+ /**
+ * Helper to convert Uint8Array into base64 using base64url alphabet, without
+ * padding.
+ * @param uint8Array An array of bytes to convert to base64.
+ * @return A base 64 encoded string
+ */
+ static Uint8ArrayToBase64(uint8Array) {
+ return new TextDecoder()
+ .decode(uint8Array)
+ .replace(/\+/g, "-") // Replace chars for base64url.
+ .replace(/\//g, "_")
+ .replace(/=*$/, ""); // Remove padding for base64url.
+ }
+
+ /**
+ * Helper to convert a hex string into base64 using base64url alphabet,
+ * without padding.
+ * @param hexString A string of hex characters.
+ * @return A base 64 encoded string
+ */
+ static HexToBase64(hexString) {
+ return btoa(
+ hexString
+ .match(/\w{2}/g) // Take chars two by two.
+ // Map to characters.
+ .map(hexByte => String.fromCharCode(parseInt(hexByte, 16)))
+ .join("")
+ )
+ .replace(/\+/g, "-") // Replace chars for base64url.
+ .replace(/\//g, "_")
+ .replace(/=*$/, ""); // Remove padding for base64url.
+ }
+
+ /**
+ * Helper to convert a base64 string (base64 or base64url) into a hex string.
+ * @param base64String A base64 encoded string. This can be base64url.
+ * @return A hex string (lower case);
+ */
+ static Base64ToHex(base64String) {
+ let binString = atob(base64String.replace(/-/g, "+").replace(/_/g, "/"));
+ let hexString = "";
+ for (let i = 0; i < binString.length; i++) {
+ // Covert to hex char. The "0" + and substr code are used to ensure we
+ // always get 2 chars, even for outputs the would normally be only one.
+ // E.g. for charcode 14 we'd get output 'e', and want to buffer that
+ // to '0e'.
+ hexString += ("0" + binString.charCodeAt(i).toString(16)).substr(-2);
+ }
+ // EMCA spec says that the num -> string conversion is lower case, so our
+ // hex string should already be lower case.
+ // https://tc39.es/ecma262/#sec-number.prototype.tostring
+ return hexString;
+ }
+
+ // End conversion helpers.
+
+ // Begin setters that setup the helper.
+ // These should be used to configure the helper prior to calling
+ // `ConfigureEme`.
+
+ /**
+ * Sets the key system that will be used by the EME helper.
+ * @param keySystem The key system to use. Probably "org.w3.clearkey", which
+ * can be fetched via `GetClearkeyKeySystemString`.
+ */
+ SetKeySystem(keySystem) {
+ this._keySystem = keySystem;
+ }
+
+ /**
+ * Sets the init data types that will be used by the EME helper. This is used
+ * when calling `navigator.requestMediaKeySystemAccess`.
+ * @param initDataTypes A list containing the init data types to be set by
+ * the helper. This will usually be ["cenc"] or ["webm"], see
+ * https://www.w3.org/TR/eme-initdata-registry/ for more info on what these
+ * mean.
+ */
+ SetInitDataTypes(initDataTypes) {
+ this._initDataTypes = initDataTypes;
+ }
+
+ /**
+ * Sets the audio capabilities that will be used by the EME helper. These are
+ * used when calling `navigator.requestMediaKeySystemAccess`.
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
+ * for more info on these.
+ * @param audioCapabilities A list containing audio capabilities. E.g.
+ * [{ contentType: 'audio/webm; codecs="opus"' }].
+ */
+ SetAudioCapabilities(audioCapabilities) {
+ this._audioCapabilities = audioCapabilities;
+ }
+
+ /**
+ * Sets the video capabilities that will be used by the EME helper. These are
+ * used when calling `navigator.requestMediaKeySystemAccess`.
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
+ * for more info on these.
+ * @param videoCapabilities A list containing video capabilities. E.g.
+ * [{ contentType: 'video/webm; codecs="vp9"' }]
+ */
+ SetVideoCapabilities(videoCapabilities) {
+ this._videoCapabilities = videoCapabilities;
+ }
+
+ /**
+ * Adds a key id and key pair to the key map. These should both be hex
+ * strings. E.g.
+ * emeHelper.AddKeyIdAndKey(
+ * "2cdb0ed6119853e7850671c3e9906c3c",
+ * "808b9adac384de1e4f56140f4ad76194"
+ * );
+ * This function will store the keyId and key in lower case to ensure
+ * consistency internally.
+ * @param keyId The key id used to lookup the following key.
+ * @param key The key associated with the earlier key id.
+ */
+ AddKeyIdAndKey(keyId, key) {
+ this._keyMap.set(keyId.toLowerCase(), key.toLowerCase());
+ }
+
+ /**
+ * Removes a key id and its associate key from the key map.
+ * @param keyId The key id to remove.
+ */
+ RemoveKeyIdAndKey(keyId) {
+ this._keyMap.delete(keyId);
+ }
+
+ // End setters that setup the helper.
+
+ /**
+ * Internal handler for `session.onmessage`. When calling this either do so
+ * from inside an arrow function or using `bind` to ensure `this` points to
+ * an EmeHelper instance (rather than a session).
+ * @param messageEvent The message event passed to `session.onmessage`.
+ */
+ _SessionMessageHandler(messageEvent) {
+ // This handles a session message and generates a clearkey license based
+ // on the information in this._keyMap. This is done by populating the
+ // appropriate keys on the session based on the keyIds surfaced in the
+ // session message (a license request).
+ let request = JSON.parse(new TextDecoder().decode(messageEvent.message));
+
+ let keys = [];
+ for (const keyId of request.kids) {
+ let id64 = keyId;
+ let idHex = EmeHelper.Base64ToHex(keyId);
+ let key = this._keyMap.get(idHex);
+
+ if (key) {
+ keys.push({
+ kty: "oct",
+ kid: id64,
+ k: EmeHelper.HexToBase64(key),
+ });
+ }
+ }
+
+ let license = new TextEncoder().encode(
+ JSON.stringify({
+ keys,
+ type: request.type || "temporary",
+ })
+ );
+
+ let session = messageEvent.target;
+ session.update(license).catch(error => {
+ if (this.onerror) {
+ this.onerror(error);
+ } else {
+ console.log(
+ `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}`
+ );
+ }
+ });
+ }
+
+ /**
+ * Configures EME on a media element using the parameters already set on the
+ * instance of EmeHelper.
+ * @param htmlMediaElement - A media element to configure EME on.
+ * @return A promise that will be resolved once the media element is
+ * configured. This promise will be rejected with an error if configuration
+ * fails.
+ */
+ async ConfigureEme(htmlMediaElement) {
+ if (!this._keySystem) {
+ throw new Error("EmeHelper needs _keySystem to configure media");
+ }
+ if (!this._initDataTypes) {
+ throw new Error("EmeHelper needs _initDataTypes to configure media");
+ }
+ if (!this._audioCapabilities.length && !this._videoCapabilities.length) {
+ throw new Error(
+ "EmeHelper needs _audioCapabilities or _videoCapabilities to configure media"
+ );
+ }
+ const options = [
+ {
+ initDataTypes: this._initDataTypes,
+ audioCapabilities: this._audioCapabilities,
+ videoCapabilities: this._videoCapabilities,
+ },
+ ];
+ let access = await window.navigator.requestMediaKeySystemAccess(
+ this._keySystem,
+ options
+ );
+ let mediaKeys = await access.createMediaKeys();
+ await htmlMediaElement.setMediaKeys(mediaKeys);
+
+ htmlMediaElement.onencrypted = async encryptedEvent => {
+ let session = htmlMediaElement.mediaKeys.createSession();
+ // Use arrow notation so that `this` is the EmeHelper in the message
+ // handler. If we do `session.onmessage = this._SessionMessageHandler`
+ // then `this` will be the session in the callback.
+ session.onmessage = messageEvent =>
+ this._SessionMessageHandler(messageEvent);
+ try {
+ await session.generateRequest(
+ encryptedEvent.initDataType,
+ encryptedEvent.initData
+ );
+ } catch (error) {
+ if (this.onerror) {
+ this.onerror(error);
+ } else {
+ console.log(
+ `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}`
+ );
+ }
+ }
+ };
+ }
+};