Skip to content

Commit eb45c01

Browse files
committed
Fix key renewal and allow renewal by setting context key-status to 'expired'.
1 parent 3daa75d commit eb45c01

File tree

3 files changed

+57
-94
lines changed

3 files changed

+57
-94
lines changed

api-extractor/report/hls.js.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,7 @@ export class EMEController extends Logger implements ComponentAPI {
12131213
// (undocumented)
12141214
loadKey(frag: EncryptedFragment): Promise<LevelKey>;
12151215
// (undocumented)
1216-
renewKeySession(levelKey: LevelKey): void;
1216+
renewKeySession(levelKey: LevelKey, context: MediaKeySessionContext): Promise<LevelKey>;
12171217
// (undocumented)
12181218
selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats>;
12191219
}

src/controller/eme-controller.ts

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,6 @@ class EMEController extends Logger implements ComponentAPI {
136136
// Tracks active `main`, `audio` (playlistType), and `previous[]` (array) of encrypted fragments and LevelKey objects
137137
private activeKeys: ActiveKeys = {};
138138

139-
// Resolves when key status starts with "usable", deleted when changed to expired or released
140-
private keyUsablePromises: {
141-
[keyId: string]: Promise<LevelKey> | undefined;
142-
} = {};
143-
144139
private mediaKeys: MediaKeys | null = null;
145140
private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
146141
? [EMEController.CDMCleanupPromise]
@@ -517,8 +512,6 @@ class EMEController extends Logger implements ComponentAPI {
517512
return Promise.reject(error);
518513
}
519514

520-
const keyId = getKeyIdString(levelKey);
521-
522515
// track active playlist fragments
523516
const playlistType = frag.type as
524517
| PlaylistLevelType.MAIN
@@ -528,52 +521,45 @@ class EMEController extends Logger implements ComponentAPI {
528521
if (encryptedFrag && !levelKey.matches(encryptedFrag.decryptdata)) {
529522
activeKeys.previous ||= [];
530523
activeKeys.previous.push(encryptedFrag);
524+
delete activeKeys[playlistType];
531525
}
532-
activeKeys[playlistType] = frag;
526+
activeKeys[playlistType] ||= frag;
533527

534-
// Get key-session context async
535-
const keyUsablePromise = this.keyUsablePromises[keyId];
536-
if (!keyUsablePromise) {
537-
this.log(
538-
`Waiting for usable key (playlist: ${playlistType} keyId: ${keyId} URI: ${levelKey.uri} format: "${levelKey.keyFormat}" method: ${levelKey.method})`,
539-
);
540-
return this.updateUsablePromise(levelKey, 'playlist-key', frag);
528+
// check for external expiration
529+
for (let i = this.mediaKeySessions.length; i--; ) {
530+
const context = this.mediaKeySessions[i];
531+
if (
532+
levelKey.keyId &&
533+
context.keyStatuses[arrayToHex(levelKey.keyId)] === 'expired'
534+
) {
535+
return this.renewKeySession(levelKey, context);
536+
}
541537
}
542538

543-
return keyUsablePromise;
539+
// Get key-session context async
540+
return this.updateUsablePromise(levelKey, 'playlist-key', frag);
544541
}
545542

546-
public renewKeySession(levelKey: LevelKey) {
547-
const keyId = getKeyIdString(levelKey);
548-
if (levelKey.pssh) {
549-
// Reset cached mediaKeys, access promise, and usable key promise so that new session and request are generated
550-
this.resetMediaKeys();
551-
delete this.keyUsablePromises[keyId];
552-
553-
// same as loadKey, mediaKeys will be set with new session
554-
const renewalPromise = this.updateUsablePromise(levelKey, 'expired');
555-
renewalPromise
556-
.then(() => {
557-
// remove old session after new one is established
558-
// return this.removeSession(mediaKeySessionContext);
559-
})
560-
.catch((error) => this.handleError(error));
561-
} else {
562-
this.warn(`Could not renew expired key ${keyId}. Missing pssh initData.`);
563-
}
543+
public renewKeySession(
544+
levelKey: LevelKey,
545+
context: MediaKeySessionContext,
546+
): Promise<LevelKey> {
547+
// Reset cached mediaKeys, access promise, and usable key promise so that new session and request are generated
548+
this.resetMediaKeys();
549+
return this.removeSession(context).then(() => {
550+
this.throwIfDestroyed();
551+
return this.updateUsablePromise(levelKey, 'expired');
552+
});
564553
}
565554

566555
private updateUsablePromise(
567556
levelKey: LevelKey,
568557
reason: LicenseRequestReason,
569558
frag?: EncryptedFragment,
570559
): Promise<LevelKey> {
571-
const keyId = getKeyIdString(levelKey);
572560
const keySessionContextPromise = this.getSessionForKey(levelKey, reason);
573561
keySessionContextPromise.catch((error) => this.handleError(error, frag));
574-
const usablePromise = (this.keyUsablePromises[keyId] =
575-
keySessionContextPromise.then((context) => levelKey));
576-
return usablePromise;
562+
return keySessionContextPromise.then((context) => levelKey);
577563
}
578564

579565
// add Key to new or existing session
@@ -593,6 +579,9 @@ class EMEController extends Logger implements ComponentAPI {
593579
// If request is in progress wait for it to resolve
594580
const requestEmitter = context.keyRequests[levelKey.uri];
595581
if (requestEmitter) {
582+
this.log(
583+
`Waiting for usable key (${reason} keyId: ${getKeyIdString(levelKey)} format: "${levelKey.keyFormat}" URI: ${levelKey.uri})`,
584+
);
596585
return getRequestToKeyUsablePromise(requestEmitter).then(
597586
() => context,
598587
);
@@ -1034,7 +1023,9 @@ class EMEController extends Logger implements ComponentAPI {
10341023
this.log(
10351024
`Expired key ${stringify(keyStatuses)} in key-session "${context.mediaKeysSession.sessionId}"`,
10361025
);
1037-
this.renewKeySession(levelKey);
1026+
this.renewKeySession(levelKey, context).catch((error) =>
1027+
this.handleError(error),
1028+
);
10381029
break;
10391030
}
10401031
}
@@ -1560,7 +1551,6 @@ class EMEController extends Logger implements ComponentAPI {
15601551
private _clear() {
15611552
this.keyFormatPromise = null;
15621553
this.keySystemAccessPromises = {};
1563-
this.keyUsablePromises = {};
15641554
this.activeKeys = {};
15651555
const mediaResolved = this.mediaResolved;
15661556
if (mediaResolved) {

tests/unit/controller/eme-controller.ts

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,9 @@ import type { MediaAttachedData } from '../../../src/types/events';
2020

2121
use(sinonChai);
2222

23-
type EMEControllerTestable = Omit<
24-
EMEController,
25-
'hls' | 'keyUsablePromises' | 'mediaKeySessions'
26-
> & {
23+
type EMEControllerTestable = Omit<EMEController, 'hls' | 'mediaKeySessions'> & {
2724
hls: HlsMock;
2825
mediaKeySessions: MediaKeySessionContext[];
29-
keyUsablePromises: {
30-
[keyUri: string]: Promise<LevelKey> | undefined;
31-
};
3226
activeKeys: {
3327
main?: EncryptedFragment;
3428
audio?: EncryptedFragment;
@@ -367,10 +361,6 @@ describe('EMEController', function () {
367361
type: 'main',
368362
} as any)
369363
.then(() => {
370-
expect(emeController.keyUsablePromises).to.deep.equal(
371-
{},
372-
'`keyUsablePromises` should be an empty dictionary when no key IDs are found',
373-
);
374364
expect(emeController.activeKeys).to.deep.equal(
375365
{},
376366
'`activeKeys` should be an empty dictionary when no playlisty-keys are found',
@@ -455,15 +445,10 @@ describe('EMEController', function () {
455445
const levelKey = getParsedLevelKey();
456446
const encryptedFrag = getEncryptedFrag(levelKey);
457447
return emeController.loadKey(encryptedFrag).then(() => {
458-
expect(emeController.keyUsablePromises['data://key-uri']).to.be.a(
459-
'Promise',
448+
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce;
449+
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledWith(
450+
sinon.match({ byteLength: 6 }),
460451
);
461-
return emeController.keyUsablePromises['data://key-uri']!.finally(() => {
462-
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce;
463-
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledWith(
464-
sinon.match({ byteLength: 6 }),
465-
);
466-
});
467452
});
468453
});
469454

@@ -521,26 +506,20 @@ describe('EMEController', function () {
521506
media: media as any as HTMLMediaElement,
522507
});
523508

524-
emeController.loadKey(encryptedFrag).catch((error) => {
525-
// expected?
526-
});
527-
528-
expect(emeController.keyUsablePromises['data://key-uri']).to.be.a(
529-
'Promise',
530-
);
531-
return emeController.keyUsablePromises['data://key-uri']!.catch(
532-
() => {},
533-
).finally(() => {
534-
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce;
535-
expect((mediaKeysSetServerCertificateSpy.args[0] as any)[0]).to.equal(
536-
xhrInstance.response,
537-
);
509+
emeController
510+
.loadKey(encryptedFrag)
511+
.catch(() => {})
512+
.finally(() => {
513+
expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce;
514+
expect((mediaKeysSetServerCertificateSpy.args[0] as any)[0]).to.equal(
515+
xhrInstance.response,
516+
);
538517

539-
expect(emeController.hls.trigger).to.have.been.calledOnce;
540-
expect(emeController.hls.trigger.args[0][1].details).to.equal(
541-
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,
542-
);
543-
});
518+
expect(emeController.hls.trigger).to.have.been.calledOnce;
519+
expect(emeController.hls.trigger.args[0][1].details).to.equal(
520+
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,
521+
);
522+
});
544523
});
545524

546525
it('should fetch the server certificate and trigger request failed error', function () {
@@ -589,21 +568,15 @@ describe('EMEController', function () {
589568
emeController.onMediaAttached(Events.MEDIA_ATTACHED, {
590569
media: media as any as HTMLMediaElement,
591570
});
592-
emeController.loadKey(encryptedFrag).catch((error) => {
593-
// expected?
594-
});
595-
596-
expect(emeController.keyUsablePromises['data://key-uri']).to.be.a(
597-
'Promise',
598-
);
599-
return emeController.keyUsablePromises['data://key-uri']!.catch(
600-
() => {},
601-
).finally(() => {
602-
expect(emeController.hls.trigger).to.have.been.calledOnce;
603-
expect(emeController.hls.trigger.args[0][1].details).to.equal(
604-
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,
605-
);
606-
});
571+
emeController
572+
.loadKey(encryptedFrag)
573+
.catch(() => {})
574+
.finally(() => {
575+
expect(emeController.hls.trigger).to.have.been.calledOnce;
576+
expect(emeController.hls.trigger.args[0][1].details).to.equal(
577+
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,
578+
);
579+
});
607580
});
608581

609582
it('should remove media property when media is detached', function () {

0 commit comments

Comments
 (0)