Skip to content

Commit

Permalink
DRM: re-implement singleLicensePer handling
Browse files Browse the repository at this point in the history
This update is build on the DRM refacto started in #1042 and fixes some
of (complex, yet relatively minor) performance issues that could be
encountered in a `singleLicensePer: "content"` mode.
It also paves the way for the future `singleLicensePer: "periods"` mode.

In particular, it does the following things:

Optimization on the in-memory MediaKeySession cache when in
`singleLicensePer: "content"` mode
-----------------------------------------------------------

The in-memory cache of MediaKeySessions presented by the
`LoadedSessionsStore` - which basically allows faster loading of
already-loaded content by re-using a recently-created MediaKeySession
before relied only on the initialization data for identification
purposes.

This means that false negatives can occur (in that: cache miss where it
should have been a hit) in the `singleLicensePer: "content"` mode
(which implies that a single license request will actually retrieve all
keys linked to the content), if when re-loading the content a different
initialization data is initially encountered.

This would be a false negative here because even if this is different
initial initialization data than before, the already-created
MediaKeySession still should have received the key to decrypt that other
quality.

This is now fixed by linking a MediaKeySession in that cache both to the
initialization data linked to it and to all key ids that are explicitely
(i.e. in the fetched license) AND implicitely (i.e. in the Manifest file
when in a `singleLicensePer: "content"` mode yet not in the license)
linked to it.

This is done through the declaration of a new structure, the
`KeySessionRecord`, which can be "associated" to supplementary key ids
at any time, and which contains an `isCompatibleWith` method to quickly
check if newly encountered initialization data and/or key id is actually
compatible to an already-created MediaKeySession.

MediaKeySessions loaded (persisted ones) AND retrieved from our memory
cache now are not subject to the `singleLicensePer` option.
----------------------------------------------------------------------

All persisted MediaKeySessions as well as all those kept in our
in-memory cache were before subject to the same `singleLicensePer`
rules than the current content that is being loaded.

This means that technically, a license fetched for a previous content in
a `singleLicensePer: "init-data"` mode (the default), could be wrongly
inferred to be a `singleLicensePer: "content"` one if a new content
compatible with it was loaded in that mode. This would result in the
impossibility to use more than the one key stored with that
MediaKeySession.

This issue is described by the problem 1 of the #1031 issue (note that
problem 2 is not yet fixed, though it is a work-in-progress).

To fix this, the RxPlayer does now two things:
  1. A cached or persisted (still in WIP for that second one)
     MediaKeySession will now be linked in the corresponding cache to
     all known key id that are linked to it either implicit or explicit
     (description of both term higher in this message).

  2. Only MediaKeySessions receiving fetched license will now follow the
     `singleLicensePer` rule, all other type of sessions (loaded or
     retrieved from cache) will only be linked to the key id known
     at the time the caching has been done.

     This could mean relatively rare false negatives when a re-loaded
     content in a `singleLicensePer: "content"` mode contains key ids
     previously unheard of, but it is still the safest solution.

Write skeleton for the future `singleLicensePer: "periods"`
-----------------------------------------------------------

