diff --git a/src/core/main/worker/worker_main.ts b/src/core/main/worker/worker_main.ts index d51327b6ca..e1d0eec5e7 100644 --- a/src/core/main/worker/worker_main.ts +++ b/src/core/main/worker/worker_main.ts @@ -471,6 +471,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. @@ -521,7 +534,14 @@ function loadOrReloadPreparedContent( segmentSinksStore, segmentQueueCreator, } = preparedContent; - const { drmSystemId, enableFastSwitching, initialTime, onCodecSwitch } = val; + const { + canFilterProtectionData, + failOnEncryptedAfterClear, + drmSystemId, + enableFastSwitching, + initialTime, + onCodecSwitch, + } = val; playbackObservationRef.onUpdate((observation) => { if (preparedContent.decipherabilityFreezeDetector.needToReload(observation)) { handleMediaSourceReload({ @@ -600,6 +620,8 @@ function loadOrReloadPreparedContent( maxVideoBufferSize, maxBufferAhead, maxBufferBehind, + canFilterProtectionData, + failOnEncryptedAfterClear, drmSystemId, enableFastSwitching, onCodecSwitch, @@ -883,6 +905,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 f680885e1b..b99ca21492 100644 --- a/src/core/stream/adaptation/adaptation_stream.ts +++ b/src/core/stream/adaptation/adaptation_stream.ts @@ -464,6 +464,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 862466a416..08366bb5a7 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 23b76bd747..14aae918b1 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 4fd7aab17a..030c1fdb6a 100644 --- a/src/core/stream/representation/representation_stream.ts +++ b/src/core/stream/representation/representation_stream.ts @@ -96,7 +96,13 @@ export default function RepresentationStream( content.representation.bitrate, ); 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` */ @@ -141,7 +147,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 4c37c5cbfc..4378779813 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 216e74ae1b..e9edb633ff 100644 --- a/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts @@ -2,7 +2,10 @@ import { describe, afterEach, it, expect, vi } from "vitest"; import type { IKeySystemOption, IPlayerError } from "../../../../public_types"; import { concat } from "../../../../utils/byte_parsing"; import type IContentDecryptor from "../../content_decryptor"; -import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; +import type { + ContentDecryptorState as IContentDecryptorState, + IContentDecryptorStateData, +} from "../../types"; import { formatFakeChallengeFromInitData, MediaKeySessionImpl, @@ -355,7 +358,7 @@ async function checkGetLicense({ : undefined, }, ]; - function checkKeyLoadError(error: unknown) { + function checkKeyLoadError(error: Error) { try { expect(error).toBeInstanceOf(Error); expect((error as IPlayerError).name).toEqual("EncryptedMediaError"); @@ -370,37 +373,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: Error) => { - 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.toString())); } - } else { - rej(new Error(`Unexpected error: ${error.toString()}`)); - } - }); + }, + ); 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 027b4e87ba..61a7c4a139 100644 --- a/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/init_data.test.ts @@ -1,6 +1,9 @@ import { describe, afterEach, it, expect, vi } from "vitest"; import type IContentDecryptor from "../../content_decryptor"; -import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; +import type { + ContentDecryptorState as IContentDecryptorState, + IContentDecryptorStateData, +} from "../../types"; import { formatFakeChallengeFromInitData, MediaKeySessionImpl, @@ -45,9 +48,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -101,9 +104,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -177,9 +180,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -281,9 +284,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -356,9 +359,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -416,9 +419,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -498,9 +501,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -598,9 +601,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); @@ -686,9 +689,9 @@ describe("decrypt - global tests - init data", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); contentDecryptor.addEventListener( "stateChange", - (newState: IContentDecryptorState) => { - if (newState !== ContentDecryptorState.WaitingForAttachment) { - rej(new Error(`Unexpected state: ${newState}`)); + (newState: IContentDecryptorStateData) => { + if (newState.name !== ContentDecryptorState.WaitingForAttachment) { + rej(new Error(`Unexpected state: ${newState.name}`)); } contentDecryptor.removeEventListener("stateChange"); contentDecryptor.attach(); 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 6821016fc4..cd735ac127 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 @@ -2,6 +2,8 @@ import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; import type { ICustomMediaKeySystemAccess } from "../../../../compat/eme"; import type { IKeySystemOption } from "../../../../public_types"; import type IContentDecryptor from "../../content_decryptor"; +import type { IContentDecryptorStateData } from "../../types"; +import { ContentDecryptorState } from "../../types"; import { defaultKSConfig, defaultPRRecommendationKSConfig, @@ -971,9 +973,14 @@ describe("decrypt - global tests - media key system access", () => { const mediaElement = document.createElement("video"); const contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error) => { - rej(error); - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } + }, + ); setTimeout(() => { expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(1); expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledWith( @@ -1007,9 +1014,14 @@ describe("decrypt - global tests - media key system access", () => { const mediaElement = document.createElement("video"); const contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error) => { - rej(error); - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } + }, + ); setTimeout(() => { expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(3); expect(mockRequestMediaKeySystemAccess).toHaveBeenNthCalledWith( @@ -1056,9 +1068,14 @@ describe("decrypt - global tests - media key system access", () => { { type: "baz", getLicense: neverCalledFn }, ]; contentDecryptor = new ContentDecryptor(mediaElement, config); - contentDecryptor.addEventListener("error", (error) => { - rej(error); - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.Error) { + rej(state.payload); + } + }, + ); setTimeout(() => { expect(rmksHasBeenCalled).toEqual(true); expect(mockRequestMediaKeySystemAccess).toHaveBeenCalledTimes(1); @@ -1088,10 +1105,15 @@ 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: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.Error) { + expect(rmksHasBeenCalled).toEqual(true); + res(); + } + }, + ); setTimeout(() => { rej(new Error("timeout exceeded")); }, 10); 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 e2c1f838e3..a3efb094f4 100644 --- a/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/media_keys.test.ts @@ -1,7 +1,10 @@ import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; import type { IKeySystemOption } from "../../../../public_types"; import type IContentDecryptor from "../../content_decryptor"; -import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; +import type { + ContentDecryptorState as IContentDecryptorState, + IContentDecryptorStateData, +} from "../../types"; import { MediaKeysImpl, MediaKeySystemAccessImpl, @@ -109,7 +112,7 @@ describe("decrypt - global tests - media key system access", () => { contentDecryptor.addEventListener("stateChange", (newState) => { receivedStateChange++; try { - expect(newState).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(newState.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); } catch (err) { rej(err); @@ -117,9 +120,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); @@ -143,55 +147,65 @@ 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) => { - receivedStateChange1++; - try { - if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); - return; - } else if (receivedStateChange1 !== 1) { - throw new Error("Unexpected stateChange event."); + contentDecryptor1.addEventListener( + "stateChange", + (state1: IContentDecryptorStateData) => { + receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } + try { + if (receivedStateChange1 === 2) { + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange1 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); + contentDecryptor1.attach(); + } catch (err) { + rej(err); } - 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) => { - 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(() => { + setTimeout(() => { + contentDecryptor1.dispose(); + const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + let receivedStateChange2 = 0; + contentDecryptor2.addEventListener( + "stateChange", + (state2: IContentDecryptorStateData) => { + receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { - contentDecryptor2.dispose(); + if (receivedStateChange2 === 2) { + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange2 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); - res(); + contentDecryptor2.attach(); + setTimeout(() => { + try { + contentDecryptor2.dispose(); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); + res(); + } catch (err) { + rej(err); + } + }); } catch (err) { rej(err); } - }); - } catch (err) { - rej(err); - } - }); - }, 10); - }); + }, + ); + }, 10); + }, + ); }); }); @@ -210,55 +224,65 @@ 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) => { - receivedStateChange1++; - try { - if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); - return; - } else if (receivedStateChange1 !== 1) { - throw new Error("Unexpected stateChange event."); + contentDecryptor1.addEventListener( + "stateChange", + (state1: IContentDecryptorStateData) => { + receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } + try { + if (receivedStateChange1 === 2) { + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange1 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); + contentDecryptor1.attach(); + } catch (err) { + rej(err); } - 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) => { - 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(() => { + setTimeout(() => { + contentDecryptor1.dispose(); + const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + let receivedStateChange2 = 0; + contentDecryptor2.addEventListener( + "stateChange", + (state2: IContentDecryptorStateData) => { + receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { - contentDecryptor2.dispose(); + if (receivedStateChange2 === 2) { + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange2 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); - res(); + contentDecryptor2.attach(); + setTimeout(() => { + try { + contentDecryptor2.dispose(); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); + res(); + } catch (err) { + rej(err); + } + }); } catch (err) { rej(err); } - }); - } catch (err) { - rej(err); - } - }); - }, 10); - }); + }, + ); + }, 10); + }, + ); }); }); @@ -278,55 +302,65 @@ 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) => { - receivedStateChange1++; - try { - if (receivedStateChange1 === 2) { - expect(state1).toEqual(ContentDecryptorState.ReadyForContent); - return; - } else if (receivedStateChange1 !== 1) { - throw new Error("Unexpected stateChange event."); + contentDecryptor1.addEventListener( + "stateChange", + (state1: IContentDecryptorStateData) => { + receivedStateChange1++; + if (state1.name === ContentDecryptorState.Error) { + rej(state1.payload); + } + try { + if (receivedStateChange1 === 2) { + expect(state1.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange1 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state1.name).toEqual(ContentDecryptorState.WaitingForAttachment); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(1); + contentDecryptor1.attach(); + } catch (err) { + rej(err); } - 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) => { - 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(() => { + setTimeout(() => { + contentDecryptor1.dispose(); + const contentDecryptor2 = new ContentDecryptor(videoElt, ksConfig); + let receivedStateChange2 = 0; + contentDecryptor2.addEventListener( + "stateChange", + (state2: IContentDecryptorStateData) => { + receivedStateChange2++; + if (state2.name === ContentDecryptorState.Error) { + rej(state2.payload); + } try { - contentDecryptor2.dispose(); + if (receivedStateChange2 === 2) { + expect(state2.name).toEqual(ContentDecryptorState.ReadyForContent); + return; + } else if (receivedStateChange2 !== 1) { + throw new Error("Unexpected stateChange event."); + } + expect(state2.name).toEqual(ContentDecryptorState.WaitingForAttachment); expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); - res(); + contentDecryptor2.attach(); + setTimeout(() => { + try { + contentDecryptor2.dispose(); + expect(mockCreateMediaKeys).toHaveBeenCalledTimes(2); + res(); + } catch (err) { + rej(err); + } + }); } catch (err) { rej(err); } - }); - } catch (err) { - rej(err); - } - }); - }, 10); - }); + }, + ); + }, 10); + }, + ); }); }); @@ -343,18 +377,24 @@ describe("decrypt - global tests - media key system access", () => { .default as typeof IContentDecryptor; const contentDecryptor = new ContentDecryptor(videoElt, ksConfig); return new Promise((res) => { - contentDecryptor.addEventListener("stateChange", (newState) => { - if (newState === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - contentDecryptor.attach(); - setTimeout(() => { - expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); - expect(mockSetMediaKeys).toHaveBeenCalledWith(videoElt, new MediaKeysImpl()); - expect(mockCreateSession).not.toHaveBeenCalled(); - res(); - }, 5); - } - }); + contentDecryptor.addEventListener( + "stateChange", + (newState: IContentDecryptorStateData) => { + if (newState.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + contentDecryptor.attach(); + setTimeout(() => { + expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); + expect(mockSetMediaKeys).toHaveBeenCalledWith( + videoElt, + new MediaKeysImpl(), + ); + expect(mockCreateSession).not.toHaveBeenCalled(); + res(); + }, 5); + } + }, + ); }); }); }); 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 62dd11e928..f698aea1d4 100644 --- a/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts +++ b/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts @@ -1,7 +1,10 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import type { IKeySystemOption } from "../../../../public_types"; import type IContentDecryptor from "../../content_decryptor"; -import type { ContentDecryptorState as IContentDecryptorState } from "../../types"; +import type { + ContentDecryptorState as IContentDecryptorState, + IContentDecryptorStateData, +} from "../../types"; import { MediaKeysImpl, MediaKeySystemAccessImpl, mockCompat } from "./utils"; describe("decrypt - global tests - server certificate", () => { @@ -40,7 +43,7 @@ describe("decrypt - global tests - server certificate", () => { const mockCreateSession = vi.spyOn(MediaKeysImpl.prototype, "createSession"); const mockSetServerCertificate = vi .spyOn(MediaKeysImpl.prototype, "setServerCertificate") - .mockImplementation((_serverCertificate: BufferSource) => { + .mockImplementation((_serverCertificate: BufferSource): Promise => { expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); expect(mockCreateSession).not.toHaveBeenCalled(); return Promise.resolve(true); @@ -53,24 +56,27 @@ describe("decrypt - global tests - server certificate", () => { const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); return new Promise((res) => { - contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - setTimeout(() => { - expect(mockSetMediaKeys).not.toHaveBeenCalled(); - expect(mockCreateSession).not.toHaveBeenCalled(); - expect(mockSetServerCertificate).not.toHaveBeenCalled(); - contentDecryptor.attach(); - }, 5); - setTimeout(() => { - contentDecryptor.dispose(); - expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); - expect(mockSetServerCertificate).toHaveBeenCalledTimes(1); - expect(mockCreateSession).not.toHaveBeenCalled(); - res(); - }, 10); - } - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + setTimeout(() => { + expect(mockSetMediaKeys).not.toHaveBeenCalled(); + expect(mockCreateSession).not.toHaveBeenCalled(); + expect(mockSetServerCertificate).not.toHaveBeenCalled(); + contentDecryptor.attach(); + }, 5); + setTimeout(() => { + contentDecryptor.dispose(); + expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); + expect(mockSetServerCertificate).toHaveBeenCalledTimes(1); + expect(mockCreateSession).not.toHaveBeenCalled(); + res(); + }, 10); + } + }, + ); }); }); @@ -84,7 +90,7 @@ describe("decrypt - global tests - server certificate", () => { const mockCreateSession = vi.spyOn(MediaKeysImpl.prototype, "createSession"); const mockSetServerCertificate = vi .spyOn(MediaKeysImpl.prototype, "setServerCertificate") - .mockImplementation((_serverCertificate: BufferSource) => { + .mockImplementation((_serverCertificate: BufferSource): Promise => { expect(mockSetMediaKeys).toHaveBeenCalledTimes(1); expect(mockCreateSession).not.toHaveBeenCalled(); return Promise.resolve(true); @@ -96,22 +102,25 @@ describe("decrypt - global tests - server certificate", () => { .default as typeof IContentDecryptor; const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); - contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - setTimeout(() => { - expect(mockSetMediaKeys).not.toHaveBeenCalled(); - expect(mockCreateSession).not.toHaveBeenCalled(); - expect(mockSetServerCertificate).not.toHaveBeenCalled(); - const initData = new Uint8Array([54, 55, 75]); - contentDecryptor.onInitializationData({ - type: "cenc2", - values: [{ systemId: "15", data: initData }], - }); - contentDecryptor.attach(); - }, 5); - } - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + setTimeout(() => { + expect(mockSetMediaKeys).not.toHaveBeenCalled(); + expect(mockCreateSession).not.toHaveBeenCalled(); + expect(mockSetServerCertificate).not.toHaveBeenCalled(); + const initData = new Uint8Array([54, 55, 75]); + contentDecryptor.onInitializationData({ + type: "cenc2", + values: [{ systemId: "15", data: initData }], + }); + contentDecryptor.attach(); + }, 5); + } + }, + ); return new Promise((res) => { setTimeout(() => { contentDecryptor.dispose(); @@ -138,12 +147,15 @@ describe("decrypt - global tests - server certificate", () => { .default as typeof IContentDecryptor; const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); - contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - contentDecryptor.attach(); - } - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + contentDecryptor.attach(); + } + }, + ); let warningsReceived = 0; contentDecryptor.addEventListener("warning", (w) => { @@ -178,12 +190,15 @@ describe("decrypt - global tests - server certificate", () => { .default as typeof IContentDecryptor; const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); - contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - contentDecryptor.attach(); - } - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + contentDecryptor.attach(); + } + }, + ); let warningsReceived = 0; contentDecryptor.addEventListener("warning", (w) => { @@ -229,23 +244,26 @@ describe("decrypt - global tests - server certificate", () => { .default as typeof IContentDecryptor; const contentDecryptor = new ContentDecryptor(videoElt, ksConfigCert); - contentDecryptor.addEventListener("stateChange", (state) => { - if (state === ContentDecryptorState.WaitingForAttachment) { - contentDecryptor.removeEventListener("stateChange"); - setTimeout(() => { - expect(mockSetMediaKeys).not.toHaveBeenCalled(); - expect(mockCreateSession).not.toHaveBeenCalled(); - expect(mockSetServerCertificate).not.toHaveBeenCalled(); - const initData = new Uint8Array([54, 55, 75]); - contentDecryptor.onInitializationData({ - type: "cenc2", - values: [{ systemId: "15", data: initData }], - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.WaitingForAttachment) { + contentDecryptor.removeEventListener("stateChange"); + setTimeout(() => { + expect(mockSetMediaKeys).not.toHaveBeenCalled(); + expect(mockCreateSession).not.toHaveBeenCalled(); + expect(mockSetServerCertificate).not.toHaveBeenCalled(); + const initData = new Uint8Array([54, 55, 75]); + contentDecryptor.onInitializationData({ + type: "cenc2", + values: [{ systemId: "15", data: initData }], + }); - contentDecryptor.attach(); - }, 5); - } - }); + contentDecryptor.attach(); + }, 5); + } + }, + ); return new Promise((res) => { setTimeout(() => { contentDecryptor.dispose(); diff --git a/src/main_thread/decrypt/__tests__/__global__/utils.ts b/src/main_thread/decrypt/__tests__/__global__/utils.ts index 57d1029d1e..6b8418569a 100644 --- a/src/main_thread/decrypt/__tests__/__global__/utils.ts +++ b/src/main_thread/decrypt/__tests__/__global__/utils.ts @@ -9,6 +9,8 @@ import flatMap from "../../../../utils/flat_map"; import { strToUtf8, utf8ToStr } from "../../../../utils/string_parsing"; import type { CancellationSignal } from "../../../../utils/task_canceller"; import type IContentDecryptor from "../../content_decryptor"; +import type { IContentDecryptorStateData } from "../../types"; +import { ContentDecryptorState } from "../../types"; /** Default MediaKeySystemAccess configuration used by the RxPlayer. */ export const defaultKSConfig: MediaKeySystemConfiguration[] = [ @@ -462,9 +464,14 @@ export function testContentDecryptorError( ): Promise { return new Promise((res, rej) => { const contentDecryptor = new ContentDecryptor(mediaElement, keySystemsConfigs); - contentDecryptor.addEventListener("error", (error) => { - res(error); - }); + contentDecryptor.addEventListener( + "stateChange", + (state: IContentDecryptorStateData) => { + if (state.name === ContentDecryptorState.Error) { + res(state.payload); + } + }, + ); setTimeout(() => { rej(new Error("Timeout exceeded")); }, 10); diff --git a/src/main_thread/decrypt/content_decryptor.ts b/src/main_thread/decrypt/content_decryptor.ts index b1e275b5dd..45f830366e 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) { log.debug("DRM: Waiting for attachment."); this._stateData = { - state: ContentDecryptorState.WaitingForAttachment, + state: { + name: ContentDecryptorState.WaitingForAttachment, + payload: null, + }, isInitDataQueueLocked: true, isMediaKeysAttached: MediaKeyAttachmentStatus.NotAttached, data: { mediaKeysInfo, mediaElement }, @@ -230,7 +223,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 @@ -1223,15 +1235,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. @@ -1259,7 +1271,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 @@ -1267,7 +1279,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 @@ -1279,7 +1291,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 @@ -1290,7 +1302,7 @@ type IReadyForContentStateDataUnattached = IContentDecryptorStateBase< * it has attached the `MediaKeys` to the media element. */ type IReadyForContentStateDataAttached = IContentDecryptorStateBase< - ContentDecryptorState.ReadyForContent, + IReadyForContentContentDecryptorState, boolean, // isInitDataQueueLocked MediaKeyAttachmentStatus.Attached, // isMediaKeysAttached { @@ -1307,7 +1319,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 @@ -1315,7 +1327,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 cfeb419487..7dc18f4ee8 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 2d20cb3dd7..87f867a54f 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 e2c004c065..f23d2a1466 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 db2c2806a9..7c3f4f2710 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"; @@ -383,7 +383,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { ); drmInitializationStatus.onUpdate( (initializationStatus, stopListeningDrm) => { - if (initializationStatus.initializationState.type === "initialized") { + if (initializationStatus.type === "initialized") { stopListeningDrm(); this._startPlaybackIfReady(playbackStartParams); } @@ -1183,7 +1183,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { reloadMediaSource: () => void, cancelSignal: CancellationSignal, ): { - statusRef: IReadOnlySharedReference; + statusRef: IReadOnlySharedReference; contentDecryptor: IContentDecryptor | null; } { const { keySystems } = this._settings; @@ -1204,12 +1204,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 }; @@ -1227,16 +1227,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; @@ -1307,31 +1304,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) => { @@ -1646,16 +1650,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; @@ -1694,7 +1699,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, }, @@ -2038,17 +2045,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 = /** @@ -2063,7 +2059,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 d8bd6b19b6..7887f0f92e 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -225,6 +225,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 8a7e72a9db..626e889e98 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"`.