/** * 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 { base64ToBytes } from "../../../utils/base64"; import { toUint8Array } from "../../../utils/byte_parsing"; import EventEmitter from "../../../utils/event_emitter"; import isNullOrUndefined from "../../../utils/is_null_or_undefined"; import noop from "../../../utils/noop"; import { utf8ToStr } from "../../../utils/string_parsing"; import wrapInPromise from "../../../utils/wrapInPromise"; import type { IMediaElement, IMediaKeySession, IMediaKeys, } from "../../browser_compatibility_types"; export interface IOldWebkitHTMLMediaElement extends HTMLVideoElement { webkitGenerateKeyRequest: (keyType: string, initData: ArrayBuffer) => void; webkitAddKey: ( keyType: string, key: BufferSource, kid: BufferSource | null, sessionId: string, ) => void; } /** * Returns true if the given media element has old webkit methods * corresponding to the IOldWebkitHTMLMediaElement interface. * @param {HTMLMediaElement} element * @returns {Boolean} */ export function isOldWebkitMediaElement( element: unknown, ): element is IOldWebkitHTMLMediaElement { return ( typeof (element as IOldWebkitHTMLMediaElement)?.webkitGenerateKeyRequest === "function" ); } /** * MediaKeySession implementation for older versions of WebKit relying on APIs * such as `webkitGenerateKeyRequest` `webkitAddKey` to be called on the * HTMLMediaElement. * @class OldWebkitMediaKeySession */ class OldWebkitMediaKeySession extends EventEmitter implements IMediaKeySession { public readonly closed: Promise; public expiration: number; public keyStatuses: MediaKeyStatusMap; public sessionId: string; private readonly _vid: IOldWebkitHTMLMediaElement; private readonly _key: string; private _closeSession: () => void; constructor(mediaElement: IOldWebkitHTMLMediaElement, keySystem: string) { super(); this._vid = mediaElement; this._key = keySystem; this.sessionId = ""; this._closeSession = noop; // Just here to make TypeScript happy this.keyStatuses = new Map(); this.expiration = NaN; const onSessionRelatedEvent = (evt: Event) => { this.trigger(evt.type as keyof MediaKeySessionEventMap, evt); }; this.closed = new Promise((resolve) => { this._closeSession = () => { ["keymessage", "message", "keyadded", "ready", "keyerror", "error"].forEach( (evt) => { mediaElement.removeEventListener(evt, onSessionRelatedEvent); mediaElement.removeEventListener(`webkit${evt}`, onSessionRelatedEvent); }, ); resolve("closed-by-application"); }; }); ["keymessage", "message", "keyadded", "ready", "keyerror", "error"].forEach((evt) => { mediaElement.addEventListener(evt, onSessionRelatedEvent); mediaElement.addEventListener(`webkit${evt}`, onSessionRelatedEvent); }); } public update(license: BufferSource): Promise { return new Promise((resolve, reject) => { try { if (this._key.indexOf("clearkey") >= 0) { const licenseTypedArray = toUint8Array(license); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const json = JSON.parse(utf8ToStr(licenseTypedArray)); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument const key = base64ToBytes(json.keys[0].k); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument const kid = base64ToBytes(json.keys[0].kid); resolve(this._vid.webkitAddKey(this._key, key, kid, /* sessionId */ "")); } else { resolve(this._vid.webkitAddKey(this._key, license, null, /* sessionId */ "")); } } catch (err) { reject(err); } }); } public generateRequest(_initDataType: string, initData: ArrayBuffer): Promise { return new Promise((resolve) => { this._vid.webkitGenerateKeyRequest(this._key, initData); resolve(); }); } public close(): Promise { return new Promise((resolve) => { this._closeSession(); resolve(); }); } /** * Load a Persistent MediaKeySession. * Do nothing here because this implementation doesn't handle them. * @returns {Promise.} */ public load(): Promise { // Not implemented. Always return false as in "no session with that id". return Promise.resolve(false); } public remove(): Promise { return Promise.resolve(); } } class OldWebKitCustomMediaKeys implements IMediaKeys { private readonly _keySystem: string; private _videoElement?: IOldWebkitHTMLMediaElement; constructor(keySystem: string) { this._keySystem = keySystem; } _setVideo(videoElement: IOldWebkitHTMLMediaElement | IMediaElement): Promise { return wrapInPromise(() => { if (!isOldWebkitMediaElement(videoElement)) { throw new Error("Video not attached to the MediaKeys"); } this._videoElement = videoElement; }); } createSession(/* sessionType */): IMediaKeySession { if (isNullOrUndefined(this._videoElement)) { throw new Error("Video not attached to the MediaKeys"); } return new OldWebkitMediaKeySession(this._videoElement, this._keySystem); } setServerCertificate(): Promise { throw new Error("Server certificate is not implemented in your browser"); } } export default function getOldWebKitMediaKeysCallbacks(): { isTypeSupported: (keyType: string) => boolean; createCustomMediaKeys: (keyType: string) => OldWebKitCustomMediaKeys; setMediaKeys: (elt: IMediaElement, mediaKeys: IMediaKeys | null) => Promise; } { const isTypeSupported = function (keyType: string): boolean { // get any