Skip to content

Commit

Permalink
Merge pull request #1175 from canalplus/fix/lg-rezap-doesnt-work
Browse files Browse the repository at this point in the history
Re-create MediaKeys at each loadVideo on WebOS
  • Loading branch information
peaBerberian authored Oct 28, 2022
2 parents 6fdd84f + e021232 commit 97730bc
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 13 deletions.
54 changes: 54 additions & 0 deletions src/compat/__tests__/can_reuse_media_keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

describe("Compat - canReuseMediaKeys", () => {
afterEach(() => {
jest.resetModules();
});

it("should return true on any browser but WebOS 2021 and 2022", () => {
jest.mock("../browser_detection", () => {
return { __esModule: true as const,
isWebOs2021: false,
isWebOs2022: false };
});
const canReuseMediaKeys =
jest.requireActual("../can_reuse_media_keys.ts");
expect(canReuseMediaKeys.default()).toBe(true);
});

it("should return false on WebOs 2021", () => {
jest.mock("../browser_detection", () => {
return { __esModule: true as const,
isWebOs2021: true,
isWebOs2022: false };
});
const canReuseMediaKeys =
jest.requireActual("../can_reuse_media_keys.ts");
expect(canReuseMediaKeys.default()).toBe(false);
});

it("should return false on WebOs 2022", () => {
jest.mock("../browser_detection", () => {
return { __esModule: true as const,
isWebOs2021: false,
isWebOs2022: true };
});
const canReuseMediaKeys =
jest.requireActual("../can_reuse_media_keys.ts");
expect(canReuseMediaKeys.default()).toBe(false);
});

it("should return false in the improbable case of both WebOs 2021 and 2022", () => {
jest.mock("../browser_detection", () => {
return { __esModule: true as const,
isWebOs2021: true,
isWebOs2022: true };
});
const canReuseMediaKeys =
jest.requireActual("../can_reuse_media_keys.ts");
expect(canReuseMediaKeys.default()).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */

describe("compat - shouldRenewMediaKeys", () => {
describe("compat - shouldRenewMediaKeySystemAccess", () => {
beforeEach(() => {
jest.resetModules();
});
Expand All @@ -33,8 +33,9 @@ describe("compat - shouldRenewMediaKeys", () => {
isIE11: false,
};
});
const shouldRenewMediaKeys = jest.requireActual("../should_renew_media_keys");
expect(shouldRenewMediaKeys.default()).toBe(false);
const shouldRenewMediaKeySystemAccess =
jest.requireActual("../should_renew_media_key_system_access");
expect(shouldRenewMediaKeySystemAccess.default()).toBe(false);
});

it("should return true if we are on IE11", () => {
Expand All @@ -44,8 +45,9 @@ describe("compat - shouldRenewMediaKeys", () => {
isIE11: true,
};
});
const shouldRenewMediaKeys = jest.requireActual("../should_renew_media_keys");
expect(shouldRenewMediaKeys.default()).toBe(true);
const shouldRenewMediaKeySystemAccess =
jest.requireActual("../should_renew_media_key_system_access");
expect(shouldRenewMediaKeySystemAccess.default()).toBe(true);
});
beforeEach(() => {
jest.resetModules();
Expand Down
10 changes: 10 additions & 0 deletions src/compat/browser_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ const isSamsungBrowser : boolean = !isNode &&
const isTizen : boolean = !isNode &&
/Tizen/.test(navigator.userAgent);

const isWebOs : boolean = !isNode &&
/Web0S/.test(navigator.userAgent);
const isWebOs2021 : boolean = !isNode &&
/WebOS.TV-2021/.test(navigator.userAgent);
const isWebOs2022 : boolean = !isNode &&
/WebOS.TV-2022/.test(navigator.userAgent);

interface ISafariWindowObject extends Window {
safari? : { pushNotification? : { toString() : string } };
}
Expand All @@ -76,4 +83,7 @@ export {
isSafariMobile,
isSamsungBrowser,
isTizen,
isWebOs,
isWebOs2021,
isWebOs2022,
};
19 changes: 19 additions & 0 deletions src/compat/can_reuse_media_keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
isWebOs2021,
isWebOs2022,
} from "./browser_detection";

/**
* Returns `true` if a `MediaKeys` instance (the `Encrypted Media Extension`
* concept) can be reused between contents.
*
* This should usually be the case but we found rare devices where this would
* cause problem:
* - (2022-10-26): WebOS (LG TVs) 2021 and 2022 just rebuffered indefinitely
* when loading a content already-loaded on the HTMLMediaElement.
*
* @returns {boolean}
*/
export default function canReuseMediaKeys() : boolean {
return !(isWebOs2021 || isWebOs2022);
}
6 changes: 4 additions & 2 deletions src/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
MediaSource_,
} from "./browser_compatibility_types";
import canPatchISOBMFFSegment from "./can_patch_isobmff";
import canReuseMediaKeys from "./can_reuse_media_keys";
import tryToChangeSourceBufferType, {
ICompatSourceBuffer,
} from "./change_source_buffer_type";
Expand Down Expand Up @@ -58,7 +59,7 @@ import play from "./play";
import setElementSrc$ from "./set_element_src";
// eslint-disable-next-line max-len
import shouldReloadMediaSourceOnDecipherabilityUpdate from "./should_reload_media_source_on_decipherability_update";
import shouldRenewMediaKeys from "./should_renew_media_keys";
import shouldRenewMediaKeySystemAccess from "./should_renew_media_key_system_access";
import shouldUnsetMediaKeys from "./should_unset_media_keys";
import shouldValidateMetadata from "./should_validate_metadata";
import shouldWaitForDataBeforeLoaded from "./should_wait_for_data_before_loaded";
Expand All @@ -73,6 +74,7 @@ export {
addClassName,
addTextTrack,
canPatchISOBMFFSegment,
canReuseMediaKeys,
clearElementSrc,
closeSession,
CustomMediaKeySystemAccess,
Expand Down Expand Up @@ -104,7 +106,7 @@ export {
setElementSrc$,
setMediaKeys,
shouldReloadMediaSourceOnDecipherabilityUpdate,
shouldRenewMediaKeys,
shouldRenewMediaKeySystemAccess,
shouldUnsetMediaKeys,
shouldValidateMetadata,
shouldWaitForDataBeforeLoaded,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
import { isIE11 } from "./browser_detection";

/**
* Returns true if the current target require the media keys to be renewed on
* each content.
* Returns true if the current target require the MediaKeySystemAccess to be
* renewed on each content.
* @returns {Boolean}
*/
export default function shouldRenewMediaKeys() : boolean {
export default function shouldRenewMediaKeySystemAccess() : boolean {
return isIE11;
}
182 changes: 182 additions & 0 deletions src/core/decrypt/__tests__/__global__/media_keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import {
MediaKeysImpl,
MediaKeySystemAccessImpl,
mockCompat,
testContentDecryptorError,
} from "./utils";
Expand Down Expand Up @@ -104,6 +105,8 @@ describe("core - decrypt - global tests - media key system access", () => {
/* eslint-enable max-len */
return new Promise<void>((res, rej) => {
mockCompat({});
const mockCreateMediaKeys = jest.spyOn(MediaKeySystemAccessImpl.prototype,
"createMediaKeys");
const { ContentDecryptorState } = jest.requireActual("../../content_decryptor");
const ContentDecryptor = jest.requireActual("../../content_decryptor").default;
const contentDecryptor = new ContentDecryptor(videoElt, ksConfig);
Expand All @@ -112,6 +115,7 @@ describe("core - decrypt - global tests - media key system access", () => {
receivedStateChange++;
try {
expect(newState).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
} catch (err) { rej(err); }
setTimeout(() => {
try {
Expand All @@ -126,6 +130,184 @@ describe("core - decrypt - global tests - media key system access", () => {
});
});

/* eslint-disable max-len */
it("should not call createMediaKeys again if previous one is compatible", () => {
/* eslint-enable max-len */
return new Promise<void>((res, rej) => {
mockCompat({});
const mockCreateMediaKeys = jest.spyOn(MediaKeySystemAccessImpl.prototype,
"createMediaKeys");
const { ContentDecryptorState } = jest.requireActual("../../content_decryptor");
const ContentDecryptor = jest.requireActual("../../content_decryptor").default;

const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange1 = 0;
contentDecryptor1.addEventListener("error", rej);
contentDecryptor1.addEventListener("stateChange", (state1: any) => {
receivedStateChange1++;
try {
if (receivedStateChange1 === 2) {
expect(state1).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange1 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
contentDecryptor1.attach();
} catch (err) { rej(err); }

setTimeout(() => {
contentDecryptor1.dispose();
const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange2 = 0;
contentDecryptor2.addEventListener("error", rej);
contentDecryptor2.addEventListener("stateChange", (state2: any) => {
receivedStateChange2++;
try {
if (receivedStateChange2 === 2) {
expect(state2).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange2 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
contentDecryptor2.attach();
setTimeout(() => {
try {
contentDecryptor2.dispose();
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
res();
} catch (err) { rej(err); }
});
} catch (err) { rej(err); }
});
}, 10);
});
});
});

/* eslint-disable max-len */
it("should call createMediaKeys again if the platform needs re-creation of the MediaKeys", () => {
/* eslint-enable max-len */
return new Promise<void>((res, rej) => {
mockCompat({
canReuseMediaKeys: jest.fn(() => false),
});
const mockCreateMediaKeys = jest.spyOn(MediaKeySystemAccessImpl.prototype,
"createMediaKeys");
const { ContentDecryptorState } = jest.requireActual("../../content_decryptor");
const ContentDecryptor = jest.requireActual("../../content_decryptor").default;

const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange1 = 0;
contentDecryptor1.addEventListener("error", rej);
contentDecryptor1.addEventListener("stateChange", (state1: any) => {
receivedStateChange1++;
try {
if (receivedStateChange1 === 2) {
expect(state1).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange1 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
contentDecryptor1.attach();
} catch (err) { rej(err); }

setTimeout(() => {
contentDecryptor1.dispose();
const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange2 = 0;
contentDecryptor2.addEventListener("error", rej);
contentDecryptor2.addEventListener("stateChange", (state2: any) => {
receivedStateChange2++;
try {
if (receivedStateChange2 === 2) {
expect(state2).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange2 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2);
contentDecryptor2.attach();
setTimeout(() => {
try {
contentDecryptor2.dispose();
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2);
res();
} catch (err) { rej(err); }
});
} catch (err) { rej(err); }
});
}, 10);
});
});
});

/* eslint-disable max-len */
it("should not call createMediaKeys again if the platform needs MediaKeySystemAccess renewal", () => {
/* eslint-enable max-len */
return new Promise<void>((res, rej) => {
mockCompat({
shouldRenewMediaKeySystemAccess: jest.fn(() => true),
});
const mockCreateMediaKeys = jest.spyOn(MediaKeySystemAccessImpl.prototype,
"createMediaKeys");
const { ContentDecryptorState } = jest.requireActual("../../content_decryptor");
const ContentDecryptor = jest.requireActual("../../content_decryptor").default;

const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange1 = 0;
contentDecryptor1.addEventListener("error", rej);
contentDecryptor1.addEventListener("stateChange", (state1: any) => {
receivedStateChange1++;
try {
if (receivedStateChange1 === 2) {
expect(state1).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange1 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1);
contentDecryptor1.attach();
} catch (err) { rej(err); }

setTimeout(() => {
contentDecryptor1.dispose();
const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig);
let receivedStateChange2 = 0;
contentDecryptor2.addEventListener("error", rej);
contentDecryptor2.addEventListener("stateChange", (state2: any) => {
receivedStateChange2++;
try {
if (receivedStateChange2 === 2) {
expect(state2).toEqual(ContentDecryptorState.ReadyForContent);
return;
} else if (receivedStateChange2 !== 1) {
throw new Error("Unexpected stateChange event.");
}
expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment);
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2);
contentDecryptor2.attach();
setTimeout(() => {
try {
contentDecryptor2.dispose();
expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2);
res();
} catch (err) { rej(err); }
});
} catch (err) { rej(err); }
});
}, 10);
});
});
});

/* eslint-disable max-len */
it("should not create any session if no encrypted event was received", (done) => {
/* eslint-enable max-len */
Expand Down
2 changes: 2 additions & 0 deletions src/core/decrypt/__tests__/__global__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ export function mockCompat(exportedFunctions = {}) {
setMediaKeys: mockSetMediaKeys,
getInitData: mockGetInitData,
generateKeyRequest: mockGenerateKeyRequest,
shouldRenewMediaKeySystemAccess: jest.fn(() => false),
canReuseMediaKeys: jest.fn(() => true),
...exportedFunctions }));

return { mockEvents,
Expand Down
Loading

0 comments on commit 97730bc

Please sign in to comment.