rx-player
Version:
Canal+ HTML5 Video Player
523 lines (493 loc) • 16.8 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
IMediaKeySession,
IMediaKeys,
} from "../../../compat/browser_compatibility_types";
import { closeSession, generateKeyRequest, loadSession } from "../../../compat/eme";
import log from "../../../log";
import assert from "../../../utils/assert";
import isNullOrUndefined from "../../../utils/is_null_or_undefined";
import type { IProcessedProtectionData } from "../types";
import KeySessionRecord from "./key_session_record";
/**
* Create and store MediaKeySessions linked to a single MediaKeys
* instance.
*
* Keep track of sessionTypes and of the initialization data each
* MediaKeySession is created for.
* @class LoadedSessionsStore
*/
export default class LoadedSessionsStore {
/** MediaKeys instance on which the MediaKeySessions are created. */
private readonly _mediaKeys: IMediaKeys;
/** Store unique MediaKeySession information per initialization data. */
private _storage: IStoredSessionEntry[];
/**
* Create a new LoadedSessionsStore, which will store information about
* loaded MediaKeySessions on the given MediaKeys instance.
* @param {MediaKeys} mediaKeys
*/
constructor(mediaKeys: IMediaKeys) {
this._mediaKeys = mediaKeys;
this._storage = [];
}
/**
* Create a new MediaKeySession and store it in this store.
* @param {Object} initData
* @param {string} sessionType
* @returns {Object}
*/
public createSession(
initData: IProcessedProtectionData,
sessionType: MediaKeySessionType,
): IStoredSessionEntry {
const keySessionRecord = new KeySessionRecord(initData);
log.debug("DRM", "calling `createSession`", { sessionType });
const mediaKeySession = this._mediaKeys.createSession(sessionType);
const entry = {
mediaKeySession,
sessionType,
keySessionRecord,
isGeneratingRequest: false,
isLoadingPersistentSession: false,
closingStatus: { type: "none" as const },
};
if (!isNullOrUndefined(mediaKeySession.closed)) {
mediaKeySession.closed
.then(() => {
log.info("DRM", "session was closed, removing it.", {
sessionId: mediaKeySession.sessionId,
});
const index = this.getIndex(keySessionRecord);
if (index >= 0 && this._storage[index].mediaKeySession === mediaKeySession) {
this._storage.splice(index, 1);
}
})
.catch((e: unknown) => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
log.warn("DRM", `MediaKeySession.closed rejected: ${e}`);
});
}
this._storage.push({ ...entry });
log.debug("DRM", "MediaKeySession added", {
sessionType: entry.sessionType,
currentlyLoaded: this._storage.length,
});
return entry;
}
/**
* Find a stored entry compatible with the initialization data given and moves
* this entry at the end of the `LoadedSessionsStore`''s storage, returned by
* its `getAll` method.
*
* This can be used for example to tell when a previously-stored
* entry is re-used to then be able to implement a caching replacement
* algorithm based on the least-recently-used values by just evicting the first
* values returned by `getAll`.
* @param {Object} initializationData
* @returns {Object|null}
*/
public reuse(initializationData: IProcessedProtectionData): IStoredSessionEntry | null {
for (let i = this._storage.length - 1; i >= 0; i--) {
const stored = this._storage[i];
if (stored.keySessionRecord.isCompatibleWith(initializationData)) {
this._storage.splice(i, 1);
this._storage.push(stored);
log.debug("DRM", "Reusing session:", {
sessionId: stored.mediaKeySession.sessionId,
sessionType: stored.sessionType,
});
return { ...stored };
}
}
return null;
}
/**
* Get `LoadedSessionsStore`'s entry for a given MediaKeySession.
* Returns `null` if the given MediaKeySession is not stored in the
* `LoadedSessionsStore`.
* @param {MediaKeySession} mediaKeySession
* @returns {Object|null}
*/
public getEntryForSession(
mediaKeySession: IMediaKeySession,
): IStoredSessionEntry | null {
for (let i = this._storage.length - 1; i >= 0; i--) {
const stored = this._storage[i];
if (stored.mediaKeySession === mediaKeySession) {
return { ...stored };
}
}
return null;
}
/**
* Generate a license request on the given MediaKeySession, while indicating
* to the LoadedSessionsStore that a license-request is pending so
* session-closing orders are properly scheduled after it is done.
* @param {Object} mediaKeySession
* @param {string|undefined} initializationDataType - Initialization data type
* given e.g. by the "encrypted" event for the corresponding request.
* @param {Uint8Array} initializationData - Initialization data given e.g. by
* the "encrypted" event for the corresponding request.
* @returns {Promise}
*/
public async generateLicenseRequest(
mediaKeySession: IMediaKeySession,
initializationDataType: string | undefined,
initializationData: Uint8Array<ArrayBuffer>,
): Promise<unknown> {
let entry: IStoredSessionEntry | undefined;
for (const stored of this._storage) {
if (stored.mediaKeySession === mediaKeySession) {
entry = stored;
break;
}
}
if (entry === undefined) {
log.error(
"DRM",
"generateRequest error. No MediaKeySession found with " +
"the given initData and initDataType",
);
return generateKeyRequest(
mediaKeySession,
initializationDataType,
initializationData,
);
}
entry.isGeneratingRequest = true;
// Note the `as string` is needed due to TypeScript not understanding that
// the `closingStatus` might change in the next checks
if ((entry.closingStatus.type as string) !== "none") {
throw new Error("The `MediaKeySession` is being closed.");
}
try {
await generateKeyRequest(
mediaKeySession,
initializationDataType,
initializationData,
);
} catch (err) {
if (entry === undefined) {
throw err;
}
entry.isGeneratingRequest = false;
if (entry.closingStatus.type === "awaiting") {
entry.closingStatus.start();
}
throw err;
}
if (entry === undefined) {
return undefined;
}
entry.isGeneratingRequest = false;
if (entry.closingStatus.type === "awaiting") {
entry.closingStatus.start();
}
}
/**
* @param {Object} mediaKeySession
* @param {string} sessionId
* @returns {Promise}
*/
public async loadPersistentSession(
mediaKeySession: IMediaKeySession,
sessionId: string,
): Promise<boolean> {
let entry: IStoredSessionEntry | undefined;
for (const stored of this._storage) {
if (stored.mediaKeySession === mediaKeySession) {
entry = stored;
break;
}
}
if (entry === undefined) {
log.error(
"DRM",
"loadPersistentSession error. No MediaKeySession found with " +
"the given initData and initDataType",
);
return loadSession(mediaKeySession, sessionId);
}
entry.isLoadingPersistentSession = true;
// Note the `as string` is needed due to TypeScript not understanding that
// the `closingStatus` might change in the next checks
if ((entry.closingStatus.type as string) !== "none") {
throw new Error("The `MediaKeySession` is being closed.");
}
let ret: boolean;
try {
ret = await loadSession(mediaKeySession, sessionId);
} catch (err) {
if (entry === undefined) {
throw err;
}
entry.isLoadingPersistentSession = false;
if (entry.closingStatus.type === "awaiting") {
entry.closingStatus.start();
}
throw err;
}
if (entry === undefined) {
return ret;
}
entry.isLoadingPersistentSession = false;
if (entry.closingStatus.type === "awaiting") {
entry.closingStatus.start();
}
return ret;
}
/**
* Close a MediaKeySession and remove its related stored information from the
* `LoadedSessionsStore`.
* Emit when done.
* @param {Object} mediaKeySession
* @returns {Promise}
*/
public async closeSession(mediaKeySession: IMediaKeySession): Promise<boolean> {
let entry: IStoredSessionEntry | undefined;
for (const stored of this._storage) {
if (stored.mediaKeySession === mediaKeySession) {
entry = stored;
break;
}
}
if (entry === undefined) {
log.warn(
"DRM",
"No MediaKeySession found with " + "the given initData and initDataType",
);
return Promise.resolve(false);
}
return this._closeEntry(entry);
}
/**
* Returns the number of stored MediaKeySessions in this LoadedSessionsStore.
* @returns {number}
*/
public getLength(): number {
return this._storage.length;
}
/**
* Returns information about all stored MediaKeySession, in the order in which
* the MediaKeySession have been created.
* @returns {Array.<Object>}
*/
public getAll(): IStoredSessionEntry[] {
return this._storage;
}
/**
* Close all sessions in this store.
* Emit `null` when done.
* @returns {Promise}
*/
public async closeAllSessions(): Promise<void> {
const allEntries = this._storage;
log.debug("DRM", "Closing all current MediaKeySessions", {
numberOfEntries: allEntries.length,
});
// re-initialize the storage, so that new interactions with the
// `LoadedSessionsStore` do not rely on MediaKeySessions we're in the
// process of removing
this._storage = [];
const closingProms = allEntries.map((entry) => this._closeEntry(entry));
await Promise.all(closingProms);
}
/**
* Find the given `MediaKeySession` in the `LoadedSessionsStore` and removes
* any reference to it without actually closing it.
*
* Returns `true` if the given `mediaKeySession` has been found and removed,
* `false` otherwise.
*
* Note that this may create a `MediaKeySession` leakage in the wrong
* conditions, cases where this method should be called should be very
* carefully evaluated.
* @param {MediaKeySession} mediaKeySession
* @returns {boolean}
*/
public removeSessionWithoutClosingIt(mediaKeySession: IMediaKeySession): boolean {
assert(
mediaKeySession.sessionId === "",
"Initialized `MediaKeySession`s should always be properly closed",
);
for (let i = this._storage.length - 1; i >= 0; i--) {
const stored = this._storage[i];
if (stored.mediaKeySession === mediaKeySession) {
log.debug("DRM", "Removing session without closing it", {
sessionId: mediaKeySession.sessionId,
});
this._storage.splice(i, 1);
return true;
}
}
return false;
}
/**
* Get the index of a stored MediaKeySession entry based on its
* `KeySessionRecord`.
* Returns -1 if not found.
* @param {Object} record
* @returns {number}
*/
private getIndex(record: KeySessionRecord): number {
for (let i = 0; i < this._storage.length; i++) {
const stored = this._storage[i];
if (stored.keySessionRecord === record) {
return i;
}
}
return -1;
}
/**
* Prepare the closure of a `MediaKeySession` stored as an entry of the
* `LoadedSessionsStore`.
* Allows to postpone the closure action if another MediaKeySession action
* is already pending.
* @param {Object} entry
* @returns {Promise.<boolean>}
*/
private async _closeEntry(entry: IStoredSessionEntry): Promise<boolean> {
const { mediaKeySession } = entry;
return new Promise((resolve, reject) => {
if (
entry !== undefined &&
(entry.isLoadingPersistentSession || entry.isGeneratingRequest)
) {
entry.closingStatus = {
type: "awaiting",
start: tryClosingEntryAndResolve,
};
} else {
tryClosingEntryAndResolve();
}
function tryClosingEntryAndResolve() {
if (entry !== undefined) {
entry.closingStatus = { type: "pending" };
}
safelyCloseMediaKeySession(mediaKeySession)
.then(() => {
if (entry !== undefined) {
entry.closingStatus = { type: "done" };
}
resolve(true);
})
.catch((err) => {
if (entry !== undefined) {
entry.closingStatus = { type: "failed" };
}
reject(err);
});
}
});
}
}
/** Information linked to a `MediaKeySession` created by the `LoadedSessionsStore`. */
export interface IStoredSessionEntry {
/**
* The `KeySessionRecord` linked to the MediaKeySession.
* It keeps track of all key ids that are currently known to be associated to
* the MediaKeySession.
*
* Initially only assiociated with the initialization data given, you may want
* to add to it other key ids if you find out that there are also linked to
* that session.
*
* Regrouping all those key ids into the `KeySessionRecord` in that way allows
* the `LoadedSessionsStore` to perform compatibility checks when future
* initialization data is encountered.
*/
keySessionRecord: KeySessionRecord;
/** The MediaKeySession created. */
mediaKeySession: IMediaKeySession;
/**
* The MediaKeySessionType (e.g. "temporary" or "persistent-license") with
* which the MediaKeySession was created.
*/
sessionType: MediaKeySessionType;
/**
* Set to `true` while a `generateRequest` call is pending.
* This information might be useful as it is one of the operation we have to
* wait for before closing a MediaKeySession.
*/
isGeneratingRequest: boolean;
/**
* Set to `true` while a `load` call is pending.
* This information might be useful as it is one of the operation we have to
* wait for before closing a MediaKeySession.
*/
isLoadingPersistentSession: boolean;
/**
* The status of a potential `MediaKeySession`'s close request.
* Closing a MediaKeySession could be made complex as it normally cannot
* happen until `generateRequest` or `load` has been called.
*
* To avoid problems while still staying compatible to the most devices
* possible - which may have strange implementation of the specification -
* we're adding the `closingStatus` property allowing to perform multiple
* type of interaction while a close operation is either pending or is
* awaited.
*/
closingStatus: /** Status when the MediaKeySession is currently being closed. */
| { type: "pending" }
/** Status when the MediaKeySession has been closed. */
| { type: "done" }
/** Status when the MediaKeySession failed to close. */
| { type: "failed" }
/**
* Status when a close order has been received for this MediaKeySession
* while some sensitive operation (examples are `generateRequest` and `load`
* calls).
* The `LoadedSessionsStore` should call `start` once it has finished those
* operations.
*/
| {
type: "awaiting";
start: () => void;
}
/** Status when the MediaKeySession failed to close. */
| { type: "none" };
}
/**
* Close a MediaKeySession and just log an error if it fails (while resolving).
* Emits then complete when done.
* @param {MediaKeySession} mediaKeySession
* @returns {Promise}
*/
async function safelyCloseMediaKeySession(
mediaKeySession: IMediaKeySession,
): Promise<void> {
const sessionId = mediaKeySession.sessionId;
log.debug("DRM", "Trying to close a MediaKeySession", {
sessionId,
});
try {
await closeSession(mediaKeySession);
log.debug("DRM", "Succeeded to close MediaKeySession", {
sessionId,
});
return;
} catch (err: unknown) {
log.error(
"DRM",
"Could not close MediaKeySession: " +
(err instanceof Error ? err.toString() : "Unknown error"),
{ sessionId },
);
return;
}
}