diff --git a/src/bigscreenplayer.js b/src/bigscreenplayer.js index ae8b2945..21d82a9f 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 && 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/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..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,6 +1020,32 @@ function MSEStrategy( }) } + 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 + + 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 +1092,7 @@ function MSEStrategy( getPlaybackRate: () => mediaPlayer.getPlaybackRate(), setBitrateConstraint, getPlaybackBitrate: (mediaKind) => currentPlaybackBitrateInKbps(mediaKind), + updateSettings, } } 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) + }) + }) }) diff --git a/src/playercomponent.js b/src/playercomponent.js index 0cbc0580..25c3551e 100644 --- a/src/playercomponent.js +++ b/src/playercomponent.js @@ -455,6 +455,10 @@ function PlayerComponent( return playbackStrategy?.getPlaybackBitrate(mediaKind) } + function updateSettings(settings) { + if (playbackStrategy?.updateSettings) playbackStrategy.updateSettings(settings) + } + return { play, pause, @@ -477,6 +481,7 @@ function PlayerComponent( setSubtitles, setBitrateConstraint, getPlaybackBitrate, + updateSettings, } } diff --git a/src/playercomponent.test.js b/src/playercomponent.test.js index e3638f52..33a8b882 100644 --- a/src/playercomponent.test.js +++ b/src/playercomponent.test.js @@ -251,6 +251,51 @@ 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) + }) + + 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", () => { 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: {