Skip to content
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

fix(DRM): handle all keys statuses #1423

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,6 @@ import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import type { IMediaConfiguration } from "../types";
import { ProberStatus } from "../types";

export type IMediaKeyStatus =
| "usable"
| "expired"
| "released"
| "output-restricted"
| "output-downscaled"
| "status-pending"
| "internal-error";

/**
* @param {Object} config
* @returns {Promise}
Expand All @@ -48,6 +39,19 @@ export default function probeHDCPPolicy(
const hdcp = "hdcp-" + config.hdcp;
const policy = { minHdcpVersion: hdcp };

/**
* These are the EME key statuses for which the playback is authorized.
* @see https://w3c.github.io/encrypted-media/#dom-mediakeystatus
*/
const playableStatuses: MediaKeyStatus[] = ["usable", "output-downscaled"];

/**
* These are the EME key statuses for which it is not possible to determine
* whether the playback is authorized or restricted.
* @see https://w3c.github.io/encrypted-media/#dom-mediakeystatus
*/
const unkownStatuses: MediaKeyStatus[] = ["status-pending"];

const keySystem = "org.w3.clearkey";
const drmConfig = {
initDataTypes: ["cenc"],
Expand Down Expand Up @@ -81,14 +85,16 @@ export default function probeHDCPPolicy(
mediaKeys as {
getStatusForPolicy: (policy: {
minHdcpVersion: string;
}) => Promise<IMediaKeyStatus>;
}) => Promise<MediaKeyStatus>;
}
)
.getStatusForPolicy(policy)
.then((result: IMediaKeyStatus) => {
.then((result: MediaKeyStatus) => {
let status: [ProberStatus];
if (result === "usable") {
if (playableStatuses.indexOf(result) !== -1) {
status = [ProberStatus.Supported];
} else if (unkownStatuses.indexOf(result) !== -1) {
status = [ProberStatus.Unknown];
} else {
status = [ProberStatus.NotSupported];
}
Expand Down
84 changes: 59 additions & 25 deletions src/main_thread/decrypt/utils/check_key_statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ export class DecommissionedSessionError extends Error {
}
}

const KEY_STATUSES = {
EXPIRED: "expired",
INTERNAL_ERROR: "internal-error",
OUTPUT_RESTRICTED: "output-restricted",
};

export type IKeyStatusesCheckingOptions = Pick<
IKeySystemOption,
"onKeyOutputRestricted" | "onKeyInternalError" | "onKeyExpiration"
Expand All @@ -76,6 +70,7 @@ type IKeyStatusesForEach = (
* appropriate warnings, whitelisted and blacklisted key ids.
*
* Throws if one of the keyID is on an error.
* @see https://w3c.github.io/encrypted-media/#dom-mediakeystatus
* @param {MediaKeySession} session - The MediaKeySession from which the keys
* will be checked.
* @param {Object} options
Expand Down Expand Up @@ -116,40 +111,38 @@ export default function checkKeyStatuses(
}

switch (keyStatus) {
case KEY_STATUSES.EXPIRED: {
case "expired": {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A decryption key expired (${bytesToHex(keyId)})`,
{ keyStatuses: [keyStatusObj, ...badKeyStatuses] },
);

if (onKeyExpiration === "error" || onKeyExpiration === undefined) {
throw error;
}

switch (onKeyExpiration) {
case undefined:
case "error":
throw error;
case "close-session":
throw new DecommissionedSessionError(error);
case "fallback":
blacklistedKeyIds.push(keyId);
break;
case "continue":
whitelistedKeyIds.push(keyId);
break;
default:
// I weirdly stopped relying on switch-cases here due to some TypeScript
// issue, not checking properly `case undefined` (bug?)
if (onKeyExpiration === "continue" || onKeyExpiration === undefined) {
whitelistedKeyIds.push(keyId);
} else {
// Compile-time check throwing when not all possible cases are handled
// typescript don't know that the value cannot be undefined here
// https://github.com/microsoft/TypeScript/issues/57999
if (onKeyExpiration !== undefined) {
assertUnreachable(onKeyExpiration);
}
break;
}

badKeyStatuses.push(keyStatusObj);
break;
}

case KEY_STATUSES.INTERNAL_ERROR: {
case "internal-error": {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
Expand All @@ -168,8 +161,8 @@ export default function checkKeyStatuses(
whitelistedKeyIds.push(keyId);
break;
default:
// Weirdly enough, TypeScript is not checking properly
// `case undefined` (bug?)
// typescript don't know that the value cannot be undefined here
// https://github.com/microsoft/TypeScript/issues/57999
if (onKeyInternalError !== undefined) {
assertUnreachable(onKeyInternalError);
} else {
Expand All @@ -181,7 +174,7 @@ export default function checkKeyStatuses(
break;
}

case KEY_STATUSES.OUTPUT_RESTRICTED: {
case "output-restricted": {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
Expand All @@ -198,8 +191,8 @@ export default function checkKeyStatuses(
whitelistedKeyIds.push(keyId);
break;
default:
// Weirdly enough, TypeScript is not checking properly
// `case undefined` (bug?)
// typescript don't know that the value cannot be undefined here
// https://github.com/microsoft/TypeScript/issues/57999
if (onKeyOutputRestricted !== undefined) {
assertUnreachable(onKeyOutputRestricted);
} else {
Expand All @@ -211,9 +204,50 @@ export default function checkKeyStatuses(
break;
}

default:
case "usable-in-future": {
/**
* The key is not yet usable for decryption because the start time is in the future.
*/
blacklistedKeyIds.push(keyId);
break;
}

case "usable": {
whitelistedKeyIds.push(keyId);
break;
}

case "output-downscaled": {
/**
* The video content has been downscaled, probably because the device is
* insufficiently protected and does not met the security policy to play
* the content with the original quality (resolution).
* The key is usable to play the downscaled content.
* */
whitelistedKeyIds.push(keyId);
break;
}

case "status-pending": {
/**
* The status of the key is not yet known.
* It should not be blacklisted nor whitelisted until the actual status
* is determined.
* */
break;
}

case "released": {
const error = new EncryptedMediaError(
"KEY_STATUS_CHANGE_ERROR",
`A "${keyStatus}" status has been encountered (${bytesToHex(keyId)})`,
{ keyStatuses: [keyStatusObj, ...badKeyStatuses] },
);
throw error;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know if "output-downscaled" should be considered an error, for resiliency.

If it is output-downscaled, it's theoretically playing, just at a lower quality, isn't that semi-OK (like a warning-type of scenario?)

Copy link
Contributor

@lfaureyt lfaureyt Apr 3, 2024

Choose a reason for hiding this comment

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

I agree with @peaBerberian : some platforms thoretically support usage rules that require the device to force the downscale of video frames before outputting them to an uncompressed digital video output interface that is insufficiently protected.
Typical use is a usage rule requiring "downscale video output to SD if HDCP 1.4 or higher cannot be established on HDMI output". When processing a key associated with such usage rule and an unprotected HDMI output:

  • a platform that doesn't support downscaling is expected to fail to decrypt content and to issue an OUTPUT_RESTRICTED error.
  • a platform that does support downscaling is expected to decrypt, downscale, output content, and, I guess, to notify an OUTPUT_DOWNSCALED key status

Playready's uncompressed_digital_video_output_protection_level=270 usage rule is the typical example of such usage rule.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks I have updated the behavior to authorized to use a key that is "output-downscaled".

I'm wondering if we should take in consideration that the key status is "output-downscaled" to select the appropriate track.
For example a content with 2 representations:

  • Representation 1: FHD (1080p) with key status usable.
  • Representation 2: UHD (4K) with key status output-downscaled.

Currently the player will select the highest quality (UHD) if the client has the required bandwidth.

But which representation has a better perceived quality between the downscaled UHD and the FHD ?

Could it be possible that the downscaled UHD representation would be at a worse quality than the FHD representation ? In it's case we don't want to select the highest quality because it has been downscaled.

Copy link
Collaborator

Choose a reason for hiding this comment

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

output-downscaled doesn't seem to indicate details about the downscaling it performs, so I'm not sure if we could be able to compare it with other Representations (if there even is other Representations).

Treating "output-downscaled" as a "whitelisted" case seems ok to me in the meantime, as we've never yet even encountered that key status, but we might have to set a more complex logic in the future if we do observe some unwanted situation with it.

}

default:
assertUnreachable(keyStatus);
}
},
);
Expand Down
Loading