diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index b7e8488f787..f6999809bc4 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -12,6 +12,7 @@ import { removeEventListener, } from '../utils/event-listener-helper'; import { arrayToHex } from '../utils/hex'; +import { changeEndianness } from '../utils/keysystem-util'; import { Logger } from '../utils/logger'; import { getKeySystemsForConfig, @@ -881,6 +882,10 @@ class EMEController extends Logger implements ComponentAPI { const sessionLevelKeyId = arrayToHex( new Uint8Array(mediaKeySessionContext.decryptdata.keyId || []), ); + + let hasMatchedKey = false; + const keyStatuses: { status: MediaKeyStatus; keyId: string }[] = []; + mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach( (status: MediaKeyStatus, keyId: BufferSource) => { // keyStatuses.forEach is not standard API so the callback value looks weird on xboxone @@ -890,27 +895,79 @@ class EMEController extends Logger implements ComponentAPI { keyId = status; status = temp; } - const keyIdWithStatusChange = arrayToHex( + + const keyIdArray: Uint8Array = 'buffer' in keyId ? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength) - : new Uint8Array(keyId), + : new Uint8Array(keyId); + + // Handle PlayReady little-endian key ID conversion for status comparison only + // Don't modify the original key ID from playlist parsing + if ( + mediaKeySessionContext.keySystem === KeySystems.PLAYREADY && + keyIdArray.length === 16 + ) { + changeEndianness(keyIdArray); + } + + const keyIdWithStatusChange = arrayToHex( + keyIdArray as Uint8Array, ); + // Store all key statuses for processing + keyStatuses.push({ status, keyId: keyIdWithStatusChange }); + // Error immediately when encountering a key ID with this status again if (status === 'internal-error') { this.bannedKeyIds[keyIdWithStatusChange] = status; } - // Only acknowledge status changes for level-key ID + // Check if this key matches the session-level key ID const matched = keyIdWithStatusChange === sessionLevelKeyId; - this.log( - `${matched ? '' : 'un'}matched key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} session keyId: ${sessionLevelKeyId} uri: ${mediaKeySessionContext.decryptdata.uri}`, - ); if (matched) { + hasMatchedKey = true; mediaKeySessionContext.keyStatus = status; + this.log( + `matched key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} session keyId: ${sessionLevelKeyId} uri: ${mediaKeySessionContext.decryptdata.uri}`, + ); + } else { + this.log( + `unmatched key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} session keyId: ${sessionLevelKeyId} uri: ${mediaKeySessionContext.decryptdata.uri}`, + ); } }, ); + + // Handle case where no keys matched but all have the same status + // This can happen with PlayReady when key IDs don't align properly + if (!hasMatchedKey && keyStatuses.length > 0) { + const firstStatus = keyStatuses[0].status; + const allSameStatus = !keyStatuses.some( + ({ status }) => status !== firstStatus, + ); + + if ( + allSameStatus && + (firstStatus === 'usable' || firstStatus.startsWith('usable')) + ) { + this.log( + `No key matched session keyId ${sessionLevelKeyId}, but all keys have usable status "${firstStatus}". Treating as usable.`, + ); + mediaKeySessionContext.keyStatus = firstStatus; + } else if ( + allSameStatus && + (firstStatus === 'internal-error' || firstStatus === 'expired') + ) { + this.log( + `No key matched session keyId ${sessionLevelKeyId}, but all keys have error status "${firstStatus}". Applying to session.`, + ); + mediaKeySessionContext.keyStatus = firstStatus; + } else { + this.log( + `No key matched session keyId ${sessionLevelKeyId}. Key statuses: ${keyStatuses.map(({ keyId, status }) => `${keyId}:${status}`).join(', ')}`, + ); + } + } } private fetchServerCertificate(