Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3229,7 +3229,7 @@ export class KeyLoader extends Logger implements ComponentAPI {
// (undocumented)
emeController: EMEController | null;
// (undocumented)
load(frag: Fragment): Promise<KeyLoadedData>;
load(frag: Fragment, initDataPromise?: Promise<void>): Promise<KeyLoadedData>;
// (undocumented)
loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[], startFragRequested: boolean): Promise<void> | null;
}
Expand Down Expand Up @@ -3837,6 +3837,8 @@ export interface LevelUpdatedData {
// @public (undocumented)
export type LicenseAndKeysRequest = EventEmitter & {
status: 'initialized' | 'started' | 'generated' | MediaKeyMessageType;
resolved?: boolean;
errored?: Error;
licenseXhr?: XMLHttpRequest;
requestErrors: {
status: number;
Expand Down
26 changes: 16 additions & 10 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,22 +930,25 @@ export default class BaseStreamController
private loadKeyFor(
frag: MediaFragment,
details: LevelDetails,
initDataPromise?: Promise<void>,
): Promise<KeyLoadedData | void> | null {
let keyLoadingPromise: Promise<KeyLoadedData | void> | null = null;
if (frag.encrypted && !frag.decryptdata?.key) {
this.log(
`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.playlistLabel()} ${frag.level}`,
);
this.state = State.KEY_LOADING;
keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => {
if (!this.fragContextChanged(keyLoadedData.frag)) {
this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
if (this.state === State.KEY_LOADING) {
this.state = State.IDLE;
keyLoadingPromise = this.keyLoader
.load(frag, initDataPromise)
.then((keyLoadedData) => {
if (!this.fragContextChanged(keyLoadedData.frag)) {
this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
if (this.state === State.KEY_LOADING) {
this.state = State.IDLE;
}
return keyLoadedData;
}
return keyLoadedData;
}
});
});
this.hls.trigger(Events.KEY_LOADING, { frag });
} else if (!frag.encrypted) {
keyLoadingPromise = this.keyLoader.loadClear(
Expand Down Expand Up @@ -1084,7 +1087,11 @@ export default class BaseStreamController
return Promise.resolve(null);
}

const keyLoadingPromise = this.loadKeyFor(frag, details);
// Start the init segment load before the key load so that EME key
// patching from tenc (KeyLoader.loadKeyEME) has the data it needs
// before the license request is generated (#7796).
const initDataPromise = this.loadInitSegmentIfNeeded(frag);
const keyLoadingPromise = this.loadKeyFor(frag, details, initDataPromise);
if (this.fragContextChanged(frag)) {
this.log(
`Context changed in KEY_LOADING sn: ${frag.sn} ${frag.relurl} > ${this.fragCurrent?.relurl}`,
Expand All @@ -1108,7 +1115,6 @@ export default class BaseStreamController
const dataOnProgress =
this.config.progressive && frag.type !== PlaylistLevelType.SUBTITLE;

const initDataPromise = this.loadInitSegmentIfNeeded(frag);
let result: Promise<PartsLoadedData | FragLoadedData | null>;
if (dataOnProgress && keyLoadingPromise) {
result = keyLoadingPromise
Expand Down
91 changes: 87 additions & 4 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type ActiveKeys = {

export type LicenseAndKeysRequest = EventEmitter & {
status: 'initialized' | 'started' | 'generated' | MediaKeyMessageType;
resolved?: boolean;
errored?: Error;
licenseXhr?: XMLHttpRequest;
requestErrors: { status: number; message: string }[];
onmessage?: (this: MediaKeySession, ev: MediaKeyMessageEvent) => any;
Expand Down Expand Up @@ -136,6 +138,9 @@ class EMEController extends Logger implements ComponentAPI {
[keysystem: string]: KeySystemAccessPromises | undefined;
} = {};

private sessionSetupByUri: {
[uri: string]: Promise<MediaKeySessionContext>;
} = {};
// MediaKey session contexts created for each Key URI needed to make license requests that produce key status maps
private mediaKeySessions: MediaKeySessionContext[] = [];

Expand Down Expand Up @@ -345,7 +350,7 @@ class EMEController extends Logger implements ComponentAPI {
reason: LicenseRequestReason,
): MediaKeySessionContext {
const { mediaKeySessions } = this;
const message = ` key-system session "${keySystem}" keyId: ${arrayToHex(
const message = ` key-session "${keySystem}" keyId: ${arrayToHex(
levelKey.keyId || ([] as number[]),
)} keyUri: ${levelKey.uri} for "${reason}"`;
for (let i = 0; i < mediaKeySessions.length; i++) {
Expand Down Expand Up @@ -581,6 +586,57 @@ class EMEController extends Logger implements ComponentAPI {
private getSessionForKey(
levelKey: LevelKey,
reason: LicenseRequestReason,
): Promise<MediaKeySessionContext> {
const existing = this.findContextForKey(levelKey);
if (existing !== undefined) {
return Promise.resolve(existing);
}
// Block concurrent loadKey calls for the same Key URI on a single
// in-flight setup so we never create duplicate sessions for that URI
// (#7796). Distinct URIs proceed in parallel. Waiters re-enter
// `resolveSessionForKey` after the predecessor settles so they can
// share sessions containing their keyId's key-status, or generate
// their own license request and key session if the license server
// does not produce multiple key statuses.
const uri = levelKey.uri;
const previous = this.sessionSetupByUri[uri];
let setup: Promise<MediaKeySessionContext>;
if (previous === undefined) {
setup = this.resolveSessionForKey(levelKey, reason);
} else {
setup = previous
.catch(() => undefined)
.then(() => {
this.throwIfDestroyed();
return this.resolveSessionForKey(levelKey, reason);
});
}
this.sessionSetupByUri[uri] = setup;
const cleanup = () => {
if (this.sessionSetupByUri[uri] === setup) {
delete this.sessionSetupByUri[uri];
}
};
setup.then(cleanup, cleanup);
return setup;
}

private findContextForKey(
levelKey: LevelKey,
): MediaKeySessionContext | undefined {
const { mediaKeySessions } = this;
for (let i = mediaKeySessions.length; i--; ) {
const context = mediaKeySessions[i];
if (getKeyStatus(levelKey, context)) {
return context;
}
}
return undefined;
}

private resolveSessionForKey(
levelKey: LevelKey,
reason: LicenseRequestReason,
): Promise<MediaKeySessionContext> {
return this.getKeySystemForKeyPromise(levelKey).then(
({ keySystem, mediaKeys }) => {
Expand All @@ -591,7 +647,7 @@ class EMEController extends Logger implements ComponentAPI {
if (getKeyStatus(levelKey, context)) {
return context;
}
// If request is in progress wait for it to resolve
// If a request is in progress for this same URI, wait for it.
const usablePromise = this.getUsableKeyPromise(
context,
levelKey,
Expand Down Expand Up @@ -958,7 +1014,9 @@ class EMEController extends Logger implements ComponentAPI {
const onmessage = (event: MediaKeyMessageEvent) => {
const keySession = context.mediaKeysSession;
if (!keySession as any) {
requestEmitter.emit('error', new Error('invalid state'));
const invalidStateError = new Error('invalid state');
requestEmitter.errored = invalidStateError;
requestEmitter.emit('error', invalidStateError);
return;
}
const { messageType, message } = event;
Expand All @@ -971,6 +1029,7 @@ class EMEController extends Logger implements ComponentAPI {
messageType === 'license-renewal'
) {
this.renewLicense(context, levelKey, message).catch((error) => {
requestEmitter.errored = error;
if (requestEmitter.eventNames().length) {
requestEmitter.emit('error', error);
} else {
Expand Down Expand Up @@ -1036,6 +1095,7 @@ class EMEController extends Logger implements ComponentAPI {
const handleKeyStatus = (keyStatus: MediaKeyStatus) => {
let keyError: EMEKeyError | Error | undefined;
if (keyStatus.startsWith('usable')) {
requestEmitter.resolved = true;
requestEmitter.emit('resolved');
} else if (
keyStatus === 'internal-error' ||
Expand All @@ -1055,6 +1115,7 @@ class EMEController extends Logger implements ComponentAPI {
);
}
if (keyError) {
requestEmitter.errored = keyError;
if (requestEmitter.eventNames().length) {
requestEmitter.emit('error', keyError);
} else {
Expand All @@ -1066,7 +1127,9 @@ class EMEController extends Logger implements ComponentAPI {
const onkeystatuseschange = (event: Event) => {
const keySession = context.mediaKeysSession;
if (!keySession as any) {
requestEmitter.emit('error', new Error('invalid state'));
const invalidStateError = new Error('invalid state');
requestEmitter.errored = invalidStateError;
requestEmitter.emit('error', invalidStateError);
return;
}

Expand Down Expand Up @@ -1166,6 +1229,19 @@ class EMEController extends Logger implements ComponentAPI {
.then(() => keyUsablePromise)
.catch((error) => {
this.log(`mediaKeysSession.generateRequest failed: ${error}`);
const errorDetails = error.data?.details;
if (
(errorDetails === ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED ||
errorDetails === ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR) &&
Object.keys(context.keyStatuses).some(
(keyId) => context.keyStatuses[keyId] === 'usable',
)
) {
// Emit key status error without resetting session containing usable keys
// to allow variant switching to resolve playback.
this.handleError(error);
return context;
}
requestEmitter.removeAllListeners();
return this.removeSession(context).then(() => {
throw error;
Expand Down Expand Up @@ -1619,6 +1695,7 @@ class EMEController extends Logger implements ComponentAPI {
this.keyFormatPromise = null;
this.keySystemAccessPromises = {};
this.activeKeys = {};
this.sessionSetupByUri = {};
const mediaResolved = this.mediaResolved;
if (mediaResolved) {
mediaResolved();
Expand Down Expand Up @@ -1873,6 +1950,12 @@ function getKeyStatusError(

function getRequestToKeyUsablePromise(requestEmitter: LicenseAndKeysRequest) {
return new Promise((resolve: (value?: void) => void, reject) => {
if (requestEmitter.resolved) {
return resolve();
}
if (requestEmitter.errored) {
return reject(requestEmitter.errored);
}
requestEmitter.on('error', reject);
requestEmitter.on('resolved', resolve);
});
Expand Down
20 changes: 14 additions & 6 deletions src/loader/key-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default class KeyLoader extends Logger implements ComponentAPI {
loader.destroy();
}
}
this.emeController = null;
this.keyLoaderInfo = {};
}

Expand All @@ -75,7 +76,10 @@ export default class KeyLoader extends Logger implements ComponentAPI {
return null;
}

public load(frag: Fragment): Promise<KeyLoadedData> {
public load(
frag: Fragment,
initDataPromise?: Promise<void>,
): Promise<KeyLoadedData> {
if (
!frag.decryptdata &&
frag.encrypted &&
Expand All @@ -86,15 +90,16 @@ export default class KeyLoader extends Logger implements ComponentAPI {
return this.emeController
.selectKeySystemFormat(frag)
.then((keySystemFormat) => {
return this.loadInternal(frag, keySystemFormat);
return this.loadInternal(frag, initDataPromise, keySystemFormat);
});
}

return this.loadInternal(frag);
return this.loadInternal(frag, initDataPromise);
}

private loadInternal(
frag: Fragment,
initDataPromise: Promise<void> | undefined,
keySystemFormat?: KeySystemFormats,
): Promise<KeyLoadedData> {
if (__USE_EME_DRM__ && keySystemFormat) {
Expand Down Expand Up @@ -122,7 +127,9 @@ export default class KeyLoader extends Logger implements ComponentAPI {
// loadKeyHTTP handles http(s) and data URLs
return this.loadKeyHTTP(encryptedFrag);
}
return this.loadKeyEME(encryptedFrag);
return initDataPromise
? initDataPromise.then(() => this.loadKeyEME(encryptedFrag))
: this.loadKeyEME(encryptedFrag);
case 'AES-128':
case 'AES-256':
case 'AES-256-CTR':
Expand Down Expand Up @@ -155,10 +162,11 @@ export default class KeyLoader extends Logger implements ComponentAPI {
);
LevelKey.setKeyIdForUri(keyUri, keyId);
} else {
const keyIdPatch = LevelKey.addKeyIdForUri(keyUri);
this.log(
`Patching empty keyId with ${arrayToHex(keyId)} keyUri: ${keyUri}`,
`Patching empty keyId with ${arrayToHex(keyIdPatch)} keyUri: ${keyUri}`,
);
keyId = LevelKey.addKeyIdForUri(keyUri);
keyId = keyIdPatch;
}
frag.decryptdata.keyId = keyId;
}
Expand Down
Loading
Loading