From 31168460d49f165ced1595348fcc424c725fab3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eirik=20Jo=20Bj=C3=B6rnerstedt?= <44924723+eirikbjornr@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:03:31 +0000 Subject: [PATCH] Add MediaPlayer capability to refresh manifest (#4330) --- index.d.ts | 2 + src/streaming/MediaPlayer.js | 37 ++++++ src/streaming/controllers/StreamController.js | 10 ++ test/unit/mocks/ManifestUpdaterMock.js | 4 + test/unit/mocks/StreamControllerMock.js | 2 + test/unit/streaming.MediaPlayer.js | 111 ++++++++++++++++++ .../streaming.controllers.StreamController.js | 33 ++++++ 7 files changed, 199 insertions(+) diff --git a/index.d.ts b/index.d.ts index 9eef3f8bcd..851f041ce3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1254,6 +1254,8 @@ declare namespace dashjs { attachSource(urlOrManifest: string | object, startTime?: number | string): void; + refreshManifest(callback: (manifest: object | null, error: unknown) => void): void; + isReady(): boolean; preload(): void; diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 40f6792873..ebe94fcddf 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -1907,6 +1907,42 @@ function MediaPlayer() { } } + /** + * Reload the manifest that the player is currently using. + * + * @memberof module:MediaPlayer + * @param {function} callback - A Callback function provided when retrieving manifests + * @instance + */ + function refreshManifest(callback) { + if (!mediaPlayerInitialized) { + throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; + } + + if (!isReady()) { + return callback(null, SOURCE_NOT_ATTACHED_ERROR); + } + + let self = this; + + if (typeof callback === 'function') { + const handler = function (e) { + eventBus.off(Events.INTERNAL_MANIFEST_LOADED, handler, self); + + if (e.error) { + callback(null, e.error); + return; + } + + callback(e.manifest); + }; + + eventBus.on(Events.INTERNAL_MANIFEST_LOADED, handler, self); + } + + streamController.refreshManifest(); + } + /** * Get the current settings object being used on the player. * @returns {PlayerSettings} The settings object being used. @@ -2449,6 +2485,7 @@ function MediaPlayer() { extend, attachView, attachSource, + refreshManifest, isReady, preload, play, diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index b5d2669ba6..e146999ab9 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1522,6 +1522,9 @@ function StreamController() { if (config.segmentBaseController) { segmentBaseController = config.segmentBaseController; } + if (config.manifestUpdater) { + manifestUpdater = config.manifestUpdater; + } } function setProtectionData(protData) { @@ -1607,6 +1610,12 @@ function StreamController() { } } + function refreshManifest() { + if (!manifestUpdater.getIsUpdating()) { + manifestUpdater.refreshManifest(); + } + } + function getStreams() { return streams; } @@ -1632,6 +1641,7 @@ function StreamController() { getActiveStream, getInitialPlayback, getAutoPlay, + refreshManifest, reset }; diff --git a/test/unit/mocks/ManifestUpdaterMock.js b/test/unit/mocks/ManifestUpdaterMock.js index d501eb9df0..8d432d4bd6 100644 --- a/test/unit/mocks/ManifestUpdaterMock.js +++ b/test/unit/mocks/ManifestUpdaterMock.js @@ -9,6 +9,10 @@ class ManifestUpdaterMock { getManifestLoader() { return this.manifestLoader; } + + refreshManifest() {} + + getIsUpdating() {} } export default ManifestUpdaterMock; \ No newline at end of file diff --git a/test/unit/mocks/StreamControllerMock.js b/test/unit/mocks/StreamControllerMock.js index 92790e9e0e..4f7ceba6a8 100644 --- a/test/unit/mocks/StreamControllerMock.js +++ b/test/unit/mocks/StreamControllerMock.js @@ -14,6 +14,8 @@ class StreamControllerMock { this.streams = streams; } + refreshManifest() {} + getStreams() { return this.streams; } diff --git a/test/unit/streaming.MediaPlayer.js b/test/unit/streaming.MediaPlayer.js index ccce0dc583..aedca63f22 100644 --- a/test/unit/streaming.MediaPlayer.js +++ b/test/unit/streaming.MediaPlayer.js @@ -10,12 +10,16 @@ import MediaPlayerModelMock from './mocks//MediaPlayerModelMock'; import MediaControllerMock from './mocks/MediaControllerMock'; import ObjectUtils from './../../src/streaming/utils/ObjectUtils'; import Constants from '../../src/streaming/constants/Constants'; +import Events from '../../src/core/events/Events'; +import EventBus from '../../src/core/EventBus' import Settings from '../../src/core/Settings'; import ABRRulesCollection from '../../src/streaming/rules/abr/ABRRulesCollection'; import CustomParametersModel from '../../src/streaming/models/CustomParametersModel'; +const sinon = require('sinon'); const expect = require('chai').expect; const ELEMENT_NOT_ATTACHED_ERROR = 'You must first call attachView() to set the video element before calling this method'; +const SOURCE_NOT_ATTACHED_ERROR = 'You must first call attachSource() with a valid source before calling this method'; const PLAYBACK_NOT_INITIALIZED_ERROR = 'You must first call initialize() and set a valid source and view before calling this method'; const STREAMING_NOT_INITIALIZED_ERROR = 'You must first call initialize() and set a source before calling this method'; const MEDIA_PLAYER_NOT_INITIALIZED_ERROR = 'MediaPlayer not initialized!'; @@ -1082,6 +1086,22 @@ describe('MediaPlayer', function () { it('Method attachSource should throw an exception', function () { expect(player.attachSource).to.throw(MediaPlayer.NOT_INITIALIZED_ERROR_MSG); }); + + it('Method refreshManifest should throw an exception', () => { + expect(player.refreshManifest).to.throw(MEDIA_PLAYER_NOT_INITIALIZED_ERROR); + }); + }); + + describe('When it is not ready', () => { + it('triggers refreshManifest callback with an error', () => { + player.initialize(videoElementMock, null, false); + + const stub = sinon.spy() + + player.refreshManifest(stub) + + expect(stub.calledWith(null, SOURCE_NOT_ATTACHED_ERROR)).to.be.true; + }) }); }); @@ -1113,3 +1133,94 @@ describe('MediaPlayer', function () { }); }); }); + +describe('MediaPlayer with context injected', () => { + const specHelper = new SpecHelper(); + const videoElementMock = new VideoElementMock(); + const capaMock = new CapabilitiesMock(); + const streamControllerMock = new StreamControllerMock(); + const abrControllerMock = new AbrControllerMock(); + const playbackControllerMock = new PlaybackControllerMock(); + const mediaPlayerModel = new MediaPlayerModelMock(); + const mediaControllerMock = new MediaControllerMock(); + + let player; + let eventBus; + let settings; + + beforeEach(function () { + // tear down + player = null; + settings?.reset(); + settings = null; + global.dashjs = {}; + + // init + const context = {}; + + const customParametersModel = CustomParametersModel(context).getInstance(); + eventBus = EventBus(context).getInstance(); + settings = Settings(context).getInstance(); + + player = MediaPlayer(context).create(); + + // to avoid unwanted log + const debug = player.getDebug(); + expect(debug).to.exist; // jshint ignore:line + + player.setConfig({ + streamController: streamControllerMock, + capabilities: capaMock, + playbackController: playbackControllerMock, + mediaPlayerModel: mediaPlayerModel, + abrController: abrControllerMock, + mediaController: mediaControllerMock, + settings: settings, + customParametersModel + }); + }); + + describe('Tools Functions', () => { + describe('When the player is initialised', () => { + before(() => { + sinon.spy(streamControllerMock, 'refreshManifest'); + }) + + beforeEach(() => { + streamControllerMock.refreshManifest.resetHistory(); + + mediaControllerMock.reset(); + }); + + it('should refresh manifest on the current stream', () => { + player.initialize(videoElementMock, specHelper.getDummyUrl(), false); + + const stub = sinon.spy(); + + player.refreshManifest(stub); + + expect(streamControllerMock.refreshManifest.calledOnce).to.be.true; + + eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, { manifest: { __mocked: true } }); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(sinon.match({ __mocked: true }))).to.be.true; + }); + + it('should trigger refreshManifest callback with an error if refresh failed', () => { + player.initialize(videoElementMock, specHelper.getDummyUrl(), false); + + const stub = sinon.spy(); + + player.refreshManifest(stub); + + expect(streamControllerMock.refreshManifest.calledOnce).to.be.true; + + eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, { error: 'Mocked!' }); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(null, 'Mocked!')).to.be.true; + }); + }) + }) +}) diff --git a/test/unit/streaming.controllers.StreamController.js b/test/unit/streaming.controllers.StreamController.js index e7d2e9853e..5325197d9e 100644 --- a/test/unit/streaming.controllers.StreamController.js +++ b/test/unit/streaming.controllers.StreamController.js @@ -19,6 +19,7 @@ import URIFragmentModelMock from './mocks/URIFragmentModelMock'; import CapabilitiesFilterMock from './mocks/CapabilitiesFilterMock'; import ContentSteeringControllerMock from './mocks/ContentSteeringControllerMock'; import TextControllerMock from './mocks/TextControllerMock'; +import ManifestUpdaterMock from './mocks/ManifestUpdaterMock'; import ServiceDescriptionController from '../../src/dash/controllers/ServiceDescriptionController'; const chai = require('chai'); @@ -48,6 +49,7 @@ const uriFragmentModelMock = new URIFragmentModelMock(); const capabilitiesFilterMock = new CapabilitiesFilterMock(); const textControllerMock = new TextControllerMock(); const contentSteeringControllerMock = new ContentSteeringControllerMock(); +const manifestUpdaterMock = new ManifestUpdaterMock(); Events.extend(ProtectionEvents); @@ -479,4 +481,35 @@ describe('StreamController', function () { }); }); }); + + describe('refreshManifest', function () { + beforeEach(function () { + streamController.setConfig({ + manifestUpdater: manifestUpdaterMock + }); + sinon.spy(manifestUpdaterMock, 'refreshManifest'); + sinon.stub(manifestUpdaterMock, 'getIsUpdating'); + }); + + + afterEach(function () { + manifestUpdaterMock.refreshManifest.restore(); + manifestUpdaterMock.getIsUpdating.restore(); + }); + + + it('calls refreshManifest on ManifestUpdater', function () { + streamController.refreshManifest(); + + expect(manifestUpdaterMock.refreshManifest.calledOnce).to.be.true; + }); + + it('does not call refreshManifest on ManifrstUpdater if it is already updating', function () { + manifestUpdaterMock.getIsUpdating.returns(true) + + streamController.refreshManifest(); + + expect(manifestUpdaterMock.refreshManifest.notCalled).to.be.true; + }) + }); });