One of the goal of that big implementation was also to pave the way for
more complex `singleLicensePer` modes, as we plan to do for the v3.27.0
(#1028).
The main differences are that the `ContentDecryptor` (the new
`EMEManager`) now also receives Manifest-related structures in the
initialization data emitted to it and perform all the blacklisting by
itself.

This allows it to e.g. know about implicit key ids (keys that should
have been in the license according to the `singleLicensePer` mode but
which aren't) without necessitating a round-trip with the Init module.

Remaining issues
----------------

One unfortunate side-effect of the current implementation, is the
creation of a lock to only allow one initialization data at a time until
either the right MediaKeySession is obtained (either created, loaded, or
retrieved from cache) in `singleLicensePer: "init-data"` mode or until the
license's keys are available in singleLicensePer: "content"` mode.
This could mean unnecessary waiting when a persistent MediaKeySession is
being loaded, and even more when we have troubles loading it - which is
a frequent occurence on some set-top-box.

I'm sure that we could write a less strict lock though, I just didn't
take the time to do it.

Another remaining issue is that I did not finish working on persisted
MediaKeySession here, most notably to fix the problem 2 exposed by
the #1031 issue.
  • Loading branch information
peaBerberian committed Jan 24, 2022
1 parent 86af4bc commit 5829342
Show file tree
Hide file tree
Showing 19 changed files with 1,249 additions and 809 deletions.
2 changes: 1 addition & 1 deletion src/core/decrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
## Overview ####################################################################

This directory exports the `ContentDecryptor`, which allows to easily interface
with the browser APIs for decrypting a crypted content, it follows the
with the browser APIs for decrypting an encrypted content, it follows the
[Encrypted Media Extensions recommandations](https://www.w3.org/TR/encrypted-media/).

The `ContentDecryptor` is a module isolated from the rest of the player taking
Expand Down
745 changes: 501 additions & 244 deletions src/core/decrypt/content_decryptor.ts

Large diffs are not rendered by default.

106 changes: 51 additions & 55 deletions src/core/decrypt/create_or_load_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,11 @@ import createSession from "./create_session";
import {
IInitializationDataInfo,
IMediaKeySessionStores,
MediaKeySessionLoadingType,
} from "./types";
import cleanOldLoadedSessions, {
ICleaningOldSessionDataPayload,
} from "./utils/clean_old_loaded_sessions";
import cleanOldLoadedSessions from "./utils/clean_old_loaded_sessions";
import isSessionUsable from "./utils/is_session_usable";

/** Information concerning a MediaKeySession. */
export interface IMediaKeySessionContext {
/** The MediaKeySession itself. */
mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
/** The type of MediaKeySession (e.g. "temporary"). */
sessionType : MediaKeySessionType;
/** Initialization data assiociated to this MediaKeySession. */
initializationData : IInitializationDataInfo;
}

/** Event emitted when a new MediaKeySession has been created. */
export interface ICreatedSession {
type : "created-session";
value : IMediaKeySessionContext;
}

/** Event emitted when an already-loaded MediaKeySession is used. */
export interface ILoadedOpenSession {
type : "loaded-open-session";
value : IMediaKeySessionContext;
}

/** Event emitted when a persistent MediaKeySession has been loaded. */
export interface ILoadedPersistentSessionEvent {
type : "loaded-persistent-session";
value : IMediaKeySessionContext;
}

/** Every possible result returned by `getSession`. */
export type IGetSessionResult = ICreatedSession |
ILoadedOpenSession |
ILoadedPersistentSessionEvent;
import KeySessionRecord from "./utils/key_session_record";

/**
* Handle MediaEncryptedEvents sent by a HTMLMediaElement:
Expand All @@ -75,53 +41,49 @@ export type IGetSessionResult = ICreatedSession |
* @param {Object} stores
* @param {string} wantedSessionType
* @param {number} maxSessionCacheSize
* @param {Function} onCleaningSession
* @param {Object} cancelSignal
* @returns {Observable}
* @returns {Promise}
*/
export default async function getSession(
export default async function createOrLoadSession(
initializationData : IInitializationDataInfo,
stores : IMediaKeySessionStores,
wantedSessionType : MediaKeySessionType,
maxSessionCacheSize : number,
onCleaningSession : (arg : ICleaningOldSessionDataPayload) => void,
cancelSignal : CancellationSignal
) : Promise<IGetSessionResult> {
/**
* Store previously-loaded MediaKeySession with the same initialization data, if one.
*/
) : Promise<ICreateOrLoadSessionResult> {
/** Store previously-loaded compatible MediaKeySession, if one. */
let previousLoadedSession : MediaKeySession |
ICustomMediaKeySession |
null = null;

const { loadedSessionsStore, persistentSessionsStore } = stores;
const entry = loadedSessionsStore.getAndReuse(initializationData);
const entry = loadedSessionsStore.reuse(initializationData);
if (entry !== null) {
previousLoadedSession = entry.mediaKeySession;
if (isSessionUsable(previousLoadedSession)) {
log.info("DRM: Reuse loaded session", previousLoadedSession.sessionId);
return { type: "loaded-open-session" as const,
return { type: MediaKeySessionLoadingType.LoadedOpenSession,
value: { mediaKeySession: previousLoadedSession,
sessionType: entry.sessionType,
initializationData } };
keySessionRecord: entry.keySessionRecord } };
} else if (persistentSessionsStore !== null) {
// If the session is not usable anymore, we can also remove it from the
// PersistentSessionsStore.
// TODO Are we sure this is always what we want?
persistentSessionsStore.delete(initializationData);
if (entry.mediaKeySession.sessionId !== "") {
persistentSessionsStore.delete(entry.mediaKeySession.sessionId);
}
}
}

if (previousLoadedSession) {
await loadedSessionsStore.closeSession(initializationData);
if (previousLoadedSession !== null) {
await loadedSessionsStore.closeSession(previousLoadedSession);
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError; // stop here if cancelled since
}
}

await cleanOldLoadedSessions(loadedSessionsStore,
maxSessionCacheSize,
onCleaningSession);
await cleanOldLoadedSessions(loadedSessionsStore, maxSessionCacheSize);
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError; // stop here if cancelled since
}
Expand All @@ -130,5 +92,39 @@ export default async function getSession(
return { type: evt.type,
value: { mediaKeySession: evt.value.mediaKeySession,
sessionType: evt.value.sessionType,
initializationData } };
keySessionRecord: evt.value.keySessionRecord } };
}

/** Information concerning a MediaKeySession. */
export interface IMediaKeySessionContext {
/** The MediaKeySession itself. */
mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
/** The type of MediaKeySession (e.g. "temporary"). */
sessionType : MediaKeySessionType;
/** `KeySessionRecord` assiociated to this MediaKeySession. */
keySessionRecord : KeySessionRecord;
}

/** Event emitted when a new MediaKeySession has been created. */
export interface ICreatedSession {
type : MediaKeySessionLoadingType.Created;
value : IMediaKeySessionContext;
}

/** Event emitted when an already-loaded MediaKeySession is used. */
export interface ILoadedOpenSession {
type : MediaKeySessionLoadingType.LoadedOpenSession;
value : IMediaKeySessionContext;
}

/** Event emitted when a persistent MediaKeySession has been loaded. */
export interface ILoadedPersistentSessionEvent {
type : MediaKeySessionLoadingType.LoadedPersistentSession;
value : IMediaKeySessionContext;
}

/** Every possible result returned by `createOrLoadSession`. */
export type ICreateOrLoadSessionResult = ICreatedSession |
ILoadedOpenSession |
ILoadedPersistentSessionEvent;
114 changes: 60 additions & 54 deletions src/core/decrypt/create_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,60 +23,46 @@ import log from "../../log";
import {
IInitializationDataInfo,
IMediaKeySessionStores,
MediaKeySessionLoadingType,
} from "./types";
import isSessionUsable from "./utils/is_session_usable";
import KeySessionRecord from "./utils/key_session_record";
import LoadedSessionsStore from "./utils/loaded_sessions_store";
import PersistentSessionsStore from "./utils/persistent_sessions_store";

export interface INewSessionCreatedEvent {
type : "created-session";
value : { mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
sessionType : MediaKeySessionType; };
}

export interface IPersistentSessionRecoveryEvent {
type : "loaded-persistent-session";
value : { mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
sessionType : MediaKeySessionType; };
}

export type ICreateSessionEvent = INewSessionCreatedEvent |
IPersistentSessionRecoveryEvent;

/**
* Create a new Session on the given MediaKeys, corresponding to the given
* initializationData.
* Create a new Session or load a persistent one on the given MediaKeys,
* according to wanted settings and what is currently stored.
*
* If session creating fails, remove the oldest MediaKeySession loaded and
* retry.
*
* /!\ This only creates new sessions.
* It will fail if loadedSessionsStore already has a MediaKeySession with
* the given initializationData.
* @param {Uint8Array} initData
* @param {string|undefined} initDataType
* @param {Object} mediaKeysInfos
* the given initialization data.
* @param {Object} stores
* @param {Object} initData
* @param {string} wantedSessionType
* @returns {Promise}
*/
export default function createSession(
stores : IMediaKeySessionStores,
initializationData : IInitializationDataInfo,
initData : IInitializationDataInfo,
wantedSessionType : MediaKeySessionType
) : Promise<ICreateSessionEvent> {
const { loadedSessionsStore,
persistentSessionsStore } = stores;

if (wantedSessionType === "temporary") {
return createTemporarySession(loadedSessionsStore, initializationData);
return createTemporarySession(loadedSessionsStore, initData);
} else if (persistentSessionsStore === null) {
log.warn("DRM: Cannot create persistent MediaKeySession, " +
"PersistentSessionsStore not created.");
return createTemporarySession(loadedSessionsStore, initializationData);
return createTemporarySession(loadedSessionsStore, initData);
}
return createAndTryToRetrievePersistentSession(loadedSessionsStore,
persistentSessionsStore,
initializationData);
initData);
}

