Skip to content
Merged
9 changes: 8 additions & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1137,8 +1137,14 @@ export class EMEController extends Logger implements ComponentAPI {
// (undocumented)
destroy(): void;
// (undocumented)
getKeySystemAccess(keySystemsToAttempt: KeySystems[]): Promise<void>;
// (undocumented)
getSelectedKeySystemFormats(): KeySystemFormats[];
// (undocumented)
loadKey(data: KeyLoadedData): Promise<MediaKeySessionContext>;
// (undocumented)
selectKeySystem(keySystemsToAttempt: KeySystems[]): Promise<KeySystemFormats>;
// (undocumented)
selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats>;
}

Expand All @@ -1153,6 +1159,7 @@ export type EMEControllerConfig = {
drmSystems: DRMSystemsConfiguration;
drmSystemOptions: DRMSystemOptions;
requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
requireKeySystemAccessOnStart: boolean;
};

// Warning: (ae-missing-release-tag) "ErrorActionFlags" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -2917,7 +2924,7 @@ export class KeyLoader implements ComponentAPI {
// (undocumented)
load(frag: Fragment): Promise<KeyLoadedData>;
// (undocumented)
loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): void | Promise<void>;
loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): null | Promise<void>;
// (undocumented)
loadInternal(frag: Fragment, keySystemFormat?: KeySystemFormats): Promise<KeyLoadedData>;
// (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type EMEControllerConfig = {
drmSystems: DRMSystemsConfiguration;
drmSystemOptions: DRMSystemOptions;
requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
requireKeySystemAccessOnStart: boolean;
};

export interface FragmentLoaderConstructor {
Expand Down Expand Up @@ -432,6 +433,7 @@ export const hlsDefaultConfig: HlsConfig = {
requestMediaKeySystemAccessFunc: __USE_EME_DRM__
? requestMediaKeySystemAccess
: null, // used by eme-controller
requireKeySystemAccessOnStart: false, // used by eme-controller
testBandwidth: true,
progressive: false,
lowLatencyMode: true,
Expand Down
11 changes: 9 additions & 2 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
getPartWith,
updateFragPTSDTS,
} from '../utils/level-helper';
import { getKeySystemsForConfig } from '../utils/mediakeys-helper';

Check warning on line 34 in src/controller/base-stream-controller.ts

View workflow job for this annotation

GitHub Actions / build

'getKeySystemsForConfig' is defined but never used
import { appendUint8Array } from '../utils/mp4-tools';
import TimeRanges from '../utils/time-ranges';
import type { FragmentTracker } from './fragment-tracker';
Expand Down Expand Up @@ -837,8 +838,14 @@
new Error(`frag load aborted, context changed in KEY_LOADING`),
);
}
} else if (!frag.encrypted && details.encryptedFragments.length) {
this.keyLoader.loadClear(frag, details.encryptedFragments);
} else if (!frag.encrypted) {
keyLoadingPromise = this.keyLoader.loadClear(
frag,
details.encryptedFragments,
);
if (keyLoadingPromise) {
this.log(`[eme] blocking frag load until media-keys acquired`);
}
}

const fragPrevious = this.fragPrevious;
Expand Down
66 changes: 47 additions & 19 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface KeySystemAccessPromises {
keySystemAccess: Promise<MediaKeySystemAccess>;
mediaKeys?: Promise<MediaKeys>;
certificate?: Promise<BufferSource | void>;
hasMediaKeys?: boolean;
}

