Skip to content

Commit

Permalink
Merge pull request #904 from canalplus/feat/singleLicensePer
Browse files Browse the repository at this point in the history
Single license per content implementation
  • Loading branch information
peaBerberian committed Mar 30, 2021
2 parents 284ec19 + 04a6201 commit 8e4c054
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 111 deletions.
10 changes: 6 additions & 4 deletions src/core/eme/__tests__/__global__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,12 @@ export class MediaKeySessionImpl extends EventEmitter<any> {
this.keyStatuses._setKeyStatus(new Uint8Array([0, 1, 2, this._currentKeyId++]),
"usable");
const event = new CustomEvent("keystatuseschange");
this.trigger("keyStatusesChange", event);
if (this.onkeystatuseschange !== null && this.onkeystatuseschange !== undefined) {
this.onkeystatuseschange(event);
}
setTimeout(() => {
this.trigger("keyStatusesChange", event);
if (this.onkeystatuseschange !== null && this.onkeystatuseschange !== undefined) {
this.onkeystatuseschange(event);
}
}, 50);
return Promise.resolve();
}
}
Expand Down
17 changes: 13 additions & 4 deletions src/core/eme/check_key_statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,26 @@ export interface IKeyStatusesCheckingOptions {

/**
* Look at the current key statuses in the sessions and construct the
* appropriate warnings and blacklisted key ids.
* appropriate warnings, whitelisted and blacklisted key ids.
*
* Throws if one of the keyID is on an error.
* @param {MediaKeySession} session - The MediaKeySession from which the keys
* will be checked.
* @param {Object} options
* @param {String} keySystem - The configuration keySystem used for deciphering
* @returns {Array} - Warnings to send and blacklisted key ids.
* @returns {Object} - Warnings to send, whitelisted and blacklisted key ids.
*/
export default function checkKeyStatuses(
session : MediaKeySession | ICustomMediaKeySession,
options: IKeyStatusesCheckingOptions,
keySystem: string
) : [IEMEWarningEvent[], Uint8Array[]] {
) : { warnings : IEMEWarningEvent[];
blacklistedKeyIDs : Uint8Array[];
whitelistedKeyIds : Uint8Array[]; }
{
const warnings : IEMEWarningEvent[] = [];
const blacklistedKeyIDs : Uint8Array[] = [];
const whitelistedKeyIds : Uint8Array[] = [];
const { fallbackOn = {}, throwOnLicenseExpiration } = options;

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
Expand All @@ -90,6 +94,7 @@ export default function checkKeyStatuses(
throw error;
}
warnings.push({ type: "warning", value: error });
whitelistedKeyIds.push(keyId);
break;
}

Expand All @@ -116,7 +121,11 @@ export default function checkKeyStatuses(
blacklistedKeyIDs.push(keyId);
break;
}

default:
whitelistedKeyIds.push(keyId);
break;
}
});
return [warnings, blacklistedKeyIDs];
return { warnings, blacklistedKeyIDs, whitelistedKeyIds };
}
137 changes: 100 additions & 37 deletions src/core/eme/eme_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
merge as observableMerge,
Observable,
of as observableOf,
ReplaySubject,
throwError,
} from "rxjs";
import {
Expand All @@ -41,6 +42,7 @@ import {
import config from "../../config";
import { EncryptedMediaError } from "../../errors";
import log from "../../log";
import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal";
import arrayIncludes from "../../utils/array_includes";
import assertUnreachable from "../../utils/assert_unreachable";
import { concat } from "../../utils/byte_parsing";
Expand All @@ -59,6 +61,7 @@ import {
IEMEManagerEvent,
IInitializationDataInfo,
IKeySystemOption,
IKeyUpdateValue,
} from "./types";
import InitDataStore from "./utils/init_data_store";

Expand Down Expand Up @@ -86,15 +89,21 @@ export default function EMEManager(
log.debug("EME: Starting EMEManager logic.");

/**
* Keep track of all initialization data handled for the current `EMEManager`
* instance.
* This allows to avoid handling multiple times the same encrypted events.
* Keep track of all decryption keys handled by this instance of the
* `EMEManager`.
* This allows to avoid creating multiple MediaKeySessions handling the same
* decryption keys.
*/
const handledInitData = new InitDataStore<boolean>();
const contentSessions = new InitDataStore<{
/** Initialization data which triggered the creation of this session. */
initializationData : IInitializationDataInfo;
/** Last key update event received for that session. */
lastKeyUpdate$ : ReplaySubject<IKeyUpdateValue>;
}>();

/**
* Keep track of which initialization data have been blacklisted (linked to
* non-decypherable content).
* Keep track of which initialization data have been blacklisted in the
* current instance of the `EMEManager`.
* If the same initialization data is encountered again, we can directly emit
* the same `BlacklistedSessionError`.
*/
Expand Down Expand Up @@ -168,7 +177,43 @@ export default function EMEManager(
value: initializationData });
}

if (!handledInitData.storeIfNone(initializationData, true)) {
const lastKeyUpdate$ = new ReplaySubject<IKeyUpdateValue>(1);

// First, check that this initialization data is not already handled
if (options.singleLicensePer === "content" && !contentSessions.isEmpty()) {
const keyIds = initializationData.keyIds;
if (keyIds === undefined) {
log.warn("EME: Initialization data linked to unknown key id, we'll " +
"not able to fallback from it.");
return observableOf({ type: "init-data-ignored" as const,
value: { initializationData } });
}
const firstSession = contentSessions.getAll()[0];
return firstSession.lastKeyUpdate$.pipe(mergeMap((evt) => {
const hasAllNeededKeyIds = keyIds.every(keyId => {
for (let i = 0; i < evt.whitelistedKeyIds.length; i++) {
if (areArraysOfNumbersEqual(evt.whitelistedKeyIds[i], keyId)) {
return true;
}
}
});

if (!hasAllNeededKeyIds) {
// Not all keys are available in the current session, blacklist those
return observableOf({ type: "keys-update" as const,
value: { blacklistedKeyIDs: keyIds,
whitelistedKeyIds: [] } });
}

// Already handled by the current session.
// Move corresponding session on top of the cache if it exists
const { loadedSessionsStore } = mediaKeysEvent.value.stores;
loadedSessionsStore.reuse(firstSession.initializationData);
return observableOf({ type: "init-data-ignored" as const,
value: { initializationData } });
}));
} else if (!contentSessions.storeIfNone(initializationData, { initializationData,
lastKeyUpdate$ })) {
log.debug("EME: Init data already received. Skipping it.");
return observableOf({ type: "init-data-ignored" as const,
value: { initializationData } });
Expand All @@ -188,7 +233,7 @@ export default function EMEManager(
.pipe(mergeMap((sessionEvt) => {
switch (sessionEvt.type) {
case "cleaning-old-session":
handledInitData.remove(sessionEvt.value.initializationData);
contentSessions.remove(sessionEvt.value.initializationData);
return EMPTY;

case "cleaned-old-session":
Expand All @@ -207,6 +252,13 @@ export default function EMEManager(
const { mediaKeySession,
sessionType } = sessionEvt.value;

/**
* We only store persistent sessions once its keys are known.
* This boolean allows to know if this session has already been
* persisted or not.
*/
let isSessionPersisted = false;

// `generateKeyRequest` awaits a single Uint8Array containing all
// initialization data.
const concatInitData = concat(...initializationData.values.map(i => i.data));
Expand All @@ -216,17 +268,6 @@ export default function EMEManager(
generateKeyRequest(mediaKeySession,
initializationData.type,
concatInitData).pipe(
tap(() => {
const { persistentSessionsStore } = stores;
if (sessionType === "persistent-license" &&
persistentSessionsStore !== null)
{
cleanOldStoredPersistentInfo(
persistentSessionsStore,
EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION - 1);
persistentSessionsStore.add(initializationData, mediaKeySession);
}
}),
catchError((error: unknown) => {
throw new EncryptedMediaError(
"KEY_GENERATE_REQUEST_ERROR",
Expand All @@ -240,27 +281,49 @@ export default function EMEManager(
mediaKeySystemAccess.keySystem,
initializationData),
generateRequest$)
.pipe(catchError(err => {
if (!(err instanceof BlacklistedSessionError)) {
throw err;
}
.pipe(
tap((evt) => {
if (evt.type !== "keys-update") {
return;
}
lastKeyUpdate$.next(evt.value);

blacklistedInitData.store(initializationData, err);
if ((evt.value.whitelistedKeyIds.length === 0 &&
evt.value.blacklistedKeyIDs.length === 0) ||
sessionType === "temporary" ||
stores.persistentSessionsStore === null ||
isSessionPersisted)
{
return;
}
const { persistentSessionsStore } = stores;
cleanOldStoredPersistentInfo(
persistentSessionsStore,
EME_MAX_STORED_PERSISTENT_SESSION_INFORMATION - 1);
persistentSessionsStore.add(initializationData, mediaKeySession);
isSessionPersisted = true;
}),
catchError(err => {
if (!(err instanceof BlacklistedSessionError)) {
throw err;
}

const { sessionError } = err;
if (initializationData.type === undefined) {
log.error("EME: Current session blacklisted and content not known. " +
"Throwing.");
sessionError.fatal = true;
throw sessionError;
}
blacklistedInitData.store(initializationData, err);

const { sessionError } = err;
if (initializationData.type === undefined) {
log.error("EME: Current session blacklisted and content not known. " +
"Throwing.");
sessionError.fatal = true;
throw sessionError;
}

log.warn("EME: Current session blacklisted. Blacklisting content.");
return observableOf({ type: "warning" as const,
value: sessionError },
{ type: "blacklist-protection-data" as const,
value: initializationData });
}));
log.warn("EME: Current session blacklisted. Blacklisting content.");
return observableOf({ type: "warning" as const,
value: sessionError },
{ type: "blacklist-protection-data" as const,
value: initializationData });
}));
}));
}));

Expand Down
37 changes: 19 additions & 18 deletions src/core/eme/session_events_listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ import checkKeyStatuses, {
IKeyStatusesCheckingOptions,
} from "./check_key_statuses";
import {
IBlacklistKeysEvent,
IInitializationDataInfo,
IEMEWarningEvent,
IKeyMessageHandledEvent,
IKeyStatusChangeHandledEvent,
IKeySystemOption,
IKeysUpdateEvent,
INoUpdateEvent,
ISessionMessageEvent,
ISessionUpdatedEvent,
Expand Down Expand Up @@ -106,8 +106,8 @@ export default function SessionEventsListener(
) : Observable<IEMEWarningEvent |
ISessionMessageEvent |
INoUpdateEvent |
ISessionUpdatedEvent |
IBlacklistKeysEvent>
IKeysUpdateEvent |
ISessionUpdatedEvent>
{
log.info("EME: Binding session events", session);
const sessionWarningSubject$ = new Subject<IEMEWarningEvent>();
Expand Down Expand Up @@ -174,13 +174,13 @@ export default function SessionEventsListener(
evt : IEMEWarningEvent |
ISessionMessageEvent |
IKeyMessageHandledEvent |
IKeyStatusChangeHandledEvent |
IBlacklistKeysEvent
IKeysUpdateEvent |
IKeyStatusChangeHandledEvent
) : Observable< IEMEWarningEvent |
ISessionMessageEvent |
INoUpdateEvent |
ISessionUpdatedEvent |
IBlacklistKeysEvent > => {
IKeysUpdateEvent > => {
switch (evt.type) {
case "key-message-handled":
case "key-status-change-handled":
Expand Down Expand Up @@ -218,19 +218,20 @@ function getKeyStatusesEvents(
session : MediaKeySession | ICustomMediaKeySession,
options : IKeyStatusesCheckingOptions,
keySystem : string
) : Observable<IEMEWarningEvent | IBlacklistKeysEvent> {
) : Observable<IEMEWarningEvent | IKeysUpdateEvent> {
return observableDefer(() => {
const [warnings, blacklistedKeyIDs] =
if (session.keyStatuses.size === 0) {
return EMPTY;
}
const { warnings, blacklistedKeyIDs, whitelistedKeyIds } =
checkKeyStatuses(session, options, keySystem);

const warnings$ = warnings.length > 0 ? observableOf(...warnings) :
EMPTY;

const blackListUpdate$ = blacklistedKeyIDs.length > 0 ?
observableOf({ type: "blacklist-keys" as const,
value: blacklistedKeyIDs }) :
EMPTY;
return observableConcat(warnings$, blackListUpdate$);
EMPTY;
const keysUpdate$ = observableOf({ type : "keys-update" as const,
value : { whitelistedKeyIds,
blacklistedKeyIDs } });
return observableConcat(warnings$, keysUpdate$);
});
}

Expand Down Expand Up @@ -300,7 +301,7 @@ function handleKeyStatusesChangeEvent(
keySystemOptions : IKeySystemOption,
keySystem : string,
keyStatusesEvent : Event
) : Observable<IKeyStatusChangeHandledEvent | IBlacklistKeysEvent | IEMEWarningEvent> {
) : Observable<IKeyStatusChangeHandledEvent | IKeysUpdateEvent | IEMEWarningEvent> {
log.info("EME: keystatuseschange event received", session, keyStatusesEvent);
const callback$ = observableDefer(() => {
return tryCatch(() => {
Expand All @@ -324,8 +325,8 @@ function handleKeyStatusesChangeEvent(
throw err;
})
);
return observableConcat(getKeyStatusesEvents(session, keySystemOptions, keySystem),
callback$);
return observableMerge(getKeyStatusesEvents(session, keySystemOptions, keySystem),
callback$);
}

/**
Expand Down
Loading

0 comments on commit 8e4c054

Please sign in to comment.