From ecd4b601fc2eacd60ed6b23c74fdbdd19288f64b Mon Sep 17 00:00:00 2001 From: KunXi-Fox Date: Mon, 17 Mar 2025 12:44:26 +0800 Subject: [PATCH 1/5] Introduce reloadMediaSource method to allow user reload media source --- src/core/main/common/FreezeResolver.ts | 9 ++++- src/main_thread/api/public_api.ts | 33 +++++++++++++++---- .../utils/create_core_playback_observer.ts | 3 ++ src/multithread_types.ts | 2 ++ .../media_element_playback_observer.ts | 6 ++++ src/playback_observer/types.ts | 8 ++++- .../worker_playback_observer.ts | 2 ++ 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/core/main/common/FreezeResolver.ts b/src/core/main/common/FreezeResolver.ts index 23ac9b846df..de79a39ebae 100644 --- a/src/core/main/common/FreezeResolver.ts +++ b/src/core/main/common/FreezeResolver.ts @@ -212,7 +212,12 @@ export default class FreezeResolver { UNFREEZING_DELTA_POSITION, FREEZING_FLUSH_FAILURE_DELAY, } = config.getCurrent(); - const { readyState, rebuffering, freezing, fullyLoaded } = observation; + const { readyState, rebuffering, freezing, fullyLoaded, needReloadMediaSource } = + observation; + + if (needReloadMediaSource) { + return { type: "reload", value: null }; + } const freezingPosition = observation.position.getPolled(); const bufferGap = normalizeBufferGap(observation.bufferGap); @@ -581,4 +586,6 @@ export interface IFreezeResolverObservation { position: ObservationPosition; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; + /** Indicates whether need to reload media source */ + needReloadMediaSource: boolean; } diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index c7aa10cbefb..33321b78669 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -686,6 +686,24 @@ class Player extends EventEmitter { this._priv_lastAutoPlay = options.autoPlay; } + /** + * Destroy current media source and re-attach new one. + * + * This method is useful when the media source is corrupted and needs to be reloaded. + * In some of legacy CDM implementations (e.g. Tizen 3.0), they try to decrypt the segment with first loaded init segment + * When the assets starts with non-drm protected content and switch to drm protected segment + * CDM trying to use the unencrypted init segment to decrypt the encrypted segment user will see the content with green artifacts or black screen + * + * To fix this issue, we need to reload the media source to make sure the CDM is using the correct init segment + */ + public reloadMediaSource(): void { + if (this._priv_contentInfos === null) { + throw new Error("API: No content is currently loaded."); + } + + this._priv_contentInfos.playbackObserver.sendReloadMediaSourceRequest(); + } + /** * Reload the last loaded content. * @param {Object} reloadOpts @@ -1019,6 +1037,12 @@ class Player extends EventEmitter { }); } + /** Global "playback observer" which will emit playback conditions */ + const playbackObserver = new MediaElementPlaybackObserver(videoElement, { + withMediaSource: !isDirectFile, + lowLatencyMode, + }); + /** Future `this._priv_contentInfos` related to this content. */ const contentInfos: IPublicApiContentInfos = { contentId: generateContentId(), @@ -1034,6 +1058,7 @@ class Player extends EventEmitter { tracksStore: null, mediaElementTracksStore, useWorker, + playbackObserver, }; // Bind events @@ -1113,12 +1138,6 @@ class Player extends EventEmitter { // content. this.stop(); - /** Global "playback observer" which will emit playback conditions */ - const playbackObserver = new MediaElementPlaybackObserver(videoElement, { - withMediaSource: !isDirectFile, - lowLatencyMode, - }); - currentContentCanceller.signal.register(() => { playbackObserver.stop(); }); @@ -3431,6 +3450,8 @@ interface IPublicApiContentInfos { * content. */ useWorker: boolean; + + playbackObserver: MediaElementPlaybackObserver; } export default Player; diff --git a/src/main_thread/init/utils/create_core_playback_observer.ts b/src/main_thread/init/utils/create_core_playback_observer.ts index d0b6d8582ab..0af01f0f7e7 100644 --- a/src/main_thread/init/utils/create_core_playback_observer.ts +++ b/src/main_thread/init/utils/create_core_playback_observer.ts @@ -94,6 +94,8 @@ export interface ICorePlaybackObservation { canStream: boolean; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; + /** Indicates whether need to reload media source */ + needReloadMediaSource: boolean; } /** @@ -173,6 +175,7 @@ export default function createCorePlaybackObserver( speed: lastSpeed, canStream: mediaSource?.streaming ?? true, fullyLoaded: observation.fullyLoaded, + needReloadMediaSource: observation.needReloadMediaSource, }; } diff --git a/src/multithread_types.ts b/src/multithread_types.ts index b69e1a21f15..e8bb5f27fb8 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -435,6 +435,8 @@ export interface ISerializedPlaybackObservation { canStream: boolean; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; + /** Indicates whether need to reload media source */ + needReloadMediaSource: boolean; } /** diff --git a/src/playback_observer/media_element_playback_observer.ts b/src/playback_observer/media_element_playback_observer.ts index f0b9233ea94..e66327e3b3d 100644 --- a/src/playback_observer/media_element_playback_observer.ts +++ b/src/playback_observer/media_element_playback_observer.ts @@ -303,6 +303,10 @@ export default class PlaybackObserver { return generateReadOnlyObserver(this, transform, this._canceller.signal); } + public sendReloadMediaSourceRequest(): void { + this._generateObservationForEvent("need-reload-media-source"); + } + private _actuallySetCurrentTime(time: number): void { log.info("API: Seeking internally", time); this._internalSeeksIncoming.push(time); @@ -510,6 +514,7 @@ export default class PlaybackObserver { bufferGap, currentRange, fullyLoaded, + needReloadMediaSource: tmpEvt === "need-reload-media-source", }); if (log.hasLevel("DEBUG")) { log.debug( @@ -638,6 +643,7 @@ function getMediaInfos(mediaElement: IMediaElement): IMediaInfos { playbackRate, readyState, seeking, + needReloadMediaSource: false, }; } diff --git a/src/playback_observer/types.ts b/src/playback_observer/types.ts index c851d1e3432..0212f0648bb 100644 --- a/src/playback_observer/types.ts +++ b/src/playback_observer/types.ts @@ -31,7 +31,9 @@ export type IPlaybackObserverEventType = /** On the HTML5 event with the same name */ | "ratechange" /** An internal seek happens */ - | "internal-seeking"; + | "internal-seeking" + /** Reload media source request happens */ + | "need-reload-media-source"; /** Information recuperated on the media element on each playback observation. */ export interface IMediaInfos { @@ -54,6 +56,8 @@ export interface IMediaInfos { readyState: number; /** Current `seeking` value on the mediaElement. */ seeking: boolean; + /** Indicates whether need to reload media source */ + needReloadMediaSource: boolean; } /** Categorize a pending seek operation. */ @@ -142,6 +146,8 @@ export interface IPlaybackObservation extends Omit Date: Thu, 20 Mar 2025 10:28:11 +0800 Subject: [PATCH 2/5] Revert "Introduce reloadMediaSource method to allow user reload media source" This reverts commit ecd4b601fc2eacd60ed6b23c74fdbdd19288f64b. --- src/core/main/common/FreezeResolver.ts | 9 +---- src/main_thread/api/public_api.ts | 33 ++++--------------- .../utils/create_core_playback_observer.ts | 3 -- src/multithread_types.ts | 2 -- .../media_element_playback_observer.ts | 6 ---- src/playback_observer/types.ts | 8 +---- .../worker_playback_observer.ts | 2 -- 7 files changed, 8 insertions(+), 55 deletions(-) diff --git a/src/core/main/common/FreezeResolver.ts b/src/core/main/common/FreezeResolver.ts index de79a39ebae..23ac9b846df 100644 --- a/src/core/main/common/FreezeResolver.ts +++ b/src/core/main/common/FreezeResolver.ts @@ -212,12 +212,7 @@ export default class FreezeResolver { UNFREEZING_DELTA_POSITION, FREEZING_FLUSH_FAILURE_DELAY, } = config.getCurrent(); - const { readyState, rebuffering, freezing, fullyLoaded, needReloadMediaSource } = - observation; - - if (needReloadMediaSource) { - return { type: "reload", value: null }; - } + const { readyState, rebuffering, freezing, fullyLoaded } = observation; const freezingPosition = observation.position.getPolled(); const bufferGap = normalizeBufferGap(observation.bufferGap); @@ -586,6 +581,4 @@ export interface IFreezeResolverObservation { position: ObservationPosition; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; - /** Indicates whether need to reload media source */ - needReloadMediaSource: boolean; } diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 33321b78669..c7aa10cbefb 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -686,24 +686,6 @@ class Player extends EventEmitter { this._priv_lastAutoPlay = options.autoPlay; } - /** - * Destroy current media source and re-attach new one. - * - * This method is useful when the media source is corrupted and needs to be reloaded. - * In some of legacy CDM implementations (e.g. Tizen 3.0), they try to decrypt the segment with first loaded init segment - * When the assets starts with non-drm protected content and switch to drm protected segment - * CDM trying to use the unencrypted init segment to decrypt the encrypted segment user will see the content with green artifacts or black screen - * - * To fix this issue, we need to reload the media source to make sure the CDM is using the correct init segment - */ - public reloadMediaSource(): void { - if (this._priv_contentInfos === null) { - throw new Error("API: No content is currently loaded."); - } - - this._priv_contentInfos.playbackObserver.sendReloadMediaSourceRequest(); - } - /** * Reload the last loaded content. * @param {Object} reloadOpts @@ -1037,12 +1019,6 @@ class Player extends EventEmitter { }); } - /** Global "playback observer" which will emit playback conditions */ - const playbackObserver = new MediaElementPlaybackObserver(videoElement, { - withMediaSource: !isDirectFile, - lowLatencyMode, - }); - /** Future `this._priv_contentInfos` related to this content. */ const contentInfos: IPublicApiContentInfos = { contentId: generateContentId(), @@ -1058,7 +1034,6 @@ class Player extends EventEmitter { tracksStore: null, mediaElementTracksStore, useWorker, - playbackObserver, }; // Bind events @@ -1138,6 +1113,12 @@ class Player extends EventEmitter { // content. this.stop(); + /** Global "playback observer" which will emit playback conditions */ + const playbackObserver = new MediaElementPlaybackObserver(videoElement, { + withMediaSource: !isDirectFile, + lowLatencyMode, + }); + currentContentCanceller.signal.register(() => { playbackObserver.stop(); }); @@ -3450,8 +3431,6 @@ interface IPublicApiContentInfos { * content. */ useWorker: boolean; - - playbackObserver: MediaElementPlaybackObserver; } export default Player; diff --git a/src/main_thread/init/utils/create_core_playback_observer.ts b/src/main_thread/init/utils/create_core_playback_observer.ts index 0af01f0f7e7..d0b6d8582ab 100644 --- a/src/main_thread/init/utils/create_core_playback_observer.ts +++ b/src/main_thread/init/utils/create_core_playback_observer.ts @@ -94,8 +94,6 @@ export interface ICorePlaybackObservation { canStream: boolean; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; - /** Indicates whether need to reload media source */ - needReloadMediaSource: boolean; } /** @@ -175,7 +173,6 @@ export default function createCorePlaybackObserver( speed: lastSpeed, canStream: mediaSource?.streaming ?? true, fullyLoaded: observation.fullyLoaded, - needReloadMediaSource: observation.needReloadMediaSource, }; } diff --git a/src/multithread_types.ts b/src/multithread_types.ts index e8bb5f27fb8..b69e1a21f15 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -435,8 +435,6 @@ export interface ISerializedPlaybackObservation { canStream: boolean; /** If `true` the content is loaded until its maximum position. */ fullyLoaded: boolean; - /** Indicates whether need to reload media source */ - needReloadMediaSource: boolean; } /** diff --git a/src/playback_observer/media_element_playback_observer.ts b/src/playback_observer/media_element_playback_observer.ts index e66327e3b3d..f0b9233ea94 100644 --- a/src/playback_observer/media_element_playback_observer.ts +++ b/src/playback_observer/media_element_playback_observer.ts @@ -303,10 +303,6 @@ export default class PlaybackObserver { return generateReadOnlyObserver(this, transform, this._canceller.signal); } - public sendReloadMediaSourceRequest(): void { - this._generateObservationForEvent("need-reload-media-source"); - } - private _actuallySetCurrentTime(time: number): void { log.info("API: Seeking internally", time); this._internalSeeksIncoming.push(time); @@ -514,7 +510,6 @@ export default class PlaybackObserver { bufferGap, currentRange, fullyLoaded, - needReloadMediaSource: tmpEvt === "need-reload-media-source", }); if (log.hasLevel("DEBUG")) { log.debug( @@ -643,7 +638,6 @@ function getMediaInfos(mediaElement: IMediaElement): IMediaInfos { playbackRate, readyState, seeking, - needReloadMediaSource: false, }; } diff --git a/src/playback_observer/types.ts b/src/playback_observer/types.ts index 0212f0648bb..c851d1e3432 100644 --- a/src/playback_observer/types.ts +++ b/src/playback_observer/types.ts @@ -31,9 +31,7 @@ export type IPlaybackObserverEventType = /** On the HTML5 event with the same name */ | "ratechange" /** An internal seek happens */ - | "internal-seeking" - /** Reload media source request happens */ - | "need-reload-media-source"; + | "internal-seeking"; /** Information recuperated on the media element on each playback observation. */ export interface IMediaInfos { @@ -56,8 +54,6 @@ export interface IMediaInfos { readyState: number; /** Current `seeking` value on the mediaElement. */ seeking: boolean; - /** Indicates whether need to reload media source */ - needReloadMediaSource: boolean; } /** Categorize a pending seek operation. */ @@ -146,8 +142,6 @@ export interface IPlaybackObservation extends Omit Date: Thu, 20 Mar 2025 12:59:24 +0800 Subject: [PATCH 3/5] introduce reload media source for first incompatible period switch fixes --- src/core/main/worker/worker_main.ts | 6 ++ .../orchestrator/stream_orchestrator.ts | 64 ++++++++++++++++++- src/index.ts | 1 + src/main_thread/api/option_utils.ts | 11 ++++ src/main_thread/api/public_api.ts | 7 +- .../init/media_source_content_initializer.ts | 2 + .../init/multi_thread_content_initializer.ts | 9 ++- src/multithread_types.ts | 3 +- src/public_types.ts | 16 ++++- 9 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/core/main/worker/worker_main.ts b/src/core/main/worker/worker_main.ts index 9c334af2012..569008eebf9 100644 --- a/src/core/main/worker/worker_main.ts +++ b/src/core/main/worker/worker_main.ts @@ -482,6 +482,8 @@ interface IBufferingInitializationInformation { enableFastSwitching: boolean; /** Behavior when a new video and/or audio codec is encountered. */ onCodecSwitch: "continue" | "reload"; + /** Whether to reload the media source for the first incompatible period switch. */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; } function loadOrReloadPreparedContent( @@ -609,6 +611,8 @@ function loadOrReloadPreparedContent( drmSystemId, enableFastSwitching, onCodecSwitch, + reloadMediaSourceForFirstIncompatiblePeriodSwitch: + val.reloadMediaSourceForFirstIncompatiblePeriodSwitch, }, handleStreamOrchestratorCallbacks(), currentLoadCanceller.signal, @@ -898,6 +902,8 @@ function loadOrReloadPreparedContent( drmSystemId: val.drmSystemId, enableFastSwitching: val.enableFastSwitching, onCodecSwitch: val.onCodecSwitch, + reloadMediaSourceForFirstIncompatiblePeriodSwitch: + val.reloadMediaSourceForFirstIncompatiblePeriodSwitch, }, contentPreparer, playbackObservationRef, diff --git a/src/core/stream/orchestrator/stream_orchestrator.ts b/src/core/stream/orchestrator/stream_orchestrator.ts index b6b63a4eb95..aad4f4ace69 100644 --- a/src/core/stream/orchestrator/stream_orchestrator.ts +++ b/src/core/stream/orchestrator/stream_orchestrator.ts @@ -43,6 +43,12 @@ import PeriodStream from "../period"; import type { IStreamStatusPayload } from "../representation"; import getTimeRangesForContent from "./get_time_ranges_for_content"; +type FirstIncompatiblePeriodSwitchIssueStatus = + | "INITIAL" + | "NEEDS_RELOAD" + | "NO_NEEDS_RELOAD" + | "RELOADED"; + /** * Create and manage the various "Streams" needed for the content to * play: @@ -96,8 +102,44 @@ export default function StreamOrchestrator( orchestratorCancelSignal: CancellationSignal, ): void { const { manifest, initialPeriod } = content; - const { maxBufferAhead, maxBufferBehind, wantedBufferAhead, maxVideoBufferSize } = - options; + + const { + maxBufferAhead, + maxBufferBehind, + wantedBufferAhead, + maxVideoBufferSize, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, + } = options; + + function isPeriodEncrypted(period: IPeriod): boolean { + const adaptations = period.adaptations.video; + + return ( + adaptations !== undefined && + adaptations.some((adaptation) => + adaptation.representations.some( + (representation) => representation.contentProtections !== undefined, + ), + ) + ); + } + + let firstIncompatiblePeriodSwitchIssueStatus: FirstIncompatiblePeriodSwitchIssueStatus = + "INITIAL"; + + const isInitialPeriodEncrypted = isPeriodEncrypted(initialPeriod); + + if (reloadMediaSourceForFirstIncompatiblePeriodSwitch) { + firstIncompatiblePeriodSwitchIssueStatus = isInitialPeriodEncrypted + ? "NO_NEEDS_RELOAD" + : "NEEDS_RELOAD"; + + // eslint-disable-next-line no-console + console.log( + "kx firstIncompatiblePeriodSwitchIssueStatus:", + firstIncompatiblePeriodSwitchIssueStatus, + ); + } const { MINIMUM_MAX_BUFFER_AHEAD, @@ -471,6 +513,23 @@ export default function StreamOrchestrator( if (basePeriod.containsTime(position.getWanted(), nextPeriod)) { return; } + + const isNextPeriodEncrypted = + nextPeriod !== null && isPeriodEncrypted(nextPeriod); + + if ( + firstIncompatiblePeriodSwitchIssueStatus === "NEEDS_RELOAD" && + isNextPeriodEncrypted && + reloadMediaSourceForFirstIncompatiblePeriodSwitch + ) { + firstIncompatiblePeriodSwitchIssueStatus = "RELOADED"; + callbacks.needsMediaSourceReload({ + timeOffset: 0.1, + minimumPosition: undefined, + maximumPosition: undefined, + }); + } + log.info( "Stream: Destroying PeriodStream as the current playhead moved above it", bufferType, @@ -652,6 +711,7 @@ export type IStreamOrchestratorOptions = IPeriodStreamOptions & { maxVideoBufferSize: IReadOnlySharedReference; maxBufferAhead: IReadOnlySharedReference; maxBufferBehind: IReadOnlySharedReference; + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; }; /** Callbacks called by the `StreamOrchestrator` on various events. */ diff --git a/src/index.ts b/src/index.ts index 41ebcef9241..1a5a15f7fe8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import isDebugModeEnabled from "./compat/is_debug_mode_enabled"; import patchWebkitSourceBuffer from "./compat/patch_webkit_source_buffer"; +import { MULTI_THREAD } from "./experimental/features"; import { DASH, DIRECTFILE, diff --git a/src/main_thread/api/option_utils.ts b/src/main_thread/api/option_utils.ts index c1832d3b8be..6bb9ae92ab2 100644 --- a/src/main_thread/api/option_utils.ts +++ b/src/main_thread/api/option_utils.ts @@ -114,6 +114,8 @@ interface IParsedLoadVideoOptionsBase { experimentalOptions: { enableRepresentationAvoidance: boolean; }; + /** @see ILoadVideoOptions.reloadMediaSourceForFirstIncompatiblePeriodSwitch */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; __priv_manifestUpdateUrl?: string | undefined; __priv_patchLastSegmentInSidx?: boolean | undefined; } @@ -309,6 +311,7 @@ function parseLoadVideoOptions(options: ILoadVideoOptions): IParsedLoadVideoOpti let mode: IRxPlayerMode; let textTrackElement: HTMLElement | undefined; let startAt: IParsedStartAtOption | undefined; + let reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; const { DEFAULT_AUTO_PLAY, @@ -455,6 +458,13 @@ function parseLoadVideoOptions(options: ILoadVideoOptions): IParsedLoadVideoOpti } } + if (!isNullOrUndefined(options.reloadMediaSourceForFirstIncompatiblePeriodSwitch)) { + reloadMediaSourceForFirstIncompatiblePeriodSwitch = + options.reloadMediaSourceForFirstIncompatiblePeriodSwitch; + } else { + reloadMediaSourceForFirstIncompatiblePeriodSwitch = false; + } + const requestConfig = options.requestConfig ?? {}; // All those eslint disable are needed because the option is voluntarily @@ -491,6 +501,7 @@ function parseLoadVideoOptions(options: ILoadVideoOptions): IParsedLoadVideoOpti enableRepresentationAvoidance: options.experimentalOptions?.enableRepresentationAvoidance === true, }, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, }; } diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index c7aa10cbefb..78378e0648e 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -787,6 +787,7 @@ class Player extends EventEmitter { __priv_manifestUpdateUrl, __priv_patchLastSegmentInSidx, url, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, } = options; // Perform multiple checks on the given options @@ -879,7 +880,11 @@ class Player extends EventEmitter { }; const bufferOptions = objectAssign( - { enableFastSwitching, onCodecSwitch }, + { + enableFastSwitching, + onCodecSwitch, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, + }, this._priv_bufferOptions, ); diff --git a/src/main_thread/init/media_source_content_initializer.ts b/src/main_thread/init/media_source_content_initializer.ts index 1a8c8d346ab..fe2df77bafc 100644 --- a/src/main_thread/init/media_source_content_initializer.ts +++ b/src/main_thread/init/media_source_content_initializer.ts @@ -1162,6 +1162,8 @@ export interface IInitializeArguments { maxBufferAhead: IReadOnlySharedReference; /** Max buffer size before the current position, in seconds (we GC further down). */ maxBufferBehind: IReadOnlySharedReference; + /** Whether to reload the media source for the first incompatible period switch. */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; /** * Enable/Disable fastSwitching: allow to replace lower-quality segments by * higher-quality ones to have a faster transition. diff --git a/src/main_thread/init/multi_thread_content_initializer.ts b/src/main_thread/init/multi_thread_content_initializer.ts index 4150b0c240e..e1a590f1f04 100644 --- a/src/main_thread/init/multi_thread_content_initializer.ts +++ b/src/main_thread/init/multi_thread_content_initializer.ts @@ -1683,7 +1683,11 @@ export default class MultiThreadContentInitializer extends ContentInitializer { this._settings.startAt, ); log.debug("MTCI: Initial time calculated:", initialTime); - const { enableFastSwitching, onCodecSwitch } = this._settings.bufferOptions; + const { + enableFastSwitching, + onCodecSwitch, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, + } = this._settings.bufferOptions; const corePlaybackObserver = this._setUpModulesOnNewMediaSource( { initialTime, @@ -1711,6 +1715,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer { drmSystemId: drmInitStatus.drmSystemId, enableFastSwitching, onCodecSwitch, + reloadMediaSourceForFirstIncompatiblePeriodSwitch, }, }); @@ -1890,6 +1895,8 @@ export interface IInitializeArguments { maxBufferAhead: IReadOnlySharedReference; /** Max buffer size before the current position, in seconds (we GC further down). */ maxBufferBehind: IReadOnlySharedReference; + /** Whether to reload the media source for the first incompatible period switch. */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; /** * Enable/Disable fastSwitching: allow to replace lower-quality segments by * higher-quality ones to have a faster transition. diff --git a/src/multithread_types.ts b/src/multithread_types.ts index b69e1a21f15..1c17b73dc70 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -238,7 +238,8 @@ export interface IStartPreparedContentMessageValue { enableFastSwitching: boolean; /** Behavior when a new video and/or audio codec is encountered. */ onCodecSwitch: "continue" | "reload"; - + /** Whether to reload the media source for the first incompatible period switch. */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch: boolean; // TODO prepare chosen Adaptations here? // In which case the Period's `id` should probably be given instead of the // `initialTime` diff --git a/src/public_types.ts b/src/public_types.ts index 97dee414d79..2d08ac6c4b9 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -57,7 +57,7 @@ export interface IConstructorOptions { maxVideoBufferSize?: number; videoResolutionLimit?: "videoElement" | "screen" | "none"; throttleVideoBitrateWhenHidden?: boolean; - + reloadMediaSourceForFirstIncompatiblePeriodSwitch?: boolean; // eslint-disable-next-line @typescript-eslint/no-restricted-types videoElement?: HTMLMediaElement; baseBandwidth?: number; @@ -139,6 +139,20 @@ export interface ILoadVideoOptions { */ onCodecSwitch?: "continue" | "reload"; + /** + * Behavior when period switch from non-drm content to drm content for first time. + * + * This value might depend on the device's capabilities. + * + * @remarks + * In some of legacy CDM implementations (e.g. Tizen 3.0), they try to decrypt the segment with first loaded init segment + * When the assets starts with non-drm protected content and switch to drm protected segment + * CDM trying to use the unencrypted init segment to decrypt the encrypted segment user will see the content with green artifacts or black screen + * + * To fix this issue, we need to reload the media source to make sure the CDM is using the correct init segment + */ + reloadMediaSourceForFirstIncompatiblePeriodSwitch?: boolean; + /** * Whether we should check that an obtain segment is truncated and retry the * request if that's the case. From 2b77c448e319e7ece03406a1240223ed058a3ce0 Mon Sep 17 00:00:00 2001 From: KunXi-Fox Date: Tue, 25 Mar 2025 11:08:07 +0800 Subject: [PATCH 4/5] Remove log --- src/core/stream/orchestrator/stream_orchestrator.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/core/stream/orchestrator/stream_orchestrator.ts b/src/core/stream/orchestrator/stream_orchestrator.ts index aad4f4ace69..e19239edd03 100644 --- a/src/core/stream/orchestrator/stream_orchestrator.ts +++ b/src/core/stream/orchestrator/stream_orchestrator.ts @@ -133,12 +133,6 @@ export default function StreamOrchestrator( firstIncompatiblePeriodSwitchIssueStatus = isInitialPeriodEncrypted ? "NO_NEEDS_RELOAD" : "NEEDS_RELOAD"; - - // eslint-disable-next-line no-console - console.log( - "kx firstIncompatiblePeriodSwitchIssueStatus:", - firstIncompatiblePeriodSwitchIssueStatus, - ); } const { From b6a2e75c535500cb74da8516707223257ce2d746 Mon Sep 17 00:00:00 2001 From: KunXi-Fox Date: Wed, 26 Mar 2025 15:11:59 +0800 Subject: [PATCH 5/5] Code refactor --- .../stream/orchestrator/stream_orchestrator.ts | 18 +----------------- src/public_types.ts | 1 - 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/core/stream/orchestrator/stream_orchestrator.ts b/src/core/stream/orchestrator/stream_orchestrator.ts index e19239edd03..b46150a0735 100644 --- a/src/core/stream/orchestrator/stream_orchestrator.ts +++ b/src/core/stream/orchestrator/stream_orchestrator.ts @@ -43,12 +43,6 @@ import PeriodStream from "../period"; import type { IStreamStatusPayload } from "../representation"; import getTimeRangesForContent from "./get_time_ranges_for_content"; -type FirstIncompatiblePeriodSwitchIssueStatus = - | "INITIAL" - | "NEEDS_RELOAD" - | "NO_NEEDS_RELOAD" - | "RELOADED"; - /** * Create and manage the various "Streams" needed for the content to * play: @@ -124,17 +118,8 @@ export default function StreamOrchestrator( ); } - let firstIncompatiblePeriodSwitchIssueStatus: FirstIncompatiblePeriodSwitchIssueStatus = - "INITIAL"; - const isInitialPeriodEncrypted = isPeriodEncrypted(initialPeriod); - if (reloadMediaSourceForFirstIncompatiblePeriodSwitch) { - firstIncompatiblePeriodSwitchIssueStatus = isInitialPeriodEncrypted - ? "NO_NEEDS_RELOAD" - : "NEEDS_RELOAD"; - } - const { MINIMUM_MAX_BUFFER_AHEAD, MAXIMUM_MAX_BUFFER_AHEAD, @@ -512,11 +497,10 @@ export default function StreamOrchestrator( nextPeriod !== null && isPeriodEncrypted(nextPeriod); if ( - firstIncompatiblePeriodSwitchIssueStatus === "NEEDS_RELOAD" && + !isInitialPeriodEncrypted && isNextPeriodEncrypted && reloadMediaSourceForFirstIncompatiblePeriodSwitch ) { - firstIncompatiblePeriodSwitchIssueStatus = "RELOADED"; callbacks.needsMediaSourceReload({ timeOffset: 0.1, minimumPosition: undefined, diff --git a/src/public_types.ts b/src/public_types.ts index 2d08ac6c4b9..2948ce4ce0b 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -57,7 +57,6 @@ export interface IConstructorOptions { maxVideoBufferSize?: number; videoResolutionLimit?: "videoElement" | "screen" | "none"; throttleVideoBitrateWhenHidden?: boolean; - reloadMediaSourceForFirstIncompatiblePeriodSwitch?: boolean; // eslint-disable-next-line @typescript-eslint/no-restricted-types videoElement?: HTMLMediaElement; baseBandwidth?: number;