UNPKG

rx-player

Version:
215 lines (198 loc) 7.81 kB
import type { IMediaElement } from "../../../compat/browser_compatibility_types"; import getEmeApiImplementation from "../../../compat/eme"; import { EncryptedMediaError } from "../../../errors"; import features from "../../../features"; import log from "../../../log"; import type { IKeySystemOption, IPlayerError } from "../../../public_types"; import type { IReadOnlySharedReference } from "../../../utils/reference"; import SharedReference from "../../../utils/reference"; import TaskCanceller from "../../../utils/task_canceller"; import type { CancellationSignal } from "../../../utils/task_canceller"; import { ContentDecryptorState } from "../../decrypt"; import type IContentDecryptor from "../../decrypt"; import type { IProcessedProtectionData } from "../../decrypt"; /** * Initialize content decryption capabilities on the given `HTMLMediaElement`. * * You can call this function even if you don't want decrytpion capabilities, in * which case you can just set the `keySystems` option as an empty array. * In this situation, the returned object will directly correspond to an * "`initialized`" state and the `onError` callback will be triggered as soon * as protection information is received. * * @param {HTMLMediaElement} mediaElement - `HTMLMediaElement` on which content * decryption may be wanted. * @param {Array.<Object>} keySystems - Key system configuration(s) wanted * Empty array if no content decryption capability is wanted. * protection initialization data will be sent through. * @param {Object} callbacks - Callbacks called at various decryption-related * events. * @param {Object} cancelSignal - When that signal emits, this function will * stop listening to various events. * @returns {Object} - Reference emitting the current status regarding DRM * initialization. */ export default function initializeContentDecryption( mediaElement: IMediaElement, keySystems: IKeySystemOption[], callbacks: { onWarning: (err: IPlayerError) => void; onError: (err: Error) => void; onBlackListProtectionData: (val: IProcessedProtectionData) => void; onKeyIdsCompatibilityUpdate: (updates: { whitelistedKeyIds: Uint8Array[]; blacklistedKeyIds: Uint8Array[]; delistedKeyIds: Uint8Array[]; }) => void; onCodecSupportUpdate?: () => void; }, cancelSignal: CancellationSignal, ): { statusRef: IReadOnlySharedReference<IDrmInitializationStatus>; contentDecryptor: | { enabled: true; value: IContentDecryptor; } | { enabled: false; value: EncryptedMediaError; }; } { if (keySystems.length === 0) { return createEmeDisabledReference("No `keySystems` option given."); } else if (features.decrypt === null) { return createEmeDisabledReference("EME feature not activated."); } const decryptorCanceller = new TaskCanceller(); decryptorCanceller.linkToSignal(cancelSignal); const drmStatusRef = new SharedReference<IDrmInitializationStatus>( { initializationState: { type: "uninitialized", value: null }, drmSystemId: undefined, }, cancelSignal, ); const ContentDecryptor = features.decrypt; const emeApi = mediaElement.FORCED_EME_API ?? getEmeApiImplementation("auto"); if (emeApi === null) { return createEmeDisabledReference("EME API not available on the current page."); } log.debug("Init", "Creating ContentDecryptor"); const contentDecryptor = new ContentDecryptor(emeApi, mediaElement, keySystems); const onStateChange = (state: ContentDecryptorState) => { if (state > ContentDecryptorState.Initializing) { callbacks.onCodecSupportUpdate?.(); contentDecryptor.removeEventListener("stateChange", onStateChange); } }; contentDecryptor.addEventListener("stateChange", onStateChange); contentDecryptor.addEventListener("stateChange", (state) => { if (state === ContentDecryptorState.WaitingForAttachment) { const isMediaLinked = new SharedReference(false); isMediaLinked.onUpdate( (isAttached, stopListening) => { if (isAttached) { stopListening(); if (state === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.attach(); } } }, { clearSignal: decryptorCanceller.signal }, ); drmStatusRef.setValue({ initializationState: { type: "awaiting-media-link", value: { isMediaLinked }, }, drmSystemId: contentDecryptor.systemId, }); } else if (state === ContentDecryptorState.ReadyForContent) { drmStatusRef.setValue({ initializationState: { type: "initialized", value: null }, drmSystemId: contentDecryptor.systemId, }); contentDecryptor.removeEventListener("stateChange"); } }); contentDecryptor.addEventListener("error", (error) => { decryptorCanceller.cancel(); callbacks.onError(error); }); contentDecryptor.addEventListener("warning", (error) => { callbacks.onWarning(error); }); contentDecryptor.addEventListener("blackListProtectionData", (x) => { callbacks.onBlackListProtectionData(x); }); contentDecryptor.addEventListener("keyIdsCompatibilityUpdate", (x) => { callbacks.onKeyIdsCompatibilityUpdate(x); }); decryptorCanceller.signal.register(() => { contentDecryptor.dispose(); }); return { statusRef: drmStatusRef, contentDecryptor: { enabled: true, value: contentDecryptor }, }; function createEmeDisabledReference(errMsg: string): { statusRef: IReadOnlySharedReference<IDrmInitializationStatus>; contentDecryptor: { enabled: false; value: EncryptedMediaError; }; } { const err = new EncryptedMediaError("MEDIA_IS_ENCRYPTED_ERROR", errMsg, { keyStatuses: undefined, keySystemConfiguration: undefined, keySystem: undefined, }); const ref = new SharedReference({ initializationState: { type: "initialized" as const, value: null }, drmSystemId: undefined, }); ref.finish(); // We know that no new value will be triggered return { statusRef: ref, contentDecryptor: { enabled: false, value: err } }; } } /** Status of content decryption initialization. */ interface IDrmInitializationStatus { /** Current initialization state the decryption logic is in. */ initializationState: IDecryptionInitializationState; /** * If set, corresponds to the hex string describing the current key system * used. * `undefined` if unknown or if it does not apply. */ drmSystemId: string | undefined; } /** Initialization steps to add decryption capabilities to an `HTMLMediaElement`. */ type IDecryptionInitializationState = /** * Decryption capabilities have not been initialized yet. * You should wait before performing any action on the concerned * `HTMLMediaElement` (such as linking a content / `MediaSource` to it). */ | { type: "uninitialized"; value: null } /** * The `MediaSource` or media url has to be linked to the `HTMLMediaElement` * before continuing. * Once it has been linked with success (e.g. the `MediaSource` has "opened"), * the `isMediaLinked` `SharedReference` should be set to `true`. * * In the `MediaSource` case, you should wait until the `"initialized"` * state before pushing segment. * * Note that the `"awaiting-media-link"` is an optional state. It can be * skipped to directly `"initialized"` instead. */ | { type: "awaiting-media-link"; value: { isMediaLinked: SharedReference<boolean> }; } /** * The `MediaSource` or media url can be linked AND segments can be pushed to * the `HTMLMediaElement` on which decryption capabilities were wanted. */ | { type: "initialized"; value: null };