rx-player
Version:
Canal+ HTML5 Video Player
460 lines (438 loc) • 14.9 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 {
IPersistentLicenseConfig,
IPersistentSessionInfo,
} from "../../../public_types";
import areArraysOfNumbersEqual from "../../../utils/are_arrays_of_numbers_equal";
import { assertInterface } from "../../../utils/assert";
import { bytesToBase64 } from "../../../utils/base64";
import hashBuffer from "../../../utils/hash_buffer";
import isNonEmptyString from "../../../utils/is_non_empty_string";
import isNullOrUndefined from "../../../utils/is_null_or_undefined";
import type { IProcessedProtectionData } from "../types";
import areInitializationValuesCompatible from "./are_init_values_compatible";
import type { IFormattedInitDataValue } from "./init_data_values_container";
import SerializableBytes from "./serializable_bytes";
/**
* Throw if the given storage does not respect the right interface.
* @param {Object} storage
*/
function checkStorage(storage: IPersistentLicenseConfig): void {
assertInterface(
storage,
{ save: "function", load: "function" },
"persistentLicenseConfig",
);
}
/**
* Set representing persisted licenses. Depends on a simple
* implementation with a `save`/`load` synchronous interface
* to persist information on persisted sessions.
*
* This set is used only for a cdm/keysystem with license persistency
* supported.
* @class PersistentSessionsStore
*/
export default class PersistentSessionsStore {
private readonly _storage: IPersistentLicenseConfig;
private _entries: IPersistentSessionInfo[];
/**
* Create a new PersistentSessionsStore.
* @param {Object} storage
*/
constructor(storage: IPersistentLicenseConfig) {
checkStorage(storage);
this._entries = [];
this._storage = storage;
try {
let entries = this._storage.load();
if (!Array.isArray(entries)) {
entries = [];
}
this._entries = entries;
} catch (e) {
log.warn(
"DRM",
"Persistent store: Could not get entries from license storage",
e instanceof Error ? e : "",
);
this.dispose();
}
}
/**
* Returns the number of stored values.
* @returns {number}
*/
public getLength(): number {
return this._entries.length;
}
/**
* Returns information about all stored MediaKeySession, in the order in which
* the MediaKeySession have been created.
* @returns {Array.<Object>}
*/
public getAll(): IPersistentSessionInfo[] {
return this._entries;
}
/**
* Retrieve an entry based on its initialization data.
* @param {Object} initData
* @returns {Object|null}
*/
public get(initData: IProcessedProtectionData): IPersistentSessionInfo | null {
const index = this._getIndex(initData);
return index === -1 ? null : this._entries[index];
}
/**
* Like `get`, but also move the corresponding value at the end of the store
* (as returned by `getAll`) if found.
* This can be used for example to tell when a previously-stored value 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} initData
* @returns {*}
*/
public getAndReuse(initData: IProcessedProtectionData): IPersistentSessionInfo | null {
const index = this._getIndex(initData);
if (index === -1) {
return null;
}
const item = this._entries.splice(index, 1)[0];
this._entries.push(item);
return item;
}
/**
* Add a new entry in the PersistentSessionsStore.
* @param {Object} initData
* @param {Array.<Uint8Array>|undefined} keyIds
* @param {MediaKeySession} session
*/
public add(
initData: IProcessedProtectionData,
keyIds: Uint8Array[] | undefined,
session: IMediaKeySession,
): void {
if (isNullOrUndefined(session) || !isNonEmptyString(session.sessionId)) {
log.warn("DRM", "Persistent Store: Invalid Persisten Session given.");
return;
}
const { sessionId } = session;
const currentIndex = this._getIndex(initData);
if (currentIndex >= 0) {
const currVersion = keyIds === undefined ? 3 : 4;
const currentEntry = this._entries[currentIndex];
const entryVersion = currentEntry.version ?? -1;
if (entryVersion >= currVersion && sessionId === currentEntry.sessionId) {
return;
}
log.info("DRM", "Persistent Store: Updating session info.", { sessionId });
this._entries.splice(currentIndex, 1);
} else {
log.info("DRM", "Persistent Store: Add new session", { sessionId });
}
const storedValues = prepareValuesForStore(initData.values.getFormattedValues());
if (keyIds === undefined) {
this._entries.push({
version: 3,
sessionId,
values: storedValues,
initDataType: initData.type,
});
} else {
this._entries.push({
version: 4,
sessionId,
keyIds: keyIds.map((k) => new SerializableBytes(k)),
values: storedValues,
initDataType: initData.type,
});
}
this._save();
}
/**
* Delete stored MediaKeySession information based on its session id.
* @param {string} sessionId
*/
public delete(sessionId: string): void {
let index = -1;
for (let i = 0; i < this._entries.length; i++) {
const entry = this._entries[i];
if (entry.sessionId === sessionId) {
index = i;
break;
}
}
if (index === -1) {
log.warn(
"DRM",
"Persistent Store: initData to delete not found in persistent store.",
);
return;
}
const entry = this._entries[index];
log.warn("DRM", "Persistent Store: Delete session from persistent store", {
sessionId: entry.sessionId,
});
this._entries.splice(index, 1);
this._save();
}
public deleteOldSessions(sessionsToDelete: number): void {
log.info("DRM", "Persistent Store: Deleting last sessions.", { sessionsToDelete });
if (sessionsToDelete <= 0) {
return;
}
if (sessionsToDelete <= this._entries.length) {
this._entries.splice(0, sessionsToDelete);
} else {
log.warn(
"DRM",
"Persistent Store: Asked to remove more information that it contains",
sessionsToDelete,
this._entries.length,
);
this._entries = [];
}
this._save();
}
/**
* Delete all saved entries.
*/
public dispose(): void {
this._entries = [];
this._save();
}
/**
* Retrieve index of an entry.
* Returns `-1` if not found.
* @param {Object} initData
* @returns {number}
*/
private _getIndex(initData: IProcessedProtectionData): number {
// Older versions of the format include a concatenation of all
// initialization data and its hash.
// This is only computed lazily, the first time it is needed.
let lazyConcatenatedData: null | {
initData: Uint8Array;
initDataHash: number;
} = null;
function getConcatenatedInitDataInfo() {
if (lazyConcatenatedData === null) {
const concatInitData = initData.values.constructRequestData();
lazyConcatenatedData = {
initData: concatInitData,
initDataHash: hashBuffer(concatInitData),
};
}
return lazyConcatenatedData;
}
for (let i = 0; i < this._entries.length; i++) {
const entry = this._entries[i];
if (entry.initDataType === initData.type) {
switch (entry.version) {
case 4:
if (Array.isArray(initData.keyIds) && initData.keyIds.length > 0) {
const foundCompatible = initData.keyIds.every((keyId) => {
const keyIdB64 = bytesToBase64(keyId);
for (const entryKid of entry.keyIds) {
if (typeof entryKid === "string") {
if (keyIdB64 === entryKid) {
log.debug(
"DRM",
"Persistent Store: Found wanted kid stored on entry",
{
index: i,
keyId: keyIdB64,
sessionId: entry.sessionId,
},
);
return true;
}
} else if (areArraysOfNumbersEqual(entryKid.initData, keyId)) {
if (log.hasLevel("DEBUG")) {
log.debug(
"DRM",
"Persistent Store: Found wanted kid stored on entry",
{
index: i,
keyId: keyIdB64,
sessionId: entry.sessionId,
},
);
}
return true;
}
}
return false;
});
if (foundCompatible) {
log.debug("DRM", "Persistent Store: Found compatible entry of v4", {
index: i,
sessionId: entry.sessionId,
});
return i;
}
} else {
const formatted = initData.values.getFormattedValues();
if (areInitializationValuesCompatible(formatted, entry.values)) {
log.debug(
"DRM",
"Persistent Store: Found compatible entry of v4 without init data",
{
index: i,
sessionId: entry.sessionId,
},
);
return i;
}
}
break;
case 3: {
const formatted = initData.values.getFormattedValues();
if (areInitializationValuesCompatible(formatted, entry.values)) {
log.debug(
"DRM",
"Persistent Store: Found compatible entry of v3 - same values",
{
index: i,
sessionId: entry.sessionId,
},
);
return i;
}
break;
}
case 2: {
const { initData: concatInitData, initDataHash: concatHash } =
getConcatenatedInitDataInfo();
if (entry.initDataHash === concatHash) {
try {
const decodedInitData: Uint8Array =
typeof entry.initData === "string"
? SerializableBytes.decode(entry.initData)
: entry.initData.initData;
if (areArraysOfNumbersEqual(decodedInitData, concatInitData)) {
log.debug(
"DRM",
"Persistent Store: Found compatible entry of v2 - same concatenated init data",
{
index: i,
sessionId: entry.sessionId,
},
);
return i;
}
} catch (e) {
log.warn(
"DRM",
"Persistent Store: Could not decode initialization data.",
e instanceof Error ? e : "",
);
}
}
break;
}
case 1: {
const { initData: concatInitData, initDataHash: concatHash } =
getConcatenatedInitDataInfo();
if (entry.initDataHash === concatHash) {
if (typeof entry.initData.length === "undefined") {
// If length is undefined, it has been linearized. We could still
// convert it back to an Uint8Array but this would necessitate some
// ugly unreadable logic for a very very minor possibility.
// Just consider that it is a match based on the hash.
log.debug(
"DRM",
"Persistent Store: Found compatible entry of v1 - same hash only",
{
index: i,
sessionId: entry.sessionId,
hash: concatHash,
},
);
return i;
} else if (areArraysOfNumbersEqual(entry.initData, concatInitData)) {
log.debug(
"DRM",
"Persistent Store: Found compatible entry of v1 - same hash and initData",
{
index: i,
sessionId: entry.sessionId,
hash: concatHash,
},
);
return i;
}
}
break;
}
default: {
const { initDataHash: concatHash } = getConcatenatedInitDataInfo();
if (entry.initData === concatHash) {
log.debug(
"DRM",
"Persistent Store: Found compatible entry - same hash only",
{
index: i,
sessionId: entry.sessionId,
hash: concatHash,
},
);
return i;
}
}
}
}
}
return -1;
}
/**
* Use the given storage to store the current entries.
*/
private _save(): void {
try {
this._storage.save(this._entries);
} catch (e) {
const err = e instanceof Error ? e : undefined;
log.warn(
"DRM",
"Persistent Store: Could not save MediaKeySession information",
err,
);
}
}
}
/**
* Format given initializationData's values so they are ready to be stored:
* - sort them by systemId, so they are faster to compare
* - add hash for each initialization data encountered.
* @param {Array.<Object>} initialValues
* @returns {Array.<Object>}
*/
function prepareValuesForStore(initialValues: IFormattedInitDataValue[]): Array<{
systemId: string | undefined;
hash: number;
data: SerializableBytes;
}> {
return initialValues.map(({ systemId, data, hash }) => ({
systemId,
hash,
data: new SerializableBytes(data),
}));
}