Skip to content

Commit

Permalink
Merge pull request #1434 from canalplus/fix/compatibilty-edge-test-ke…
Browse files Browse the repository at this point in the history
…ysystems

fix(compat): on Edge test comprehensively KeySystems before considering them as usable
  • Loading branch information
peaBerberian authored May 15, 2024
2 parents 2386e3d + e86a289 commit 6f68baf
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 5 deletions.
19 changes: 19 additions & 0 deletions src/compat/__tests__/generate_init_data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { utf16LEToStr } from "../../utils/string_parsing";
import { generatePlayReadyInitData } from "../generate_init_data";

describe("utils - generatePlayReadyInitData", () => {
const playReadyHeader =
'<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>ckB07BNLskeUq0qd83fTbA==</KID><DS_ID>yYIPDBca1kmMfL60IsfgAQ==</DS_ID><CUSTOMATTRIBUTES xmlns=""><encryptionref>312_4024_2018127108</encryptionref></CUSTOMATTRIBUTES></DATA></WRMHEADER>';

const initData = generatePlayReadyInitData(playReadyHeader);
const decodedInitDataUtf16LE = utf16LEToStr(initData);

it("has correct length", () => {
// the expected length for an initData with that PlayReady header.
expect(initData.length).toBe(754);
});

it("has the playerReadyHeader in it", () => {
expect(decodedInitDataUtf16LE).toMatch(playReadyHeader);
});
});
42 changes: 42 additions & 0 deletions src/compat/can_rely_on_request_media_key_system_access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { isEdgeChromium } from "./browser_detection";

/**
* This functions tells if the RxPlayer can trust the browser when it has
* successfully granted the MediaKeySystemAccess with
* `navigator.requestMediaKeySystemAccess(keySystem)` function, or if it should do
* some additional testing to confirm that the `keySystem` is supported on the device.
*
* This behavior has been experienced on the following device:
*
* On a Microsoft Surface with Edge v.124:
* - Althought `requestMediaKeySystemAccess` resolve correctly with the keySystem
* "com.microsoft.playready.recommendation.3000", generating a request with
* `generateRequest` throws an error: "NotSupportedError: Failed to execute
* 'generateRequest' on 'MediaKeySession': Failed to create MF PR CdmSession".
* In this particular case, the work-around was to consider recommendation.3000 as not supported
* and try another keySystem.
* @param keySystem - The key system in use.
* @returns {boolean}
*/
export function canRelyOnRequestMediaKeySystemAccess(keySystem: string): boolean {
if (isEdgeChromium && keySystem.indexOf("playready") !== -1) {
return false;
}
return true;
}
69 changes: 69 additions & 0 deletions src/compat/generate_init_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { itole4, itobe4, itole2, concat } from "../utils/byte_parsing";
import { strToUtf8, strToUtf16LE, hexToBytes } from "../utils/string_parsing";

/**
* The PlayReadyHeader sample that will be used to test if the CDM is supported.
* The KID does not matter because no content will be played, it's only to check if
* the CDM is capable of creating a session and generating a request.
*/
export const DUMMY_PLAY_READY_HEADER =
'<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>ckB07BNLskeUq0qd83fTbA==</KID><DS_ID>yYIPDBca1kmMfL60IsfgAQ==</DS_ID><CUSTOMATTRIBUTES xmlns=""><encryptionref>312_4024_2018127108</encryptionref></CUSTOMATTRIBUTES><CHECKSUM>U/tsUYRgMzw=</CHECKSUM></DATA></WRMHEADER>';

/**
* Generate the "cenc" init data for playready from the PlayreadyHeader string.
* @param {string} playreadyHeader - String representing the PlayreadyHeader XML.
* @returns {Uint8Array} The init data generated for that PlayreadyHeader.
* @see https://learn.microsoft.com/en-us/playready/specifications/playready-header-specification
*/
export function generatePlayReadyInitData(playreadyHeader: string): Uint8Array {
const recordValueEncoded = strToUtf16LE(playreadyHeader);
const recordLength = itole2(recordValueEncoded.length);
// RecordType: 0x0001 Indicates that the record contains a PlayReady Header (PRH).
const recordType = new Uint8Array([1, 0]);
const numberOfObjects = new Uint8Array([1, 0]); // 1 PlayReady object

/* playReadyObjectLength equals = X bytes for record + 2 bytes for record length,
+ 2 bytes for record types + 2 bytes for number of object */
const playReadyObjectLength = itole4(recordValueEncoded.length + 6);
const playReadyObject = concat(
playReadyObjectLength, // 4 bytes for the Playready object length
numberOfObjects, // 2 bytes for the number of PlayReady objects
recordType, // 2 bytes for record type
recordLength, // 2 bytes for record length
recordValueEncoded, // X bytes for record value
);

/** the systemId is define at https://dashif.org/identifiers/content_protection/ */
const playreadySystemId = hexToBytes("9a04f07998404286ab92e65be0885f95");

return generateInitData(playReadyObject, playreadySystemId);
}

