rx-player
Version:
Canal+ HTML5 Video Player
218 lines (202 loc) • 8.16 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 } from "../../compat/browser_compatibility_types";
import log from "../../log";
import type { CancellationSignal } from "../../utils/task_canceller";
import type { IProcessedProtectionData, IMediaKeySessionStores } from "./types";
import { MediaKeySessionLoadingType } from "./types";
import isSessionUsable from "./utils/is_session_usable";
import type KeySessionRecord from "./utils/key_session_record";
import type LoadedSessionsStore from "./utils/loaded_sessions_store";
import type PersistentSessionsStore from "./utils/persistent_sessions_store";
/**
* 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 initialization data.
* @param {Object} stores
* @param {Object} initData
* @param {string} wantedSessionType
* @param {Object} cancelSignal
* @returns {Promise}
*/
export default function createSession(
stores: IMediaKeySessionStores,
initData: IProcessedProtectionData,
wantedSessionType: MediaKeySessionType,
cancelSignal: CancellationSignal,
): Promise<ICreateSessionEvent> {
const { loadedSessionsStore, persistentSessionsStore } = stores;
if (wantedSessionType === "temporary") {
return createTemporarySession(loadedSessionsStore, initData);
} else if (persistentSessionsStore === null) {
log.warn(
"DRM",
"Cannot create persistent MediaKeySession, " +
"PersistentSessionsStore not created.",
);
return createTemporarySession(loadedSessionsStore, initData);
}
return createAndTryToRetrievePersistentSession(
loadedSessionsStore,
persistentSessionsStore,
initData,
cancelSignal,
);
}
/**
* Create a new temporary MediaKeySession linked to the given initData and
* initDataType.
* @param {Object} loadedSessionsStore
* @param {Object} initData
* @returns {Promise}
*/
function createTemporarySession(
loadedSessionsStore: LoadedSessionsStore,
initData: IProcessedProtectionData,
): Promise<INewSessionCreatedEvent> {
log.info("DRM", "Creating a new temporary session");
const entry = loadedSessionsStore.createSession(initData, "temporary");
return Promise.resolve({
type: MediaKeySessionLoadingType.Created,
value: entry,
});
}
/**
* Create a persistent MediaKeySession and try to load on it a previous
* MediaKeySession linked to the same initialization data.
* @param {Object} loadedSessionsStore
* @param {Object} persistentSessionsStore
* @param {Object} initData
* @param {Object} cancelSignal
* @returns {Promise}
*/
async function createAndTryToRetrievePersistentSession(
loadedSessionsStore: LoadedSessionsStore,
persistentSessionsStore: PersistentSessionsStore,
initData: IProcessedProtectionData,
cancelSignal: CancellationSignal,
): Promise<INewSessionCreatedEvent | IPersistentSessionRecoveryEvent> {
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
log.info("DRM", "Creating persistent MediaKeySession");
const entry = loadedSessionsStore.createSession(initData, "persistent-license");
const storedEntry = persistentSessionsStore.getAndReuse(initData);
if (storedEntry === null) {
return { type: MediaKeySessionLoadingType.Created, value: entry };
}
try {
const hasLoadedSession = await loadedSessionsStore.loadPersistentSession(
entry.mediaKeySession,
storedEntry.sessionId,
);
if (!hasLoadedSession) {
log.warn("DRM", "No data stored for the loaded session", {
sessionId: storedEntry.sessionId,
});
persistentSessionsStore.delete(storedEntry.sessionId);
// The EME specification is kind of implicit about it but it seems from my
// understanding (Paul B.) that a MediaKeySession on wich a `load` attempt
// did not succeed due to the loaded session not being found by the
// browser/CDM, should neither be used anymore nor closed.
// Thus, we're creating another `"persistent-license"` `MediaKeySession`
// in that specific case.
loadedSessionsStore.removeSessionWithoutClosingIt(entry.mediaKeySession);
const newEntry = loadedSessionsStore.createSession(initData, "persistent-license");
return { type: MediaKeySessionLoadingType.Created, value: newEntry };
}
if (hasLoadedSession && isSessionUsable(entry.mediaKeySession)) {
persistentSessionsStore.add(initData, initData.keyIds, entry.mediaKeySession);
log.info("DRM", "Succeeded to load persistent session.");
return {
type: MediaKeySessionLoadingType.LoadedPersistentSession,
value: entry,
};
}
// Unusable persistent session: recreate a new session from scratch.
log.warn("DRM", "Previous persistent session not usable anymore.");
return recreatePersistentSession();
} catch (err) {
log.warn(
"DRM",
"Unable to load persistent session: " +
(err instanceof Error ? err.toString() : "Unknown Error"),
);
return recreatePersistentSession();
}
/**
* Helper function to close and restart the current persistent session
* considered, and re-create it from scratch.
* @returns {Promise.<Object>}
*/
async function recreatePersistentSession(): Promise<INewSessionCreatedEvent> {
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
log.info("DRM", "Removing previous persistent session.");
const persistentEntry = persistentSessionsStore.get(initData);
if (persistentEntry !== null) {
persistentSessionsStore.delete(persistentEntry.sessionId);
}
try {
await loadedSessionsStore.closeSession(entry.mediaKeySession);
} catch (err) {
// From reading the EME specification in details, it seems that a
// `MediaKeySession`'s ability to be closed is tightly linked to its
// possession of a "sanitized session ID" set as `sessionId`.
// This is never clearly stated however and I'm (Paul B.) always afraid of
// breaking compatibility when it comes to EME code.
// So we still try to close the `MediaKeySession` in any case, only, if it
// fails and it didn't had any `sessionId` set, we just ignore the error.
// Note that trying to close the `MediaKeySession` might incur some delays
// in those rare cases.
if (entry.mediaKeySession.sessionId !== "") {
throw err;
}
loadedSessionsStore.removeSessionWithoutClosingIt(entry.mediaKeySession);
}
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
const newEntry = loadedSessionsStore.createSession(initData, "persistent-license");
return { type: MediaKeySessionLoadingType.Created, value: newEntry };
}
}
export interface INewSessionCreatedEvent {
type: MediaKeySessionLoadingType.Created;
value: {
mediaKeySession: IMediaKeySession;
sessionType: MediaKeySessionType;
keySessionRecord: KeySessionRecord;
};
}
export interface IPersistentSessionRecoveryEvent {
type: MediaKeySessionLoadingType.LoadedPersistentSession;
value: {
mediaKeySession: IMediaKeySession;
sessionType: MediaKeySessionType;
keySessionRecord: KeySessionRecord;
};
}
export type ICreateSessionEvent =
| INewSessionCreatedEvent
| IPersistentSessionRecoveryEvent;