Skip to content

Commit

Permalink
DRM: Refacto implementation selection mechanism
Browse files Browse the repository at this point in the history
While debugging a safari issue that in the end seemed link to both DRM
and audio track selection of a directfile contents, we experimented with
the possibility for an application to choose between EME implementation.

For example, an application could decide on Safari to either rely on the
latest EME recommendation or on the still-available webkit-vendored API,
which are both incompatible to one another.

This was a feature we didn't really want, but what we thought we might
have to implement because Safari had issues with both API that could
lead to the impossibility to play the content, but the RxPlayer couldn't
determine which content would work best with which API, but the
application could (as it depended on the packager used).

Thankfully, we ended up finding a better solution, relying only on timing
audio track switches requests on the application-side instead.

Still, we had the feature ready and though this new API appears
unnecessarily complex now that we don't need it anymore, some work on it
still appear beneficial to the RxPlayer.
Consequently, this commit removes any notion of the new API but keeps:

  - EME polyfills (webkit-vendored and so on) are now only brought into
    your application when you rely on the "minimal build" if you
    actually import the `EME` feature (it makes sense, shouldn't break
    anything, and just remove some code from your bundle(s) if you don't
    need decryption)

  - The choice of the EME implementation to rely on is now isolated in
    its own `getEmeApiImplementation` function based on an
    implementation's "name", instead of just chosen once at evaluation
    time, which should make future debugging easier.

  - MediaKeys-related clean-up is now always performed with the EME API
    that was used for the corresponding content.

  - The webkit implementation now only rely on the `webkitneedkey` event
    for initialization data and not on the `encrypted` event anymore, as
    its format is different.

  - Switching EME implementation between contents is now a possibility,
    and we clean-up the previous implementation's side effect when we do
    so.

  - We remove the potentiality of a race condition between
    `setMediaKeys` EME API call by awaiting the returned promise when
    we're removing the previous MediaKeys instance.

  - Some minor `ContentDecryptor` improvements, brought initially when
    we wanted to attach MediaKeys to the HTMLMediaElement only _AFTER_
    the encrypted events - which is not needed now - but the
    modifications appeared to lead to a simpler architecture that what
    we had before.
  • Loading branch information
peaBerberian committed Jul 7, 2023
1 parent e80e99a commit 049201a
Show file tree
Hide file tree
Showing 53 changed files with 1,057 additions and 849 deletions.
56 changes: 0 additions & 56 deletions src/compat/__tests__/has_eme_apis.test.ts

This file was deleted.