export interface MediaKeySessionContext {
Expand Down Expand Up @@ -284,6 +285,7 @@ class EMEController extends Logger implements ComponentAPI {
.createMediaKeys()
.then((mediaKeys) => {
this.log(`Media-keys created for "${keySystem}"`);
keySystemAccessPromises.hasMediaKeys = true;
return certificateRequest.then((certificate) => {
if (certificate) {
return this.setMediaKeysServerCertificate(
Expand Down Expand Up @@ -383,29 +385,29 @@ class EMEController extends Logger implements ComponentAPI {
return keySession.update(data);
}

public selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats> {
const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[];
if (!this.keyFormatPromise) {
this.log(
`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${
frag.level
}) key formats ${keyFormats.join(', ')}`,
);
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
}
return this.keyFormatPromise;
public getSelectedKeySystemFormats(): KeySystemFormats[] {
return (Object.keys(this.keySystemAccessPromises) as KeySystems[])
.map((keySystem) => ({
keySystem,
hasMediaKeys: this.keySystemAccessPromises[keySystem].hasMediaKeys,
}))
.filter(({ hasMediaKeys }) => !!hasMediaKeys)
.map(({ keySystem }) => keySystemToKeySystemFormat(keySystem))
.filter((keySystem): keySystem is KeySystemFormats => !!keySystem);
}

private getKeyFormatPromise(
keyFormats: KeySystemFormats[],
public getKeySystemAccess(keySystemsToAttempt: KeySystems[]): Promise<void> {
return this.getKeySystemSelectionPromise(keySystemsToAttempt).then(
({ keySystem, mediaKeys }) => {
return this.attemptSetMediaKeys(keySystem, mediaKeys);
},
);
}

public selectKeySystem(
keySystemsToAttempt: KeySystems[],
): Promise<KeySystemFormats> {
return new Promise((resolve, reject) => {
const keySystemsInConfig = getKeySystemsForConfig(this.config);
const keySystemsToAttempt = keyFormats
.map(keySystemFormatToKeySystemDomain)
.filter(
(value) => !!value && keySystemsInConfig.indexOf(value) !== -1,
) as any as KeySystems[];
return this.getKeySystemSelectionPromise(keySystemsToAttempt)
.then(({ keySystem }) => {
const keySystemFormat = keySystemToKeySystemFormat(keySystem);
Expand All @@ -421,6 +423,32 @@ class EMEController extends Logger implements ComponentAPI {
});
}

public selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats> {
const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[];
if (!this.keyFormatPromise) {
this.log(
`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${
frag.level
}) key formats ${keyFormats.join(', ')}`,
);
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
}
return this.keyFormatPromise;
}

private getKeyFormatPromise(
keyFormats: KeySystemFormats[],
): Promise<KeySystemFormats> {
const keySystemsInConfig = getKeySystemsForConfig(this.config);
const keySystemsToAttempt = keyFormats
.map(keySystemFormatToKeySystemDomain)
.filter(
(value) => !!value && keySystemsInConfig.indexOf(value) !== -1,
) as any as KeySystems[];

return this.selectKeySystem(keySystemsToAttempt);
}

public loadKey(data: KeyLoadedData): Promise<MediaKeySessionContext> {
const decryptdata = data.keyInfo.decryptdata;

Expand Down
59 changes: 42 additions & 17 deletions src/loader/key-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { LoadError } from './fragment-loader';
import { ErrorDetails, ErrorTypes } from '../errors';
import type { HlsConfig } from '../config';
import {
getKeySystemsForConfig,
keySystemFormatToKeySystemDomain,
} from '../utils/mediakeys-helper';
import type { LevelKey } from './level-key';
import type { HlsConfig } from '../config';
import type EMEController from '../controller/eme-controller';
import type { MediaKeySessionContext } from '../controller/eme-controller';
import type { Fragment } from '../loader/fragment';
Expand Down Expand Up @@ -90,25 +94,46 @@ export default class KeyLoader implements ComponentAPI {
loadClear(
loadingFrag: Fragment,
encryptedFragments: Fragment[],
): void | Promise<void> {
if (this.emeController && this.config.emeEnabled) {
// access key-system with nearest key on start (loaidng frag is unencrypted)
const { sn, cc } = loadingFrag;
for (let i = 0; i < encryptedFragments.length; i++) {
const frag = encryptedFragments[i];
if (
cc <= frag.cc &&
(sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)
) {
this.emeController
.selectKeySystemFormat(frag)
.then((keySystemFormat) => {
frag.setKeyFormat(keySystemFormat);
});
break;
): null | Promise<void> {
if (
this.emeController &&
this.config.emeEnabled &&
!this.emeController.getSelectedKeySystemFormats().length
) {
// access key-system with nearest key on start (loading frag is unencrypted)
if (encryptedFragments.length) {
const { sn, cc } = loadingFrag;
for (let i = 0; i < encryptedFragments.length; i++) {
const frag = encryptedFragments[i];
if (
cc <= frag.cc &&
(sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)
) {
return this.emeController
.selectKeySystemFormat(frag)
.then((keySystemFormat) => {
frag.setKeyFormat(keySystemFormat);
if (
this.emeController &&
this.config.requireKeySystemAccessOnStart
) {
const keySystem =
keySystemFormatToKeySystemDomain(keySystemFormat);
if (keySystem) {
return this.emeController.getKeySystemAccess([keySystem]);
}
}
});
}
}
} else if (this.config.requireKeySystemAccessOnStart) {
const keySystemsInConfig = getKeySystemsForConfig(this.config);
if (keySystemsInConfig.length) {
return this.emeController.getKeySystemAccess(keySystemsInConfig);
}
}
}
return null;
Copy link
Collaborator

@robwalch robwalch May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadClear should more or less do the same thing whether or not the playlist has encrypted segments and requireKeySystemAccessOnStart is enabled.

Here I've modified it to only follow either path when there is not a selected format (this.emeController.getSelectedKeySystemFormats().length) and to call getKeySystemAccess, defined above, in either path when requireKeySystemAccessOnStart is enabled.

  loadClear(
    loadingFrag: Fragment,
    encryptedFragments: Fragment[],
  ): null | Promise<void> {
    if (
      this.emeController &&
      this.config.emeEnabled &&
      !this.emeController.getSelectedKeySystemFormats().length
    ) {
      // access key-system with nearest key on start (loading frag is unencrypted)
      if (encryptedFragments.length) {
        const { sn, cc } = loadingFrag;
        for (let i = 0; i < encryptedFragments.length; i++) {
          const frag = encryptedFragments[i];
          if (
            cc <= frag.cc &&
            (sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)
          ) {
            return this.emeController
              .selectKeySystemFormat(frag)
              .then((keySystemFormat) => {
                frag.setKeyFormat(keySystemFormat);
                if (
                  this.emeController &&
                  this.config.requireKeySystemAccessOnStart
                ) {
                  const keySystem =
                    keySystemFormatToKeySystemDomain(keySystemFormat);
                  if (keySystem) {
                    return this.emeController.getKeySystemAccess([keySystem]);
                  }
                }
              });
          }
        }
      } else if (this.config.requireKeySystemAccessOnStart) {
        const keySystemsInConfig = getKeySystemsForConfig(this.config);
        if (keySystemsInConfig.length) {
          return this.emeController.getKeySystemAccess(keySystemsInConfig);
        }
      }
    }
    return null;
  }

I'm still not 100% on how useful getSelectedKeySystemFormats() is and whether or not there is a simpler way to test if a format needs to be selected and set when we have encrypted fragments.

}

load(frag: Fragment): Promise<KeyLoadedData> {
Expand Down
Loading