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 8fad022
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 1 deletion.
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;
}
29 changes: 28 additions & 1 deletion src/main_thread/decrypt/content_decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
*/

import type { ICustomMediaKeys, ICustomMediaKeySystemAccess } from "../../compat/eme";
import eme, { getInitData } from "../../compat/eme";
import eme, { closeSession, getInitData } from "../../compat/eme";
import {
DUMMY_PLAY_READY_HEADER,
generatePlayReadyInitData,
} from "../../compat/generate_init_data";
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 @@ -291,6 +296,28 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
return;
}

if (
shouldCallGenerateRequestBeforeBufferingMedia(mediaKeySystemAccess.keySystem)
) {
try {
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 initial fake MediaKeySession", error);
});
} 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

0 comments on commit 8fad022

Please sign in to comment.