From 37a5364a19f9e23e31e02327620d5ad39797389c Mon Sep 17 00:00:00 2001 From: Sophia Lydia Morris-Hind Date: Fri, 1 Aug 2025 16:06:32 +0100 Subject: [PATCH 1/4] First stab at updatable mediaplayer settings --- src/bigscreenplayer.js | 9 +++++++++ src/mediasources.ts | 9 +++++++++ src/playbackstrategy/msestrategy.js | 24 ++++++++++++++++++++++++ src/playercomponent.js | 5 +++++ src/types.ts | 2 +- 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/bigscreenplayer.js b/src/bigscreenplayer.js index ae8b2945..c8bd2c14 100644 --- a/src/bigscreenplayer.js +++ b/src/bigscreenplayer.js @@ -788,6 +788,15 @@ function BigscreenPlayer() { getInitialPlaybackTime, getTimeShiftBufferDepthInMilliseconds, getPresentationTimeOffsetInMilliseconds, + + /** + * Updates the settings of an active player. + * + * @param {Partial} settings The settings to update the player with. + */ + updateSettings(settings) { + playerComponent.updateSettings(settings) + }, } } diff --git a/src/mediasources.ts b/src/mediasources.ts index 6c1b6404..0da0bf2b 100644 --- a/src/mediasources.ts +++ b/src/mediasources.ts @@ -362,6 +362,14 @@ function MediaSources() { subtitlesSources = [] } + function updateSettings(settings: { + failoverResetTime: typeof failoverResetTimeMs + failoverSort: typeof failoverSort + }) { + if (settings.failoverResetTime) failoverResetTimeMs = settings.failoverResetTime + if (settings.failoverSort) failoverSort = settings.failoverSort + } + return { init, failover, @@ -381,6 +389,7 @@ function MediaSources() { time: generateTime, transferFormat: getCurrentTransferFormat, tearDown, + updateSettings, } } diff --git a/src/playbackstrategy/msestrategy.js b/src/playbackstrategy/msestrategy.js index cade7d13..ae67a456 100644 --- a/src/playbackstrategy/msestrategy.js +++ b/src/playbackstrategy/msestrategy.js @@ -1017,6 +1017,29 @@ function MSEStrategy( }) } + function updateSettings(settings) { + if (settings?.failoverResetTime) { + mediaSources.updateSettings({ failoverResetTime: settings.failoverResetTime }) + mediaPlayer.updateSettings({ streaming: { blacklistExpiryTime: mediaSources.failoverResetTime() } }) + } + + if (settings?.failoverSort) { + mediaSources.updateSettings({ failoverSort: settings.failoverSort }) + } + + if (settings?.streaming?.seekDurationPadding) { + seekDurationPadding = settings.streaming.seekDurationPadding + } + + // Remove BSP specific settings + delete settings.failoverResetTime + delete settings.failoverSort + delete settings.streaming?.seekDurationPadding + + // If we still have settings, pass them to Dash + if (Object.keys(settings).length > 0) mediaPlayer.updateSettings(settings) + } + return { transitions: { canBePaused: () => true, @@ -1063,6 +1086,7 @@ function MSEStrategy( getPlaybackRate: () => mediaPlayer.getPlaybackRate(), setBitrateConstraint, getPlaybackBitrate: (mediaKind) => currentPlaybackBitrateInKbps(mediaKind), + updateSettings, } } diff --git a/src/playercomponent.js b/src/playercomponent.js index 0cbc0580..01e447d9 100644 --- a/src/playercomponent.js +++ b/src/playercomponent.js @@ -455,6 +455,10 @@ function PlayerComponent( return playbackStrategy?.getPlaybackBitrate(mediaKind) } + function updateSettings(settings) { + playbackStrategy?.updateSettings(settings) + } + return { play, pause, @@ -477,6 +481,7 @@ function PlayerComponent( setSubtitles, setBitrateConstraint, getPlaybackBitrate, + updateSettings, } } diff --git a/src/types.ts b/src/types.ts index 53544d1e..9bb26480 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,7 +10,7 @@ export type CaptionsConnection = Connection & { segmentLength: number } -type Settings = MediaPlayerSettingClass & { +export type Settings = MediaPlayerSettingClass & { failoverResetTime: number failoverSort: (sources: Connection[]) => Connection[] streaming: { From e2459d05157dd2f6f1f51038f99a820335d244f9 Mon Sep 17 00:00:00 2001 From: Sophia Lydia Morris-Hind Date: Fri, 1 Aug 2025 16:36:55 +0100 Subject: [PATCH 2/4] Player Component test --- src/playercomponent.test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/playercomponent.test.js b/src/playercomponent.test.js index e3638f52..0f35276f 100644 --- a/src/playercomponent.test.js +++ b/src/playercomponent.test.js @@ -251,6 +251,30 @@ describe("Player Component", () => { expect(mockAudioDescribedCallback).not.toHaveBeenCalled() }) + + it("Calls through to the strategy's updateSettings if it exists", async () => { + const updateSettings = jest.fn() + const settings = { updatedSetting1: true, updatedSetting2: 1 } + const mockStrategy = createMockPlaybackStrategy(LiveSupport.SEEKABLE, { updateSettings }) + const mockPlaybackStrategyClass = jest.fn().mockReturnValue(mockStrategy) + StrategyPicker.mockResolvedValueOnce(mockPlaybackStrategyClass) + + const playbackElement = createPlaybackElement() + + const playerComponent = new PlayerComponent( + playbackElement, + bigscreenPlayerData, + mockMediaSources, + jest.fn(), + jest.fn() + ) + + await jest.runOnlyPendingTimersAsync() + + playerComponent.updateSettings(settings) + + expect(updateSettings).toHaveBeenCalledWith(settings) + }) }) describe("pause", () => { From 0ed59fc0e4ad803a267d4b89bdf302008b2257a4 Mon Sep 17 00:00:00 2001 From: Sophia Lydia Morris-Hind Date: Fri, 1 Aug 2025 17:07:56 +0100 Subject: [PATCH 3/4] BSP Test --- src/bigscreenplayer.js | 2 +- src/bigscreenplayer.test.js | 11 +++++++++++ src/playercomponent.js | 2 +- src/playercomponent.test.js | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/bigscreenplayer.js b/src/bigscreenplayer.js index c8bd2c14..21d82a9f 100644 --- a/src/bigscreenplayer.js +++ b/src/bigscreenplayer.js @@ -795,7 +795,7 @@ function BigscreenPlayer() { * @param {Partial} settings The settings to update the player with. */ updateSettings(settings) { - playerComponent.updateSettings(settings) + playerComponent && playerComponent.updateSettings(settings) }, } } diff --git a/src/bigscreenplayer.test.js b/src/bigscreenplayer.test.js index 535aba60..ca17192c 100644 --- a/src/bigscreenplayer.test.js +++ b/src/bigscreenplayer.test.js @@ -118,6 +118,7 @@ describe("Bigscreen Player", () => { setAudioDescribed: jest.fn(), setBitrateConstraint: jest.fn(), getPlaybackBitrate: jest.fn(), + updateSettings: jest.fn(), } jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.SEEKABLE) @@ -1702,4 +1703,14 @@ describe("Bigscreen Player", () => { expect(mockPlayerComponentInstance.getPlaybackBitrate()).toBe(100) }) }) + + it("should call through to PlayerComponent.updateSettings when updateSettings is called", async () => { + const settings = { updatedSetting1: true, updatedSetting2: 1 } + + await asyncInitialiseBigscreenPlayer(createPlaybackElement(), bigscreenPlayerData) + + bigscreenPlayer.updateSettings(settings) + + expect(mockPlayerComponentInstance.updateSettings).toHaveBeenCalledWith(settings) + }) }) diff --git a/src/playercomponent.js b/src/playercomponent.js index 01e447d9..25c3551e 100644 --- a/src/playercomponent.js +++ b/src/playercomponent.js @@ -456,7 +456,7 @@ function PlayerComponent( } function updateSettings(settings) { - playbackStrategy?.updateSettings(settings) + if (playbackStrategy?.updateSettings) playbackStrategy.updateSettings(settings) } return { diff --git a/src/playercomponent.test.js b/src/playercomponent.test.js index 0f35276f..33a8b882 100644 --- a/src/playercomponent.test.js +++ b/src/playercomponent.test.js @@ -275,6 +275,27 @@ describe("Player Component", () => { expect(updateSettings).toHaveBeenCalledWith(settings) }) + + it("Does not throw if the strategy does not have updateSettings", async () => { + const settings = { updatedSetting1: true, updatedSetting2: 1 } + const mockStrategy = createMockPlaybackStrategy(LiveSupport.SEEKABLE) + const mockPlaybackStrategyClass = jest.fn().mockReturnValue(mockStrategy) + StrategyPicker.mockResolvedValueOnce(mockPlaybackStrategyClass) + + const playbackElement = createPlaybackElement() + + const playerComponent = new PlayerComponent( + playbackElement, + bigscreenPlayerData, + mockMediaSources, + jest.fn(), + jest.fn() + ) + + await jest.runOnlyPendingTimersAsync() + + expect(() => playerComponent.updateSettings(settings)).not.toThrow() + }) }) describe("pause", () => { From 69aa8b8009a9d06bfc9ae95c19fe09f5a2dc5ee5 Mon Sep 17 00:00:00 2001 From: Sophia Lydia Morris-Hind Date: Sat, 2 Aug 2025 21:53:38 +0100 Subject: [PATCH 4/4] MSE Strat Tests only seekDurationPadding doesn't work --- src/playbackstrategy/msestrategy.js | 20 ++++--- src/playbackstrategy/msestrategy.test.js | 71 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/playbackstrategy/msestrategy.js b/src/playbackstrategy/msestrategy.js index ae67a456..2a6fae3f 100644 --- a/src/playbackstrategy/msestrategy.js +++ b/src/playbackstrategy/msestrategy.js @@ -58,13 +58,16 @@ function MSEStrategy( let errorCallback let timeUpdateCallback - const seekDurationPadding = isNaN(playerSettings.streaming?.seekDurationPadding) + let seekDurationPadding = isNaN(playerSettings.streaming?.seekDurationPadding) ? DEFAULT_SETTINGS.seekDurationPadding : playerSettings.streaming?.seekDurationPadding + const liveDelay = isNaN(playerSettings.streaming?.delay?.liveDelay) ? DEFAULT_SETTINGS.liveDelay : playerSettings.streaming?.delay?.liveDelay + let isEnded = false + const cached = { seekableRange: undefined, duration: 0, @@ -1017,24 +1020,27 @@ function MSEStrategy( }) } - function updateSettings(settings) { + function updateSettings(playerSettings) { + const settings = Utils.deepClone(playerSettings) + if (settings?.failoverResetTime) { mediaSources.updateSettings({ failoverResetTime: settings.failoverResetTime }) mediaPlayer.updateSettings({ streaming: { blacklistExpiryTime: mediaSources.failoverResetTime() } }) + + delete settings.failoverResetTime } if (settings?.failoverSort) { mediaSources.updateSettings({ failoverSort: settings.failoverSort }) + + delete settings.failoverSort } if (settings?.streaming?.seekDurationPadding) { seekDurationPadding = settings.streaming.seekDurationPadding - } - // Remove BSP specific settings - delete settings.failoverResetTime - delete settings.failoverSort - delete settings.streaming?.seekDurationPadding + delete settings.streaming?.seekDurationPadding + } // If we still have settings, pass them to Dash if (Object.keys(settings).length > 0) mediaPlayer.updateSettings(settings) diff --git a/src/playbackstrategy/msestrategy.test.js b/src/playbackstrategy/msestrategy.test.js index e7b0b48f..588705d5 100644 --- a/src/playbackstrategy/msestrategy.test.js +++ b/src/playbackstrategy/msestrategy.test.js @@ -107,6 +107,7 @@ const mockMediaSources = { currentProtectionData: jest.fn(), availableSources: jest.fn().mockReturnValue([]), failover: jest.fn().mockResolvedValue(), + updateSettings: jest.fn(), } describe("Media Source Extensions Playback Strategy", () => { @@ -1940,4 +1941,74 @@ describe("Media Source Extensions Playback Strategy", () => { expect(bitrate).toBe(100) }) }) + + describe("Update Settings", () => { + let mseStrategy + + beforeEach(() => { + mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement) + mseStrategy.load(null, 0) + }) + + it("updates Media Sources and Dash with a new failoverResetTime", () => { + const newFailoverResetTime = 42 + const settings = { failoverResetTime: newFailoverResetTime } + + mockMediaSources.failoverResetTime.mockReturnValueOnce(newFailoverResetTime) + + mseStrategy.updateSettings(settings) + + expect(mockMediaSources.updateSettings).toHaveBeenCalledWith(settings) + expect(mockDashInstance.updateSettings).toHaveBeenCalledWith({ + streaming: { + blacklistExpiryTime: newFailoverResetTime, + }, + }) + }) + + it("updates Media Sources with a new failoverSort", () => { + const newFailoverSort = jest.fn() + const settings = { failoverSort: newFailoverSort } + + mseStrategy.updateSettings(settings) + + expect(mockMediaSources.updateSettings).toHaveBeenCalledWith(settings) + }) + + it("updates with a new seekDurationPadding", () => { + mockDashInstance.duration.mockReturnValueOnce(360) + + const seekDurationPadding = 0.1 + + const mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement, false, { + streaming: { seekDurationPadding }, + }) + + mseStrategy.load(null, 0) + + mockDashInstance.isReady.mockReturnValue(true) + mseStrategy.setCurrentTime(360) + + expect(mockDashInstance.seek).toHaveBeenCalledWith(359.9) + + mediaElement.dispatchEvent(new Event("seeking")) + mediaElement.dispatchEvent(new Event("seeked")) + + const newSeekDurationPadding = 1 + const settings = { seekDurationPadding: newSeekDurationPadding } + + mseStrategy.updateSettings(settings) + mseStrategy.setCurrentTime(360) + + expect(mockDashInstance.seek).toHaveBeenCalledWith(359) + }) + + it("passes non BSP Extended settings to Dash", () => { + const settings = { someNonBSPExtendedSetting: true } + + mseStrategy.updateSettings(settings) + + expect(mockDashInstance.updateSettings).toHaveBeenCalledWith(settings) + }) + }) })