diff --git a/src/core/main/worker/worker_main.ts b/src/core/main/worker/worker_main.ts index 96ba1b3e7d..9be37b9710 100644 --- a/src/core/main/worker/worker_main.ts +++ b/src/core/main/worker/worker_main.ts @@ -473,6 +473,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. @@ -523,7 +536,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({ @@ -602,6 +622,8 @@ function loadOrReloadPreparedContent( maxVideoBufferSize, maxBufferAhead, maxBufferBehind, + canFilterProtectionData, + failOnEncryptedAfterClear, drmSystemId, enableFastSwitching, onCodecSwitch, @@ -884,6 +906,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..6a543221cb 100644 --- a/src/core/stream/orchestrator/stream_orchestrator.ts +++ b/src/core/stream/orchestrator/stream_orchestrator.ts @@ -23,6 +23,7 @@ import type { IPeriod, } from "../../../manifest"; import type { IReadOnlyPlaybackObserver } from "../../../playback_observer"; +import type { ITrackType } from "../../../public_types"; import isNullOrUndefined from "../../../utils/is_null_or_undefined"; import queueMicrotask from "../../../utils/queue_microtask"; import type { IReadOnlySharedReference } from "../../../utils/reference"; @@ -109,6 +110,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 +466,27 @@ export default function StreamOrchestrator( ): void { log.info("Stream: Creating new Stream for", bufferType, basePeriod.start); + if (shouldReloadOnEncryptedContent) { + const trackType = hasEncryptedContentInPeriod(basePeriod); + if (trackType !== null) { + 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: trackType, + }); + } + }); + return; + } + } + /** * Contains properties linnked to the next chronological `PeriodStream` that * may be created here. @@ -656,6 +694,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 +803,26 @@ export interface ILockedStreamPayload { bufferType: IBufferType; } +/** + * If the given Period has at least one Representation which is known to be + * encrypted, returns the related track type. + * + * Else if the Period is fully clear or if the status is unknown, return `null`. + * + * @param {Object} period + * @returns {string|null} + */ +function hasEncryptedContentInPeriod(period: IPeriod): ITrackType | null { + for (const adaptation of period.getAdaptations()) { + for (const representation of adaptation.representations) { + if (representation.contentProtections !== undefined) { + return adaptation.type; + } + } + } + return null; +} + /** * 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/content_decryptor.ts b/src/main_thread/decrypt/content_decryptor.ts index 1c8e186781..32bb93d554 100644 --- a/src/main_thread/decrypt/content_decryptor.ts +++ b/src/main_thread/decrypt/content_decryptor.ts @@ -44,6 +44,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"; @@ -84,12 +90,6 @@ export default class ContentDecryptor extends EventEmitter { - const { options, mediaKeySystemAccess } = mediaKeysInfo; + const { mediaKeySystemAccess } = mediaKeysInfo; /** * 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 }, @@ -221,7 +214,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 @@ -1127,15 +1139,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. @@ -1163,7 +1175,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 @@ -1171,7 +1183,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 @@ -1183,7 +1195,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 @@ -1194,7 +1206,7 @@ type IReadyForContentStateDataUnattached = IContentDecryptorStateBase< * it has attached the `MediaKeys` to the media element. */ type IReadyForContentStateDataAttached = IContentDecryptorStateBase< - ContentDecryptorState.ReadyForContent, + IReadyForContentContentDecryptorState, boolean, // isInitDataQueueLocked MediaKeyAttachmentStatus.Attached, // isMediaKeysAttached { @@ -1211,7 +1223,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 @@ -1219,7 +1231,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 e6219f2bcb..ed2fc370c3 100644 --- a/src/main_thread/init/directfile_content_initializer.ts +++ b/src/main_thread/init/directfile_content_initializer.ts @@ -146,7 +146,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(); @@ -159,11 +159,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); } @@ -171,7 +171,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 6032969705..7762a523bd 100644 --- a/src/main_thread/init/media_source_content_initializer.ts +++ b/src/main_thread/init/media_source_content_initializer.ts @@ -184,8 +184,12 @@ export default class MediaSourceContentInitializer extends ContentInitializer { mediaElement, initResult.mediaSource, playbackObserver, - initResult.drmSystemId, - protectionRef, + { + protectionRef, + drmSystemId: initResult.drmSystemId, + canFilterProtectionData: initResult.canFilterProtectionData, + failOnEncryptedAfterClear: initResult.failOnEncryptedAfterClear, + }, initResult.unlinkMediaSource, ), ) @@ -223,6 +227,8 @@ export default class MediaSourceContentInitializer extends ContentInitializer { ): Promise<{ mediaSource: MainMediaSourceInterface; drmSystemId: string | undefined; + canFilterProtectionData: boolean; + failOnEncryptedAfterClear: boolean; unlinkMediaSource: TaskCanceller; }> { const initCanceller = this._initCanceller; @@ -270,7 +276,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { drmInitRef.onUpdate( (drmStatus, stopListeningToDrmUpdates) => { - if (drmStatus.initializationState.type === "uninitialized") { + if (drmStatus.type === "uninitialized") { return; } stopListeningToDrmUpdates(); @@ -280,15 +286,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; @@ -296,10 +306,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; @@ -321,8 +333,12 @@ export default class MediaSourceContentInitializer extends ContentInitializer { mediaElement: IMediaElement, initialMediaSource: MainMediaSourceInterface, playbackObserver: IMediaElementPlaybackObserver, - drmSystemId: string | undefined, - protectionRef: SharedReference, + decryptionParameters: { + protectionRef: SharedReference; + drmSystemId: string | undefined; + canFilterProtectionData: boolean; + failOnEncryptedAfterClear: boolean; + }, initialMediaSourceCanceller: TaskCanceller, ): Promise { const { @@ -368,7 +384,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, ); @@ -422,7 +443,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { representationEstimator, segmentFetcherCreator, speed, - protectionRef, + protectionRef: decryptionParameters.protectionRef, bufferOptions: subBufferOptions, }; bufferOnMediaSource(opts, onReloadMediaSource, currentCanceller.signal); diff --git a/src/main_thread/init/multi_thread_content_initializer.ts b/src/main_thread/init/multi_thread_content_initializer.ts index 00d6f8d403..5e56c2c4cf 100644 --- a/src/main_thread/init/multi_thread_content_initializer.ts +++ b/src/main_thread/init/multi_thread_content_initializer.ts @@ -298,7 +298,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { ); drmInitializationStatus.onUpdate( (initializationStatus, stopListeningDrm) => { - if (initializationStatus.initializationState.type === "initialized") { + if (initializationStatus.type === "initialized") { stopListeningDrm(); this._startPlaybackIfReady(playbackStartParams); } @@ -1140,7 +1140,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { mediaSourceStatus: SharedReference, reloadMediaSource: () => void, cancelSignal: CancellationSignal, - ): IReadOnlySharedReference { + ): IReadOnlySharedReference { const { keySystems } = this._settings; // TODO private? @@ -1159,8 +1159,12 @@ export default class MultiThreadContentInitializer extends ContentInitializer { { clearSignal: cancelSignal }, ); 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 ref; @@ -1172,11 +1176,8 @@ export default class MultiThreadContentInitializer extends ContentInitializer { return createEmeDisabledReference("EME feature not activated."); } - const drmStatusRef = new SharedReference( - { - initializationState: { type: "uninitialized", value: null }, - drmSystemId: undefined, - }, + const drmStatusRef = new SharedReference( + { type: "uninitialized", value: null }, cancelSignal, ); const ContentDecryptor = features.decrypt; @@ -1243,31 +1244,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) => { @@ -1557,16 +1565,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; @@ -1605,7 +1614,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, }, @@ -1950,17 +1961,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 = /** @@ -1989,7 +1989,30 @@ 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; + }; + }; /** * Ensure that all `Representation` and `Adaptation` have a known status diff --git a/src/main_thread/init/utils/initialize_content_decryption.ts b/src/main_thread/init/utils/initialize_content_decryption.ts index 8727171f1f..d16fa4365d 100644 --- a/src/main_thread/init/utils/initialize_content_decryption.ts +++ b/src/main_thread/init/utils/initialize_content_decryption.ts @@ -48,7 +48,7 @@ export default function initializeContentDecryption( }) => void; }, cancelSignal: CancellationSignal, -): IReadOnlySharedReference { +): IReadOnlySharedReference { if (keySystems.length === 0) { return createEmeDisabledReference("No `keySystems` option given."); } else if (features.decrypt === null) { @@ -57,10 +57,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, ); @@ -75,40 +75,45 @@ export default function initializeContentDecryption( const contentDecryptor = new ContentDecryptor(mediaElement, keySystems); 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); }); @@ -151,26 +156,18 @@ export default function initializeContentDecryption( { clearSignal: cancelSignal }, ); 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 ref; } } -/** 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 = /** @@ -199,4 +196,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/multithread_types.ts b/src/multithread_types.ts index 36cd4e8cef..daecd6fe27 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 4e456473bd..36cea811c3 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"`.