Skip to content

Commit 351ec6d

Browse files
committed
Call generateRequest BEFORE pushing any segment with PlayReady
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.
1 parent 74b603a commit 351ec6d

4 files changed

+76
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* (2024-07-23) We noticed issues with most devices relying on PlayReady when
3+
* playing some contents with a mix of encrypted and clear segments (not with
4+
* Canal+ own contents weirdly enough, yet with multiple others both encoded
5+
* and packaged differently).
6+
* The issue fixed itself when we called the
7+
* `MediaKeySession.prototype.generateRequest` EME API **BEFORE** any segment
8+
* was buffered.
9+
*
10+
* So this function returns `true` when calling `generateRequest` should
11+
* probably be performed before buffering any segment.
12+
*
13+
* @param {string} keySystem - The key system in use.
14+
* @returns {boolean}
15+
*/
16+
export default function shouldCallGenerateRequestBeforeBufferingMedia(
17+
keySystem: string,
18+
): boolean {
19+
if (keySystem.indexOf("playready") !== -1) {
20+
return true;
21+
}
22+
return false;
23+
}

src/main_thread/decrypt/content_decryptor.ts

+18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import type { ICustomMediaKeys, ICustomMediaKeySystemAccess } from "../../compat/eme";
1818
import eme, { getInitData } from "../../compat/eme";
19+
import shouldCallGenerateRequestBeforeBufferingMedia from "../../compat/should_call_generate_request_before_buffering_media";
1920
import config from "../../config";
2021
import { EncryptedMediaError, OtherError } from "../../errors";
2122
import log from "../../log";
@@ -54,6 +55,7 @@ import {
5455
areSomeKeyIdsContainedIn,
5556
} from "./utils/key_id_comparison";
5657
import type KeySessionRecord from "./utils/key_session_record";
58+
import performFakeGenerateRequest from "./utils/perform_fake_generate_request";
5759

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

296+
if (
297+
shouldCallGenerateRequestBeforeBufferingMedia(mediaKeySystemAccess.keySystem)
298+
) {
299+
try {
300+
await performFakeGenerateRequest(mediaKeys);
301+
} catch (err) {
302+
const error = err instanceof Error ? err : new Error("Unknown Error");
303+
log.warn("DRM: unable to fully perform fake generateRequest call", error);
304+
}
305+
}
306+
307+
if (this._isStopped()) {
308+
// We might be stopped since then
309+
return;
310+
}
311+
294312
const prevState = this._stateData.state;
295313
this._stateData = {
296314
state: ContentDecryptorState.ReadyForContent,

src/main_thread/decrypt/find_key_system.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
import { canRelyOnRequestMediaKeySystemAccess } from "../../compat/can_rely_on_request_media_key_system_access";
1818
import type { ICustomMediaKeySystemAccess } from "../../compat/eme";
1919
import eme from "../../compat/eme";
20-
import {
21-
generatePlayReadyInitData,
22-
DUMMY_PLAY_READY_HEADER,
23-
} from "../../compat/generate_init_data";
2420
import shouldRenewMediaKeySystemAccess from "../../compat/should_renew_media_key_system_access";
2521
import config from "../../config";
2622
import { EncryptedMediaError } from "../../errors";
@@ -31,6 +27,7 @@ import flatMap from "../../utils/flat_map";
3127
import isNullOrUndefined from "../../utils/is_null_or_undefined";
3228
import type { CancellationSignal } from "../../utils/task_canceller";
3329
import MediaKeysInfosStore from "./utils/media_keys_infos_store";
30+
import performFakeGenerateRequest from "./utils/perform_fake_generate_request";
3431

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

@@ -418,9 +415,7 @@ export async function testKeySystem(
418415
if (!canRelyOnRequestMediaKeySystemAccess(keyType)) {
419416
try {
420417
const mediaKeys = await keySystemAccess.createMediaKeys();
421-
const session = mediaKeys.createSession();
422-
const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
423-
await session.generateRequest("cenc", initData);
418+
await performFakeGenerateRequest(mediaKeys);
424419
} catch (err) {
425420
log.debug("DRM: KeySystemAccess was granted but it is not usable");
426421
throw err;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { closeSession } from "../../../compat/eme";
2+
import type { ICustomMediaKeys } from "../../../compat/eme";
3+
import {
4+
DUMMY_PLAY_READY_HEADER,
5+
generatePlayReadyInitData,
6+
} from "../../../compat/generate_init_data";
7+
import log from "../../../log";
8+
9+
/**
10+
* The EME API is badly implemented on many devices, leading us toward the need
11+
* to perform some heavy work-arounds.
12+
*
13+
* A frequent one is to call the `MediaKeySession.prototype.generateRequest` API
14+
* at some point for dummy data an see if it fails (or not, sometimes just
15+
* calling it is important).
16+
*
17+
* This method does just that, resolving the returned Promise if the
18+
* `generateRequest` call could be performed and succeeded or rejecting in other
19+
* cases.
20+
* @param {MediaKeys} mediaKeys
21+
* @returns {Promise}
22+
*/
23+
export default async function performFakeGenerateRequest(
24+
mediaKeys: MediaKeys | ICustomMediaKeys,
25+
): Promise<void> {
26+
const session = mediaKeys.createSession();
27+
const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
28+
await session.generateRequest("cenc", initData);
29+
closeSession(session).catch((err) => {
30+
const error = err instanceof Error ? err : new Error("Unknown Error");
31+
log.warn("DRM: unable to close fake MediaKeySession", error);
32+
});
33+
}

0 commit comments

Comments
 (0)