/**
* Generate the "cenc" initData given the data and the systemId to use.
* Note this will generate an initData for version 0 of pssh.
* @param data - The data that is contained inside the pssh.
* @param systemId - The systemId to use.
* @returns
*/
function generateInitData(data: Uint8Array, systemId: Uint8Array): Uint8Array {
const psshBoxName = strToUtf8("pssh");
const versionAndFlags = new Uint8Array([0, 0, 0, 0]); // pssh version 0
const sizeOfData = itobe4(data.length);
const psshSize = itobe4(
4 /* pssh size */ +
4 /* pssh box */ +
4 /* version and flags */ +
16 /* systemId */ +
4 /* size of data */ +
data.length /* data */,
);
return concat(
psshSize, // 4 bytes for the pssh size
psshBoxName, // 4 bytes for the pssh box
versionAndFlags, // 4 bytes for version and flags
systemId, // 16 bytes for the systemId
sizeOfData, // 4 bytes for the data size
data, // X bytes for data
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as compat from "../../../../compat/can_rely_on_request_media_key_system_access";
import eme from "../../../../compat/eme";
import { testKeySystem } from "../../find_key_system";

describe("find_key_systems - ", () => {
let requestMediaKeySystemAccessMock: jest.SpyInstance;
let canRelyOnEMEMock: jest.SpyInstance;
const keySystem = "com.microsoft.playready.recommendation";

beforeEach(() => {
jest.resetModules();
jest.restoreAllMocks();
requestMediaKeySystemAccessMock = jest.spyOn(eme, "requestMediaKeySystemAccess");
canRelyOnEMEMock = jest.spyOn(compat, "canRelyOnRequestMediaKeySystemAccess");
});

it("should resolve if the keySystem is supported", async () => {
/* mock implementation of requestMediaKeySystemAccess that support the keySystem */
requestMediaKeySystemAccessMock.mockImplementation(() => ({
createMediaKeys: () => ({
createSession: () => ({
// eslint-disable-next-line @typescript-eslint/no-empty-function
generateRequest: () => {},
}),
}),
}));
await expect(testKeySystem(keySystem, [])).resolves.toBeTruthy();
expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1);
});

it("should reject if the keySystem is not supported", async () => {
/* mock implementation of requestMediaKeySystemAccess that does not support the keySystem */
requestMediaKeySystemAccessMock.mockImplementation(() => {
throw new Error();
});
await expect(testKeySystem(keySystem, [])).rejects.toThrow();
expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1);
});

it("should reject if the keySystem seems to be supported but the EME workflow fail", async () => {
/* mock implementation of requestMediaKeySystemAccess that seems to support the keySystem
but that is failing when performing the usual EME workflow of creating mediaKeys, creating a session
and generating a request. */

canRelyOnEMEMock.mockImplementation(() => false);
requestMediaKeySystemAccessMock.mockImplementation(() => ({
createMediaKeys: () => ({
createSession: () => ({
generateRequest: () => {
throw new Error("generateRequest failed");
},
}),
}),
}));
await expect(testKeySystem(keySystem, [])).rejects.toThrow();
expect(requestMediaKeySystemAccessMock).toHaveBeenCalledTimes(1);
});
});
39 changes: 35 additions & 4 deletions src/main_thread/decrypt/find_key_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
* limitations under the License.
*/

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 Down Expand Up @@ -376,10 +381,7 @@ export default function getMediaKeySystemAccess(
);

try {
const keySystemAccess = await eme.requestMediaKeySystemAccess(
keyType,
keySystemConfigurations,
);
const keySystemAccess = await testKeySystem(keyType, keySystemConfigurations);
log.info("DRM: Found compatible keysystem", keyType, index + 1);
return {
type: "create-media-key-system-access" as const,
Expand All @@ -397,3 +399,32 @@ export default function getMediaKeySystemAccess(
}
}
}
/**
* Test a key system configuration, resolves with the MediaKeySystemAccess
* or reject if the key system is unsupported.
* @param {string} keyType - The KeySystem string to test (ex: com.microsoft.playready.recommendation)
* @param {Array.<MediaKeySystemMediaCapability>} keySystemConfigurations - Configurations for this keySystem
* @returns Promise resolving with the MediaKeySystemAccess. Rejects if unsupported.
*/
export async function testKeySystem(
keyType: string,
keySystemConfigurations: MediaKeySystemConfiguration[],
) {
const keySystemAccess = await eme.requestMediaKeySystemAccess(
keyType,
keySystemConfigurations,
);

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);
} catch (err) {
log.debug("DRM: KeySystemAccess was granted but it is not usable");
throw err;
}
}
return keySystemAccess;
}
2 changes: 1 addition & 1 deletion src/utils/string_parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function strToUtf16LE(str: string): Uint8Array {

/**
* Convert a string to an Uint8Array containing the corresponding UTF-16 code
* units in little-endian.
* units in big-endian.
* @param {string} str
* @returns {Uint8Array}
*/
Expand Down

0 comments on commit 6f68baf

Please sign in to comment.