Skip to content

Commit

Permalink
Call generateRequest BEFORE pushing any segment with PlayReady
Browse files Browse the repository at this point in the history
Another day, another PlayReady-specific issue :/

A partner signalled to us that they weren't able to play a mix of
unencrypted and encrypted content on any PlayReady devices.

After investigation, it seems that calling `generateRequest` for the
first time after clear segments are already present on a MSE
`SourceBuffer` associated to the MediaSource linked to the
corresponding media element immediately triggered an HTML5
`MEDIA_ERR_DECODE` error.

We tried A LOT of work-arounds:

  - patching clear segments with a `tenc` box with a `0x0` key id to
    incite the CDM to understand that encrypted contents may be pushed
    in the future

  - Rewriting the pssh sent through the EME `generateRequest` API so
    that it is barebone to limit weird PlayReady edge cases.

  - Replacing those stream clear segments' with those in our demo page,
    just to check that the clear segments were not at fault here

  - Waiting more time between the association of a MediaKeys to the
    media element and pushing the first segments.

None of those actions had an effect.

However, what had an effect, was to call the `generateRequest` API
BEFORE buffering any segment yet AFTER attaching the MediaKeys (and
perhaps MediaSource) to the media element.

So this commit does just that, communicating dummy initialization data
for a session that will be closed directly after.

Note that we already do a fake `generateRequest` on Edge Chromium with
Playready since #1434, yet this test was not sufficient, seemingly
because it is performed BEFORE MediaKeys attachment.

Note that this commit fixes the clear -> encrypted issues our partner
were having, but we're unsure yet of if it fixes the encrypted -> clear
issues (and I have good reasons to think it does not).

So, uh, yeah, PlayReady seems to keep being hard-at-work giving us
challenges and head-scratchers.
  • Loading branch information
peaBerberian committed Jul 23, 2024
1 parent 74b603a commit d5ba127
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 7 deletions.
19 changes: 19 additions & 0 deletions src/compat/should_call_generate_request_before_buffering_media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* (2024-07-23) We noticed issues with most devices relying on PlayReady when
* playing some contents with mix encrypted and clear contents (not with
* Canal+ own contents weirdly enough, yet with multiple other contents
* encoded/packaged differently).
* The issue fixed itself when we called the
* `MediaKeySession.prototype.generateRequest` EME API **BEFORE** any segment
* was buffered.
* @param {string} keySystem - The key system in use.
* @returns {boolean}
*/
export default function shouldCallGenerateRequestBeforeBufferingMedia(
keySystem: string,
): boolean {
if (keySystem.indexOf("playready") !== -1) {
return true;
}
return false;
}
18 changes: 18 additions & 0 deletions src/main_thread/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import type { ICustomMediaKeys, ICustomMediaKeySystemAccess } from "../../compat/eme";
import eme, { getInitData } from "../../compat/eme";
import shouldCallGenerateRequestBeforeBufferingMedia from "../../compat/should_call_generate_request_before_buffering_media";
import config from "../../config";
import { EncryptedMediaError, OtherError } from "../../errors";
import log from "../../log";
Expand Down Expand Up @@ -54,6 +55,7 @@ import {
areSomeKeyIdsContainedIn,
} from "./utils/key_id_comparison";
import type KeySessionRecord from "./utils/key_session_record";
import performFakeGenerateRequest from "./utils/perform_fake_generate_request";

/**
* Module communicating with the Content Decryption Module (or CDM) to be able
Expand Down Expand Up @@ -291,6 +293,22 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
return;
}

if (
shouldCallGenerateRequestBeforeBufferingMedia(mediaKeySystemAccess.keySystem)
) {
try {
await performFakeGenerateRequest(mediaKeys);
} catch (err) {
const error = err instanceof Error ? err : new Error("Unknown Error");
log.warn("DRM: unable to fully perform fake generateRequest call", error);
}
}

if (this._isStopped()) {
// We might be stopped since then
return;
}

const prevState = this._stateData.state;
this._stateData = {
state: ContentDecryptorState.ReadyForContent,
Expand Down
9 changes: 2 additions & 7 deletions src/main_thread/decrypt/find_key_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
import { canRelyOnRequestMediaKeySystemAccess } from "../../compat/can_rely_on_request_media_key_system_access";
import type { ICustomMediaKeySystemAccess } from "../../compat/eme";
import eme from "../../compat/eme";
import {
generatePlayReadyInitData,
DUMMY_PLAY_READY_HEADER,
} from "../../compat/generate_init_data";
import shouldRenewMediaKeySystemAccess from "../../compat/should_renew_media_key_system_access";
import config from "../../config";
import { EncryptedMediaError } from "../../errors";
Expand All @@ -31,6 +27,7 @@ import flatMap from "../../utils/flat_map";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import type { CancellationSignal } from "../../utils/task_canceller";
import MediaKeysInfosStore from "./utils/media_keys_infos_store";
import performFakeGenerateRequest from "./utils/perform_fake_generate_request";

type MediaKeysRequirement = "optional" | "required" | "not-allowed";

Expand Down Expand Up @@ -418,9 +415,7 @@ export async function testKeySystem(
if (!canRelyOnRequestMediaKeySystemAccess(keyType)) {
try {
const mediaKeys = await keySystemAccess.createMediaKeys();
const session = mediaKeys.createSession();
const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
await session.generateRequest("cenc", initData);
await performFakeGenerateRequest(mediaKeys);
} catch (err) {
log.debug("DRM: KeySystemAccess was granted but it is not usable");
throw err;
Expand Down
33 changes: 33 additions & 0 deletions src/main_thread/decrypt/utils/perform_fake_generate_request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { closeSession } from "../../../compat/eme";
import type { ICustomMediaKeys } from "../../../compat/eme";
import {
DUMMY_PLAY_READY_HEADER,
generatePlayReadyInitData,
} from "../../../compat/generate_init_data";
import log from "../../../log";

/**
* The EME is badly implemented on many devices, leading us toward the need to
* perform some heavy work-arounds.
*
* A frequent one is to call the `MediaKeySession.prototype.generateRequest` API
* at some point for dummy data an see if it fails (or not, sometimes just
* calling it is important).
*
* This method does just that, resolving the returned Promise if the
* `generateRequest` call could be performed and succeeded or rejecting in other
* cases.
* @param {MediaKeys} mediaKeys
* @returns {Promise}
*/
export default async function performFakeGenerateRequest(
mediaKeys: MediaKeys | ICustomMediaKeys,
): Promise<void> {
const session = mediaKeys.createSession();
const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
await session.generateRequest("cenc", initData);
closeSession(session).catch((err) => {
const error = err instanceof Error ? err : new Error("Unknown Error");
log.warn("DRM: unable to close fake MediaKeySession", error);
});
}

0 comments on commit d5ba127

Please sign in to comment.