Skip to content

Commit

Permalink
Merge pull request #1535 from canalplus/fix/safari-stuck-webkitneedkeyv3
Browse files Browse the repository at this point in the history
[v3] fix(safari): video not starting because key are never considered…
  • Loading branch information
peaBerberian committed Sep 19, 2024
2 parents 9be06de + 21c6371 commit 2ee686d
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 30 deletions.
20 changes: 13 additions & 7 deletions src/compat/eme/custom_media_keys/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,16 @@ export interface ICustomMediaKeyStatusMap {
) : boolean;
}

export interface IMediaKeySessionEvents { [key : string] : MediaKeyMessageEvent|Event;
// "keymessage"
// "message"
// "keyadded"
// "ready"
// "keyerror"
/* "error" */ }
export interface IMediaKeySessionEvents {
[key: string]: MediaKeyMessageEvent | Event;
// "keymessage"
// "message"
// "keyadded"
// "ready"
// "keyerror"
/* "error" */
}

export interface ICustomMediaEncryptedEvent extends MediaEncryptedEvent {
forceSessionRecreation?: boolean | undefined;
}
35 changes: 32 additions & 3 deletions src/compat/eme/eme-api-implementation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MediaError } from "../../errors";
import assert from "../../utils/assert";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import objectAssign from "../../utils/object_assign";
import { CancellationSignal } from "../../utils/task_canceller";
import { ICompatHTMLMediaElement } from "../browser_compatibility_types";
import { isIE11 } from "../browser_detection";
Expand All @@ -18,6 +19,7 @@ import getOldKitWebKitMediaKeyCallbacks, {
isOldWebkitMediaElement,
} from "./custom_media_keys/old_webkit_media_keys";
import {
ICustomMediaEncryptedEvent,
ICustomMediaKeys,
} from "./custom_media_keys/types";
import getWebKitMediaKeysCallbacks from "./custom_media_keys/webkit_media_keys";
Expand Down Expand Up @@ -65,7 +67,7 @@ export interface IEmeApiImplementation {
*/
onEncrypted : (
target : IEventTargetLike,
listener : (evt : unknown) => void,
listener : (evt : ICustomMediaEncryptedEvent) => void,
cancelSignal : CancellationSignal,
) => void;

Expand Down Expand Up @@ -149,8 +151,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 @@ -166,7 +168,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 @@ -271,6 +273,33 @@ function getEmeApiImplementation(
setMediaKeys,
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(event as MediaEncryptedEvent, {
forceSessionRecreation: true,
});
listener(patchedEvent);
},
cancelSignal
);
};
return onEncrypted;
}

/**
* Set the given MediaKeys on the given HTMLMediaElement.
Expand Down
13 changes: 8 additions & 5 deletions src/compat/eme/get_init_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import log from "../../log";
import { getPsshSystemID } from "../../parsers/containers/isobmff";
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 { ICustomMediaEncryptedEvent } from "./custom_media_keys/types";

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

/**
Expand Down Expand Up @@ -145,15 +148,15 @@ function isPSSHAlreadyEncountered(
* encountered in the given event.
*/
export default function getInitData(
encryptedEvent : MediaEncryptedEvent
) : IEncryptedEventData | null {
const { initData, initDataType } = encryptedEvent;
if (initData == null) {
encryptedEvent: ICustomMediaEncryptedEvent
): IEncryptedEventData | null {
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 };
}
52 changes: 38 additions & 14 deletions src/compat/event_listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ICompatHTMLMediaElement,
ICompatPictureInPictureWindow,
} from "./browser_compatibility_types";
import { ICustomMediaEncryptedEvent } from "./eme/custom_media_keys/types";
import isNode from "./is_node";

const BROWSER_PREFIXES = ["", "webkit", "moz", "ms"];
Expand Down Expand Up @@ -87,8 +88,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 |
Expand All @@ -106,28 +113,45 @@ export type IEventTargetLike = HTMLElement |
* @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[]
) :
eventNames: string[],
prefixes?: string[],
): (
element: IEventTargetLike,
listener: (event?: Event) => void,
cancelSignal: CancellationSignal,
) => void;

function createCompatibleEventListener(
eventNames: string[] | Array<"needkey" | "encrypted">,
prefixes?: string[]
):
(
element : IEventTargetLike,
listener : (event? : unknown) => void,
cancelSignal: CancellationSignal
) => void
{
let mem : string|undefined;
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,
element: IEventTargetLike,
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 (element instanceof HTMLElement) {
Expand Down
61 changes: 60 additions & 1 deletion src/core/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven

eme.onEncrypted(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 @@ -636,6 +636,22 @@ 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 @@ -726,6 +742,49 @@ 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
5 changes: 5 additions & 0 deletions src/core/decrypt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ export interface IProtectionData {
/** Protection initialization data actually processed by the `ContentDecryptor`. */
export interface IProcessedProtectionData extends Omit<IProtectionData, "values"> {
values: InitDataValuesContainer;
/**
* Enforce to recreate the media key session if there is already a session created
* with this init data
*/
forceSessionRecreation?: boolean | undefined;
}

/**
Expand Down

0 comments on commit 2ee686d

Please sign in to comment.