diff --git a/src/core/main/worker/worker_main.ts b/src/core/main/worker/worker_main.ts index c3532e789b..dc49effc8c 100644 --- a/src/core/main/worker/worker_main.ts +++ b/src/core/main/worker/worker_main.ts @@ -463,6 +463,19 @@ interface IBufferingInitializationInformation { * optimizations. */ drmSystemId: string | undefined; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; + /** + * If `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; /** * Enable/Disable fastSwitching: allow to replace lower-quality segments by * higher-quality ones to have a faster transition. @@ -513,7 +526,14 @@ function loadOrReloadPreparedContent( segmentSinksStore, segmentFetcherCreator, } = preparedContent; - const { drmSystemId, enableFastSwitching, initialTime, onCodecSwitch } = val; + const { + canFilterProtectionData, + failOnEncryptedAfterClear, + drmSystemId, + enableFastSwitching, + initialTime, + onCodecSwitch, + } = val; playbackObservationRef.onUpdate((observation) => { if (preparedContent.decipherabilityFreezeDetector.needToReload(observation)) { handleMediaSourceReload({ @@ -592,6 +612,8 @@ function loadOrReloadPreparedContent( maxVideoBufferSize, maxBufferAhead, maxBufferBehind, + canFilterProtectionData, + failOnEncryptedAfterClear, drmSystemId, enableFastSwitching, onCodecSwitch, @@ -874,6 +896,8 @@ function loadOrReloadPreparedContent( { initialTime: newInitialTime, drmSystemId: val.drmSystemId, + canFilterProtectionData: val.canFilterProtectionData, + failOnEncryptedAfterClear: val.failOnEncryptedAfterClear, enableFastSwitching: val.enableFastSwitching, onCodecSwitch: val.onCodecSwitch, }, diff --git a/src/core/stream/adaptation/adaptation_stream.ts b/src/core/stream/adaptation/adaptation_stream.ts index 6ba8b16daf..73e76d7a2c 100644 --- a/src/core/stream/adaptation/adaptation_stream.ts +++ b/src/core/stream/adaptation/adaptation_stream.ts @@ -441,6 +441,7 @@ export default function AdaptationStream( bufferGoal, maxBufferSize, drmSystemId: options.drmSystemId, + canFilterProtectionData: options.canFilterProtectionData, fastSwitchThreshold, }, }, diff --git a/src/core/stream/adaptation/types.ts b/src/core/stream/adaptation/types.ts index f1708127d7..9f15aab4ae 100644 --- a/src/core/stream/adaptation/types.ts +++ b/src/core/stream/adaptation/types.ts @@ -188,6 +188,14 @@ export interface IAdaptationStreamOptions { * those devices. */ enableFastSwitching: boolean; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; } /** Object indicating a choice of Adaptation made by the user. */ diff --git a/src/core/stream/orchestrator/stream_orchestrator.ts b/src/core/stream/orchestrator/stream_orchestrator.ts index c3c30cfcb8..e32c819e1d 100644 --- a/src/core/stream/orchestrator/stream_orchestrator.ts +++ b/src/core/stream/orchestrator/stream_orchestrator.ts @@ -109,6 +109,22 @@ export default function StreamOrchestrator( MAXIMUM_MAX_BUFFER_BEHIND, } = config.getCurrent(); + // Some DRM issues force us to check whether we're initially pushing clear + // segments. + // + // NOTE: Theoretically `initialPeriod` may not be the first Period for + // which we push segments (e.g. we may have a small seek which lead to + // another Period being streamed instead). + // This means that we could have an issue if `initialPeriod` leads to encrypted + // content, but the actually first-pushed segments do not. Here + // `shouldReloadOnEncryptedContent` would be set to `false` despiste the fact + // that it should be set to `true`. + // Yet, checking the first Period for which we pushed segments seems very hard, + // and all that for what is now (2024-07-31) a PlayReady bug, I don't have the + // will. + const shouldReloadOnEncryptedContent = + options.failOnEncryptedAfterClear && !hasEncryptedContentInPeriod(initialPeriod); + // Keep track of a unique BufferGarbageCollector created per // SegmentSink. const garbageCollectors = new WeakMapMemory((segmentSink: SegmentSink) => { @@ -449,6 +465,26 @@ export default function StreamOrchestrator( ): void { log.info("Stream: Creating new Stream for", bufferType, basePeriod.start); + if (shouldReloadOnEncryptedContent) { + if (hasEncryptedContentInPeriod(basePeriod)) { + playbackObserver.listen((pos) => { + if (pos.position.getWanted() > basePeriod.start - 0.01) { + callbacks.needsMediaSourceReload({ + timeOffset: 0, + minimumPosition: basePeriod.start, + maximumPosition: basePeriod.end, + }); + } else { + callbacks.lockedStream({ + period: basePeriod, + bufferType, + }); + } + }); + return; + } + } + /** * Contains properties linnked to the next chronological `PeriodStream` that * may be created here. @@ -656,6 +692,11 @@ export type IStreamOrchestratorOptions = IPeriodStreamOptions & { maxVideoBufferSize: IReadOnlySharedReference; maxBufferAhead: IReadOnlySharedReference; maxBufferBehind: IReadOnlySharedReference; + /** + * If `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; }; /** Callbacks called by the `StreamOrchestrator` on various events. */ @@ -760,6 +801,24 @@ export interface ILockedStreamPayload { bufferType: IBufferType; } +/** + * Return `true` if the given Period has at least one Representation which is + * known to be encrypted. + * + * @param {Object} period + * @returns {boolean} + */ +function hasEncryptedContentInPeriod(period: IPeriod): boolean { + for (const adaptation of period.getAdaptations()) { + for (const representation of adaptation.representations) { + if (representation.contentProtections !== undefined) { + return true; + } + } + } + return false; +} + /** * Returns `true` if low-level buffers have to be "flushed" after the given * `cleanedRanges` time ranges have been removed from an audio or video diff --git a/src/core/stream/representation/representation_stream.ts b/src/core/stream/representation/representation_stream.ts index 6bf6b57d90..c507c336cb 100644 --- a/src/core/stream/representation/representation_stream.ts +++ b/src/core/stream/representation/representation_stream.ts @@ -94,7 +94,13 @@ export default function RepresentationStream( parentCancelSignal: CancellationSignal, ): void { const { period, adaptation, representation } = content; - const { bufferGoal, maxBufferSize, drmSystemId, fastSwitchThreshold } = options; + const { + bufferGoal, + maxBufferSize, + drmSystemId, + canFilterProtectionData, + fastSwitchThreshold, + } = options; const bufferType = adaptation.type; /** `TaskCanceller` stopping ALL operations performed by the `RepresentationStream` */ @@ -148,7 +154,7 @@ export default function RepresentationStream( // If the DRM system id is already known, and if we already have encryption data // for it, we may not need to wait until the initialization segment is loaded to // signal required protection data, thus performing License negotiations sooner - if (drmSystemId !== undefined) { + if (canFilterProtectionData && drmSystemId !== undefined) { const encryptionData = representation.getEncryptionData(drmSystemId); // If some key ids are not known yet, it may be safer to wait for this initialization diff --git a/src/core/stream/representation/types.ts b/src/core/stream/representation/types.ts index da5d4389b8..a3274afebb 100644 --- a/src/core/stream/representation/types.ts +++ b/src/core/stream/representation/types.ts @@ -323,6 +323,14 @@ export interface IRepresentationStreamOptions { * `0` can be emitted to disable any kind of fast-switching. */ fastSwitchThreshold: IReadOnlySharedReference; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; } /** Object indicating a choice of allowed Representations made by the user. */ diff --git a/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts b/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts index 8f5a8a3003..6a8020c583 100644 --- a/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts @@ -10,6 +10,7 @@ import { describe, afterEach, it, expect, vi } from "vitest"; import type { IPlayerError } from "../../../../public_types"; import { concat } from "../../../../utils/byte_parsing"; +import type { IContentDecryptorStateData } from "../../types"; import { formatFakeChallengeFromInitData, MediaKeySessionImpl, @@ -376,37 +377,44 @@ async function checkGetLicense({ // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); - contentDecryptor.addEventListener("stateChange", (newState: number) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + const waitForAttachmentCallback = (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } - contentDecryptor.removeEventListener("stateChange"); + contentDecryptor.removeEventListener("stateChange", waitForAttachmentCallback); contentDecryptor.attach(); - }); - - contentDecryptor.addEventListener("error", (error: unknown) => { - if (shouldFail) { - try { - checkKeyLoadError(error); - expect(mockGetLicense).toHaveBeenCalledTimes(maxRetries + 1); - for (let i = 1; i <= maxRetries + 1; i++) { - // TODO there's seem to be an issue with how vitest check Uint8Array - // equality - expect(mockGetLicense).toHaveBeenNthCalledWith( - i, - challenge, - "license-request", - ); + }; + contentDecryptor.addEventListener("stateChange", waitForAttachmentCallback); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name !== ContentDecryptorState.Error) { + return; + } + const error = state.payload; + if (shouldFail) { + try { + checkKeyLoadError(error); + expect(mockGetLicense).toHaveBeenCalledTimes(maxRetries + 1); + for (let i = 1; i <= maxRetries + 1; i++) { + // TODO there's seem to be an issue with how vitest check Uint8Array + // equality + expect(mockGetLicense).toHaveBeenNthCalledWith( + i, + challenge, + "license-request", + ); + } + expect(mockUpdate).toHaveBeenCalledTimes(0); + res(); + } catch (e) { + rej(e); } - expect(mockUpdate).toHaveBeenCalledTimes(0); - res(); - } catch (e) { - rej(e); + } else { + rej(new Error(`Unexpected error: ${error}`)); } - } else { - rej(new Error(`Unexpected error: ${error}`)); - } - }); + }, + ); contentDecryptor.addEventListener("warning", (warning: Error) => { if (warningsLeft-- > 0) { diff --git a/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts b/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts index e8bc4bf327..dabba3c607 100644 --- a/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts @@ -52,7 +52,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -104,7 +104,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -176,7 +176,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -276,7 +276,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -347,7 +347,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -403,7 +403,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -481,7 +481,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -577,7 +577,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); @@ -661,7 +661,7 @@ describe("decrypt - global tests - init data", () => { // == test == const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { rej(new Error(`Unexpected state: ${newState}`)); } contentDecryptor.removeEventListener("stateChange"); diff --git a/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts b/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts index a4c3834607..51c511b170 100644 --- a/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/media_key_system_access.test.ts @@ -9,6 +9,7 @@ import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; /* eslint-disable no-restricted-properties */ import type { ICustomMediaKeySystemAccess } from "../../../../compat/eme"; +import { ContentDecryptorState } from "../../types"; import { defaultKSConfig, defaultPRRecommendationKSConfig, @@ -708,8 +709,10 @@ describe("decrypt - global tests - media key system access", () => { const mediaElement = document.createElement("video"); const contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error: any) => { - rej(error); + contentDecryptor.addEventListener("stateChange", (state: any) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } }); setTimeout(() => { expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(1); @@ -745,8 +748,10 @@ describe("decrypt - global tests - media key system access", () => { const mediaElement = document.createElement("video"); const contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error: any) => { - rej(error); + contentDecryptor.addEventListener("stateChange", (state: any) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } }); setTimeout(() => { expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(2); @@ -789,8 +794,10 @@ describe("decrypt - global tests - media key system access", () => { { type: "baz", getLicense: neverCalledFn }, ]; contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error: any) => { - rej(error); + contentDecryptor.addEventListener("stateChange", (state: any) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } }); setTimeout(() => { expect(rmksHasBeenCalled).toEqual(true); @@ -821,9 +828,11 @@ describe("decrypt - global tests - media key system access", () => { const config = [{ type: "foo", getLicense: neverCalledFn }]; const contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", () => { - expect(rmksHasBeenCalled).toEqual(true); - res(); + contentDecryptor.addEventListener("stateChange", (state: any) => { + if (state.name === ContentDecryptorState.Error) { + expect(rmksHasBeenCalled).toEqual(true); + res(); + } }); setTimeout(() => { rej(new Error("timeout exceeded")); diff --git a/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts b/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts index d17b5c3c42..c55c7de646 100644 --- a/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts @@ -115,7 +115,7 @@ describe("decrypt - global tests - media key system access", () => { contentDecryptor.addEventListener("stateChange", (newState: any) => { receivedStateChange++; try { - expect(newState).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(newState.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); } catch (err) { rej(err); @@ -123,9 +123,10 @@ describe("decrypt - global tests - media key system access", () => { setTimeout(() => { try { expect(receivedStateChange).toEqual(1); - expect(contentDecryptor.getState()).toEqual( - ContentDecryptorState.WaitingForAttachment, - ); + expect(contentDecryptor.getState()).toEqual({ + name: ContentDecryptorState.WaitingForAttachment, + payload: null, + }); contentDecryptor.dispose(); } catch (err) { rej(err); @@ -148,17 +149,19 @@ describe("decrypt - global tests - media key system access", () => { return new Promise((res, rej) => { const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange1 = 0; - contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1: any) => { receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } try { if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange1 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); contentDecryptor1.attach(); } catch (err) { @@ -169,17 +172,19 @@ describe("decrypt - global tests - media key system access", () => { contentDecryptor1.dispose(); const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange2 = 0; - contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2: any) => { receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { if (receivedStateChange2 === 2) { - expect(state2).toEqual(ContentDecryptorState.ReadyForContent); + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange2 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); contentDecryptor2.attach(); setTimeout(() => { @@ -214,17 +219,19 @@ describe("decrypt - global tests - media key system access", () => { return new Promise((res, rej) => { const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange1 = 0; - contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1: any) => { receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } try { if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange1 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); contentDecryptor1.attach(); } catch (err) { @@ -235,17 +242,19 @@ describe("decrypt - global tests - media key system access", () => { contentDecryptor1.dispose(); const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange2 = 0; - contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2: any) => { receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { if (receivedStateChange2 === 2) { - expect(state2).toEqual(ContentDecryptorState.ReadyForContent); + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange2 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); contentDecryptor2.attach(); setTimeout(() => { @@ -281,17 +290,19 @@ describe("decrypt - global tests - media key system access", () => { return new Promise((res, rej) => { const contentDecryptor1 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange1 = 0; - contentDecryptor1.addEventListener("error", rej); contentDecryptor1.addEventListener("stateChange", (state1: any) => { receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } try { if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange1 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state1).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); contentDecryptor1.attach(); } catch (err) { @@ -302,17 +313,19 @@ describe("decrypt - global tests - media key system access", () => { contentDecryptor1.dispose(); const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); let receivedStateChange2 = 0; - contentDecryptor2.addEventListener("error", rej); contentDecryptor2.addEventListener("stateChange", (state2: any) => { receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { if (receivedStateChange2 === 2) { - expect(state2).toEqual(ContentDecryptorState.ReadyForContent); + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); return; } else if (receivedStateChange2 !== 1) { throw new Error("Unexpected stateChange event."); } - expect(state2).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); contentDecryptor2.attach(); setTimeout(() => { @@ -346,7 +359,7 @@ describe("decrypt - global tests - media key system access", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); return new Promise((res) => { contentDecryptor.addEventListener("stateChange", (newState: any) => { - if (newState === ContentDecryptorState.WaitingForAttachment) { + if (newState.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); setTimeout(() => { diff --git a/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts b/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts index 42fd9f01ab..424e9aac83 100644 --- a/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts @@ -59,7 +59,7 @@ describe("decrypt - global tests - server certificate", () => { return new Promise((res) => { contentDecryptor.addEventListener("stateChange", (state: any) => { - if (state === ContentDecryptorState.WaitingForAttachment) { + if (state.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); setTimeout(() => { expect(mockSetMediaKeys).not.toHaveBeenCalled(); @@ -101,7 +101,7 @@ describe("decrypt - global tests - server certificate", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state: any) => { - if (state === ContentDecryptorState.WaitingForAttachment) { + if (state.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); setTimeout(() => { expect(mockSetMediaKeys).not.toHaveBeenCalled(); @@ -142,7 +142,7 @@ describe("decrypt - global tests - server certificate", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state: any) => { - if (state === ContentDecryptorState.WaitingForAttachment) { + if (state.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); } @@ -181,7 +181,7 @@ describe("decrypt - global tests - server certificate", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state: any) => { - if (state === ContentDecryptorState.WaitingForAttachment) { + if (state.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); } @@ -231,7 +231,7 @@ describe("decrypt - global tests - server certificate", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); contentDecryptor.addEventListener("stateChange", (state: any) => { - if (state === ContentDecryptorState.WaitingForAttachment) { + if (state.name === ContentDecryptorState.WaitingForAttachment) { contentDecryptor.removeEventListener("stateChange"); setTimeout(() => { expect(mockSetMediaKeys).not.toHaveBeenCalled(); diff --git a/src/main_thread/decrypt/__tests__/__global__/utils.ts b/src/main_thread/decrypt/__tests__/__global__/utils.ts index 3eea8f2667..8977ff1a96 100644 --- a/src/main_thread/decrypt/__tests__/__global__/utils.ts +++ b/src/main_thread/decrypt/__tests__/__global__/utils.ts @@ -19,6 +19,7 @@ import EventEmitter from "../../../../utils/event_emitter"; import flatMap from "../../../../utils/flat_map"; import { strToUtf8, utf8ToStr } from "../../../../utils/string_parsing"; import type { CancellationSignal } from "../../../../utils/task_canceller"; +import { ContentDecryptorState } from "../../types"; /** Default MediaKeySystemAccess configuration used by the RxPlayer. */ export const defaultKSConfig = [ @@ -485,8 +486,10 @@ export function testContentDecryptorError( ): Promise { return new Promise((res, rej) => { const contentDecryptor = new ContentDecryptor(mediaElement, keySystemsConfigs); - contentDecryptor.addEventListener("error", (error: any) => { - res(error); + contentDecryptor.addEventListener("stateChange", (state: any) => { + if (state.name === ContentDecryptorState.Error) { + res(state.payload); + } }); setTimeout(() => { rej(new Error("Timeout exceeded")); diff --git a/src/main_thread/decrypt/content_decryptor.ts b/src/main_thread/decrypt/content_decryptor.ts index 8da27ae06d..c0caf5901c 100644 --- a/src/main_thread/decrypt/content_decryptor.ts +++ b/src/main_thread/decrypt/content_decryptor.ts @@ -45,6 +45,12 @@ import type { IMediaKeySessionStores, IProcessedProtectionData, IContentDecryptorEvent, + IContentDecryptorStateData, + IInitializingContentDecryptorState, + IWaitingForAttachmentContentDecryptorState, + IReadyForContentContentDecryptorState, + IDisposedContentDecryptorState, + IErrorContentDecryptorState, } from "./types"; import { MediaKeySessionLoadingType, ContentDecryptorState } from "./types"; import { DecommissionedSessionError } from "./utils/check_key_statuses"; @@ -86,12 +92,6 @@ export default class ContentDecryptor extends EventEmitter { - const { options, mediaKeySystemAccess } = mediaKeysInfo; + const { mediaKeySystemAccess } = mediaKeysInfo; this._supportedCodecWhenEncrypted = mediaKeysInfo.codecSupport; /** * String identifying the key system, allowing the rest of the code to * only advertise the required initialization data for license requests. - * - * Note that we only set this value if retro-compatibility to older - * persistent logic in the RxPlayer is not important, as the - * optimizations this property unlocks can break the loading of - * MediaKeySessions persisted in older RxPlayer's versions. */ - let systemId: string | undefined; - if ( - isNullOrUndefined(options.persistentLicenseConfig) || - options.persistentLicenseConfig.disableRetroCompatibility === true - ) { - systemId = getDrmSystemId(mediaKeySystemAccess.keySystem); - } - + const systemId = getDrmSystemId(mediaKeySystemAccess.keySystem); this.systemId = systemId; - if (this._stateData.state === ContentDecryptorState.Initializing) { + if (this._stateData.state.name === ContentDecryptorState.Initializing) { this._stateData = { - state: ContentDecryptorState.WaitingForAttachment, + state: { + name: ContentDecryptorState.WaitingForAttachment, + payload: null, + }, isInitDataQueueLocked: true, isMediaKeysAttached: MediaKeyAttachmentStatus.NotAttached, data: { mediaKeysInfo, mediaElement }, @@ -229,7 +222,7 @@ export default class ContentDecryptor extends EventEmitter, period: IPeriodMetadata) { } /** Possible states the ContentDecryptor is in and associated data for each one. */ -type IContentDecryptorStateData = +type IContentDecryptorStateMetadata = | IInitializingStateData | IWaitingForAttachmentStateData | IReadyForContentStateDataUnattached @@ -1162,15 +1174,15 @@ type IContentDecryptorStateData = | IDisposeStateData | IErrorStateData; -/** Skeleton that all variants of `IContentDecryptorStateData` use. */ +/** Skeleton that all variants of `IContentDecryptorStateMetadata` use. */ interface IContentDecryptorStateBase< - TStateName extends ContentDecryptorState, + TStateData extends IContentDecryptorStateData, TIsQueueLocked extends boolean | undefined, TIsMediaKeyAttached extends MediaKeyAttachmentStatus | undefined, TData, > { /** Identify the ContentDecryptor's state. */ - state: TStateName; + state: TStateData; /** * If `true`, the `ContentDecryptor` will wait before processing * newly-received initialization data. @@ -1198,7 +1210,7 @@ const enum MediaKeyAttachmentStatus { /** ContentDecryptor's internal data when in the `Initializing` state. */ type IInitializingStateData = IContentDecryptorStateBase< - ContentDecryptorState.Initializing, + IInitializingContentDecryptorState, true, // isInitDataQueueLocked MediaKeyAttachmentStatus.NotAttached, // isMediaKeysAttached null // data @@ -1206,7 +1218,7 @@ type IInitializingStateData = IContentDecryptorStateBase< /** ContentDecryptor's internal data when in the `WaitingForAttachment` state. */ type IWaitingForAttachmentStateData = IContentDecryptorStateBase< - ContentDecryptorState.WaitingForAttachment, + IWaitingForAttachmentContentDecryptorState, true, // isInitDataQueueLocked MediaKeyAttachmentStatus.NotAttached, // isMediaKeysAttached // data @@ -1218,7 +1230,7 @@ type IWaitingForAttachmentStateData = IContentDecryptorStateBase< * it has attached the `MediaKeys` to the media element. */ type IReadyForContentStateDataUnattached = IContentDecryptorStateBase< - ContentDecryptorState.ReadyForContent, + IReadyForContentContentDecryptorState, true, // isInitDataQueueLocked MediaKeyAttachmentStatus.NotAttached | MediaKeyAttachmentStatus.Pending, // isMediaKeysAttached { mediaKeysInfo: IMediaKeysInfos; mediaElement: IMediaElement } // data @@ -1229,7 +1241,7 @@ type IReadyForContentStateDataUnattached = IContentDecryptorStateBase< * it has attached the `MediaKeys` to the media element. */ type IReadyForContentStateDataAttached = IContentDecryptorStateBase< - ContentDecryptorState.ReadyForContent, + IReadyForContentContentDecryptorState, boolean, // isInitDataQueueLocked MediaKeyAttachmentStatus.Attached, // isMediaKeysAttached { @@ -1246,7 +1258,7 @@ type IReadyForContentStateDataAttached = IContentDecryptorStateBase< /** ContentDecryptor's internal data when in the `Disposed` state. */ type IDisposeStateData = IContentDecryptorStateBase< - ContentDecryptorState.Disposed, + IDisposedContentDecryptorState, undefined, // isInitDataQueueLocked undefined, // isMediaKeysAttached null // data @@ -1254,7 +1266,7 @@ type IDisposeStateData = IContentDecryptorStateBase< /** ContentDecryptor's internal data when in the `Error` state. */ type IErrorStateData = IContentDecryptorStateBase< - ContentDecryptorState.Error, + IErrorContentDecryptorState, undefined, // isInitDataQueueLocked undefined, // isMediaKeysAttached null // data diff --git a/src/main_thread/decrypt/types.ts b/src/main_thread/decrypt/types.ts index 04e42868a4..fe2888b1b4 100644 --- a/src/main_thread/decrypt/types.ts +++ b/src/main_thread/decrypt/types.ts @@ -27,14 +27,6 @@ import type PersistentSessionsStore from "./utils/persistent_sessions_store"; /** Events sent by the `ContentDecryptor`, in a `{ event: payload }` format. */ export interface IContentDecryptorEvent { - /** - * Event emitted when a major error occured which made the ContentDecryptor - * stopped. - * When that event is sent, the `ContentDecryptor` is in the `Error` state and - * cannot be used anymore. - */ - error: Error; - /** * Event emitted when a minor error occured which the ContentDecryptor can * recover from. @@ -46,7 +38,7 @@ export interface IContentDecryptorEvent { * States are a central aspect of the `ContentDecryptor`, be sure to check the * ContentDecryptorState type. */ - stateChange: ContentDecryptorState; + stateChange: IContentDecryptorStateData; blackListProtectionData: IProcessedProtectionData; @@ -57,6 +49,64 @@ export interface IContentDecryptorEvent { }; } +/** Describes the current "state" the `ContentDecryptor` is in. */ +export type IContentDecryptorStateData = + | IInitializingContentDecryptorState + | IWaitingForAttachmentContentDecryptorState + | IReadyForContentContentDecryptorState + | IErrorContentDecryptorState + | IDisposedContentDecryptorState; + +/** Object sent when the `ContentDecryptorState` switches to `Initializing`. */ +export interface IInitializingContentDecryptorState { + name: ContentDecryptorState.Initializing; + payload: null; +} + +/** Object sent when the `ContentDecryptorState` switches to `WaitingForAttachment`. */ +export interface IWaitingForAttachmentContentDecryptorState { + name: ContentDecryptorState.WaitingForAttachment; + payload: null; +} + +/** Object sent when the `ContentDecryptorState` switches to `ReadyForContent`. */ +export interface IReadyForContentContentDecryptorState { + name: ContentDecryptorState.ReadyForContent; + payload: { + /** + * Hexa characters identifying the "system id" of the content protection + * technology. + * `undefined` if unknown. + */ + systemId: string | undefined; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; + /** + * If `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; + }; +} + +/** Object sent when the `ContentDecryptorState` switches to `Error`. */ +export interface IErrorContentDecryptorState { + name: ContentDecryptorState.Error; + payload: Error; +} + +/** Object sent when the `ContentDecryptorState` switches to `Disposed`. */ +export interface IDisposedContentDecryptorState { + name: ContentDecryptorState.Disposed; + payload: null; +} + /** Enumeration of the various "state" the `ContentDecryptor` can be in. */ export enum ContentDecryptorState { /** @@ -65,7 +115,6 @@ export enum ContentDecryptorState { * This is is the initial state of the ContentDecryptor. */ Initializing, - /** * The `ContentDecryptor` has been initialized. * You should now called the `attach` method when you want to add decryption diff --git a/src/main_thread/init/directfile_content_initializer.ts b/src/main_thread/init/directfile_content_initializer.ts index 94501cd309..2b82017140 100644 --- a/src/main_thread/init/directfile_content_initializer.ts +++ b/src/main_thread/init/directfile_content_initializer.ts @@ -137,7 +137,7 @@ export default class DirectFileContentInitializer extends ContentInitializer { drmInitRef.onUpdate( (evt, stopListeningToDrmUpdates) => { - if (evt.initializationState.type === "uninitialized") { + if (evt.type === "uninitialized") { return; // nothing done yet } stopListeningToDrmUpdates(); @@ -150,11 +150,11 @@ export default class DirectFileContentInitializer extends ContentInitializer { clearElementSrc(mediaElement); }); - if (evt.initializationState.type === "awaiting-media-link") { - evt.initializationState.value.isMediaLinked.setValue(true); + if (evt.type === "awaiting-media-link") { + evt.value.isMediaLinked.setValue(true); drmInitRef.onUpdate( (newDrmStatus, stopListeningToDrmUpdatesAgain) => { - if (newDrmStatus.initializationState.type === "initialized") { + if (newDrmStatus.type === "initialized") { stopListeningToDrmUpdatesAgain(); this._seekAndPlay(mediaElement, playbackObserver); } @@ -162,7 +162,7 @@ export default class DirectFileContentInitializer extends ContentInitializer { { emitCurrentValue: true, clearSignal: cancelSignal }, ); } else { - assert(evt.initializationState.type === "initialized"); + assert(evt.type === "initialized"); this._seekAndPlay(mediaElement, playbackObserver); } }, diff --git a/src/main_thread/init/media_source_content_initializer.ts b/src/main_thread/init/media_source_content_initializer.ts index 2b9df958dc..e9d64e1378 100644 --- a/src/main_thread/init/media_source_content_initializer.ts +++ b/src/main_thread/init/media_source_content_initializer.ts @@ -210,7 +210,11 @@ export default class MediaSourceContentInitializer extends ContentInitializer { mediaElement, initResult.mediaSource, playbackObserver, - initResult.drmSystemId, + { + drmSystemId: initResult.drmSystemId, + canFilterProtectionData: initResult.canFilterProtectionData, + failOnEncryptedAfterClear: initResult.failOnEncryptedAfterClear, + }, initResult.unlinkMediaSource, ), ) @@ -245,6 +249,8 @@ export default class MediaSourceContentInitializer extends ContentInitializer { private _initializeMediaSourceAndDecryption(mediaElement: IMediaElement): Promise<{ mediaSource: MainMediaSourceInterface; drmSystemId: string | undefined; + canFilterProtectionData: boolean; + failOnEncryptedAfterClear: boolean; unlinkMediaSource: TaskCanceller; }> { const initCanceller = this._initCanceller; @@ -319,7 +325,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { drmInitRef.onUpdate( (drmStatus, stopListeningToDrmUpdates) => { - if (drmStatus.initializationState.type === "uninitialized") { + if (drmStatus.type === "uninitialized") { return; } stopListeningToDrmUpdates(); @@ -329,15 +335,19 @@ export default class MediaSourceContentInitializer extends ContentInitializer { createMediaSource(mediaElement, mediaSourceCanceller.signal) .then((mediaSource) => { const lastDrmStatus = drmInitRef.getValue(); - if (lastDrmStatus.initializationState.type === "awaiting-media-link") { - lastDrmStatus.initializationState.value.isMediaLinked.setValue(true); + if (lastDrmStatus.type === "awaiting-media-link") { + lastDrmStatus.value.isMediaLinked.setValue(true); drmInitRef.onUpdate( (newDrmStatus, stopListeningToDrmUpdatesAgain) => { - if (newDrmStatus.initializationState.type === "initialized") { + if (newDrmStatus.type === "initialized") { stopListeningToDrmUpdatesAgain(); resolve({ mediaSource, - drmSystemId: newDrmStatus.drmSystemId, + drmSystemId: newDrmStatus.value.drmSystemId, + canFilterProtectionData: + newDrmStatus.value.canFilterProtectionData, + failOnEncryptedAfterClear: + newDrmStatus.value.failOnEncryptedAfterClear, unlinkMediaSource: mediaSourceCanceller, }); return; @@ -345,10 +355,12 @@ export default class MediaSourceContentInitializer extends ContentInitializer { }, { emitCurrentValue: true, clearSignal: initCanceller.signal }, ); - } else if (drmStatus.initializationState.type === "initialized") { + } else if (drmStatus.type === "initialized") { resolve({ mediaSource, - drmSystemId: drmStatus.drmSystemId, + drmSystemId: drmStatus.value.drmSystemId, + canFilterProtectionData: drmStatus.value.canFilterProtectionData, + failOnEncryptedAfterClear: drmStatus.value.failOnEncryptedAfterClear, unlinkMediaSource: mediaSourceCanceller, }); return; @@ -370,7 +382,11 @@ export default class MediaSourceContentInitializer extends ContentInitializer { mediaElement: IMediaElement, initialMediaSource: MainMediaSourceInterface, playbackObserver: IMediaElementPlaybackObserver, - drmSystemId: string | undefined, + decryptionParameters: { + drmSystemId: string | undefined; + canFilterProtectionData: boolean; + failOnEncryptedAfterClear: boolean; + }, initialMediaSourceCanceller: TaskCanceller, ): Promise { const { @@ -425,7 +441,12 @@ export default class MediaSourceContentInitializer extends ContentInitializer { /** Choose the right "Representation" for a given "Adaptation". */ const representationEstimator = AdaptiveRepresentationSelector(adaptiveOptions); const subBufferOptions = objectAssign( - { textTrackOptions, drmSystemId }, + { + textTrackOptions, + drmSystemId: decryptionParameters.drmSystemId, + canFilterProtectionData: decryptionParameters.canFilterProtectionData, + failOnEncryptedAfterClear: decryptionParameters.failOnEncryptedAfterClear, + }, bufferOptions, ); @@ -1083,7 +1104,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { supportedIfEncrypted = true; } else { const contentDecryptor = this._decryptionCapabilities.value; - if (contentDecryptor.getState() !== ContentDecryptorState.Initializing) { + if (contentDecryptor.getState().name !== ContentDecryptorState.Initializing) { // No information is available regarding the support status. // Defaulting to assume the codec is supported. supportedIfEncrypted = diff --git a/src/main_thread/init/multi_thread_content_initializer.ts b/src/main_thread/init/multi_thread_content_initializer.ts index b45fa44faa..86d6ff71b3 100644 --- a/src/main_thread/init/multi_thread_content_initializer.ts +++ b/src/main_thread/init/multi_thread_content_initializer.ts @@ -51,7 +51,7 @@ import SharedReference from "../../utils/reference"; import { RequestError } from "../../utils/request"; import type { CancellationSignal } from "../../utils/task_canceller"; import TaskCanceller, { CancellationError } from "../../utils/task_canceller"; -import type { IContentProtection } from "../decrypt"; +import type { IContentDecryptorStateData, IContentProtection } from "../decrypt"; import type IContentDecryptor from "../decrypt"; import { ContentDecryptorState, getKeySystemConfiguration } from "../decrypt"; import type { ITextDisplayer } from "../text_displayer"; @@ -308,7 +308,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { ); drmInitializationStatus.onUpdate( (initializationStatus, stopListeningDrm) => { - if (initializationStatus.initializationState.type === "initialized") { + if (initializationStatus.type === "initialized") { stopListeningDrm(); this._startPlaybackIfReady(playbackStartParams); } @@ -1130,7 +1130,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { reloadMediaSource: () => void, cancelSignal: CancellationSignal, ): { - statusRef: IReadOnlySharedReference; + statusRef: IReadOnlySharedReference; contentDecryptor: IContentDecryptor | null; } { const { keySystems } = this._settings; @@ -1151,12 +1151,12 @@ export default class MultiThreadContentInitializer extends ContentInitializer { { clearSignal: cancelSignal }, ); const ref = new SharedReference({ - initializationState: { - type: "initialized" as const, - value: null, + type: "initialized" as const, + value: { + drmSystemId: undefined, + canFilterProtectionData: true, + failOnEncryptedAfterClear: false, }, - contentDecryptor: null, - drmSystemId: undefined, }); ref.finish(); // We know that no new value will be triggered return { statusRef: ref, contentDecryptor: null }; @@ -1174,16 +1174,13 @@ export default class MultiThreadContentInitializer extends ContentInitializer { } log.debug("MTCI: Creating ContentDecryptor"); const contentDecryptor = new ContentDecryptor(mediaElement, keySystems); - const drmStatusRef = new SharedReference( - { - initializationState: { type: "uninitialized", value: null }, - drmSystemId: undefined, - }, + const drmStatusRef = new SharedReference( + { type: "uninitialized", value: null }, cancelSignal, ); - const updateCodecSupportOnStateChange = (state: ContentDecryptorState) => { - if (state > ContentDecryptorState.Initializing) { + const updateCodecSupportOnStateChange = (state: IContentDecryptorStateData) => { + if (state.name > ContentDecryptorState.Initializing) { const manifest = this._currentContentInfo?.manifest; if (isNullOrUndefined(manifest)) { return; @@ -1254,31 +1251,38 @@ export default class MultiThreadContentInitializer extends ContentInitializer { this.trigger("decipherabilityUpdate", manUpdates); }); contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - mediaSourceStatus.onUpdate( - (currStatus, stopListening) => { - if (currStatus === MediaSourceInitializationStatus.Nothing) { - mediaSourceStatus.setValue(MediaSourceInitializationStatus.AttachNow); - } else if (currStatus === MediaSourceInitializationStatus.Attached) { - stopListening(); - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.attach(); + switch (state.name) { + case ContentDecryptorState.Error: + this._onFatalError(state.payload); + break; + + case ContentDecryptorState.WaitingForAttachment: + mediaSourceStatus.onUpdate( + (currStatus, stopListening) => { + if (currStatus === MediaSourceInitializationStatus.Nothing) { + mediaSourceStatus.setValue(MediaSourceInitializationStatus.AttachNow); + } else if (currStatus === MediaSourceInitializationStatus.Attached) { + stopListening(); + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.attach(); + } } - } - }, - { clearSignal: cancelSignal, emitCurrentValue: true }, - ); - } else if (state === ContentDecryptorState.ReadyForContent) { - drmStatusRef.setValue({ - initializationState: { type: "initialized", value: null }, - drmSystemId: contentDecryptor.systemId, - }); - contentDecryptor.removeEventListener("stateChange"); - } - }); + }, + { clearSignal: cancelSignal, emitCurrentValue: true }, + ); + break; - contentDecryptor.addEventListener("error", (error) => { - this._onFatalError(error); + case ContentDecryptorState.ReadyForContent: + drmStatusRef.setValue({ + type: "initialized", + value: { + drmSystemId: state.payload.systemId, + canFilterProtectionData: state.payload.canFilterProtectionData, + failOnEncryptedAfterClear: state.payload.failOnEncryptedAfterClear, + }, + }); + break; + } }); contentDecryptor.addEventListener("warning", (error) => { @@ -1593,16 +1597,17 @@ export default class MultiThreadContentInitializer extends ContentInitializer { mediaElement: IMediaElement; textDisplayer: ITextDisplayer | null; playbackObserver: IMediaElementPlaybackObserver; - drmInitializationStatus: IReadOnlySharedReference; + drmInitializationStatus: IReadOnlySharedReference; mediaSourceStatus: IReadOnlySharedReference; }): boolean { if (this._currentContentInfo === null || this._currentContentInfo.manifest === null) { return false; } const drmInitStatus = parameters.drmInitializationStatus.getValue(); - if (drmInitStatus.initializationState.type !== "initialized") { + if (drmInitStatus.type !== "initialized") { return false; } + const drmPayload = drmInitStatus.value; const msInitStatus = parameters.mediaSourceStatus.getValue(); if (msInitStatus !== MediaSourceInitializationStatus.Attached) { return false; @@ -1641,7 +1646,9 @@ export default class MultiThreadContentInitializer extends ContentInitializer { value: { initialTime, initialObservation: sentInitialObservation, - drmSystemId: drmInitStatus.drmSystemId, + drmSystemId: drmPayload.drmSystemId, + canFilterProtectionData: drmPayload.canFilterProtectionData, + failOnEncryptedAfterClear: drmPayload.failOnEncryptedAfterClear, enableFastSwitching, onCodecSwitch, }, @@ -1992,17 +1999,6 @@ const enum MediaSourceInitializationStatus { Attached, } -interface IDrmInitializationStatus { - /** Current initialization state the decryption logic is in. */ - initializationState: IDecryptionInitializationState; - /** - * If set, corresponds to the hex string describing the current key system - * used. - * `undefined` if unknown or if it does not apply. - */ - drmSystemId: string | undefined; -} - /** Initialization steps to add decryption capabilities to an `HTMLMediaElement`. */ type IDecryptionInitializationState = /** @@ -2035,7 +2031,27 @@ type IDecryptionInitializationState = */ | { type: "initialized"; - value: null; + value: { + /** + * If set, corresponds to the hex string describing the current key system + * used. + * `undefined` if unknown. + */ + drmSystemId: string | undefined; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; + /** + * if `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; + }; }; function formatSourceBufferError(error: unknown): SourceBufferError { diff --git a/src/main_thread/init/utils/initialize_content_decryption.ts b/src/main_thread/init/utils/initialize_content_decryption.ts index a4659946c0..a2200df43b 100644 --- a/src/main_thread/init/utils/initialize_content_decryption.ts +++ b/src/main_thread/init/utils/initialize_content_decryption.ts @@ -9,7 +9,7 @@ import TaskCanceller from "../../../utils/task_canceller"; import type { CancellationSignal } from "../../../utils/task_canceller"; import { ContentDecryptorState } from "../../decrypt"; import type IContentDecryptor from "../../decrypt"; -import type { IProcessedProtectionData } from "../../decrypt"; +import type { IContentDecryptorStateData, IProcessedProtectionData } from "../../decrypt"; /** * Initialize content decryption capabilities on the given `HTMLMediaElement`. @@ -48,7 +48,7 @@ export default function initializeContentDecryption( }, cancelSignal: CancellationSignal, ): { - statusRef: IReadOnlySharedReference; + statusRef: IReadOnlySharedReference; contentDecryptor: | { enabled: true; @@ -67,10 +67,10 @@ export default function initializeContentDecryption( const decryptorCanceller = new TaskCanceller(); decryptorCanceller.linkToSignal(cancelSignal); - const drmStatusRef = new SharedReference( + const drmStatusRef = new SharedReference( { - initializationState: { type: "uninitialized", value: null }, - drmSystemId: undefined, + type: "uninitialized", + value: null, }, cancelSignal, ); @@ -84,8 +84,8 @@ export default function initializeContentDecryption( log.debug("Init: Creating ContentDecryptor"); const contentDecryptor = new ContentDecryptor(mediaElement, keySystems); - const onStateChange = (state: ContentDecryptorState) => { - if (state > ContentDecryptorState.Initializing) { + const onStateChange = (stateData: IContentDecryptorStateData) => { + if (stateData.name > ContentDecryptorState.Initializing) { callbacks.onCodecSupportUpdate?.(); contentDecryptor.removeEventListener("stateChange", onStateChange); } @@ -93,40 +93,45 @@ export default function initializeContentDecryption( contentDecryptor.addEventListener("stateChange", onStateChange); contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - const isMediaLinked = new SharedReference(false); - isMediaLinked.onUpdate( - (isAttached, stopListening) => { - if (isAttached) { - stopListening(); - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.attach(); + switch (state.name) { + case ContentDecryptorState.Error: + decryptorCanceller.cancel(); + callbacks.onError(state.payload); + break; + + case ContentDecryptorState.WaitingForAttachment: + const isMediaLinked = new SharedReference(false); + isMediaLinked.onUpdate( + (isAttached, stopListening) => { + if (isAttached) { + stopListening(); + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.attach(); + } } - } - }, - { clearSignal: decryptorCanceller.signal }, - ); - drmStatusRef.setValue({ - initializationState: { + }, + { clearSignal: decryptorCanceller.signal }, + ); + drmStatusRef.setValue({ type: "awaiting-media-link", value: { isMediaLinked }, - }, - drmSystemId: contentDecryptor.systemId, - }); - } else if (state === ContentDecryptorState.ReadyForContent) { - drmStatusRef.setValue({ - initializationState: { type: "initialized", value: null }, - drmSystemId: contentDecryptor.systemId, - }); - contentDecryptor.removeEventListener("stateChange"); + }); + break; + + case ContentDecryptorState.ReadyForContent: + drmStatusRef.setValue({ + type: "initialized", + value: { + drmSystemId: state.payload.systemId, + canFilterProtectionData: state.payload.canFilterProtectionData, + failOnEncryptedAfterClear: state.payload.failOnEncryptedAfterClear, + }, + }); + drmStatusRef.finish(); + break; } }); - contentDecryptor.addEventListener("error", (error) => { - decryptorCanceller.cancel(); - callbacks.onError(error); - }); - contentDecryptor.addEventListener("warning", (error) => { callbacks.onWarning(error); }); @@ -149,7 +154,7 @@ export default function initializeContentDecryption( }; function createEmeDisabledReference(errMsg: string): { - statusRef: IReadOnlySharedReference; + statusRef: IReadOnlySharedReference; contentDecryptor: { enabled: false; value: EncryptedMediaError; @@ -157,26 +162,18 @@ export default function initializeContentDecryption( } { const err = new EncryptedMediaError("MEDIA_IS_ENCRYPTED_ERROR", errMsg); const ref = new SharedReference({ - initializationState: { type: "initialized" as const, value: null }, - drmSystemId: undefined, + type: "initialized" as const, + value: { + drmSystemId: undefined, + canFilterProtectionData: true, + failOnEncryptedAfterClear: false, + }, }); ref.finish(); // We know that no new value will be triggered return { statusRef: ref, contentDecryptor: { enabled: false, value: err } }; } } -/** Status of content decryption initialization. */ -interface IDrmInitializationStatus { - /** Current initialization state the decryption logic is in. */ - initializationState: IDecryptionInitializationState; - /** - * If set, corresponds to the hex string describing the current key system - * used. - * `undefined` if unknown or if it does not apply. - */ - drmSystemId: string | undefined; -} - /** Initialization steps to add decryption capabilities to an `HTMLMediaElement`. */ type IDecryptionInitializationState = /** @@ -205,4 +202,27 @@ type IDecryptionInitializationState = * The `MediaSource` or media url can be linked AND segments can be pushed to * the `HTMLMediaElement` on which decryption capabilities were wanted. */ - | { type: "initialized"; value: null }; + | { + type: "initialized"; + value: { + /** + * If set, corresponds to the hex string describing the current key system + * used. + * `undefined` if unknown. + */ + drmSystemId: string | undefined; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; + /** + * if `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; + }; + }; diff --git a/src/main_thread/init/utils/update_manifest_codec_support.ts b/src/main_thread/init/utils/update_manifest_codec_support.ts index 61049207f1..3ac862830f 100644 --- a/src/main_thread/init/utils/update_manifest_codec_support.ts +++ b/src/main_thread/init/utils/update_manifest_codec_support.ts @@ -100,7 +100,7 @@ export function updateManifestCodecSupport( // encrypted codec is supported isSupportedEncrypted: true, }; - } else if (contentDecryptor.getState() === ContentDecryptorState.Initializing) { + } else if (contentDecryptor.getState().name === ContentDecryptorState.Initializing) { newData = { isSupportedClear: true, isSupportedEncrypted: undefined, diff --git a/src/multithread_types.ts b/src/multithread_types.ts index a9460942cd..3c73d8e60f 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -218,6 +218,19 @@ export interface IStartPreparedContentMessageValue { * optimizations. */ drmSystemId: string | undefined; + /** + * If `true`, protection data as found in the content can be manipulated so + * e.g. only the data linked to the given systemId may be communicated. + * + * If `false` the full extent of the protection data, in exactly the way it + * has been found in the content, should be communicated. + */ + canFilterProtectionData: boolean; + /** + * If `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear: boolean; /** * Enable/Disable fastSwitching: allow to replace lower-quality segments by * higher-quality ones to have a faster transition. diff --git a/src/public_types.ts b/src/public_types.ts index 33d7ef7ea3..ffba531406 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -582,6 +582,12 @@ export interface IKeySystemOption { */ disableMediaKeysAttachmentLock?: boolean; + /** + * If `true`, the current device is known to not be able to begin playback of + * encrypted content if there's already clear content playing. + */ + failOnEncryptedAfterClear?: boolean | undefined; + /** * Behavior the RxPlayer should have when one of the key has the * `MediaKeyStatus` `"internal-error"`.