-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Only use selected key-system in EME "mediaencrypted" event handler #6948
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -535,147 +535,164 @@ class EMEController extends Logger implements ComponentAPI { | |
return; | ||
} | ||
|
||
let keyId: Uint8Array | null | undefined; | ||
let keySystemDomain: KeySystems | undefined; | ||
|
||
if ( | ||
initDataType === 'sinf' && | ||
this.getLicenseServerUrl(KeySystems.FAIRPLAY) | ||
) { | ||
// Match sinf keyId to playlist skd://keyId= | ||
const json = bin2str(new Uint8Array(initData)); | ||
try { | ||
const sinf = base64Decode(JSON.parse(json).sinf); | ||
const tenc = parseSinf(sinf); | ||
if (!tenc) { | ||
throw new Error( | ||
`'schm' box missing or not cbcs/cenc with schi > tenc`, | ||
); | ||
} | ||
keyId = tenc.subarray(8, 24); | ||
keySystemDomain = KeySystems.FAIRPLAY; | ||
} catch (error) { | ||
this.warn(`${logMessage} Failed to parse sinf: ${error}`); | ||
return; | ||
} | ||
} else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) { | ||
// Support Widevine clear-lead key-session creation (otherwise depend on playlist keys) | ||
const psshResults = parseMultiPssh(initData); | ||
|
||
// TODO: If using keySystemAccessPromises we might want to wait until one is resolved | ||
if (!this.keyFormatPromise) { | ||
let keySystems = Object.keys( | ||
this.keySystemAccessPromises, | ||
) as KeySystems[]; | ||
if (!keySystems.length) { | ||
keySystems = getKeySystemsForConfig(this.config); | ||
} | ||
const keyFormats = keySystems | ||
.map(keySystemToKeySystemFormat) | ||
.filter((k) => !!k) as KeySystemFormats[]; | ||
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); | ||
} | ||
|
||
const psshInfo = psshResults.filter((pssh): pssh is PsshData => { | ||
const keySystem = pssh.systemId | ||
? keySystemIdToKeySystemDomain(pssh.systemId) | ||
: null; | ||
return keySystem ? keySystems.indexOf(keySystem) > -1 : false; | ||
})[0]; | ||
this.keyFormatPromise.then((keySystemFormat) => { | ||
const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat); | ||
|
||
if (!psshInfo) { | ||
if ( | ||
psshResults.length === 0 || | ||
psshResults.some((pssh): pssh is PsshInvalidResult => !pssh.systemId) | ||
) { | ||
this.warn(`${logMessage} contains incomplete or invalid pssh data`); | ||
} else { | ||
this.log( | ||
`ignoring ${logMessage} for ${(psshResults as PsshData[]) | ||
.map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId)) | ||
.join(',')} pssh data in favor of playlist keys`, | ||
); | ||
} | ||
return; | ||
} | ||
let keyId: Uint8Array | null | undefined; | ||
let keySystemDomain: KeySystems | undefined; | ||
|
||
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); | ||
if (psshInfo.version === 0 && psshInfo.data) { | ||
if (keySystemDomain === KeySystems.WIDEVINE) { | ||
const offset = psshInfo.data.length - 22; | ||
keyId = psshInfo.data.subarray(offset, offset + 16); | ||
} else if (keySystemDomain === KeySystems.PLAYREADY) { | ||
keyId = parsePlayReadyWRM(psshInfo.data); | ||
if (initDataType === 'sinf') { | ||
if (keySystem !== KeySystems.FAIRPLAY) { | ||
this.log(`Ignoring "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`); | ||
return; | ||
} | ||
} | ||
} | ||
// Match sinf keyId to playlist skd://keyId= | ||
const json = bin2str(new Uint8Array(initData)); | ||
try { | ||
const sinf = base64Decode(JSON.parse(json).sinf); | ||
const tenc = parseSinf(sinf); | ||
if (!tenc) { | ||
throw new Error( | ||
`'schm' box missing or not cbcs/cenc with schi > tenc`, | ||
); | ||
} | ||
keyId = tenc.subarray(8, 24); | ||
keySystemDomain = KeySystems.FAIRPLAY; | ||
} catch (error) { | ||
this.warn(`${logMessage} Failed to parse sinf: ${error}`); | ||
return; | ||
} | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should probably still check if the key system is either widevine or playready here and early bail + log/warn like we do for the fairplay + |
||
// Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys) | ||
const psshResults = parseMultiPssh(initData); | ||
|
||
if (!keySystemDomain || !keyId) { | ||
return; | ||
} | ||
const psshInfo = psshResults.filter( | ||
(pssh): pssh is PsshData => | ||
!!pssh.systemId && | ||
keySystemIdToKeySystemDomain(pssh.systemId) === keySystem, | ||
)[0]; | ||
Comment on lines
+582
to
+586
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you wanted to iterate through these to find the first key that matched any in mediaKeySessions that would work too. I don't expect we would have multiple pssh with the same key system. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably fine. Small improvement might be to save off the filter, always use the |
||
|
||
const keyIdHex = Hex.hexDump(keyId); | ||
const { keyIdToKeySessionPromise, mediaKeySessions } = this; | ||
if (!psshInfo) { | ||
if ( | ||
psshResults.length === 0 || | ||
psshResults.some( | ||
(pssh): pssh is PsshInvalidResult => !pssh.systemId, | ||
) | ||
) { | ||
this.warn(`${logMessage} contains incomplete or invalid pssh data`); | ||
} else { | ||
this.log( | ||
`ignoring ${logMessage} for ${(psshResults as PsshData[]) | ||
.map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId)) | ||
.join(',')} pssh data in favor of playlist keys`, | ||
); | ||
} | ||
return; | ||
} | ||
|
||
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; | ||
for (let i = 0; i < mediaKeySessions.length; i++) { | ||
// Match playlist key | ||
const keyContext = mediaKeySessions[i]; | ||
const decryptdata = keyContext.decryptdata; | ||
if (!decryptdata.keyId) { | ||
continue; | ||
} | ||
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); | ||
if ( | ||
keyIdHex === oldKeyIdHex || | ||
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 | ||
) { | ||
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; | ||
if (decryptdata.pssh) { | ||
break; | ||
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); | ||
if (psshInfo.version === 0 && psshInfo.data) { | ||
if (keySystemDomain === KeySystems.WIDEVINE) { | ||
const offset = psshInfo.data.length - 22; | ||
keyId = psshInfo.data.subarray(offset, offset + 16); | ||
} else if (keySystemDomain === KeySystems.PLAYREADY) { | ||
keyId = parsePlayReadyWRM(psshInfo.data); | ||
} | ||
} | ||
delete keyIdToKeySessionPromise[oldKeyIdHex]; | ||
decryptdata.pssh = new Uint8Array(initData); | ||
decryptdata.keyId = keyId; | ||
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = | ||
keySessionContextPromise.then(() => { | ||
return this.generateRequestWithPreferredKeySession( | ||
keyContext, | ||
initDataType, | ||
initData, | ||
'encrypted-event-key-match', | ||
); | ||
}); | ||
keySessionContextPromise.catch((error) => this.handleError(error)); | ||
break; | ||
} | ||
} | ||
|
||
if (!keySessionContextPromise) { | ||
// Clear-lead key (not encountered in playlist) | ||
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = | ||
this.getKeySystemSelectionPromise([keySystemDomain]).then( | ||
({ keySystem, mediaKeys }) => { | ||
this.throwIfDestroyed(); | ||
const decryptdata = new LevelKey( | ||
'ISO-23001-7', | ||
keyIdHex, | ||
keySystemToKeySystemFormat(keySystem) ?? '', | ||
); | ||
decryptdata.pssh = new Uint8Array(initData); | ||
decryptdata.keyId = keyId as Uint8Array; | ||
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { | ||
this.throwIfDestroyed(); | ||
const keySessionContext = this.createMediaKeySessionContext({ | ||
decryptdata, | ||
keySystem, | ||
mediaKeys, | ||
}); | ||
if (!keySystemDomain || !keyId) { | ||
return; | ||
} | ||
|
||
const keyIdHex = Hex.hexDump(keyId); | ||
const { keyIdToKeySessionPromise, mediaKeySessions } = this; | ||
|
||
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; | ||
for (let i = 0; i < mediaKeySessions.length; i++) { | ||
// Match playlist key | ||
const keyContext = mediaKeySessions[i]; | ||
const decryptdata = keyContext.decryptdata; | ||
if (!decryptdata.keyId) { | ||
continue; | ||
} | ||
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); | ||
if ( | ||
keyIdHex === oldKeyIdHex || | ||
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 | ||
) { | ||
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; | ||
if (decryptdata.pssh) { | ||
break; | ||
} | ||
delete keyIdToKeySessionPromise[oldKeyIdHex]; | ||
decryptdata.pssh = new Uint8Array(initData); | ||
decryptdata.keyId = keyId; | ||
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = | ||
keySessionContextPromise.then(() => { | ||
return this.generateRequestWithPreferredKeySession( | ||
keySessionContext, | ||
keyContext, | ||
initDataType, | ||
initData, | ||
'encrypted-event-no-match', | ||
'encrypted-event-key-match', | ||
); | ||
}); | ||
}, | ||
); | ||
keySessionContextPromise.catch((error) => this.handleError(error)); | ||
} | ||
keySessionContextPromise.catch((error) => this.handleError(error)); | ||
break; | ||
} | ||
} | ||
|
||
if (!keySessionContextPromise) { | ||
if (keySystemDomain !== keySystem) { | ||
this.log(`Ignoring "${event.type}" event with ${keySystemDomain} init data for selected key-system ${keySystem}`); | ||
return; | ||
} | ||
// "Clear-lead" (misc key not encountered in playlist) | ||
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = | ||
this.getKeySystemSelectionPromise([keySystemDomain]).then( | ||
({ keySystem, mediaKeys }) => { | ||
this.throwIfDestroyed(); | ||
|
||
const decryptdata = new LevelKey( | ||
'ISO-23001-7', | ||
keyIdHex, | ||
keySystemToKeySystemFormat(keySystem) ?? '', | ||
); | ||
decryptdata.pssh = new Uint8Array(initData); | ||
decryptdata.keyId = keyId as Uint8Array; | ||
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { | ||
this.throwIfDestroyed(); | ||
const keySessionContext = this.createMediaKeySessionContext({ | ||
decryptdata, | ||
keySystem, | ||
mediaKeys, | ||
}); | ||
return this.generateRequestWithPreferredKeySession( | ||
keySessionContext, | ||
initDataType, | ||
initData, | ||
'encrypted-event-no-match', | ||
); | ||
}); | ||
}, | ||
); | ||
|
||
keySessionContextPromise.catch((error) => this.handleError(error)); | ||
} | ||
}); | ||
}; | ||
|
||
private onWaitingForKey = (event: Event) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI { | |
} | ||
|
||
load(frag: Fragment): Promise<KeyLoadedData> { | ||
if (!frag.decryptdata && frag.encrypted && this.emeController) { | ||
if ( | ||
!frag.decryptdata && | ||
frag.encrypted && | ||
this.emeController && | ||
this.config.emeEnabled | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cjpillsbury I remember you mentioning that you had issues with the behavior when loading encrypted content that requires EME with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should treat that as a separate PR/effort, but this looks like a good candidate. There were a few hard to identify cases of errors and this is definitely one. Ideally we'd surface the error as early as possible for root cause analysis reasons. E.g. if we encounter a multi-variant playlist or media playlist with (session) keys but don't have sufficient config for said keys, dispatch an error then (near the playlist parsing stage). The hardest one that I'm not sure belongs in hls.js at all is when folks think some media is DRM protected but actually isn't. We could make assumptions that if we encounter drm config that means folks think the content is DRM protected, but that's a bad assumption for folks who have mixed content with a shared config. Given the cases of clear lead or valid cases where lower quality renditions may be unencrypted, there may be too many permutations to safely assume this in an overly generic way. Food for thought, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree this is for another PR.
Session keys don't point to variants and may or may not be used. Encrypted fragments (media playlist keys) is the earliest place we can error with certainty.
Is there an example where that errors? The eme-controller with |
||
) { | ||
// Multiple keys, but none selected, resolve in eme-controller | ||
return this.emeController | ||
.selectKeySystemFormat(frag) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keyFormatPromise = this.getKeyFormatPromise
now could happen in three places:onManifestLoaded
for EXT-X-SESSION-KEY session keysselectKeySystemFormat(frag: Fragment)
for HLS media playlist keys (assigned to a fragment being loaded)onMediaEncrypted
at this point we would expect an encrypted fragment to signal its key format(s), but if it hasn't we can check the configured key-systems. It may be good to limit this to the intersection of configured key-systems and init data type and formats. IMO it would be incorrect to configure for a key system supported the device but not present in the media so I don't think that is important to cover.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the key format promise returned all the results of it's subroutine
getKeySystemSelectionPromise
(keySystem
(format) andmediaKey
), the last part pf this method (clear-lead resolution) could be simplified by just returning the results ofattemptSetMediaKeys
andgenerateRequestWithPreferredKeySession
.