Skip to content

Commit

Permalink
Merge pull request #1479 from canalplus/fix/safari-stuck-webkitneedkey
Browse files Browse the repository at this point in the history
fix(safari): video not starting because key are never considered usable for a track
  • Loading branch information
peaBerberian authored Aug 5, 2024
2 parents 774610e + d563ea2 commit eea2f84
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 13 deletions.
4 changes: 4 additions & 0 deletions src/compat/eme/custom_media_keys/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ export interface IMediaKeySessionEvents {
// "keyerror"
/* "error" */
}

export interface ICustomMediaEncryptedEvent extends MediaEncryptedEvent {
forceSessionRecreation?: boolean | undefined;
}
40 changes: 36 additions & 4 deletions src/compat/eme/eme-api-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import assert from "../../utils/assert";
import globalScope from "../../utils/global_scope";
import isNode from "../../utils/is_node";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import objectAssign from "../../utils/object_assign";
import type { CancellationSignal } from "../../utils/task_canceller";
import type { IMediaElement } from "../browser_compatibility_types";
import { isIE11 } from "../browser_detection";
Expand All @@ -19,7 +20,10 @@ import getMozMediaKeysCallbacks, {
import getOldKitWebKitMediaKeyCallbacks, {
isOldWebkitMediaElement,
} from "./custom_media_keys/old_webkit_media_keys";
import type { ICustomMediaKeys } from "./custom_media_keys/types";
import type {
ICustomMediaEncryptedEvent,
ICustomMediaKeys,
} from "./custom_media_keys/types";
import getWebKitMediaKeysCallbacks from "./custom_media_keys/webkit_media_keys";
import { WebKitMediaKeysConstructor } from "./custom_media_keys/webkit_media_keys_constructor";

Expand Down Expand Up @@ -63,7 +67,7 @@ export interface IEmeApiImplementation {
*/
onEncrypted: (
target: IEventTargetLike,
listener: (evt: unknown) => void,
listener: (evt: ICustomMediaEncryptedEvent) => void,
cancelSignal: CancellationSignal,
) => void;

Expand Down Expand Up @@ -143,8 +147,8 @@ function getEmeApiImplementation(
let createCustomMediaKeys: (keyType: string) => ICustomMediaKeys;

if (preferredApiType === "webkit" && WebKitMediaKeysConstructor !== undefined) {
onEncrypted = createCompatibleEventListener(["needkey"]);
const callbacks = getWebKitMediaKeysCallbacks();
onEncrypted = createOnEncryptedForWebkit();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
setMediaKeys = callbacks.setMediaKeys;
Expand All @@ -160,7 +164,7 @@ function getEmeApiImplementation(
implementation = "older-webkit";
// This is for WebKit with prefixed EME api
} else if (WebKitMediaKeysConstructor !== undefined) {
onEncrypted = createCompatibleEventListener(["needkey"]);
onEncrypted = createOnEncryptedForWebkit();
const callbacks = getWebKitMediaKeysCallbacks();
isTypeSupported = callbacks.isTypeSupported;
createCustomMediaKeys = callbacks.createCustomMediaKeys;
Expand Down Expand Up @@ -274,6 +278,34 @@ function getEmeApiImplementation(
implementation,
};
}
/**
* Create an event listener for the "webkitneedkey" event
* @returns
*/
function createOnEncryptedForWebkit(): IEmeApiImplementation["onEncrypted"] {
const compatibleEventListener = createCompatibleEventListener(
["needkey"],
undefined /* prefixes */,
);
const onEncrypted = (
target: IEventTargetLike,
listener: (event: ICustomMediaEncryptedEvent) => void,
cancelSignal: CancellationSignal,
) => {
compatibleEventListener(
target,
(event?: Event) => {
const patchedEvent = objectAssign(
{ forceSessionRecreation: true },
event as MediaEncryptedEvent,
);
listener(patchedEvent);
},
cancelSignal,
);
};
return onEncrypted;
}

/**
* Set the given MediaKeys on the given HTMLMediaElement.
Expand Down
8 changes: 5 additions & 3 deletions src/compat/eme/get_init_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal";
import { be4toi } from "../../utils/byte_parsing";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import { PSSH_TO_INTEGER } from "./constants";
import type { ICustomMediaEncryptedEvent } from "./custom_media_keys/types";

/** Data recuperated from parsing the payload of an `encrypted` event. */
export interface IEncryptedEventData {
Expand Down Expand Up @@ -50,6 +51,7 @@ export interface IEncryptedEventData {
*/
data: Uint8Array;
}>;
forceSessionRecreation?: boolean | undefined;
}

/**
Expand Down Expand Up @@ -145,15 +147,15 @@ function isPSSHAlreadyEncountered(
* encountered in the given event.
*/
export default function getInitData(
encryptedEvent: MediaEncryptedEvent,
encryptedEvent: ICustomMediaEncryptedEvent,
): IEncryptedEventData | null {
const { initData, initDataType } = encryptedEvent;
const { initData, initDataType, forceSessionRecreation } = encryptedEvent;
if (isNullOrUndefined(initData)) {
log.warn("Compat: No init data found on media encrypted event.");
return null;
}

const initDataBytes = new Uint8Array(initData);
const values = getInitializationDataValues(initDataBytes);
return { type: initDataType, values };
return { type: initDataType, values, forceSessionRecreation };
}
35 changes: 30 additions & 5 deletions src/compat/event_listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
IEventTarget,
IMediaElement,
} from "./browser_compatibility_types";
import type { ICustomMediaEncryptedEvent } from "./eme/custom_media_keys/types";

const BROWSER_PREFIXES = ["", "webkit", "moz", "ms"];

Expand Down Expand Up @@ -82,8 +83,14 @@ function eventPrefixed(eventNames: string[], prefixes?: string[]): string[] {
}

export interface IEventEmitterLike {
addEventListener: (eventName: string, handler: () => void) => void;
removeEventListener: (eventName: string, handler: () => void) => void;
addEventListener: (
eventName: string,
handler: EventListenerOrEventListenerObject,
) => void;
removeEventListener: (
eventName: string,
handler: EventListenerOrEventListenerObject,
) => void;
}

export type IEventTargetLike = HTMLElement | IEventEmitterLike | IEventEmitter<unknown>;
Expand All @@ -99,26 +106,44 @@ export type IEventTargetLike = HTMLElement | IEventEmitterLike | IEventEmitter<u
* @returns {Function} - Returns function allowing to easily add a callback to
* be triggered when that event is emitted on a given event target.
*/

function createCompatibleEventListener(
eventNames: Array<"needkey" | "encrypted">,
prefixes?: string[],
): (
element: IEventTargetLike,
listener: (event: ICustomMediaEncryptedEvent) => void,
cancelSignal: CancellationSignal,
) => void;

function createCompatibleEventListener(
eventNames: string[],
prefixes?: string[],
): (
element: IEventTargetLike,
listener: (event?: unknown) => void,
listener: (event?: Event) => void,
cancelSignal: CancellationSignal,
) => void;

function createCompatibleEventListener(
eventNames: string[] | Array<"needkey" | "encrypted">,
prefixes?: string[],
): (
element: IEventTargetLike,
listener: (event?: Event | MediaEncryptedEvent) => void,
cancelSignal: CancellationSignal,
) => void {
let mem: string | undefined;
const prefixedEvents = eventPrefixed(eventNames, prefixes);

return (
element: IEventTargetLike,
listener: (event?: unknown) => void,
listener: (event?: Event) => void,
cancelSignal: CancellationSignal,
) => {
if (cancelSignal.isCancelled()) {
return;
}

// if the element is a HTMLElement we can detect
// the supported event, and memoize it in `mem`
if (typeof HTMLElement !== "undefined" && element instanceof HTMLElement) {
Expand Down
59 changes: 58 additions & 1 deletion src/main_thread/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
mediaElement,
(evt) => {
log.debug("DRM: Encrypted event received from media element.");
const initData = getInitData(evt as MediaEncryptedEvent);
const initData = getInitData(evt);
if (initData !== null) {
this.onInitializationData(initData);
}
Expand Down Expand Up @@ -728,6 +728,21 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
return false;
}

/**
* On Safari using Directfile, the old EME implementation triggers
* the "webkitneedkey" event instead of "encrypted". There's an issue in Safari
* where "webkitneedkey" fires too early before all tracks are added from an HLS playlist.
* Safari incorrectly assumes some keys are missing for these tracks,
* leading to repeated "webkitneedkey" events. Because RxPlayer recognizes
* it already has a session for these keys and ignores the events,
* the content remains frozen. To resolve this, the session is re-created.
*/
const forceSessionRecreation = initializationData.forceSessionRecreation;
if (forceSessionRecreation === true) {
this.removeSessionForInitData(initializationData, mediaKeysData);
return false;
}

// Check if the compatible session is blacklisted
const blacklistedSessionError = compatibleSessionInfo.blacklistedSessionError;
if (!isNullOrUndefined(blacklistedSessionError)) {
Expand Down Expand Up @@ -832,6 +847,48 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
return false;
}

/**
* Remove the session corresponding to the initData provided, and close it.
* It does nothing if no session was found for this initData.
* @param {Object} initData : The initialization data corresponding to the session
* that need to be removed
* @param {Object} mediaKeysData : The media keys data
*/
private removeSessionForInitData(
initData: IProcessedProtectionData,
mediaKeysData: IAttachedMediaKeysData,
) {
const { stores } = mediaKeysData;
/** Remove the session and close it from the loadedSessionStore */
const entry = stores.loadedSessionsStore.reuse(initData);
if (entry !== null) {
stores.loadedSessionsStore
.closeSession(entry.mediaKeySession)
.catch(() =>
log.error("DRM: Cannot close the session from the loaded session store"),
);
}

/**
* If set, a currently-used key session is already compatible to this
* initialization data.
*/
const compatibleSessionInfo = arrayFind(this._currentSessions, (x) =>
x.record.isCompatibleWith(initData),
);
if (compatibleSessionInfo === undefined) {
return;
}
/** Remove the session from the currentSessions */
const indexOf = this._currentSessions.indexOf(compatibleSessionInfo);
if (indexOf !== -1) {
log.debug(
"DRM: A session from a processed init is removed due to forceSessionRecreation policy.",
);
this._currentSessions.splice(indexOf, 1);
}
}

/**
* Callback that should be called if an error that made the current
* `ContentDecryptor` instance unusable arised.
Expand Down
2 changes: 2 additions & 0 deletions src/main_thread/decrypt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export interface IProtectionData {
content?: IContent;
/** Every initialization data for that type. */
values: IInitDataValue[];

forceSessionRecreation?: boolean | undefined;
}

/** Protection initialization data actually processed by the `ContentDecryptor`. */
Expand Down

0 comments on commit eea2f84

Please sign in to comment.