19 changes: 11 additions & 8 deletions src/compat/eme/custom_media_keys/ie11_media_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import EventEmitter from "../../../utils/event_emitter";
import TaskCanceller from "../../../utils/task_canceller";
import wrapInPromise from "../../../utils/wrapInPromise";
import { ICompatHTMLMediaElement } from "../../browser_compatibility_types";
import * as events from "../../event_listeners";
import {
Expand Down Expand Up @@ -119,11 +120,13 @@ class IE11CustomMediaKeys implements ICustomMediaKeys {
this._mediaKeys = new MSMediaKeysConstructor(keyType);
}

_setVideo(videoElement: HTMLMediaElement): void {
this._videoElement = videoElement as ICompatHTMLMediaElement;
if (this._videoElement.msSetMediaKeys !== undefined) {
return this._videoElement.msSetMediaKeys(this._mediaKeys);
}
_setVideo(videoElement: HTMLMediaElement): Promise<unknown> {
return wrapInPromise(() => {
this._videoElement = videoElement as ICompatHTMLMediaElement;
if (this._videoElement.msSetMediaKeys !== undefined) {
this._videoElement.msSetMediaKeys(this._mediaKeys);
}
});
}

createSession(/* sessionType */): ICustomMediaKeySession {
Expand All @@ -144,7 +147,7 @@ export default function getIE11MediaKeysCallbacks() : {
setMediaKeys: (
elt: HTMLMediaElement,
mediaKeys: MediaKeys|ICustomMediaKeys|null
) => void;
) => Promise<unknown>;
} {
const isTypeSupported = (keySystem: string, type?: string|null) => {
if (MSMediaKeysConstructor === undefined) {
Expand All @@ -160,12 +163,12 @@ export default function getIE11MediaKeysCallbacks() : {
const setMediaKeys = (
elt: HTMLMediaElement,
mediaKeys: MediaKeys|ICustomMediaKeys|null
): void => {
): Promise<unknown> => {
if (mediaKeys === null) {
// msSetMediaKeys only accepts native MSMediaKeys as argument.
// Calling it with null or undefined will raise an exception.
// There is no way to unset the mediakeys in that case, so return here.
return;
return Promise.resolve(undefined);
}
if (!(mediaKeys instanceof IE11CustomMediaKeys)) {
throw new Error("Custom setMediaKeys is supposed to be called " +
Expand Down
192 changes: 8 additions & 184 deletions src/compat/eme/custom_media_keys/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,3 @@
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { MediaError } from "../../../errors";
import assert from "../../../utils/assert";
import { ICompatHTMLMediaElement } from "../../browser_compatibility_types";
import { isIE11 } from "../../browser_detection";
import isNode from "../../is_node";
import shouldFavourCustomSafariEME from "../../should_favour_custom_safari_EME";
import CustomMediaKeySystemAccess from "./../custom_key_system_access";
import getIE11MediaKeysCallbacks, {
MSMediaKeysConstructor,
} from "./ie11_media_keys";
Expand All @@ -37,168 +14,15 @@ import {
import getWebKitMediaKeysCallbacks from "./webkit_media_keys";
import { WebKitMediaKeysConstructor } from "./webkit_media_keys_constructor";

/** Generic implementation of the navigator.requestMediaKeySystemAccess API. */
type ICompatRequestMediaKeySystemAccessFn =
(keyType : string, config : MediaKeySystemConfiguration[]) =>
Promise<MediaKeySystemAccess | CustomMediaKeySystemAccess>;

let requestMediaKeySystemAccess : ICompatRequestMediaKeySystemAccessFn | null = null;

/**
* Set the given MediaKeys on the given HTMLMediaElement.
* Emits null when done then complete.
* @param {HTMLMediaElement} elt
* @param {Object} mediaKeys
*/
let setMediaKeys :
((elt: HTMLMediaElement, mediaKeys: MediaKeys | ICustomMediaKeys | null) => unknown) =
function defaultSetMediaKeys(
mediaElement: HTMLMediaElement,
mediaKeys: MediaKeys | ICustomMediaKeys | null
) {
const elt : ICompatHTMLMediaElement = mediaElement;
/* eslint-disable @typescript-eslint/unbound-method */
if (typeof elt.setMediaKeys === "function") {
return elt.setMediaKeys(mediaKeys as MediaKeys);
}
/* eslint-enable @typescript-eslint/unbound-method */

// If we get in the following code, it means that no compat case has been
// found and no standard setMediaKeys API exists. This case is particulary
// rare. We will try to call each API with native media keys.
if (typeof elt.webkitSetMediaKeys === "function") {
return elt.webkitSetMediaKeys(mediaKeys);
}

if (typeof elt.mozSetMediaKeys === "function") {
return elt.mozSetMediaKeys(mediaKeys);
}

if (typeof elt.msSetMediaKeys === "function" && mediaKeys !== null) {
return elt.msSetMediaKeys(mediaKeys);
}
};

/**
* Since Safari 12.1, EME APIs are available without webkit prefix.
* However, it seems that since fairplay CDM implementation within the browser is not
* standard with EME w3c current spec, the requestMediaKeySystemAccess API doesn't resolve
* positively, even if the drm (fairplay in most cases) is supported.
*
* Therefore, we prefer not to use requestMediaKeySystemAccess on Safari when webkit API
* is available.
*/
if (isNode ||
(navigator.requestMediaKeySystemAccess != null && !shouldFavourCustomSafariEME())
) {
requestMediaKeySystemAccess = (...args) =>
navigator.requestMediaKeySystemAccess(...args);
} else {
let isTypeSupported: (keyType: string) => boolean;
let createCustomMediaKeys: (keyType: string) => ICustomMediaKeys;

// This is for Chrome with unprefixed EME api
if (isOldWebkitMediaElement(HTMLVideoElement.prototype)) {
const callbacks = getOldKitWebKitMediaKeyCallbacks();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
setMediaKeys = callbacks.setMediaKeys;
// This is for WebKit with prefixed EME api
} else if (WebKitMediaKeysConstructor !== undefined) {
const callbacks = getWebKitMediaKeysCallbacks();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
setMediaKeys = callbacks.setMediaKeys;
} else if (isIE11 && MSMediaKeysConstructor !== undefined) {
const callbacks = getIE11MediaKeysCallbacks();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
setMediaKeys = callbacks.setMediaKeys;
} else if (MozMediaKeysConstructor !== undefined) {
const callbacks = getMozMediaKeysCallbacks();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
setMediaKeys = callbacks.setMediaKeys;
} else {
const MK = window.MediaKeys as unknown as typeof MediaKeys & {
isTypeSupported? : (keyType : string) => boolean;
new(keyType? : string) : ICustomMediaKeys;
};
const checkForStandardMediaKeys = () => {
if (MK === undefined) {
throw new MediaError("MEDIA_KEYS_NOT_SUPPORTED",
"No `MediaKeys` implementation found " +
"in the current browser.");
}
if (typeof MK.isTypeSupported === "undefined") {
const message = "This browser seems to be unable to play encrypted contents " +
"currently. Note: Some browsers do not allow decryption " +
"in some situations, like when not using HTTPS.";
throw new Error(message);
}
};
isTypeSupported = (keyType: string): boolean => {
checkForStandardMediaKeys();
assert(typeof MK.isTypeSupported === "function");
return MK.isTypeSupported(keyType);
};
createCustomMediaKeys = (keyType: string) => {
checkForStandardMediaKeys();
return new MK(keyType);
};
}

requestMediaKeySystemAccess = function(
keyType : string,
keySystemConfigurations : MediaKeySystemConfiguration[]
) : Promise<MediaKeySystemAccess|CustomMediaKeySystemAccess> {
if (!isTypeSupported(keyType)) {
return Promise.reject(new Error("Unsupported key type"));
}

for (let i = 0; i < keySystemConfigurations.length; i++) {
const keySystemConfiguration = keySystemConfigurations[i];
const { videoCapabilities,
audioCapabilities,
initDataTypes,
distinctiveIdentifier } = keySystemConfiguration;
let supported = true;
supported = supported &&
(initDataTypes == null ||
initDataTypes.some((idt) => idt === "cenc"));
supported = supported && (distinctiveIdentifier !== "required");

if (supported) {
const keySystemConfigurationResponse : MediaKeySystemConfiguration = {
initDataTypes: ["cenc"],
distinctiveIdentifier: "not-allowed" as const,
persistentState: "required" as const,
sessionTypes: ["temporary", "persistent-license"],
};
if (videoCapabilities !== undefined) {
keySystemConfigurationResponse.videoCapabilities = videoCapabilities;
}
if (audioCapabilities !== undefined) {
keySystemConfigurationResponse.audioCapabilities = audioCapabilities;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const customMediaKeys = createCustomMediaKeys(keyType);
return Promise.resolve(
new CustomMediaKeySystemAccess(keyType,
customMediaKeys,
keySystemConfigurationResponse)
);
}
}

return Promise.reject(new Error("Unsupported configuration"));
};
}

export {
requestMediaKeySystemAccess,
setMediaKeys,
getIE11MediaKeysCallbacks,
MSMediaKeysConstructor,
getMozMediaKeysCallbacks,
MozMediaKeysConstructor,
getOldKitWebKitMediaKeyCallbacks,
isOldWebkitMediaElement,
ICustomMediaKeys,
ICustomMediaKeySession,
getWebKitMediaKeysCallbacks,
WebKitMediaKeysConstructor,
};
20 changes: 13 additions & 7 deletions src/compat/eme/custom_media_keys/moz_media_keys_constructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import wrapInPromise from "../../../utils/wrapInPromise";
import { ICompatHTMLMediaElement } from "../../browser_compatibility_types";
import isNode from "../../is_node";
import { ICustomMediaKeys } from "./types";
Expand Down Expand Up @@ -47,7 +48,7 @@ export default function getMozMediaKeysCallbacks() : {
setMediaKeys: (
elt: HTMLMediaElement,
mediaKeys: MediaKeys|ICustomMediaKeys|null
) => void;
) => Promise<unknown>;
} {
const isTypeSupported = (keySystem: string, type?: string|null) => {
if (MozMediaKeysConstructor === undefined) {
Expand All @@ -67,12 +68,17 @@ export default function getMozMediaKeysCallbacks() : {
const setMediaKeys = (
mediaElement: HTMLMediaElement,
mediaKeys: MediaKeys|ICustomMediaKeys|null
): void => {
const elt : ICompatHTMLMediaElement = mediaElement;
if (elt.mozSetMediaKeys === undefined || typeof elt.mozSetMediaKeys !== "function") {
throw new Error("Can't set video on MozMediaKeys.");
}
return elt.mozSetMediaKeys(mediaKeys);
): Promise<unknown> => {
return wrapInPromise(() => {
const elt : ICompatHTMLMediaElement = mediaElement;
if (
elt.mozSetMediaKeys === undefined ||
typeof elt.mozSetMediaKeys !== "function"
) {
throw new Error("Can't set video on MozMediaKeys.");
}
return elt.mozSetMediaKeys(mediaKeys);
});
};
return {
isTypeSupported,
Expand Down
Loading

0 comments on commit 049201a

Please sign in to comment.