/**
Expand All @@ -91,15 +77,14 @@ function createTemporarySession(
initData : IInitializationDataInfo
) : Promise<INewSessionCreatedEvent> {
log.info("DRM: Creating a new temporary session");
const session = loadedSessionsStore.createSession(initData, "temporary");
return PPromise.resolve({ type: "created-session" as const,
value: { mediaKeySession: session,
sessionType: "temporary" as const } });
const entry = loadedSessionsStore.createSession(initData, "temporary");
return PPromise.resolve({ type: MediaKeySessionLoadingType.Created,
value: entry });
}

/**
* Create a persistent MediaKeySession and try to load on it a previous
* MediaKeySession linked to the same initData and initDataType.
* MediaKeySession linked to the same initialization data.
* @param {Object} loadedSessionsStore
* @param {Object} persistentSessionsStore
* @param {Object} initData
Expand All @@ -112,30 +97,28 @@ async function createAndTryToRetrievePersistentSession(
) : Promise<INewSessionCreatedEvent | IPersistentSessionRecoveryEvent> {
log.info("DRM: Creating persistent MediaKeySession");

const session = loadedSessionsStore.createSession(initData, "persistent-license");
const entry = loadedSessionsStore.createSession(initData, "persistent-license");
const storedEntry = persistentSessionsStore.getAndReuse(initData);
if (storedEntry === null) {
return { type: "created-session" as const,
value: { mediaKeySession: session,
sessionType: "persistent-license" as const } };
return { type: MediaKeySessionLoadingType.Created,
value: entry };
}

try {
const hasLoadedSession = await loadSession(session, storedEntry.sessionId);
const hasLoadedSession = await loadSession(entry.mediaKeySession,
storedEntry.sessionId);
if (!hasLoadedSession) {
log.warn("DRM: No data stored for the loaded session");
persistentSessionsStore.delete(initData);
return { type: "created-session" as const,
value: { mediaKeySession: session,
sessionType: "persistent-license" } };
persistentSessionsStore.delete(storedEntry.sessionId);
return { type: MediaKeySessionLoadingType.Created,
value: entry };
}

if (hasLoadedSession && isSessionUsable(session)) {
persistentSessionsStore.add(initData, session);
if (hasLoadedSession && isSessionUsable(entry.mediaKeySession)) {
persistentSessionsStore.add(initData, initData.keyIds, entry.mediaKeySession);
log.info("DRM: Succeeded to load persistent session.");
return { type: "loaded-persistent-session" as const,
value: { mediaKeySession: session,
sessionType: "persistent-license" } };
return { type: MediaKeySessionLoadingType.LoadedPersistentSession,
value: entry };
}

// Unusable persistent session: recreate a new session from scratch.
Expand All @@ -155,15 +138,38 @@ async function createAndTryToRetrievePersistentSession(
*/
async function recreatePersistentSession() : Promise<INewSessionCreatedEvent> {
log.info("DRM: Removing previous persistent session.");
if (persistentSessionsStore.get(initData) !== null) {
persistentSessionsStore.delete(initData);
const persistentEntry = persistentSessionsStore.get(initData);
if (persistentEntry !== null) {
persistentSessionsStore.delete(persistentEntry.sessionId);
}

await loadedSessionsStore.closeSession(initData);
const newSession = loadedSessionsStore.createSession(initData,
"persistent-license");
return { type: "created-session" as const,
value: { mediaKeySession: newSession,
sessionType: "persistent-license" } };
await loadedSessionsStore.closeSession(entry.mediaKeySession);
const newEntry = loadedSessionsStore.createSession(initData,
"persistent-license");
return { type: MediaKeySessionLoadingType.Created,
value: newEntry };
}
}

export interface INewSessionCreatedEvent {
type : MediaKeySessionLoadingType.Created;
value : {
mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
sessionType : MediaKeySessionType;
keySessionRecord : KeySessionRecord;
};
}

export interface IPersistentSessionRecoveryEvent {
type : MediaKeySessionLoadingType.LoadedPersistentSession;
value : {
mediaKeySession : MediaKeySession |
ICustomMediaKeySession;
sessionType : MediaKeySessionType;
keySessionRecord : KeySessionRecord;
};
}

export type ICreateSessionEvent = INewSessionCreatedEvent |
IPersistentSessionRecoveryEvent;
Loading

0 comments on commit 5829342

Please sign in to comment.