rx-player
Version:
Canal+ HTML5 Video Player
274 lines (247 loc) • 8.93 kB
text/typescript
/**
* 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 { toUint8Array } from "../../../utils/byte_parsing";
import EventEmitter from "../../../utils/event_emitter";
import noop from "../../../utils/noop";
import startsWith from "../../../utils/starts_with";
import wrapInPromise from "../../../utils/wrapInPromise";
import type {
IMediaElement,
IMediaKeySession,
IMediaKeys,
} from "../../browser_compatibility_types";
import getWebKitFairplayInitData from "../get_webkit_fairplay_initdata";
import type { IWebKitMediaKeys } from "./webkit_media_keys_constructor";
import getWebKitMediaKeysConstructor from "./webkit_media_keys_constructor";
/**
* Check if keyType is for fairplay DRM
* @param {string} keyType
* @returns {boolean}
*/
function isFairplayKeyType(keyType: string): boolean {
return startsWith(keyType, "com.apple.fps");
}
/**
* Set media keys on video element using native HTMLMediaElement
* setMediaKeys from WebKit.
* @param {HTMLMediaElement} elt
* @param {Object|null} mediaKeys
* @returns {Promise}
*/
function setWebKitMediaKeys(elt: IMediaElement, mediaKeys: unknown): Promise<unknown> {
return wrapInPromise(() => {
if (elt.webkitSetMediaKeys === undefined) {
throw new Error("No webKitMediaKeys API.");
}
elt.webkitSetMediaKeys(mediaKeys);
});
}
/**
* On Safari browsers (>= 9), there are specific webkit prefixed APIs for cyphered
* content playback. Standard EME APIs are therefore available since Safari 12.1, but they
* don't allow to play fairplay cyphered content.
*
* This class implements a standard EME API polyfill that wraps webkit prefixed Safari
* EME custom APIs.
*/
class WebkitMediaKeySession
extends EventEmitter<MediaKeySessionEventMap>
implements IMediaKeySession
{
public readonly closed: Promise<MediaKeySessionClosedReason>;
public expiration: number;
public keyStatuses: MediaKeyStatusMap;
private readonly _videoElement: IMediaElement;
private readonly _keyType: string;
private _nativeSession: undefined | IMediaKeySession;
private _serverCertificate: BufferSource | undefined;
private _closeSession: (val: MediaKeySessionClosedReason) => void;
private _unbindSession: () => void;
/**
* @param {HTMLMediaElement} mediaElement
* @param {string} keyType
* @param {Uint8Array | undefined} serverCertificate
*/
constructor(
mediaElement: IMediaElement,
keyType: string,
serverCertificate?: BufferSource,
) {
super();
this._serverCertificate = serverCertificate;
this._videoElement = mediaElement;
this._keyType = keyType;
this._unbindSession = noop;
this._closeSession = noop; // Just here to make TypeScript happy
this.closed = new Promise((resolve: (val: MediaKeySessionClosedReason) => void) => {
this._closeSession = resolve;
});
this.keyStatuses = new Map();
this.expiration = NaN;
}
public update(license: BufferSource): Promise<void> {
return new Promise((resolve, reject) => {
if (
this._nativeSession === undefined ||
this._nativeSession.update === undefined ||
typeof this._nativeSession.update !== "function"
) {
return reject("Unavailable WebKit key session.");
}
try {
const uInt8Arraylicense = toUint8Array(license);
resolve(this._nativeSession.update(uInt8Arraylicense));
} catch (err) {
reject(err);
}
});
}
public generateRequest(_initDataType: string, initData: BufferSource): Promise<void> {
return new Promise((resolve) => {
const elt = this._videoElement;
if (elt.webkitKeys?.createSession === undefined) {
throw new Error("No WebKitMediaKeys API.");
}
let formattedInitData: BufferSource;
if (isFairplayKeyType(this._keyType)) {
if (this._serverCertificate === undefined) {
throw new Error(
"A server certificate is needed for creating fairplay session.",
);
}
formattedInitData = getWebKitFairplayInitData(initData, this._serverCertificate);
} else {
formattedInitData = initData;
}
const keySession = elt.webkitKeys.createSession("video/mp4", formattedInitData);
if (keySession === undefined || keySession === null) {
throw new Error("Impossible to get the key sessions");
}
this._listenEvent(keySession);
this._nativeSession = keySession;
resolve();
});
}
public close(): Promise<void> {
return new Promise((resolve, reject) => {
this._unbindSession();
this._closeSession("closed-by-application");
if (this._nativeSession === undefined) {
reject("No session to close.");
return;
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._nativeSession.close();
resolve();
});
}
load(): Promise<boolean> {
return Promise.resolve(false);
}
remove(): Promise<void> {
return Promise.resolve();
}
get sessionId(): string {
return this._nativeSession?.sessionId ?? "";
}
private _listenEvent(session: IMediaKeySession): void {
this._unbindSession(); // If previous session was linked
const onEvent = (evt: Event) => {
this.trigger(evt.type as keyof MediaKeySessionEventMap, evt);
};
["keymessage", "message", "keyadded", "ready", "keyerror", "error"].forEach((evt) => {
session.addEventListener(evt as keyof MediaKeySessionEventMap, onEvent);
session.addEventListener(`webkit${evt}` as keyof MediaKeySessionEventMap, onEvent);
});
this._unbindSession = () => {
["keymessage", "message", "keyadded", "ready", "keyerror", "error"].forEach(
(evt) => {
session.removeEventListener(evt as keyof MediaKeySessionEventMap, onEvent);
session.removeEventListener(
`webkit${evt}` as keyof MediaKeySessionEventMap,
onEvent,
);
},
);
};
}
}
class WebKitCustomMediaKeys implements IMediaKeys {
private _videoElement?: IMediaElement;
private _mediaKeys?: IWebKitMediaKeys;
private _serverCertificate?: BufferSource;
private _keyType: string;
constructor(keyType: string) {
const WebKitMediaKeysConstructor = getWebKitMediaKeysConstructor();
if (WebKitMediaKeysConstructor === undefined) {
throw new Error("No WebKitMediaKeys API.");
}
this._keyType = keyType;
this._mediaKeys = new WebKitMediaKeysConstructor(keyType);
}
_setVideo(videoElement: IMediaElement): Promise<unknown> {
this._videoElement = videoElement;
if (this._videoElement === undefined) {
throw new Error("Video not attached to the MediaKeys");
}
return setWebKitMediaKeys(this._videoElement, this._mediaKeys);
}
createSession(/* sessionType */): IMediaKeySession {
if (this._videoElement === undefined || this._mediaKeys === undefined) {
throw new Error("Video not attached to the MediaKeys");
}
return new WebkitMediaKeySession(
this._videoElement,
this._keyType,
this._serverCertificate,
);
}
setServerCertificate(serverCertificate: BufferSource): Promise<boolean> {
this._serverCertificate = serverCertificate;
return Promise.resolve(true);
}
}
export default function getWebKitMediaKeysCallbacks(): {
isTypeSupported: (keyType: string) => boolean;
createCustomMediaKeys: (keyType: string) => WebKitCustomMediaKeys;
setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise<unknown>;
} {
const WebKitMediaKeysConstructor = getWebKitMediaKeysConstructor();
if (WebKitMediaKeysConstructor === undefined) {
throw new Error("No WebKitMediaKeys API.");
}
const isTypeSupported = WebKitMediaKeysConstructor.isTypeSupported;
const createCustomMediaKeys = (keyType: string) => new WebKitCustomMediaKeys(keyType);
const setMediaKeys = (
elt: IMediaElement,
mediaKeys: IMediaKeys | null,
): Promise<unknown> => {
if (mediaKeys === null) {
return setWebKitMediaKeys(elt, mediaKeys);
}
if (!(mediaKeys instanceof WebKitCustomMediaKeys)) {
throw new Error(
"Custom setMediaKeys is supposed to be called " + "with webkit custom MediaKeys.",
);
}
return mediaKeys._setVideo(elt);
};
return {
isTypeSupported,
createCustomMediaKeys,
setMediaKeys,
};
}