diff --git a/docs/api/bigscreenplayer.js.html b/docs/api/bigscreenplayer.js.html index f8c806b8..62c1088e 100644 --- a/docs/api/bigscreenplayer.js.html +++ b/docs/api/bigscreenplayer.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -113,634 +116,667 @@

/**
  * @module bigscreenplayer/bigscreenplayer
  */
-import MediaState from './models/mediastate'
-import PlayerComponent from './playercomponent'
-import PauseTriggers from './models/pausetriggers'
-import DynamicWindowUtils from './dynamicwindowutils'
-import WindowTypes from './models/windowtypes'
-import MockBigscreenPlayer from './mockbigscreenplayer'
-import Plugins from './plugins'
-import Chronicle from './debugger/chronicle'
-import DebugTool from './debugger/debugtool'
-import SlidingWindowUtils from './utils/timeutils'
-import callCallbacks from './utils/callcallbacks'
-import MediaSources from './mediasources'
-import Version from './version'
-import Resizer from './resizer'
-import ReadyHelper from './readyhelper'
-import Subtitles from './subtitles/subtitles'
-import './typedefs'
-
-function BigscreenPlayer () {
-  let stateChangeCallbacks = []
-  let timeUpdateCallbacks = []
-  let subtitleCallbacks = []
-
-  let playerReadyCallback
-  let playerErrorCallback
-  let mediaKind
-  let initialPlaybackTimeEpoch
-  let serverDate
-  let playerComponent
-  let resizer
-  let pauseTrigger
-  let isSeeking = false
-  let endOfStream
-  let windowType
-  let mediaSources
-  let playbackElement
-  let readyHelper
-  let subtitles
-
-  const END_OF_STREAM_TOLERANCE = 10
-
-  function mediaStateUpdateCallback (evt) {
-    if (evt.timeUpdate) {
-      DebugTool.time(evt.data.currentTime)
-      callCallbacks(timeUpdateCallbacks, {
-        currentTime: evt.data.currentTime,
-        endOfStream: endOfStream
+import MediaState from "./models/mediastate"
+import PlayerComponent from "./playercomponent"
+import PauseTriggers from "./models/pausetriggers"
+import DynamicWindowUtils from "./dynamicwindowutils"
+import WindowTypes from "./models/windowtypes"
+import MockBigscreenPlayer from "./mockbigscreenplayer"
+import Plugins from "./plugins"
+import Chronicle from "./debugger/chronicle"
+import DebugTool from "./debugger/debugtool"
+import SlidingWindowUtils from "./utils/timeutils"
+import callCallbacks from "./utils/callcallbacks"
+import MediaSources from "./mediasources"
+import Version from "./version"
+import Resizer from "./resizer"
+import ReadyHelper from "./readyhelper"
+import Subtitles from "./subtitles/subtitles"
+
+export default class BigscreenPlayer {
+  static version = Version
+
+  static END_OF_STREAM_TOLERANCE = 10
+
+  /**
+   * @return the live support of the device.
+   */
+  static getLiveSupport() {
+    return PlayerComponent.getLiveSupport()
+  }
+
+  #stateChangeCallbacks = []
+  #timeUpdateCallbacks = []
+  #subtitleCallbacks = []
+  #playerReadyCallback
+  #playerErrorCallback
+  #mediaKind
+  #initialPlaybackTimeEpoch
+  #serverDate
+  #playerComponent
+  #resizer
+  #pauseTrigger
+  #isSeeking = false
+  #endOfStream
+  #windowType
+  #mediaSources
+  #playbackElement
+  #readyHelper
+  #subtitles
+
+  /**
+   * @typedef {Object} MediaConnection
+   * @property {String} url - media endpoint
+   * @property {String} cdn - identifier for the endpoint
+   */
+
+  /**
+   * Data required for playback
+   * @typedef {Object} BigscreenPlayerData
+   * @property {Object} media
+   * @property {string} media.type - source type e.g 'application/dash+xml'
+   * @property {string} media.mimeType - mimeType e.g 'video/mp4'
+   * @property {string} media.kind - 'video' or 'audio'
+   * @property {string} media.captionsUrl - 'Location for a captions file'
+   * @property {MediaConnection[]} media.urls - Media urls to use
+   * @property {Date} serverDate - Date object with server time offset
+   * @property {WindowTypes} windowType
+   * @property {boolean} subtitlesEnabled – Enable subtitles on initialisation
+   */
+
+  /**
+   *
+   * @typedef {object} ConstructionCallbacks
+   * @property {function} [callbacks.onSuccess] - Called after Bigscreen Player is initialised
+   * @property {function} [callbacks.onError] - Called when an error occurs during initialisation
+   */
+
+  /**
+   * Call first to initialise bigscreen player for playback.
+   * @param {HTMLDivElement} el - The Div element where content elements should be rendered
+   * @param {BigscreenPlayerData} data
+   * @param {ConstructionCallbacks} callbacks
+   */
+  constructor(el, data, callbacks) {
+    this.#playbackElement = el
+    this.#resizer = Resizer()
+    this.#windowType = data.windowType
+    this.#serverDate = data.serverDate
+    Chronicle.init()
+    DebugTool.setRootElement(this.#playbackElement)
+    DebugTool.keyValue({ key: "framework-version", value: Version })
+
+    this.#playerReadyCallback = callbacks?.onSuccess?.bind(this)
+    this.#playerErrorCallback = callbacks?.onError?.bind(this)
+
+    // Backwards compatibility with Old API; to be removed on Major Version Update
+    if (data.media && !data.media.captions && data.media.captionsUrl) {
+      data.media.captions = [
+        {
+          url: data.media.captionsUrl,
+        },
+      ]
+    }
+
+    this.#mediaSources = MediaSources()
+
+    const handleDataLoaded = this.#bigscreenPlayerDataLoaded.bind(this)
+
+    const mediaSourceCallbacks = {
+      onSuccess: () => handleDataLoaded(data),
+      onError: (error) => {
+        if (typeof callbacks?.onError === "function") {
+          callbacks.onError(error)
+        }
+      },
+    }
+
+    this.#mediaSources.init(
+      data.media,
+      this.#serverDate,
+      this.#windowType,
+      BigscreenPlayer.getLiveSupport(),
+      mediaSourceCallbacks
+    )
+  }
+
+  #bigscreenPlayerDataLoaded(data) {
+    if (this.#windowType !== WindowTypes.STATIC) {
+      data.time = this.#mediaSources.time()
+
+      this.#serverDate = data.serverDate
+      this.#initialPlaybackTimeEpoch = data.initialPlaybackTime
+
+      // overwrite initialPlaybackTime with video time (it comes in as epoch time for a sliding/growing window)
+      data.initialPlaybackTime = SlidingWindowUtils.convertToSeekableVideoTime(
+        data.initialPlaybackTime,
+        data.time.windowStartTime
+      )
+    }
+
+    this.#mediaKind = data.media.kind
+    this.#endOfStream =
+      this.#windowType !== WindowTypes.STATIC && !data.initialPlaybackTime && data.initialPlaybackTime !== 0
+
+    this.#readyHelper = new ReadyHelper(
+      data.initialPlaybackTime,
+      this.#windowType,
+      PlayerComponent.getLiveSupport(),
+      this.#playerReadyCallback
+    )
+
+    this.#playerComponent = new PlayerComponent(
+      this.#playbackElement,
+      data,
+      this.#mediaSources,
+      this.#windowType,
+      this.#mediaStateUpdateCallback.bind(this),
+      this.#playerErrorCallback
+    )
+
+    this.#subtitles = Subtitles(
+      this.#playerComponent,
+      data.subtitlesEnabled,
+      this.#playbackElement,
+      data.media.subtitleCustomisation,
+      this.#mediaSources,
+      this.#callSubtitlesCallbacks.bind(this)
+    )
+  }
+
+  #mediaStateUpdateCallback(event) {
+    if (event.timeUpdate) {
+      DebugTool.time(event.data.currentTime)
+      callCallbacks(this.#timeUpdateCallbacks, {
+        currentTime: event.data.currentTime,
+        endOfStream: this.#endOfStream,
       })
     } else {
-      let stateObject = {state: evt.data.state}
+      let stateObject = { state: event.data.state }
 
-      if (evt.data.state === MediaState.PAUSED) {
-        endOfStream = false
-        stateObject.trigger = pauseTrigger || PauseTriggers.DEVICE
-        pauseTrigger = undefined
+      if (event.data.state === MediaState.PAUSED) {
+        this.#endOfStream = false
+        stateObject.trigger = this.#pauseTrigger || PauseTriggers.DEVICE
+        this.#pauseTrigger = undefined
       }
 
-      if (evt.data.state === MediaState.FATAL_ERROR) {
+      if (event.data.state === MediaState.FATAL_ERROR) {
         stateObject = {
           state: MediaState.FATAL_ERROR,
-          isBufferingTimeoutError: evt.isBufferingTimeoutError
+          isBufferingTimeoutError: event.isBufferingTimeoutError,
+          code: event.code,
+          message: event.message,
         }
       }
 
-      if (evt.data.state === MediaState.WAITING) {
-        stateObject.isSeeking = isSeeking
-        isSeeking = false
+      if (event.data.state === MediaState.WAITING) {
+        stateObject.isSeeking = this.#isSeeking
+        this.#isSeeking = false
       }
 
-      stateObject.endOfStream = endOfStream
+      stateObject.endOfStream = this.#endOfStream
       DebugTool.event(stateObject)
 
-      callCallbacks(stateChangeCallbacks, stateObject)
+      callCallbacks(this.#stateChangeCallbacks, stateObject)
     }
 
-    if (evt.data.seekableRange) {
-      DebugTool.keyValue({key: 'seekableRangeStart', value: deviceTimeToDate(evt.data.seekableRange.start)})
-      DebugTool.keyValue({key: 'seekableRangeEnd', value: deviceTimeToDate(evt.data.seekableRange.end)})
+    if (event.data.seekableRange) {
+      DebugTool.keyValue({ key: "seekableRangeStart", value: this.#deviceTimeToDate(event.data.seekableRange.start) })
+      DebugTool.keyValue({ key: "seekableRangeEnd", value: this.#deviceTimeToDate(event.data.seekableRange.end) })
     }
 
-    if (evt.data.duration) {
-      DebugTool.keyValue({key: 'duration', value: evt.data.duration})
+    if (event.data.duration) {
+      DebugTool.keyValue({ key: "duration", value: event.data.duration })
     }
 
-    if (playerComponent && readyHelper) {
-      readyHelper.callbackWhenReady(evt)
+    if (this.#playerComponent && this.#readyHelper) {
+      this.#readyHelper.callbackWhenReady(event)
     }
   }
 
-  function deviceTimeToDate (time) {
-    if (getWindowStartTime()) {
-      return new Date(convertVideoTimeSecondsToEpochMs(time))
-    } else {
-      return new Date(time * 1000)
+  #deviceTimeToDate(time) {
+    return this.#getWindowStartTime() ? new Date(this.convertVideoTimeSecondsToEpochMs(time)) : new Date(time * 1000)
+  }
+
+  #getWindowStartTime() {
+    return this.#mediaSources && this.#mediaSources.time().windowStartTime
+  }
+
+  #getWindowEndTime() {
+    return this.#mediaSources && this.#mediaSources.time().windowEndTime
+  }
+
+  #callSubtitlesCallbacks(enabled) {
+    callCallbacks(this.#subtitleCallbacks, { enabled })
+  }
+
+  /**
+   * Should be called at the end of all playback sessions. Resets state and clears any UI.
+   */
+  tearDown() {
+    if (this.#subtitles) {
+      this.#subtitles.tearDown()
+      this.#subtitles = undefined
+    }
+
+    if (this.#playerComponent) {
+      this.#playerComponent.tearDown()
+      this.#playerComponent = undefined
+    }
+
+    if (this.#mediaSources) {
+      this.#mediaSources.tearDown()
+      this.#mediaSources = undefined
     }
+
+    this.#stateChangeCallbacks = []
+    this.#timeUpdateCallbacks = []
+    this.#subtitleCallbacks = []
+    this.#endOfStream = undefined
+    this.#mediaKind = undefined
+    this.#pauseTrigger = undefined
+    this.#windowType = undefined
+    this.#resizer = undefined
+    this.unregisterPlugin()
+    DebugTool.tearDown()
+    Chronicle.tearDown()
   }
 
-  function convertVideoTimeSecondsToEpochMs (seconds) {
-    return getWindowStartTime() ? getWindowStartTime() + (seconds * 1000) : undefined
+  /**
+   * Pass a function to call whenever the player transitions state.
+   * @see {@link module:models/mediastate}
+   * @param {Function} callback
+   */
+  registerForStateChanges(callback) {
+    this.#stateChangeCallbacks.push(callback)
+    return callback
   }
 
-  function bigscreenPlayerDataLoaded (bigscreenPlayerData, enableSubtitles) {
-    if (windowType !== WindowTypes.STATIC) {
-      bigscreenPlayerData.time = mediaSources.time()
-      serverDate = bigscreenPlayerData.serverDate
+  /**
+   * Unregisters a previously registered callback.
+   * @param {Function} callback
+   */
+  unregisterForStateChanges(callback) {
+    const indexOf = this.#stateChangeCallbacks.indexOf(callback)
+    if (indexOf !== -1) {
+      this.#stateChangeCallbacks.splice(indexOf, 1)
+    }
+  }
 
-      initialPlaybackTimeEpoch = bigscreenPlayerData.initialPlaybackTime
-      // overwrite initialPlaybackTime with video time (it comes in as epoch time for a sliding/growing window)
-      bigscreenPlayerData.initialPlaybackTime = SlidingWindowUtils.convertToSeekableVideoTime(bigscreenPlayerData.initialPlaybackTime, bigscreenPlayerData.time.windowStartTime)
+  /**
+   * Pass a function to call whenever the player issues a time update.
+   * @param {Function} callback
+   */
+  registerForTimeUpdates(callback) {
+    this.#timeUpdateCallbacks.push(callback)
+    return callback
+  }
+
+  /**
+   * Unregisters a previously registered callback.
+   * @param {Function} callback
+   */
+  unregisterForTimeUpdates(callback) {
+    const indexOf = this.#timeUpdateCallbacks.indexOf(callback)
+    if (indexOf !== -1) {
+      this.#timeUpdateCallbacks.splice(indexOf, 1)
     }
+  }
 
-    mediaKind = bigscreenPlayerData.media.kind
-    endOfStream = windowType !== WindowTypes.STATIC && (!bigscreenPlayerData.initialPlaybackTime && bigscreenPlayerData.initialPlaybackTime !== 0)
+  /**
+   * Pass a function to be called whenever subtitles are enabled or disabled.
+   * @param {Function} callback
+   */
+  registerForSubtitleChanges(callback) {
+    this.#subtitleCallbacks.push(callback)
+    return callback
+  }
 
-    readyHelper = new ReadyHelper(
-      bigscreenPlayerData.initialPlaybackTime,
-      windowType,
-      PlayerComponent.getLiveSupport(),
-      playerReadyCallback
-    )
-    playerComponent = new PlayerComponent(
-      playbackElement,
-      bigscreenPlayerData,
-      mediaSources,
-      windowType,
-      mediaStateUpdateCallback,
-      playerErrorCallback
-    )
+  /**
+   * Unregisters a previously registered callback for changes to subtitles.
+   * @param {Function} callback
+   */
+  unregisterForSubtitleChanges(callback) {
+    const indexOf = this.#subtitleCallbacks.indexOf(callback)
+    if (indexOf !== -1) {
+      this.#subtitleCallbacks.splice(indexOf, 1)
+    }
+  }
 
-    subtitles = Subtitles(
-      playerComponent,
-      enableSubtitles,
-      playbackElement,
-      bigscreenPlayerData.media.subtitleCustomisation,
-      mediaSources,
-      callSubtitlesCallbacks
+  /**
+   * Sets the current time of the media asset.
+   * @param {Number} time - In seconds
+   */
+  setCurrentTime(time) {
+    DebugTool.apicall("setCurrentTime")
+    if (this.#playerComponent) {
+      // this flag must be set before calling into playerComponent.setCurrentTime
+      // as this synchronously fires a WAITING event (when native strategy).
+      this.#isSeeking = true
+      this.#playerComponent.setCurrentTime(time)
+      this.#endOfStream =
+        this.#windowType !== WindowTypes.STATIC &&
+        Math.abs(this.getSeekableRange().end - time) < BigscreenPlayer.END_OF_STREAM_TOLERANCE
+    }
+  }
+
+  setPlaybackRate(rate) {
+    this.#playerComponent?.setPlaybackRate(rate)
+  }
+
+  getPlaybackRate() {
+    return this.#playerComponent?.getPlaybackRate()
+  }
+
+  /**
+   * Returns the media asset's current time in seconds.
+   */
+  getCurrentTime() {
+    return this.#playerComponent?.getCurrentTime() ?? 0
+  }
+
+  /**
+   * Returns the current media kind.
+   * 'audio' or 'video'
+   */
+  getMediaKind() {
+    return this.#mediaKind
+  }
+
+  /**
+   * Returns the current window type.
+   * @see {@link module:bigscreenplayer/models/windowtypes}
+   */
+  getWindowType() {
+    return this.#windowType
+  }
+
+  /**
+   * Returns an object including the current start and end times.
+   * @returns {Object} {start: Number, end: Number}
+   */
+  getSeekableRange() {
+    return this.#playerComponent?.getSeekableRange() ?? {}
+  }
+
+  /**
+   * @returns {boolean} Returns true if media is initialised and playing a live stream within a tolerance of the end of the seekable range (10 seconds).
+   */
+  isPlayingAtLiveEdge() {
+    return !!(
+      this.#playerComponent &&
+      this.#windowType !== WindowTypes.STATIC &&
+      Math.abs(this.getSeekableRange().end - this.getCurrentTime()) < BigscreenPlayer.END_OF_STREAM_TOLERANCE
     )
   }
 
-  function getWindowStartTime () {
-    return mediaSources && mediaSources.time().windowStartTime
+  /**
+   * @return {Object} An object of the shape {windowStartTime: Number, windowEndTime: Number, initialPlaybackTime: Number, serverDate: Date}
+   */
+  getLiveWindowData() {
+    if (this.#windowType === WindowTypes.STATIC) {
+      return {}
+    }
+
+    return {
+      windowStartTime: this.#getWindowStartTime(),
+      windowEndTime: this.#getWindowEndTime(),
+      initialPlaybackTime: this.#initialPlaybackTimeEpoch,
+      serverDate: this.#serverDate,
+    }
   }
 
-  function getWindowEndTime () {
-    return mediaSources && mediaSources.time().windowEndTime
+  /**
+   * @returns the duration of the media asset.
+   */
+  getDuration() {
+    return this.#playerComponent?.getDuration()
   }
 
-  function toggleDebug () {
-    if (playerComponent) {
-      DebugTool.toggleVisibility()
+  /**
+   * @returns if the player is paused.
+   */
+  isPaused() {
+    return this.#playerComponent?.isPaused() ?? true
+  }
+
+  /**
+   * @returns if the media asset has ended.
+   */
+  isEnded() {
+    return this.#playerComponent?.isEnded() ?? false
+  }
+
+  /**
+   * Play the media assest from the current point in time.
+   */
+  play() {
+    DebugTool.apicall("play")
+    this.#playerComponent.play()
+  }
+
+  /**
+   * Pause the media asset.
+   * @param {*} opts
+   * @param {boolean} opts.userPause
+   * @param {boolean} opts.disableAutoResume
+   */
+  pause(opts) {
+    DebugTool.apicall("pause")
+    this.#pauseTrigger = opts?.userPause === false ? PauseTriggers.APP : PauseTriggers.USER
+    this.#playerComponent.pause(opts)
+  }
+
+  resize(top, left, width, height, zIndex) {
+    this.#subtitles.hide()
+    this.#resizer.resize(this.#playbackElement, top, left, width, height, zIndex)
+  }
+
+  clearResize() {
+    if (this.#subtitles.enabled()) this.#subtitles.show()
+    else this.#subtitles.hide()
+
+    this.#resizer.clear(this.#playbackElement)
+  }
+
+  /**
+   * Set whether or not subtitles should be enabled.
+   * @param {boolean} value
+   */
+  setSubtitlesEnabled(enabled) {
+    if (enabled) this.#subtitles.enable()
+    else this.#subtitles.disable()
+
+    this.#callSubtitlesCallbacks(enabled)
+
+    if (this.#resizer.isResized()) {
+      return
     }
+
+    if (enabled) this.#subtitles.show()
+    else this.#subtitles.hide()
+  }
+
+  /**
+   * @return if subtitles are currently enabled.
+   */
+  isSubtitlesEnabled() {
+    return this.#subtitles ? this.#subtitles.enabled() : false
   }
 
-  function callSubtitlesCallbacks (enabled) {
-    callCallbacks(subtitleCallbacks, { enabled: enabled })
+  /**
+   * @return Returns whether or not subtitles are currently enabled.
+   */
+  isSubtitlesAvailable() {
+    return this.#subtitles ? this.#subtitles.available() : false
   }
 
-  function setSubtitlesEnabled (enabled) {
-    enabled ? subtitles.enable() : subtitles.disable()
-    callSubtitlesCallbacks(enabled)
+  areSubtitlesCustomisable() {
+    return !window.bigscreenPlayer?.overrides?.legacySubtitles
+  }
 
-    if (!resizer.isResized()) {
-      enabled ? subtitles.show() : subtitles.hide()
+  customiseSubtitles(styleOpts) {
+    if (this.#subtitles) {
+      this.#subtitles.customise(styleOpts)
     }
   }
 
-  function isSubtitlesEnabled () {
-    return subtitles ? subtitles.enabled() : false
-  }
-
-  function isSubtitlesAvailable () {
-    return subtitles ? subtitles.available() : false
-  }
-
-  return /** @alias module:bigscreenplayer/bigscreenplayer */{
-
-    /**
-     * Call first to initialise bigscreen player for playback.
-     * @function
-     * @name init
-     * @param {HTMLDivElement} playbackElement - The Div element where content elements should be rendered
-     * @param {BigscreenPlayerData} bigscreenPlayerData
-     * @param {WindowTypes} newWindowType
-     * @param {boolean} enableSubtitles - Enable subtitles on initialisation
-     * @param {InitCallbacks} callbacks
-     */
-    init: (newPlaybackElement, bigscreenPlayerData, newWindowType, enableSubtitles, callbacks) => {
-      playbackElement = newPlaybackElement
-      Chronicle.init()
-      resizer = Resizer()
-      DebugTool.setRootElement(playbackElement)
-      DebugTool.keyValue({key: 'framework-version', value: Version})
-      windowType = newWindowType
-      serverDate = bigscreenPlayerData.serverDate
-      if (!callbacks) {
-        callbacks = {}
-      }
+  renderSubtitleExample(xmlString, styleOpts, safePosition) {
+    if (this.#subtitles) {
+      this.#subtitles.renderExample(xmlString, styleOpts, safePosition)
+    }
+  }
 
-      playerReadyCallback = callbacks.onSuccess
-      playerErrorCallback = callbacks.onError
+  clearSubtitleExample() {
+    if (this.#subtitles) {
+      this.#subtitles.clearExample()
+    }
+  }
 
-      const mediaSourceCallbacks = {
-        onSuccess: () => bigscreenPlayerDataLoaded(bigscreenPlayerData, enableSubtitles),
-        onError: (error) => {
-          if (callbacks.onError) {
-            callbacks.onError(error)
-          }
-        }
-      }
+  /**
+   *
+   * An enum may be used to set the on-screen position of any transport controls
+   * (work in progress to remove this - UI concern).
+   * @param {*} position
+   */
+  setTransportControlsPosition(position) {
+    if (this.#subtitles) {
+      this.#subtitles.setPosition(position)
+    }
+  }
 
-      mediaSources = MediaSources()
+  /**
+   * @return Returns whether the current media asset is seekable.
+   */
+  canSeek() {
+    return (
+      this.#windowType === WindowTypes.STATIC ||
+      DynamicWindowUtils.canSeek(
+        this.#getWindowStartTime(),
+        this.#getWindowEndTime(),
+        BigscreenPlayer.getLiveSupport(),
+        this.getSeekableRange()
+      )
+    )
+  }
 
-      // Backwards compatibility with Old API; to be removed on Major Version Update
-      if (bigscreenPlayerData.media && !bigscreenPlayerData.media.captions && bigscreenPlayerData.media.captionsUrl) {
-        bigscreenPlayerData.media.captions = [{
-          url: bigscreenPlayerData.media.captionsUrl
-        }]
-      }
+  /**
+   * @return Returns whether the current media asset is pausable.
+   */
+  canPause() {
+    return (
+      this.#windowType === WindowTypes.STATIC ||
+      DynamicWindowUtils.canPause(
+        this.#getWindowStartTime(),
+        this.#getWindowEndTime(),
+        BigscreenPlayer.getLiveSupport()
+      )
+    )
+  }
 
-      mediaSources.init(bigscreenPlayerData.media, serverDate, windowType, getLiveSupport(), mediaSourceCallbacks)
-    },
-
-    /**
-     * Should be called at the end of all playback sessions. Resets state and clears any UI.
-     * @function
-     * @name tearDown
-     */
-    tearDown: function () {
-      if (subtitles) {
-        subtitles.tearDown()
-        subtitles = undefined
-      }
+  /**
+   * Return a mock for in place testing.
+   * @param {*} opts
+   */
+  mock(opts) {
+    MockBigscreenPlayer.mock(this, opts)
+  }
 
-      if (playerComponent) {
-        playerComponent.tearDown()
-        playerComponent = undefined
-      }
+  /**
+   * Unmock the player.
+   */
+  unmock() {
+    MockBigscreenPlayer.unmock(this)
+  }
 
-      if (mediaSources) {
-        mediaSources.tearDown()
-        mediaSources = undefined
-      }
+  /**
+   * Return a mock for unit tests.
+   * @param {*} opts
+   */
+  mockJasmine(opts) {
+    MockBigscreenPlayer.mockJasmine(this, opts)
+  }
 
-      stateChangeCallbacks = []
-      timeUpdateCallbacks = []
-      subtitleCallbacks = []
-      endOfStream = undefined
-      mediaKind = undefined
-      pauseTrigger = undefined
-      windowType = undefined
-      resizer = undefined
-      this.unregisterPlugin()
-      DebugTool.tearDown()
-      Chronicle.tearDown()
-    },
-
-    /**
-     * Pass a function to call whenever the player transitions state.
-     * @see {@link module:models/mediastate}
-     * @function
-     * @param {Function} callback
-     */
-    registerForStateChanges: (callback) => {
-      stateChangeCallbacks.push(callback)
-      return callback
-    },
-
-    /**
-     * Unregisters a previously registered callback.
-     * @function
-     * @param {Function} callback
-     */
-    unregisterForStateChanges: (callback) => {
-      const indexOf = stateChangeCallbacks.indexOf(callback)
-      if (indexOf !== -1) {
-        stateChangeCallbacks.splice(indexOf, 1)
-      }
-    },
-
-    /**
-     * Pass a function to call whenever the player issues a time update.
-     * @function
-     * @param {Function} callback
-     */
-    registerForTimeUpdates: (callback) => {
-      timeUpdateCallbacks.push(callback)
-      return callback
-    },
-
-    /**
-     * Unregisters a previously registered callback.
-     * @function
-     * @param {Function} callback
-     */
-    unregisterForTimeUpdates: (callback) => {
-      const indexOf = timeUpdateCallbacks.indexOf(callback)
-
-      if (indexOf !== -1) {
-        timeUpdateCallbacks.splice(indexOf, 1)
-      }
-    },
-
-    /**
-     * Pass a function to be called whenever subtitles are enabled or disabled.
-     * @function
-     * @param {Function} callback
-     */
-    registerForSubtitleChanges: (callback) => {
-      subtitleCallbacks.push(callback)
-      return callback
-    },
-
-    /**
-     * Unregisters a previously registered callback for changes to subtitles.
-     * @function
-     * @param {Function} callback
-     */
-    unregisterForSubtitleChanges: (callback) => {
-      const indexOf = subtitleCallbacks.indexOf(callback)
-      if (indexOf !== -1) {
-        subtitleCallbacks.splice(indexOf, 1)
-      }
-    },
-
-    /**
-     * Sets the current time of the media asset.
-     * @function
-     * @param {Number} time - In seconds
-     */
-    setCurrentTime: function (time) {
-      DebugTool.apicall('setCurrentTime')
-      if (playerComponent) {
-        // this flag must be set before calling into playerComponent.setCurrentTime - as this synchronously fires a WAITING event (when native strategy).
-        isSeeking = true
-        playerComponent.setCurrentTime(time)
-        endOfStream = windowType !== WindowTypes.STATIC && Math.abs(this.getSeekableRange().end - time) < END_OF_STREAM_TOLERANCE
-      }
-    },
+  /**
+   * Register a plugin for extended events.
+   * @param {*} plugin
+   */
+  registerPlugin(plugin) {
+    Plugins.registerPlugin(plugin)
+  }
 
-    setPlaybackRate: (rate) => {
-      if (playerComponent) {
-        playerComponent.setPlaybackRate(rate)
-      }
-    },
-
-    getPlaybackRate: () => playerComponent && playerComponent.getPlaybackRate(),
-
-    /**
-     * Returns the media asset's current time in seconds.
-     * @function
-     */
-    getCurrentTime: () => playerComponent && playerComponent.getCurrentTime() || 0,
-
-    /**
-     * Returns the current media kind.
-     * 'audio' or 'video'
-     * @function
-     */
-    getMediaKind: () => mediaKind,
-
-    /**
-     * Returns the current window type.
-     * @see {@link module:bigscreenplayer/models/windowtypes}
-     * @function
-     */
-    getWindowType: () => windowType,
-
-    /**
-     * Returns an object including the current start and end times.
-     * @function
-     * @returns {Object} {start: Number, end: Number}
-     */
-    getSeekableRange: () => playerComponent ? playerComponent.getSeekableRange() : {},
-
-    /**
-    * @function
-    * @returns {boolean} Returns true if media is initialised and playing a live stream within a tolerance of the end of the seekable range (10 seconds).
-    */
-    isPlayingAtLiveEdge: function () {
-      return !!playerComponent && windowType !== WindowTypes.STATIC && Math.abs(this.getSeekableRange().end - this.getCurrentTime()) < END_OF_STREAM_TOLERANCE
-    },
-
-    /**
-     * @function
-     * @return {Object} An object of the shape {windowStartTime: Number, windowEndTime: Number, initialPlaybackTime: Number, serverDate: Date}
-     */
-    getLiveWindowData: () => {
-      if (windowType === WindowTypes.STATIC) {
-        return {}
-      }
+  /**
+   * Unregister a previously registered plugin.
+   * @param {*} plugin
+   */
+  unregisterPlugin(plugin) {
+    Plugins.unregisterPlugin(plugin)
+  }
 
-      return {
-        windowStartTime: getWindowStartTime(),
-        windowEndTime: getWindowEndTime(),
-        initialPlaybackTime: initialPlaybackTimeEpoch,
-        serverDate: serverDate
-      }
-    },
-
-    /**
-     * @function
-     * @returns the duration of the media asset.
-     */
-    getDuration: () => playerComponent && playerComponent.getDuration(),
-
-    /**
-     * @function
-     * @returns if the player is paused.
-     */
-    isPaused: () => playerComponent ? playerComponent.isPaused() : true,
-
-    /**
-     * @function
-     * @returns if the media asset has ended.
-     */
-    isEnded: () => playerComponent ? playerComponent.isEnded() : false,
-
-    /**
-     * Play the media assest from the current point in time.
-     * @function
-     */
-    play: () => {
-      DebugTool.apicall('play')
-      playerComponent.play()
-    },
-    /**
-     * Pause the media asset.
-     * @function
-     * @param {*} opts
-     * @param {boolean} opts.userPause
-     * @param {boolean} opts.disableAutoResume
-     */
-    pause: (opts) => {
-      DebugTool.apicall('pause')
-      pauseTrigger = opts && opts.userPause === false ? PauseTriggers.APP : PauseTriggers.USER
-      playerComponent.pause(opts)
-    },
-
-    resize: (top, left, width, height, zIndex) => {
-      subtitles.hide()
-      resizer.resize(playbackElement, top, left, width, height, zIndex)
-    },
-
-    clearResize: () => {
-      if (subtitles.enabled()) {
-        subtitles.show()
-      } else {
-        subtitles.hide()
-      }
-      resizer.clear(playbackElement)
-    },
-
-    /**
-     * Set whether or not subtitles should be enabled.
-     * @function
-     * @param {boolean} value
-     */
-    setSubtitlesEnabled: setSubtitlesEnabled,
-
-    /**
-     * @function
-     * @return if subtitles are currently enabled.
-     */
-    isSubtitlesEnabled: isSubtitlesEnabled,
-
-    /**
-     * @function
-     * @return Returns whether or not subtitles are currently enabled.
-     */
-    isSubtitlesAvailable: isSubtitlesAvailable,
-
-    areSubtitlesCustomisable: () => {
-      return !(window.bigscreenPlayer && window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.legacySubtitles)
-    },
-
-    customiseSubtitles: (styleOpts) => {
-      if (subtitles) {
-        subtitles.customise(styleOpts)
-      }
-    },
+  /**
+   * Returns an object with a number of functions related to the ability to transition state
+   * given the current state and the playback strategy in use.
+   */
+  transitions() {
+    return this.#playerComponent?.transitions() ?? {}
+  }
 
-    renderSubtitleExample: (xmlString, styleOpts, safePosition) => {
-      if (subtitles) {
-        subtitles.renderExample(xmlString, styleOpts, safePosition)
-      }
-    },
+  /**
+   * @return The media element currently being used.
+   */
+  getPlayerElement() {
+    return this.#playerComponent?.getPlayerElement()
+  }
 
-    clearSubtitleExample: () => {
-      if (subtitles) {
-        subtitles.clearExample()
-      }
-    },
-
-    /**
-     *
-     * An enum may be used to set the on-screen position of any transport controls
-     * (work in progress to remove this - UI concern).
-     * @function
-     * @param {*} position
-     */
-    setTransportControlsPosition: (position) => {
-      if (subtitles) {
-        subtitles.setPosition(position)
-      }
-    },
-
-    /**
-     * @function
-     * @return Returns whether the current media asset is seekable.
-     */
-    canSeek: function () {
-      return windowType === WindowTypes.STATIC || DynamicWindowUtils.canSeek(getWindowStartTime(), getWindowEndTime(), getLiveSupport(), this.getSeekableRange())
-    },
-
-    /**
-     * @function
-     * @return Returns whether the current media asset is pausable.
-     */
-    canPause: () => {
-      return windowType === WindowTypes.STATIC || DynamicWindowUtils.canPause(getWindowStartTime(), getWindowEndTime(), getLiveSupport())
-    },
-
-    /**
-     * Return a mock for in place testing.
-     * @function
-     * @param {*} opts
-     */
-    mock: function (opts) { MockBigscreenPlayer.mock(this, opts) },
-
-    /**
-     * Unmock the player.
-     * @function
-     */
-    unmock: function () { MockBigscreenPlayer.unmock(this) },
-
-    /**
-     * Return a mock for unit tests.
-     * @function
-     * @param {*} opts
-     */
-    mockJasmine: function (opts) { MockBigscreenPlayer.mockJasmine(this, opts) },
-
-    /**
-     * Register a plugin for extended events.
-     * @function
-     * @param {*} plugin
-     */
-    registerPlugin: (plugin) => Plugins.registerPlugin(plugin),
-
-    /**
-     * Unregister a previously registered plugin.
-     * @function
-     * @param {*} plugin
-     */
-    unregisterPlugin: (plugin) => Plugins.unregisterPlugin(plugin),
-
-    /**
-     * Returns an object with a number of functions related to the ability to transition state
-     * given the current state and the playback strategy in use.
-     * @function
-     */
-    transitions: () => playerComponent ? playerComponent.transitions() : {},
-
-    /**
-     * @function
-     * @return The media element currently being used.
-     */
-    getPlayerElement: () => playerComponent && playerComponent.getPlayerElement(),
-
-    /**
-     * @function
-     * @param {Number} epochTime - Unix Epoch based time in milliseconds.
-     * @return the time in seconds within the current sliding window.
-     */
-    convertEpochMsToVideoTimeSeconds: (epochTime) => {
-      return getWindowStartTime() ? Math.floor((epochTime - getWindowStartTime()) / 1000) : undefined
-    },
-
-    /**
-     * @function
-     * @return The runtime version of the library.
-     */
-    getFrameworkVersion: () => {
-      return Version
-    },
-
-    /**
-     * @function
-     * @param {Number} time - Seconds
-     * @return the time in milliseconds within the current sliding window.
-     */
-    convertVideoTimeSecondsToEpochMs: convertVideoTimeSecondsToEpochMs,
-
-    /**
-     * Toggle the visibility of the debug tool overlay.
-     * @function
-     */
-    toggleDebug: toggleDebug,
-
-    /**
-     * @function
-     * @return {Object} - Key value pairs of available log levels
-     */
-    getLogLevels: () => DebugTool.logLevels,
-
-    /**
-     * @function
-     * @param logLevel -  log level to display @see getLogLevels
-     */
-    setLogLevel: DebugTool.setLogLevel,
-    getDebugLogs: () => Chronicle.retrieve()
+  /**
+   * @param {Number} epochTime - Unix Epoch based time in milliseconds.
+   * @return the time in seconds within the current sliding window.
+   */
+  convertEpochMsToVideoTimeSeconds(epochTime) {
+    return this.#getWindowStartTime() ? Math.floor((epochTime - this.#getWindowStartTime()) / 1000) : null
   }
-}
 
-/**
- * @function
- * @param {TALDevice} device
- * @return the live support of the device.
- */
-function getLiveSupport () {
-  return PlayerComponent.getLiveSupport()
-}
+  /**
+   * @return The runtime version of the library.
+   */
+  getFrameworkVersion() {
+    return Version
+  }
 
-BigscreenPlayer.getLiveSupport = getLiveSupport
+  /**
+   * @param {Number} time - Seconds
+   * @return the time in milliseconds within the current sliding window.
+   */
+  convertVideoTimeSecondsToEpochMs(seconds) {
+    return this.#getWindowStartTime() ? this.#getWindowStartTime() + seconds * 1000 : null
+  }
 
-BigscreenPlayer.version = Version
+  /**
+   * Toggle the visibility of the debug tool overlay.
+   */
+  toggleDebug() {
+    if (this.#playerComponent) {
+      DebugTool.toggleVisibility()
+    }
+  }
+
+  /**
+   * @return {Object} - Key value pairs of available log levels
+   */
+  static getLogLevels() {
+    return DebugTool.logLevels
+  }
+
+  /**
+   * @borrows DebugTool.setLogLevel as setLogLevel
+   * @param logLevel -  log level to display @see getLogLevels
+   */
+  static setLogLevel = DebugTool.setLogLevel
 
-export default BigscreenPlayer
+  static getDebugLogs() {
+    return Chronicle.retrieve()
+  }
+}
 
@@ -766,7 +802,7 @@

@@ -778,8 +814,6 @@

- - diff --git a/docs/api/domhelpers.js.html b/docs/api/domhelpers.js.html index 706a8a12..c4bb8555 100644 --- a/docs/api/domhelpers.js.html +++ b/docs/api/domhelpers.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -194,7 +197,7 @@

@@ -206,8 +209,6 @@

- - diff --git a/docs/api/global.html b/docs/api/global.html index 0cd467ef..f5c1896f 100644 --- a/docs/api/global.html +++ b/docs/api/global.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -1042,666 +1045,34 @@

-

Type Definitions

- - - -

- # - BigscreenPlayerData -

- - - - -
-

Data required for playback

-
- - - -
- Type: - -
- - - - - - -
Properties
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
media - - -Object - - - - -
Properties
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
type - - -String - - - -

source type e.g 'application/dash+xml'

mimeType - - -String - - - -

mimeType e.g 'video/mp4'

kind - - -String - - - -

'video' or 'audio'

captionsUrl - - -String - - - -

'Location for a captions file'

urls - - -Array.<MediaUrl> - - - -

Media urls to use

-
- -
serverDate - - -Date - - - -

Date object with server time offset

-
- - - - - -
- - - - + - + - - - - + - + - + + + - - - - - -
-
Source:
-
-
- - - - - - - -
- - - - - - - - - - -

- # - InitCallbacks -

- - - - - - -
- Type: - -
- - - - - - -
Properties
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
callbacks.onSuccess - - -function - - - - - - <optional>
- - - -

Called after Bigscreen Player is initialised

callbacks.onError - - -function - - - - - - <optional>
- - - -

Called when an error occurs during initialisation

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Source:
-
-
- - - - - - - -
- - - - - - - - - - -

- # - MediaUrl -

- - - - - - -
- Type: - -
- - - - - - -
Properties
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
url - - -String - - - -

media endpoint

cdn - - -String - - - -

identifier for the endpoint

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Source:
-
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + @@ -1713,8 +1084,6 @@
Properties
- - diff --git a/docs/api/index.html b/docs/api/index.html index f5914108..516faa68 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -180,7 +183,7 @@

License

@@ -192,8 +195,6 @@

License

- - diff --git a/docs/api/models_mediastate.js.html b/docs/api/models_mediastate.js.html index a37d9fb0..1ce098b0 100644 --- a/docs/api/models_mediastate.js.html +++ b/docs/api/models_mediastate.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -154,7 +157,7 @@

@@ -166,8 +169,6 @@

- - diff --git a/docs/api/models_transportcontrolposition.js.html b/docs/api/models_transportcontrolposition.js.html index b74e0d65..486a286e 100644 --- a/docs/api/models_transportcontrolposition.js.html +++ b/docs/api/models_transportcontrolposition.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -154,7 +157,7 @@

@@ -166,8 +169,6 @@

- - diff --git a/docs/api/models_windowtypes.js.html b/docs/api/models_windowtypes.js.html index 89145216..1b4b724d 100644 --- a/docs/api/models_windowtypes.js.html +++ b/docs/api/models_windowtypes.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -150,7 +153,7 @@

@@ -162,8 +165,6 @@

- - diff --git a/docs/api/module-bigscreenplayer_bigscreenplayer.html b/docs/api/module-bigscreenplayer_bigscreenplayer.html index 4c1b2bc3..9e579716 100644 --- a/docs/api/module-bigscreenplayer_bigscreenplayer.html +++ b/docs/api/module-bigscreenplayer_bigscreenplayer.html @@ -5,6 +5,9 @@ + + + @@ -32,7 +35,7 @@ - bigscreenplayer/bigscreenplayer + module:bigscreenplayer/bigscreenplayer + + @@ -84,7 +87,7 @@ @@ -112,6 +115,8 @@

import MediaPlayerBase from '../modifiers/mediaplayerbase'
 import DOMHelpers from '../../domhelpers'
+import handlePlayPromise from '../../utils/handleplaypromise'
+import DebugTool from '../../debugger/debugtool'
 
 function Html5 () {
   const sentinelLimits = {
@@ -404,8 +409,9 @@ 

}, 1100) } - function reportError (_errorMessage) { - emitEvent(MediaPlayerBase.EVENT.ERROR) + function reportError (errorString, mediaError) { + DebugTool.info('HTML5 Media Player error: ' + errorString) + emitEvent(MediaPlayerBase.EVENT.ERROR, mediaError) } function toBuffering () { @@ -460,22 +466,15 @@

readyToCache = true }, 250) - cachedSeekableRange = { - start: mediaElement.seekable.start(0), - end: mediaElement.seekable.end(0) - } + cachedSeekableRange = getElementSeekableRange() } - function getSeekableRange () { + function getElementSeekableRange () { if (mediaElement) { if (isReadyToPlayFrom() && mediaElement.seekable && mediaElement.seekable.length > 0) { - if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.cacheSeekableRange) { - return getCachedSeekableRange() - } else { - return { - start: mediaElement.seekable.start(0), - end: mediaElement.seekable.end(0) - } + return { + start: mediaElement.seekable.start(0), + end: mediaElement.seekable.end(0) } } else if (mediaElement.duration !== undefined) { return { @@ -484,7 +483,14 @@

} } } - return undefined + } + + function getSeekableRange () { + if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.cacheSeekableRange) { + return getCachedSeekableRange() + } else { + return getElementSeekableRange() + } } function onFinishedBuffering () { @@ -508,7 +514,7 @@

} function onError () { - reportError('Media element error code: ' + mediaElement.error.code) + reportError('Media element error code: ' + mediaElement.error.code, { code: mediaElement.error.code, message: mediaElement.error.message }) } function onSourceError () { @@ -605,11 +611,11 @@

function deferredPlayFrom () { if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.deferredPlayback) { - mediaElement.play() + handlePlayPromise(mediaElement.play()) seekTo(targetSeekTime) } else { seekTo(targetSeekTime) - mediaElement.play() + handlePlayPromise(mediaElement.play()) } if (postBufferingState === MediaPlayerBase.STATE.PAUSED) { @@ -856,7 +862,7 @@

case MediaPlayerBase.STATE.STOPPED: trustZeroes = true toBuffering() - mediaElement.play() + handlePlayPromise(mediaElement.play()) break default: @@ -918,12 +924,12 @@

case MediaPlayerBase.STATE.BUFFERING: if (isReadyToPlayFrom()) { // If we are not ready to playFrom, then calling play would seek to the start of media, which we might not want. - mediaElement.play() + handlePlayPromise(mediaElement.play()) } break case MediaPlayerBase.STATE.PAUSED: - mediaElement.play() + handlePlayPromise(mediaElement.play()) toPlaying() break @@ -1017,7 +1023,7 @@

@@ -1029,8 +1035,6 @@

- - diff --git a/docs/api/playbackstrategy_modifiers_samsungmaple.js.html b/docs/api/playbackstrategy_modifiers_samsungmaple.js.html index 7ec1c6ca..25f6f663 100644 --- a/docs/api/playbackstrategy_modifiers_samsungmaple.js.html +++ b/docs/api/playbackstrategy_modifiers_samsungmaple.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -596,7 +599,7 @@

function _reportError (errorMessage) { DebugTool.info(errorMessage) - _emitEvent(MediaPlayerBase.EVENT.ERROR, {'errorMessage': errorMessage}) + _emitEvent(MediaPlayerBase.EVENT.ERROR, { 'errorMessage': errorMessage }) } function _setDisplayFullScreenForVideo () { @@ -747,7 +750,7 @@

@@ -759,8 +762,6 @@

- - diff --git a/docs/api/playbackstrategy_modifiers_samsungstreaming.js.html b/docs/api/playbackstrategy_modifiers_samsungstreaming.js.html index 7ce6813a..d5980824 100644 --- a/docs/api/playbackstrategy_modifiers_samsungstreaming.js.html +++ b/docs/api/playbackstrategy_modifiers_samsungstreaming.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -826,7 +829,7 @@

function _reportError (errorMessage) { DebugTool.info(errorMessage) - _emitEvent(MediaPlayerBase.EVENT.ERROR, {'errorMessage': errorMessage}) + _emitEvent(MediaPlayerBase.EVENT.ERROR, { 'errorMessage': errorMessage }) } function _isNearToCurrentTime (seconds) { @@ -926,7 +929,7 @@

@@ -938,8 +941,6 @@

- - diff --git a/docs/api/playbackstrategy_modifiers_samsungstreaming2015.js.html b/docs/api/playbackstrategy_modifiers_samsungstreaming2015.js.html index f5157fed..20569d45 100644 --- a/docs/api/playbackstrategy_modifiers_samsungstreaming2015.js.html +++ b/docs/api/playbackstrategy_modifiers_samsungstreaming2015.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -811,7 +814,7 @@

function _reportError (errorMessage) { DebugTool.info(errorMessage) - _emitEvent(MediaPlayerBase.EVENT.ERROR, {'errorMessage': errorMessage}) + _emitEvent(MediaPlayerBase.EVENT.ERROR, { 'errorMessage': errorMessage }) } function _isNearToCurrentTime (seconds) { @@ -911,7 +914,7 @@

@@ -923,8 +926,6 @@

- - diff --git a/docs/api/playbackstrategy_msestrategy.js.html b/docs/api/playbackstrategy_msestrategy.js.html index 72c1ec6f..c5fd43b4 100644 --- a/docs/api/playbackstrategy_msestrategy.js.html +++ b/docs/api/playbackstrategy_msestrategy.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -124,8 +127,6 @@

import { MediaPlayer } from 'dashjs/index_mediaplayerOnly' function MSEStrategy (mediaSources, windowType, mediaKind, playbackElement, isUHD, customPlayerSettings) { - const LIVE_DELAY_SECONDS = 1.1 - let mediaPlayer let mediaElement @@ -140,6 +141,7 @@

let isEnded = false let dashMetrics + let lastError let publishedSeekEvent = false let isSeeking = false @@ -153,14 +155,26 @@

} } + let playerSettings = Utils.merge({ + debug: { + logLevel: 2 + }, + streaming: { + liveDelay: 1.1, + bufferToKeep: 4, + bufferTimeAtTopQuality: 12, + bufferTimeAtTopQualityLongForm: 15 + } + }, customPlayerSettings) + const DashJSEvents = { LOG: 'log', ERROR: 'error', MANIFEST_LOADED: 'manifestLoaded', DOWNLOAD_MANIFEST_ERROR_CODE: 25, - DOWNLOAD_SIDX_ERROR_CODE: 26, DOWNLOAD_CONTENT_ERROR_CODE: 27, DOWNLOAD_INIT_SEGMENT_ERROR_CODE: 28, + UNSUPPORTED_CODEC: 30, MANIFEST_VALIDITY_CHANGED: 'manifestValidityChanged', QUALITY_CHANGE_RENDERED: 'qualityChangeRendered', BASE_URL_SELECTED: 'baseUrlSelected', @@ -233,31 +247,37 @@

} if (event.error && event.error.message) { - DebugTool.info('MSE Error: ' + event.error.message) + DebugTool.info('MSE Error: ' + event.error.message + ' Code: ' + event.error.code) + lastError = event.error // Don't raise an error on fragment download error - if (event.error.code === DashJSEvents.DOWNLOAD_SIDX_ERROR_CODE || - event.error.code === DashJSEvents.DOWNLOAD_CONTENT_ERROR_CODE || - event.error.code === DashJSEvents.DOWNLOAD_INIT_SEGMENT_ERROR_CODE) { + if (event.error.code === DashJSEvents.DOWNLOAD_CONTENT_ERROR_CODE || event.error.code === DashJSEvents.DOWNLOAD_INIT_SEGMENT_ERROR_CODE) { return } if (event.error.code === DashJSEvents.DOWNLOAD_MANIFEST_ERROR_CODE) { - manifestDownloadError(event) + manifestDownloadError(event.error) return } + + // It is possible audio could play back even if the video codec is not supported. Resetting here prevents this. + if (event.error.code === DashJSEvents.UNSUPPORTED_CODEC) { + mediaPlayer.reset() + } } - publishError() + + publishError(event.error) } - function manifestDownloadError (event) { - const error = () => publishError() + function manifestDownloadError (mediaError) { + const error = () => publishError(mediaError) const failoverParams = { - errorMessage: 'manifest-refresh', isBufferingTimeoutError: false, currentTime: getCurrentTime(), - duration: getDuration() + duration: getDuration(), + code: mediaError.code, + message: mediaError.message } mediaSources.failover(load, error, failoverParams) @@ -354,12 +374,14 @@

*/ function onBaseUrlSelected (event) { const failoverInfo = { - errorMessage: 'download', - isBufferingTimeoutError: false + isBufferingTimeoutError: false, + code: lastError && lastError.code, + message: lastError && lastError.message } function log () { DebugTool.info('BaseUrl selected: ' + event.baseUrl.url) + lastError = undefined } failoverInfo.serviceLocation = event.baseUrl.serviceLocation @@ -417,9 +439,9 @@

} } - function publishError () { + function publishError (mediaError) { if (errorCallback) { - errorCallback() + errorCallback(mediaError) } } @@ -428,7 +450,7 @@

} function getClampedTime (time, range) { - return Math.min(Math.max(time, range.start), range.end - LIVE_DELAY_SECONDS) + return Math.min(Math.max(time, range.start), range.end - playerSettings.streaming.liveDelay) } function load (mimeType, playbackTime) { @@ -458,17 +480,6 @@

function setUpMediaPlayer (playbackTime) { mediaPlayer = MediaPlayer().create() - const playerSettings = Utils.merge({ - debug: { - logLevel: 2 - }, - streaming: { - liveDelay: LIVE_DELAY_SECONDS, - bufferToKeep: 4, - bufferTimeAtTopQuality: 12, - bufferTimeAtTopQualityLongForm: 15 - } - }, customPlayerSettings) mediaPlayer.updateSettings(playerSettings) mediaPlayer.initialize(mediaElement, null, true) modifySource(playbackTime) @@ -486,7 +497,6 @@

mediaElement.addEventListener('seeking', onBuffering) mediaElement.addEventListener('seeked', onSeeked) mediaElement.addEventListener('ended', onEnded) - mediaElement.addEventListener('error', onError) mediaPlayer.on(DashJSEvents.ERROR, onError) mediaPlayer.on(DashJSEvents.MANIFEST_LOADED, onManifestLoaded) mediaPlayer.on(DashJSEvents.STREAM_INITIALIZED, onStreamInitialised) @@ -531,7 +541,7 @@

if (dvrInfo) { return { start: dvrInfo.range.start - timeCorrection, - end: dvrInfo.range.end - timeCorrection + end: dvrInfo.range.end - timeCorrection - playerSettings.streaming.liveDelay } } } @@ -566,7 +576,7 @@

function calculateSeekOffset (time) { function getClampedTimeForLive (time) { - return Math.min(Math.max(time, 0), mediaPlayer.getDVRWindowSize() - LIVE_DELAY_SECONDS) + return Math.min(Math.max(time, 0), mediaPlayer.getDVRWindowSize() - playerSettings.streaming.liveDelay) } if (windowType === WindowTypes.SLIDING) { @@ -618,6 +628,9 @@

getSeekableRange: getSeekableRange, getCurrentTime: getCurrentTime, getDuration: getDuration, + getPlayerElement: () => { + return mediaElement + }, tearDown: () => { mediaPlayer.reset() @@ -628,7 +641,6 @@

mediaElement.removeEventListener('seeking', onBuffering) mediaElement.removeEventListener('seeked', onSeeked) mediaElement.removeEventListener('ended', onEnded) - mediaElement.removeEventListener('error', onError) mediaPlayer.off(DashJSEvents.ERROR, onError) mediaPlayer.off(DashJSEvents.MANIFEST_LOADED, onManifestLoaded) mediaPlayer.off(DashJSEvents.MANIFEST_VALIDITY_CHANGED, onManifestValidityChange) @@ -642,6 +654,7 @@

DOMHelpers.safeRemoveElement(mediaElement) + lastError = undefined mediaPlayer = undefined mediaElement = undefined eventCallbacks = [] @@ -723,7 +736,7 @@

@@ -735,8 +748,6 @@

- - diff --git a/docs/api/scripts/misc.js b/docs/api/scripts/misc.js index f5614bca..9a4561be 100644 --- a/docs/api/scripts/misc.js +++ b/docs/api/scripts/misc.js @@ -67,8 +67,15 @@ function copyFunction(id) { var copyToClipboard = '
' + tooltip + '
'; // extract the code language - var langName = classList[classList.length - 1].split('-')[1]; + var langName = classList[classList.length - 1]; + if (typeof langName === 'string') { + langName = langName.split('-')[1]; + } + + /** + * By default language name is javascript. + */ if ( langName === undefined ) { langName = 'JavaScript'; } // if(langName != undefined) diff --git a/docs/api/scripts/search.js b/docs/api/scripts/search.js index ad526d34..ba86c777 100644 --- a/docs/api/scripts/search.js +++ b/docs/api/scripts/search.js @@ -39,20 +39,16 @@ function search(list, options, keys, searchKey) { var result = fuse.search(searchKey); - console.log(result, result.length); if (result.length > 20) { result = result.slice(0, 20); } - console.log(result); var searchUL = document.getElementById('search-item-ul'); - searchUL.innerHTML = ''; - if (result.length === 0) { - searchUL.innerHTML += '
  • No Result Found
  • '; + searchUL.innerHTML = '
  • No Result Found
  • '; } else { - result.forEach(function(obj) { - searchUL.innerHTML += '
  • ' + obj.item.link + '
  • '; - }); + searchUL.innerHTML = result.reduce(function(html, obj) { + return html + '
  • ' + obj.item.link + '
  • '; + }, ''); } } @@ -79,5 +75,3 @@ function setupSearch(list, options) { window.addEventListener('click', checkClick); }); } - - diff --git a/docs/api/styles/reset.css b/docs/api/styles/reset.css index 922f26fb..8e643fae 100644 --- a/docs/api/styles/reset.css +++ b/docs/api/styles/reset.css @@ -240,8 +240,13 @@ ol.linenums li { padding-left: 0; } +.prettyprint.linenums code, .prettyprint.linenums li { - min-height: 18px; + min-height: 25px; +} + +.prettyprint.linenums code { + display: inline; } .prettyprint.linenums li.selected, diff --git a/docs/api/subtitles_timedtext.js.html b/docs/api/subtitles_timedtext.js.html index 1da90a07..c5449015 100644 --- a/docs/api/subtitles_timedtext.js.html +++ b/docs/api/subtitles_timedtext.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -231,7 +234,7 @@

    @@ -243,8 +246,6 @@

    - - diff --git a/docs/api/subtitles_transformer.js.html b/docs/api/subtitles_transformer.js.html index ad4c0f0a..cb5b4642 100644 --- a/docs/api/subtitles_transformer.js.html +++ b/docs/api/subtitles_transformer.js.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -299,7 +302,7 @@

    @@ -311,8 +314,6 @@

    - - diff --git a/docs/api/tutorial-CDN Failover.html b/docs/api/tutorial-CDN Failover.html index b62de365..e371024c 100644 --- a/docs/api/tutorial-CDN Failover.html +++ b/docs/api/tutorial-CDN Failover.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -179,7 +182,7 @@

    Seamless Failover - Only on MSE Strategy Devices

    @@ -191,8 +194,6 @@

    Seamless Failover - Only on MSE Strategy Devices

    - - diff --git a/docs/api/tutorial-Configuration.html b/docs/api/tutorial-Configuration.html index 806845a9..e63c9e84 100644 --- a/docs/api/tutorial-Configuration.html +++ b/docs/api/tutorial-Configuration.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -118,6 +121,8 @@

    Configuration

    Playback Strategy

    As mentioned in the "Getting Started" guide, bigscreen-player requires a playbackStrategy to be set:

    +
    window.bigscreenPlayer.playbackStrategy = 'msestrategy' // OR 'nativestrategy' OR 'hybridstrategy' OR 'basicstrategy'
    +

    Overrides

    This library works across a multitude of different devices. But in order to do so, different configuration options are available to ensure the experience is good on those devices.

    In order to add an override, simply add an overiddes object to the object above.

    @@ -209,7 +214,7 @@

    Overrides

    @@ -221,8 +226,6 @@

    Overrides

    - - diff --git a/docs/api/tutorial-Debugging.html b/docs/api/tutorial-Debugging.html index 3aab99ae..493ffbb3 100644 --- a/docs/api/tutorial-Debugging.html +++ b/docs/api/tutorial-Debugging.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -141,7 +144,7 @@

    The Chronicle

    @@ -153,8 +156,6 @@

    The Chronicle

    - - diff --git a/docs/api/tutorial-Design.html b/docs/api/tutorial-Design.html index ec38f4fd..d55b2591 100644 --- a/docs/api/tutorial-Design.html +++ b/docs/api/tutorial-Design.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -146,7 +149,7 @@

    Player Component

    @@ -158,8 +161,6 @@

    Player Component

    - - diff --git a/docs/api/tutorial-Events.html b/docs/api/tutorial-Events.html index b1c2ef37..cad5f320 100644 --- a/docs/api/tutorial-Events.html +++ b/docs/api/tutorial-Events.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -182,7 +185,7 @@

    Reacting to subtitles being turned on/off

    @@ -194,8 +197,6 @@

    Reacting to subtitles being turned on/off

    - - diff --git a/docs/api/tutorial-Getting Started.html b/docs/api/tutorial-Getting Started.html index 5ea87d77..73876032 100644 --- a/docs/api/tutorial-Getting Started.html +++ b/docs/api/tutorial-Getting Started.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -117,7 +120,7 @@

    Installation

    Configuration

    Bigscreen Player has some global configuration that is needed before initialisation. A playback strategy must be configured:

    -
    window.bigscreenPlayer.playbackStrategy = 'msestrategy' // OR 'nativestrategy' OR 'hybridstrategy' OR 'talstrategy' (deprecated)
    +
    window.bigscreenPlayer.playbackStrategy = 'msestrategy' // OR 'nativestrategy' OR 'hybridstrategy' OR 'basicstrategy'
     

    Initialisation

    A playback session can be initialised by simply calling the init() function with some initial data.

    @@ -127,7 +130,7 @@

    Initialisation

    // configure the media player that will be used before loading // see below for further details of ths config -// options are: msestrategy, nativestrategy, hybridstrategy +// options are: msestrategy, nativestrategy, hybridstrategy, basicstrategy window.bigscreenPlayer.playbackStrategy = 'msestrategy' const bigscreenPlayer = BigscreenPlayer() @@ -219,7 +222,7 @@

    Initialisation

    @@ -231,8 +234,6 @@

    Initialisation

    - - diff --git a/docs/api/tutorial-Mocking Playback.html b/docs/api/tutorial-Mocking Playback.html index d145b256..d6c52e9e 100644 --- a/docs/api/tutorial-Mocking Playback.html +++ b/docs/api/tutorial-Mocking Playback.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -189,7 +192,7 @@

    triggerErrorHandled()

    @@ -201,8 +204,6 @@

    triggerErrorHandled()

    - - diff --git a/docs/api/tutorial-Playback Strategies.html b/docs/api/tutorial-Playback Strategies.html index 288b80f3..db503cba 100644 --- a/docs/api/tutorial-Playback Strategies.html +++ b/docs/api/tutorial-Playback Strategies.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -172,7 +175,7 @@

    Basic Strategy

    @@ -184,8 +187,6 @@

    Basic Strategy

    - - diff --git a/docs/api/tutorial-Plugins.html b/docs/api/tutorial-Plugins.html index 83e35bc0..59061b8f 100644 --- a/docs/api/tutorial-Plugins.html +++ b/docs/api/tutorial-Plugins.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -178,7 +181,7 @@

    Plugins

    @@ -190,8 +193,6 @@

    Plugins

    - - diff --git a/docs/api/tutorial-State Changes.html b/docs/api/tutorial-State Changes.html index 3f783c61..b80395e0 100644 --- a/docs/api/tutorial-State Changes.html +++ b/docs/api/tutorial-State Changes.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -137,7 +140,7 @@

    State Changes

    @@ -149,8 +152,6 @@

    State Changes

    - - diff --git a/docs/api/tutorial-Testing.html b/docs/api/tutorial-Testing.html index d2e2a14a..e4bd5f27 100644 --- a/docs/api/tutorial-Testing.html +++ b/docs/api/tutorial-Testing.html @@ -5,6 +5,9 @@ + + + @@ -84,7 +87,7 @@ @@ -174,7 +177,7 @@

    Growing Windows

    @@ -186,8 +189,6 @@

    Growing Windows

    - - diff --git a/docs/arch/004-use-class.md b/docs/arch/004-use-class.md new file mode 100644 index 00000000..914beda5 --- /dev/null +++ b/docs/arch/004-use-class.md @@ -0,0 +1,27 @@ +#  004 Use Class + +Originally added: 1 December, 2022. + +##  Context + +BigscreenPlayer would benefit from being more in-line with established Web APIs such as `EventTarget`. To acheive this it should first become a class, so we can extend f.ex. `EventTarget`. `EventTarget` specifically would deprecate all `onStateChange` and `onTimeChange` handlers, as well as the Plugin interface. + +Implementing EventTarget would deprecate the current `onStateChange` and `onTimeChange` callbacks in favour of `addEventListener` as well as BSP plugins. We have had issues before with downstream consumers using these APIs in unexpected ways (see [BSP v5.6.1: Fix unregistered callbacks being called](https://github.com/bbc/bigscreen-player/releases/tag/5.6.1). Raw link: ). + +##  Decision + +Refactor BigscreenPlayer to use the `Class` JavaScript construct and syntax instead of `Function` syntax. + +##  Status + +Proposed. + +##  Consequences + +- Implementing BigscreenPlayer as a class makes it easier to implement Web APIs such as `EventTarget` to combat our tech debt. `EventTarget` specifically would deprecate all `onStateChange` and `onTimeChange` handlers, as well as the Plugin interface. + +##  Further Reading + +- [ADR Github Organisation](https://adr.github.io/). Raw link: +- [MDN web docs on JavaScript Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). Raw link: +- [MDN web docs on EventTarget Web API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget). Raw link: diff --git a/package.json b/package.json index 943697a5..a303a4cd 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,15 @@ "CONTRIBUTING.md" ], "scripts": { - "prepare": "[ ! -d dist/ ] && npm run build || exit 0", - "docs": "npx jsdoc -c jsdoc.conf.json", - "build": "npm run build:clean && npm run build:bundle", - "build:clean": "rm -rf dist", - "build:bundle": "rollup -c rollup.config.js", - "watch": "rollup -c rollup.config.js -w", - "start": "rollup -c rollup.dev.config.js -w", + "start": "rollup --config rollup.dev.config.js --watch", + "watch": "rollup --config rollup.config.js --watch", "test": "jest", + "docs": "npx jsdoc --config jsdoc.conf.json", + "prebuild": "rm -rf dist", + "build": "rollup --config rollup.config.js", "coverage": "jest --coverage", - "lint": "eslint ." + "lint": "eslint .", + "prepare": "[ ! -d dist/ ] && npm run build || exit 0" }, "husky": { "hooks": { @@ -82,4 +81,4 @@ "url": "https://github.com/bbc/bigscreen-player/issues" }, "homepage": "https://github.com/bbc/bigscreen-player#readme" -} \ No newline at end of file +} diff --git a/src/bigscreenplayer.js b/src/bigscreenplayer.js index 47c77e76..585011ad 100644 --- a/src/bigscreenplayer.js +++ b/src/bigscreenplayer.js @@ -1,632 +1,664 @@ /** * @module bigscreenplayer/bigscreenplayer */ -import MediaState from './models/mediastate' -import PlayerComponent from './playercomponent' -import PauseTriggers from './models/pausetriggers' -import DynamicWindowUtils from './dynamicwindowutils' -import WindowTypes from './models/windowtypes' -import MockBigscreenPlayer from './mockbigscreenplayer' -import Plugins from './plugins' -import Chronicle from './debugger/chronicle' -import DebugTool from './debugger/debugtool' -import SlidingWindowUtils from './utils/timeutils' -import callCallbacks from './utils/callcallbacks' -import MediaSources from './mediasources' -import Version from './version' -import Resizer from './resizer' -import ReadyHelper from './readyhelper' -import Subtitles from './subtitles/subtitles' -import './typedefs' - -function BigscreenPlayer () { - let stateChangeCallbacks = [] - let timeUpdateCallbacks = [] - let subtitleCallbacks = [] - - let playerReadyCallback - let playerErrorCallback - let mediaKind - let initialPlaybackTimeEpoch - let serverDate - let playerComponent - let resizer - let pauseTrigger - let isSeeking = false - let endOfStream - let windowType - let mediaSources - let playbackElement - let readyHelper - let subtitles - - const END_OF_STREAM_TOLERANCE = 10 - - function mediaStateUpdateCallback (evt) { - if (evt.timeUpdate) { - DebugTool.time(evt.data.currentTime) - callCallbacks(timeUpdateCallbacks, { - currentTime: evt.data.currentTime, - endOfStream: endOfStream +import MediaState from "./models/mediastate" +import PlayerComponent from "./playercomponent" +import PauseTriggers from "./models/pausetriggers" +import DynamicWindowUtils from "./dynamicwindowutils" +import WindowTypes from "./models/windowtypes" +import MockBigscreenPlayer from "./mockbigscreenplayer" +import Plugins from "./plugins" +import Chronicle from "./debugger/chronicle" +import DebugTool from "./debugger/debugtool" +import SlidingWindowUtils from "./utils/timeutils" +import callCallbacks from "./utils/callcallbacks" +import MediaSources from "./mediasources" +import Version from "./version" +import Resizer from "./resizer" +import ReadyHelper from "./readyhelper" +import Subtitles from "./subtitles/subtitles" + +export default class BigscreenPlayer { + static version = Version + + static END_OF_STREAM_TOLERANCE = 10 + + /** + * @return the live support of the device. + */ + static getLiveSupport() { + return PlayerComponent.getLiveSupport() + } + + #stateChangeCallbacks = [] + #timeUpdateCallbacks = [] + #subtitleCallbacks = [] + #playerReadyCallback + #playerErrorCallback + #mediaKind + #initialPlaybackTimeEpoch + #serverDate + #playerComponent + #resizer + #pauseTrigger + #isSeeking = false + #endOfStream + #windowType + #mediaSources + #playbackElement + #readyHelper + #subtitles + + /** + * @typedef {Object} MediaConnection + * @property {String} url - media endpoint + * @property {String} cdn - identifier for the endpoint + */ + + /** + * Data required for playback + * @typedef {Object} BigscreenPlayerData + * @property {Object} media + * @property {string} media.type - source type e.g 'application/dash+xml' + * @property {string} media.mimeType - mimeType e.g 'video/mp4' + * @property {string} media.kind - 'video' or 'audio' + * @property {string} media.captionsUrl - 'Location for a captions file' + * @property {MediaConnection[]} media.urls - Media urls to use + * @property {Date} serverDate - Date object with server time offset + * @property {WindowTypes} windowType + * @property {boolean} subtitlesEnabled – Enable subtitles on initialisation + */ + + /** + * + * @typedef {object} ConstructionCallbacks + * @property {function} [callbacks.onSuccess] - Called after Bigscreen Player is initialised + * @property {function} [callbacks.onError] - Called when an error occurs during initialisation + */ + + /** + * Call first to initialise bigscreen player for playback. + * @param {HTMLDivElement} el - The Div element where content elements should be rendered + * @param {BigscreenPlayerData} data + * @param {ConstructionCallbacks} callbacks + */ + constructor(el, data, callbacks) { + this.#playbackElement = el + this.#resizer = Resizer() + this.#windowType = data.windowType + this.#serverDate = data.serverDate + Chronicle.init() + DebugTool.setRootElement(this.#playbackElement) + DebugTool.keyValue({ key: "framework-version", value: Version }) + + this.#playerReadyCallback = callbacks?.onSuccess?.bind(this) + this.#playerErrorCallback = callbacks?.onError?.bind(this) + + // Backwards compatibility with Old API; to be removed on Major Version Update + if (data.media && !data.media.captions && data.media.captionsUrl) { + data.media.captions = [ + { + url: data.media.captionsUrl, + }, + ] + } + + this.#mediaSources = MediaSources() + + const handleDataLoaded = this.#bigscreenPlayerDataLoaded.bind(this) + + const mediaSourceCallbacks = { + onSuccess: () => handleDataLoaded(data), + onError: (error) => { + if (typeof callbacks?.onError === "function") { + callbacks.onError(error) + } + }, + } + + this.#mediaSources.init( + data.media, + this.#serverDate, + this.#windowType, + BigscreenPlayer.getLiveSupport(), + mediaSourceCallbacks + ) + } + + #bigscreenPlayerDataLoaded(data) { + if (this.#windowType !== WindowTypes.STATIC) { + data.time = this.#mediaSources.time() + + this.#serverDate = data.serverDate + this.#initialPlaybackTimeEpoch = data.initialPlaybackTime + + // overwrite initialPlaybackTime with video time (it comes in as epoch time for a sliding/growing window) + data.initialPlaybackTime = SlidingWindowUtils.convertToSeekableVideoTime( + data.initialPlaybackTime, + data.time.windowStartTime + ) + } + + this.#mediaKind = data.media.kind + this.#endOfStream = + this.#windowType !== WindowTypes.STATIC && !data.initialPlaybackTime && data.initialPlaybackTime !== 0 + + this.#readyHelper = new ReadyHelper( + data.initialPlaybackTime, + this.#windowType, + PlayerComponent.getLiveSupport(), + this.#playerReadyCallback + ) + + this.#playerComponent = new PlayerComponent( + this.#playbackElement, + data, + this.#mediaSources, + this.#windowType, + this.#mediaStateUpdateCallback.bind(this), + this.#playerErrorCallback + ) + + this.#subtitles = Subtitles( + this.#playerComponent, + data.subtitlesEnabled, + this.#playbackElement, + data.media.subtitleCustomisation, + this.#mediaSources, + this.#callSubtitlesCallbacks.bind(this) + ) + } + + #mediaStateUpdateCallback(event) { + if (event.timeUpdate) { + DebugTool.time(event.data.currentTime) + callCallbacks(this.#timeUpdateCallbacks, { + currentTime: event.data.currentTime, + endOfStream: this.#endOfStream, }) } else { - let stateObject = { state: evt.data.state } + let stateObject = { state: event.data.state } - if (evt.data.state === MediaState.PAUSED) { - endOfStream = false - stateObject.trigger = pauseTrigger || PauseTriggers.DEVICE - pauseTrigger = undefined + if (event.data.state === MediaState.PAUSED) { + this.#endOfStream = false + stateObject.trigger = this.#pauseTrigger || PauseTriggers.DEVICE + this.#pauseTrigger = undefined } - if (evt.data.state === MediaState.FATAL_ERROR) { + if (event.data.state === MediaState.FATAL_ERROR) { stateObject = { state: MediaState.FATAL_ERROR, - isBufferingTimeoutError: evt.isBufferingTimeoutError, - code: evt.code, - message: evt.message + isBufferingTimeoutError: event.isBufferingTimeoutError, + code: event.code, + message: event.message, } } - if (evt.data.state === MediaState.WAITING) { - stateObject.isSeeking = isSeeking - isSeeking = false + if (event.data.state === MediaState.WAITING) { + stateObject.isSeeking = this.#isSeeking + this.#isSeeking = false } - stateObject.endOfStream = endOfStream + stateObject.endOfStream = this.#endOfStream DebugTool.event(stateObject) - callCallbacks(stateChangeCallbacks, stateObject) + callCallbacks(this.#stateChangeCallbacks, stateObject) } - if (evt.data.seekableRange) { - DebugTool.keyValue({ key: 'seekableRangeStart', value: deviceTimeToDate(evt.data.seekableRange.start) }) - DebugTool.keyValue({ key: 'seekableRangeEnd', value: deviceTimeToDate(evt.data.seekableRange.end) }) + if (event.data.seekableRange) { + DebugTool.keyValue({ key: "seekableRangeStart", value: this.#deviceTimeToDate(event.data.seekableRange.start) }) + DebugTool.keyValue({ key: "seekableRangeEnd", value: this.#deviceTimeToDate(event.data.seekableRange.end) }) } - if (evt.data.duration) { - DebugTool.keyValue({ key: 'duration', value: evt.data.duration }) + if (event.data.duration) { + DebugTool.keyValue({ key: "duration", value: event.data.duration }) } - if (playerComponent && readyHelper) { - readyHelper.callbackWhenReady(evt) + if (this.#playerComponent && this.#readyHelper) { + this.#readyHelper.callbackWhenReady(event) } } - function deviceTimeToDate (time) { - if (getWindowStartTime()) { - return new Date(convertVideoTimeSecondsToEpochMs(time)) - } else { - return new Date(time * 1000) + #deviceTimeToDate(time) { + return this.#getWindowStartTime() ? new Date(this.convertVideoTimeSecondsToEpochMs(time)) : new Date(time * 1000) + } + + #getWindowStartTime() { + return this.#mediaSources && this.#mediaSources.time().windowStartTime + } + + #getWindowEndTime() { + return this.#mediaSources && this.#mediaSources.time().windowEndTime + } + + #callSubtitlesCallbacks(enabled) { + callCallbacks(this.#subtitleCallbacks, { enabled }) + } + + /** + * Should be called at the end of all playback sessions. Resets state and clears any UI. + */ + tearDown() { + if (this.#subtitles) { + this.#subtitles.tearDown() + this.#subtitles = undefined } + + if (this.#playerComponent) { + this.#playerComponent.tearDown() + this.#playerComponent = undefined + } + + if (this.#mediaSources) { + this.#mediaSources.tearDown() + this.#mediaSources = undefined + } + + this.#stateChangeCallbacks = [] + this.#timeUpdateCallbacks = [] + this.#subtitleCallbacks = [] + this.#endOfStream = undefined + this.#mediaKind = undefined + this.#pauseTrigger = undefined + this.#windowType = undefined + this.#resizer = undefined + this.unregisterPlugin() + DebugTool.tearDown() + Chronicle.tearDown() } - function convertVideoTimeSecondsToEpochMs (seconds) { - return getWindowStartTime() ? getWindowStartTime() + (seconds * 1000) : null + /** + * Pass a function to call whenever the player transitions state. + * @see {@link module:models/mediastate} + * @param {Function} callback + */ + registerForStateChanges(callback) { + this.#stateChangeCallbacks.push(callback) + return callback } - function bigscreenPlayerDataLoaded (bigscreenPlayerData, enableSubtitles) { - if (windowType !== WindowTypes.STATIC) { - bigscreenPlayerData.time = mediaSources.time() - serverDate = bigscreenPlayerData.serverDate + /** + * Unregisters a previously registered callback. + * @param {Function} callback + */ + unregisterForStateChanges(callback) { + const indexOf = this.#stateChangeCallbacks.indexOf(callback) + if (indexOf !== -1) { + this.#stateChangeCallbacks.splice(indexOf, 1) + } + } - initialPlaybackTimeEpoch = bigscreenPlayerData.initialPlaybackTime - // overwrite initialPlaybackTime with video time (it comes in as epoch time for a sliding/growing window) - bigscreenPlayerData.initialPlaybackTime = SlidingWindowUtils.convertToSeekableVideoTime(bigscreenPlayerData.initialPlaybackTime, bigscreenPlayerData.time.windowStartTime) + /** + * Pass a function to call whenever the player issues a time update. + * @param {Function} callback + */ + registerForTimeUpdates(callback) { + this.#timeUpdateCallbacks.push(callback) + return callback + } + + /** + * Unregisters a previously registered callback. + * @param {Function} callback + */ + unregisterForTimeUpdates(callback) { + const indexOf = this.#timeUpdateCallbacks.indexOf(callback) + if (indexOf !== -1) { + this.#timeUpdateCallbacks.splice(indexOf, 1) } + } - mediaKind = bigscreenPlayerData.media.kind - endOfStream = windowType !== WindowTypes.STATIC && (!bigscreenPlayerData.initialPlaybackTime && bigscreenPlayerData.initialPlaybackTime !== 0) + /** + * Pass a function to be called whenever subtitles are enabled or disabled. + * @param {Function} callback + */ + registerForSubtitleChanges(callback) { + this.#subtitleCallbacks.push(callback) + return callback + } - readyHelper = new ReadyHelper( - bigscreenPlayerData.initialPlaybackTime, - windowType, - PlayerComponent.getLiveSupport(), - playerReadyCallback - ) - playerComponent = new PlayerComponent( - playbackElement, - bigscreenPlayerData, - mediaSources, - windowType, - mediaStateUpdateCallback, - playerErrorCallback - ) + /** + * Unregisters a previously registered callback for changes to subtitles. + * @param {Function} callback + */ + unregisterForSubtitleChanges(callback) { + const indexOf = this.#subtitleCallbacks.indexOf(callback) + if (indexOf !== -1) { + this.#subtitleCallbacks.splice(indexOf, 1) + } + } - subtitles = Subtitles( - playerComponent, - enableSubtitles, - playbackElement, - bigscreenPlayerData.media.subtitleCustomisation, - mediaSources, - callSubtitlesCallbacks + /** + * Sets the current time of the media asset. + * @param {Number} time - In seconds + */ + setCurrentTime(time) { + DebugTool.apicall("setCurrentTime") + if (this.#playerComponent) { + // this flag must be set before calling into playerComponent.setCurrentTime + // as this synchronously fires a WAITING event (when native strategy). + this.#isSeeking = true + this.#playerComponent.setCurrentTime(time) + this.#endOfStream = + this.#windowType !== WindowTypes.STATIC && + Math.abs(this.getSeekableRange().end - time) < BigscreenPlayer.END_OF_STREAM_TOLERANCE + } + } + + setPlaybackRate(rate) { + this.#playerComponent?.setPlaybackRate(rate) + } + + getPlaybackRate() { + return this.#playerComponent?.getPlaybackRate() + } + + /** + * Returns the media asset's current time in seconds. + */ + getCurrentTime() { + return this.#playerComponent?.getCurrentTime() ?? 0 + } + + /** + * Returns the current media kind. + * 'audio' or 'video' + */ + getMediaKind() { + return this.#mediaKind + } + + /** + * Returns the current window type. + * @see {@link module:bigscreenplayer/models/windowtypes} + */ + getWindowType() { + return this.#windowType + } + + /** + * Returns an object including the current start and end times. + * @returns {Object} {start: Number, end: Number} + */ + getSeekableRange() { + return this.#playerComponent?.getSeekableRange() ?? {} + } + + /** + * @returns {boolean} Returns true if media is initialised and playing a live stream within a tolerance of the end of the seekable range (10 seconds). + */ + isPlayingAtLiveEdge() { + return !!( + this.#playerComponent && + this.#windowType !== WindowTypes.STATIC && + Math.abs(this.getSeekableRange().end - this.getCurrentTime()) < BigscreenPlayer.END_OF_STREAM_TOLERANCE ) } - function getWindowStartTime () { - return mediaSources && mediaSources.time().windowStartTime + /** + * @return {Object} An object of the shape {windowStartTime: Number, windowEndTime: Number, initialPlaybackTime: Number, serverDate: Date} + */ + getLiveWindowData() { + if (this.#windowType === WindowTypes.STATIC) { + return {} + } + + return { + windowStartTime: this.#getWindowStartTime(), + windowEndTime: this.#getWindowEndTime(), + initialPlaybackTime: this.#initialPlaybackTimeEpoch, + serverDate: this.#serverDate, + } + } + + /** + * @returns the duration of the media asset. + */ + getDuration() { + return this.#playerComponent?.getDuration() } - function getWindowEndTime () { - return mediaSources && mediaSources.time().windowEndTime + /** + * @returns if the player is paused. + */ + isPaused() { + return this.#playerComponent?.isPaused() ?? true } - function toggleDebug () { - if (playerComponent) { - DebugTool.toggleVisibility() + /** + * @returns if the media asset has ended. + */ + isEnded() { + return this.#playerComponent?.isEnded() ?? false + } + + /** + * Play the media assest from the current point in time. + */ + play() { + DebugTool.apicall("play") + this.#playerComponent.play() + } + + /** + * Pause the media asset. + * @param {*} opts + * @param {boolean} opts.userPause + * @param {boolean} opts.disableAutoResume + */ + pause(opts) { + DebugTool.apicall("pause") + this.#pauseTrigger = opts?.userPause === false ? PauseTriggers.APP : PauseTriggers.USER + this.#playerComponent.pause(opts) + } + + resize(top, left, width, height, zIndex) { + this.#subtitles.hide() + this.#resizer.resize(this.#playbackElement, top, left, width, height, zIndex) + } + + clearResize() { + if (this.#subtitles.enabled()) this.#subtitles.show() + else this.#subtitles.hide() + + this.#resizer.clear(this.#playbackElement) + } + + /** + * Set whether or not subtitles should be enabled. + * @param {boolean} value + */ + setSubtitlesEnabled(enabled) { + if (enabled) this.#subtitles.enable() + else this.#subtitles.disable() + + this.#callSubtitlesCallbacks(enabled) + + if (this.#resizer.isResized()) { + return } + + if (enabled) this.#subtitles.show() + else this.#subtitles.hide() } - function callSubtitlesCallbacks (enabled) { - callCallbacks(subtitleCallbacks, { enabled: enabled }) + /** + * @return if subtitles are currently enabled. + */ + isSubtitlesEnabled() { + return this.#subtitles ? this.#subtitles.enabled() : false } - function setSubtitlesEnabled (enabled) { - enabled ? subtitles.enable() : subtitles.disable() - callSubtitlesCallbacks(enabled) + /** + * @return Returns whether or not subtitles are currently enabled. + */ + isSubtitlesAvailable() { + return this.#subtitles ? this.#subtitles.available() : false + } - if (!resizer.isResized()) { - enabled ? subtitles.show() : subtitles.hide() + areSubtitlesCustomisable() { + return !window.bigscreenPlayer?.overrides?.legacySubtitles + } + + customiseSubtitles(styleOpts) { + if (this.#subtitles) { + this.#subtitles.customise(styleOpts) } } - function isSubtitlesEnabled () { - return subtitles ? subtitles.enabled() : false - } - - function isSubtitlesAvailable () { - return subtitles ? subtitles.available() : false - } - - return /** @alias module:bigscreenplayer/bigscreenplayer */{ - - /** - * Call first to initialise bigscreen player for playback. - * @function - * @name init - * @param {HTMLDivElement} playbackElement - The Div element where content elements should be rendered - * @param {BigscreenPlayerData} bigscreenPlayerData - * @param {WindowTypes} newWindowType - * @param {boolean} enableSubtitles - Enable subtitles on initialisation - * @param {InitCallbacks} callbacks - */ - init: (newPlaybackElement, bigscreenPlayerData, newWindowType, enableSubtitles, callbacks) => { - playbackElement = newPlaybackElement - Chronicle.init() - resizer = Resizer() - DebugTool.setRootElement(playbackElement) - DebugTool.keyValue({ key: 'framework-version', value: Version }) - windowType = newWindowType - serverDate = bigscreenPlayerData.serverDate - if (!callbacks) { - callbacks = {} - } + renderSubtitleExample(xmlString, styleOpts, safePosition) { + if (this.#subtitles) { + this.#subtitles.renderExample(xmlString, styleOpts, safePosition) + } + } - playerReadyCallback = callbacks.onSuccess - playerErrorCallback = callbacks.onError + clearSubtitleExample() { + if (this.#subtitles) { + this.#subtitles.clearExample() + } + } - const mediaSourceCallbacks = { - onSuccess: () => bigscreenPlayerDataLoaded(bigscreenPlayerData, enableSubtitles), - onError: (error) => { - if (callbacks.onError) { - callbacks.onError(error) - } - } - } + /** + * + * An enum may be used to set the on-screen position of any transport controls + * (work in progress to remove this - UI concern). + * @param {*} position + */ + setTransportControlsPosition(position) { + if (this.#subtitles) { + this.#subtitles.setPosition(position) + } + } - mediaSources = MediaSources() + /** + * @return Returns whether the current media asset is seekable. + */ + canSeek() { + return ( + this.#windowType === WindowTypes.STATIC || + DynamicWindowUtils.canSeek( + this.#getWindowStartTime(), + this.#getWindowEndTime(), + BigscreenPlayer.getLiveSupport(), + this.getSeekableRange() + ) + ) + } - // Backwards compatibility with Old API; to be removed on Major Version Update - if (bigscreenPlayerData.media && !bigscreenPlayerData.media.captions && bigscreenPlayerData.media.captionsUrl) { - bigscreenPlayerData.media.captions = [{ - url: bigscreenPlayerData.media.captionsUrl - }] - } + /** + * @return Returns whether the current media asset is pausable. + */ + canPause() { + return ( + this.#windowType === WindowTypes.STATIC || + DynamicWindowUtils.canPause( + this.#getWindowStartTime(), + this.#getWindowEndTime(), + BigscreenPlayer.getLiveSupport() + ) + ) + } - mediaSources.init(bigscreenPlayerData.media, serverDate, windowType, getLiveSupport(), mediaSourceCallbacks) - }, - - /** - * Should be called at the end of all playback sessions. Resets state and clears any UI. - * @function - * @name tearDown - */ - tearDown: function () { - if (subtitles) { - subtitles.tearDown() - subtitles = undefined - } + /** + * Return a mock for in place testing. + * @param {*} opts + */ + mock(opts) { + MockBigscreenPlayer.mock(this, opts) + } - if (playerComponent) { - playerComponent.tearDown() - playerComponent = undefined - } + /** + * Unmock the player. + */ + unmock() { + MockBigscreenPlayer.unmock(this) + } - if (mediaSources) { - mediaSources.tearDown() - mediaSources = undefined - } + /** + * Return a mock for unit tests. + * @param {*} opts + */ + mockJasmine(opts) { + MockBigscreenPlayer.mockJasmine(this, opts) + } - stateChangeCallbacks = [] - timeUpdateCallbacks = [] - subtitleCallbacks = [] - endOfStream = undefined - mediaKind = undefined - pauseTrigger = undefined - windowType = undefined - resizer = undefined - this.unregisterPlugin() - DebugTool.tearDown() - Chronicle.tearDown() - }, - - /** - * Pass a function to call whenever the player transitions state. - * @see {@link module:models/mediastate} - * @function - * @param {Function} callback - */ - registerForStateChanges: (callback) => { - stateChangeCallbacks.push(callback) - return callback - }, - - /** - * Unregisters a previously registered callback. - * @function - * @param {Function} callback - */ - unregisterForStateChanges: (callback) => { - const indexOf = stateChangeCallbacks.indexOf(callback) - if (indexOf !== -1) { - stateChangeCallbacks.splice(indexOf, 1) - } - }, - - /** - * Pass a function to call whenever the player issues a time update. - * @function - * @param {Function} callback - */ - registerForTimeUpdates: (callback) => { - timeUpdateCallbacks.push(callback) - return callback - }, - - /** - * Unregisters a previously registered callback. - * @function - * @param {Function} callback - */ - unregisterForTimeUpdates: (callback) => { - const indexOf = timeUpdateCallbacks.indexOf(callback) - if (indexOf !== -1) { - timeUpdateCallbacks.splice(indexOf, 1) - } - }, - - /** - * Pass a function to be called whenever subtitles are enabled or disabled. - * @function - * @param {Function} callback - */ - registerForSubtitleChanges: (callback) => { - subtitleCallbacks.push(callback) - return callback - }, - - /** - * Unregisters a previously registered callback for changes to subtitles. - * @function - * @param {Function} callback - */ - unregisterForSubtitleChanges: (callback) => { - const indexOf = subtitleCallbacks.indexOf(callback) - if (indexOf !== -1) { - subtitleCallbacks.splice(indexOf, 1) - } - }, - - /** - * Sets the current time of the media asset. - * @function - * @param {Number} time - In seconds - */ - setCurrentTime: function (time) { - DebugTool.apicall('setCurrentTime') - if (playerComponent) { - // this flag must be set before calling into playerComponent.setCurrentTime - as this synchronously fires a WAITING event (when native strategy). - isSeeking = true - playerComponent.setCurrentTime(time) - endOfStream = windowType !== WindowTypes.STATIC && Math.abs(this.getSeekableRange().end - time) < END_OF_STREAM_TOLERANCE - } - }, + /** + * Register a plugin for extended events. + * @param {*} plugin + */ + registerPlugin(plugin) { + Plugins.registerPlugin(plugin) + } - setPlaybackRate: (rate) => { - if (playerComponent) { - playerComponent.setPlaybackRate(rate) - } - }, - - getPlaybackRate: () => playerComponent && playerComponent.getPlaybackRate(), - - /** - * Returns the media asset's current time in seconds. - * @function - */ - getCurrentTime: () => playerComponent && playerComponent.getCurrentTime() || 0, - - /** - * Returns the current media kind. - * 'audio' or 'video' - * @function - */ - getMediaKind: () => mediaKind, - - /** - * Returns the current window type. - * @see {@link module:bigscreenplayer/models/windowtypes} - * @function - */ - getWindowType: () => windowType, - - /** - * Returns an object including the current start and end times. - * @function - * @returns {Object} {start: Number, end: Number} - */ - getSeekableRange: () => playerComponent ? playerComponent.getSeekableRange() : {}, - - /** - * @function - * @returns {boolean} Returns true if media is initialised and playing a live stream within a tolerance of the end of the seekable range (10 seconds). - */ - isPlayingAtLiveEdge: function () { - return !!playerComponent && windowType !== WindowTypes.STATIC && Math.abs(this.getSeekableRange().end - this.getCurrentTime()) < END_OF_STREAM_TOLERANCE - }, - - /** - * @function - * @return {Object} An object of the shape {windowStartTime: Number, windowEndTime: Number, initialPlaybackTime: Number, serverDate: Date} - */ - getLiveWindowData: () => { - if (windowType === WindowTypes.STATIC) { - return {} - } + /** + * Unregister a previously registered plugin. + * @param {*} plugin + */ + unregisterPlugin(plugin) { + Plugins.unregisterPlugin(plugin) + } - return { - windowStartTime: getWindowStartTime(), - windowEndTime: getWindowEndTime(), - initialPlaybackTime: initialPlaybackTimeEpoch, - serverDate: serverDate - } - }, - - /** - * @function - * @returns the duration of the media asset. - */ - getDuration: () => playerComponent && playerComponent.getDuration(), - - /** - * @function - * @returns if the player is paused. - */ - isPaused: () => playerComponent ? playerComponent.isPaused() : true, - - /** - * @function - * @returns if the media asset has ended. - */ - isEnded: () => playerComponent ? playerComponent.isEnded() : false, - - /** - * Play the media assest from the current point in time. - * @function - */ - play: () => { - DebugTool.apicall('play') - playerComponent.play() - }, - /** - * Pause the media asset. - * @function - * @param {*} opts - * @param {boolean} opts.userPause - * @param {boolean} opts.disableAutoResume - */ - pause: (opts) => { - DebugTool.apicall('pause') - pauseTrigger = opts && opts.userPause === false ? PauseTriggers.APP : PauseTriggers.USER - playerComponent.pause(opts) - }, - - resize: (top, left, width, height, zIndex) => { - subtitles.hide() - resizer.resize(playbackElement, top, left, width, height, zIndex) - }, - - clearResize: () => { - if (subtitles.enabled()) { - subtitles.show() - } else { - subtitles.hide() - } - resizer.clear(playbackElement) - }, - - /** - * Set whether or not subtitles should be enabled. - * @function - * @param {boolean} value - */ - setSubtitlesEnabled: setSubtitlesEnabled, - - /** - * @function - * @return if subtitles are currently enabled. - */ - isSubtitlesEnabled: isSubtitlesEnabled, - - /** - * @function - * @return Returns whether or not subtitles are currently enabled. - */ - isSubtitlesAvailable: isSubtitlesAvailable, - - areSubtitlesCustomisable: () => { - return !(window.bigscreenPlayer && window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.legacySubtitles) - }, - - customiseSubtitles: (styleOpts) => { - if (subtitles) { - subtitles.customise(styleOpts) - } - }, + /** + * Returns an object with a number of functions related to the ability to transition state + * given the current state and the playback strategy in use. + */ + transitions() { + return this.#playerComponent?.transitions() ?? {} + } - renderSubtitleExample: (xmlString, styleOpts, safePosition) => { - if (subtitles) { - subtitles.renderExample(xmlString, styleOpts, safePosition) - } - }, + /** + * @return The media element currently being used. + */ + getPlayerElement() { + return this.#playerComponent?.getPlayerElement() + } - clearSubtitleExample: () => { - if (subtitles) { - subtitles.clearExample() - } - }, - - /** - * - * An enum may be used to set the on-screen position of any transport controls - * (work in progress to remove this - UI concern). - * @function - * @param {*} position - */ - setTransportControlsPosition: (position) => { - if (subtitles) { - subtitles.setPosition(position) - } - }, - - /** - * @function - * @return Returns whether the current media asset is seekable. - */ - canSeek: function () { - return windowType === WindowTypes.STATIC || DynamicWindowUtils.canSeek(getWindowStartTime(), getWindowEndTime(), getLiveSupport(), this.getSeekableRange()) - }, - - /** - * @function - * @return Returns whether the current media asset is pausable. - */ - canPause: () => { - return windowType === WindowTypes.STATIC || DynamicWindowUtils.canPause(getWindowStartTime(), getWindowEndTime(), getLiveSupport()) - }, - - /** - * Return a mock for in place testing. - * @function - * @param {*} opts - */ - mock: function (opts) { MockBigscreenPlayer.mock(this, opts) }, - - /** - * Unmock the player. - * @function - */ - unmock: function () { MockBigscreenPlayer.unmock(this) }, - - /** - * Return a mock for unit tests. - * @function - * @param {*} opts - */ - mockJasmine: function (opts) { MockBigscreenPlayer.mockJasmine(this, opts) }, - - /** - * Register a plugin for extended events. - * @function - * @param {*} plugin - */ - registerPlugin: (plugin) => Plugins.registerPlugin(plugin), - - /** - * Unregister a previously registered plugin. - * @function - * @param {*} plugin - */ - unregisterPlugin: (plugin) => Plugins.unregisterPlugin(plugin), - - /** - * Returns an object with a number of functions related to the ability to transition state - * given the current state and the playback strategy in use. - * @function - */ - transitions: () => playerComponent ? playerComponent.transitions() : {}, - - /** - * @function - * @return The media element currently being used. - */ - getPlayerElement: () => playerComponent && playerComponent.getPlayerElement(), - - /** - * @function - * @param {Number} epochTime - Unix Epoch based time in milliseconds. - * @return the time in seconds within the current sliding window. - */ - convertEpochMsToVideoTimeSeconds: (epochTime) => { - return getWindowStartTime() ? Math.floor((epochTime - getWindowStartTime()) / 1000) : null - }, - - /** - * @function - * @return The runtime version of the library. - */ - getFrameworkVersion: () => { - return Version - }, - - /** - * @function - * @param {Number} time - Seconds - * @return the time in milliseconds within the current sliding window. - */ - convertVideoTimeSecondsToEpochMs: convertVideoTimeSecondsToEpochMs, - - /** - * Toggle the visibility of the debug tool overlay. - * @function - */ - toggleDebug: toggleDebug, - - /** - * @function - * @return {Object} - Key value pairs of available log levels - */ - getLogLevels: () => DebugTool.logLevels, - - /** - * @function - * @param logLevel - log level to display @see getLogLevels - */ - setLogLevel: DebugTool.setLogLevel, - getDebugLogs: () => Chronicle.retrieve() + /** + * @param {Number} epochTime - Unix Epoch based time in milliseconds. + * @return the time in seconds within the current sliding window. + */ + convertEpochMsToVideoTimeSeconds(epochTime) { + return this.#getWindowStartTime() ? Math.floor((epochTime - this.#getWindowStartTime()) / 1000) : null } -} -/** - * @function - * @param {TALDevice} device - * @return the live support of the device. - */ -function getLiveSupport () { - return PlayerComponent.getLiveSupport() -} + /** + * @return The runtime version of the library. + */ + getFrameworkVersion() { + return Version + } -BigscreenPlayer.getLiveSupport = getLiveSupport + /** + * @param {Number} time - Seconds + * @return the time in milliseconds within the current sliding window. + */ + convertVideoTimeSecondsToEpochMs(seconds) { + return this.#getWindowStartTime() ? this.#getWindowStartTime() + seconds * 1000 : null + } -BigscreenPlayer.version = Version + /** + * Toggle the visibility of the debug tool overlay. + */ + toggleDebug() { + if (this.#playerComponent) { + DebugTool.toggleVisibility() + } + } + + /** + * @return {Object} - Key value pairs of available log levels + */ + static getLogLevels() { + return DebugTool.logLevels + } + + /** + * @borrows DebugTool.setLogLevel as setLogLevel + * @param logLevel - log level to display @see getLogLevels + */ + static setLogLevel = DebugTool.setLogLevel -export default BigscreenPlayer + static getDebugLogs() { + return Chronicle.retrieve() + } +} diff --git a/src/bigscreenplayer.test.js b/src/bigscreenplayer.test.js index da9a3bb8..c0d3789a 100644 --- a/src/bigscreenplayer.test.js +++ b/src/bigscreenplayer.test.js @@ -1,12 +1,12 @@ -import MediaState from './models/mediastate' -import WindowTypes from './models/windowtypes' -import PauseTriggers from './models/pausetriggers' -import Plugins from './plugins' -import TransferFormats from './models/transferformats' -import LiveSupport from './models/livesupport' -import BigscreenPlayer from './bigscreenplayer' -import Chronicle from './debugger/chronicle' -import PlayerComponent from './playercomponent' +import MediaState from "./models/mediastate" +import WindowTypes from "./models/windowtypes" +import PauseTriggers from "./models/pausetriggers" +import Plugins from "./plugins" +import TransferFormats from "./models/transferformats" +import LiveSupport from "./models/livesupport" +import BigscreenPlayer from "./bigscreenplayer" +import Chronicle from "./debugger/chronicle" +import PlayerComponent from "./playercomponent" let bigscreenPlayer let bigscreenPlayerData @@ -22,7 +22,7 @@ let forceMediaSourcesConstructionFailure = false const mockMediaSources = { init: (media, serverDate, windowType, liveSupport, callbacks) => { - mediaSourcesCallbackErrorSpy = jest.spyOn(callbacks, 'onError') + mediaSourcesCallbackErrorSpy = jest.spyOn(callbacks, "onError") if (forceMediaSourcesConstructionFailure) { callbacks.onError() } else { @@ -30,7 +30,7 @@ const mockMediaSources = { } }, time: () => manifestData.time, - tearDown: jest.fn() + tearDown: jest.fn(), } const mockSubtitlesInstance = { @@ -44,86 +44,99 @@ const mockSubtitlesInstance = { customise: jest.fn(), renderExample: jest.fn(), clearExample: jest.fn(), - tearDown: jest.fn() + tearDown: jest.fn(), } const mockResizer = { resize: jest.fn(), clear: jest.fn(), - isResized: jest.fn() + isResized: jest.fn(), } -jest.mock('./mediasources', () => jest.fn(() => mockMediaSources)) -jest.mock('./playercomponent') -jest.mock('./plugins') -jest.mock('./debugger/debugtool') -jest.mock('./resizer', () => jest.fn(() => mockResizer)) -jest.mock('./subtitles/subtitles', () => jest.fn(() => mockSubtitlesInstance)) +jest.mock("./mediasources", () => jest.fn(() => mockMediaSources)) +jest.mock("./playercomponent") +jest.mock("./plugins") +jest.mock("./debugger/debugtool") +jest.mock("./resizer", () => jest.fn(() => mockResizer)) +jest.mock("./subtitles/subtitles", () => jest.fn(() => mockSubtitlesInstance)) -function setupManifestData (options) { +function setupManifestData(options) { manifestData = { - time: options && options.time || { + time: (options && options.time) || { windowStartTime: 724000, windowEndTime: 4324000, - correction: 0 - } + correction: 0, + }, } } // options = subtitlesAvailable, windowType, windowStartTime, windowEndTime -function initialiseBigscreenPlayer (options) { - options = options || {} - - const windowType = options.windowType || WindowTypes.STATIC - const subtitlesEnabled = options.subtitlesEnabled || false - - playbackElement = document.createElement('div') - playbackElement.id = 'app' +function initialiseBigscreenPlayer(options = {}) { + playbackElement = document.createElement("div") + playbackElement.id = "app" + + const { + mediaKind = "video", + subtitlesEnabled = false, + windowType = WindowTypes.STATIC, + initialPlaybackTime, + serverDate, + transferFormat, + } = options bigscreenPlayerData = { + initialPlaybackTime, + serverDate, + subtitlesEnabled, + windowType, media: { - codec: 'codec', - urls: [{ url: 'videoUrl', cdn: 'cdn' }], - kind: options.mediaKind || 'video', - type: 'mimeType', - bitrate: 'bitrate', - transferFormat: options.transferFormat + transferFormat, + kind: mediaKind, + codec: "codec", + type: "mimeType", + bitrate: "bitrate", + urls: [{ url: "videoUrl", cdn: "cdn" }], }, - serverDate: options.serverDate, - initialPlaybackTime: options.initialPlaybackTime } - if (options.windowStartTime && options.windowEndTime) { + const { windowStartTime, windowEndTime } = options + if (windowStartTime && windowEndTime) { manifestData.time = { - windowStartTime: options.windowStartTime, - windowEndTime: options.windowEndTime + windowStartTime, + windowEndTime, } } - if (options.subtitlesAvailable) { + const { subtitlesAvailable } = options + if (subtitlesAvailable) { bigscreenPlayerData.media.captions = [ { - url: 'captions1', - segmentLength: 3.84 + url: "captions1", + segmentLength: 3.84, }, { - url: 'captions2', - segmentLength: 3.84 - } + url: "captions2", + segmentLength: 3.84, + }, ] } let callbacks - if (!noCallbacks) { callbacks = { onSuccess: successCallback, onError: errorCallback } } - bigscreenPlayer.init(playbackElement, bigscreenPlayerData, windowType, subtitlesEnabled, callbacks) + return new BigscreenPlayer(playbackElement, bigscreenPlayerData, callbacks) } -describe('Bigscreen Player', () => { +describe("Bigscreen Player", () => { beforeEach(() => { + // Cleanup + jest.clearAllMocks() + forceMediaSourcesConstructionFailure = false + bigscreenPlayer?.tearDown() + bigscreenPlayer = undefined + setupManifestData() mockPlayerComponentInstance = { @@ -140,10 +153,10 @@ describe('Bigscreen Player', () => { getWindowStartTime: jest.fn(), getWindowEndTime: jest.fn(), setPlaybackRate: jest.fn(), - getPlaybackRate: jest.fn() + getPlaybackRate: jest.fn(), } - jest.spyOn(PlayerComponent, 'getLiveSupport').mockReturnValue(LiveSupport.SEEKABLE) + jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.SEEKABLE) PlayerComponent.mockImplementation((playbackElement, bigscreenPlayerData, mediaSources, windowType, callback) => { mockEventHook = callback @@ -153,22 +166,14 @@ describe('Bigscreen Player', () => { successCallback = jest.fn() errorCallback = jest.fn() noCallbacks = false - - bigscreenPlayer = BigscreenPlayer() }) - afterEach(() => { - jest.clearAllMocks() - forceMediaSourcesConstructionFailure = false - bigscreenPlayer.tearDown() - bigscreenPlayer = undefined - }) - - describe('init', () => { - it('should set endOfStream to true when playing live and no initial playback time is set', () => { + describe("init", () => { + it("should set endOfStream to true when playing live and no initial playback time is set", () => { const callback = jest.fn() - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer.registerForTimeUpdates(callback) mockEventHook({ data: { currentTime: 30 }, timeUpdate: true, isBufferingTimeoutError: false }) @@ -176,10 +181,10 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ currentTime: 30, endOfStream: true }) }) - it('should set endOfStream to false when playing live and initialPlaybackTime is 0', () => { + it("should set endOfStream to false when playing live and initialPlaybackTime is 0", () => { const callback = jest.fn() - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING, initialPlaybackTime: 0 }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING, initialPlaybackTime: 0 }) bigscreenPlayer.registerForTimeUpdates(callback) @@ -188,36 +193,38 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ currentTime: 0, endOfStream: false }) }) - it('should call the supplied error callback if manifest fails to load', () => { + it("should call the supplied error callback if manifest fails to load", () => { forceMediaSourcesConstructionFailure = true - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) expect(mediaSourcesCallbackErrorSpy).toHaveBeenCalledTimes(1) expect(errorCallback).toHaveBeenCalledTimes(1) expect(successCallback).not.toHaveBeenCalled() }) - it('should not attempt to call onSuccess callback if one is not provided', () => { + it("should not attempt to call onSuccess callback if one is not provided", () => { noCallbacks = true - initialiseBigscreenPlayer() + + bigscreenPlayer = initialiseBigscreenPlayer() expect(successCallback).not.toHaveBeenCalled() }) - it('should not attempt to call onError callback if one is not provided', () => { + it("should not attempt to call onError callback if one is not provided", () => { noCallbacks = true - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) expect(errorCallback).not.toHaveBeenCalled() }) }) - describe('getPlayerElement', () => { - it('Should call through to getPlayerElement on the playback strategy', () => { - initialiseBigscreenPlayer() + describe("getPlayerElement", () => { + it("Should call through to getPlayerElement on the playback strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() - const mockedVideo = document.createElement('video') + const mockedVideo = document.createElement("video") mockPlayerComponentInstance.getPlayerElement.mockReturnValue(mockedVideo) @@ -225,16 +232,16 @@ describe('Bigscreen Player', () => { }) }) - describe('registerForStateChanges', () => { + describe("registerForStateChanges", () => { let callback beforeEach(() => { callback = jest.fn() - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.registerForStateChanges(callback) }) - it('should fire the callback when a state event comes back from the strategy', () => { + it("should fire the callback when a state event comes back from the strategy", () => { mockEventHook({ data: { state: MediaState.PLAYING } }) expect(callback).toHaveBeenCalledWith({ state: MediaState.PLAYING, endOfStream: false }) @@ -246,7 +253,7 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ state: MediaState.WAITING, isSeeking: false, endOfStream: false }) }) - it('should set the isPaused flag to true when waiting after a setCurrentTime', () => { + it("should set the isPaused flag to true when waiting after a setCurrentTime", () => { mockEventHook({ data: { state: MediaState.PLAYING } }) expect(callback).toHaveBeenCalledWith({ state: MediaState.PLAYING, endOfStream: false }) @@ -254,12 +261,13 @@ describe('Bigscreen Player', () => { callback.mockClear() bigscreenPlayer.setCurrentTime(60) + mockEventHook({ data: { state: MediaState.WAITING } }) expect(callback).toHaveBeenCalledWith({ state: MediaState.WAITING, isSeeking: true, endOfStream: false }) }) - it('should set clear the isPaused flag after a waiting event is fired', () => { + it("should set clear the isPaused flag after a waiting event is fired", () => { mockEventHook({ data: { state: MediaState.PLAYING } }) bigscreenPlayer.setCurrentTime(60) @@ -274,41 +282,60 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ state: MediaState.WAITING, isSeeking: false, endOfStream: false }) }) - it('should set the pause trigger to the one set when a pause event comes back from strategy', () => { + it("should set the pause trigger to the one set when a pause event comes back from strategy", () => { bigscreenPlayer.pause() mockEventHook({ data: { state: MediaState.PAUSED } }) - expect(callback).toHaveBeenCalledWith({ state: MediaState.PAUSED, trigger: PauseTriggers.USER, endOfStream: false }) + expect(callback).toHaveBeenCalledWith({ + state: MediaState.PAUSED, + trigger: PauseTriggers.USER, + endOfStream: false, + }) }) - it('should set the pause trigger to device when a pause event comes back from strategy and a trigger is not set', () => { + it("should set the pause trigger to device when a pause event comes back from strategy and a trigger is not set", () => { mockEventHook({ data: { state: MediaState.PAUSED } }) - expect(callback).toHaveBeenCalledWith({ state: MediaState.PAUSED, trigger: PauseTriggers.DEVICE, endOfStream: false }) + expect(callback).toHaveBeenCalledWith({ + state: MediaState.PAUSED, + trigger: PauseTriggers.DEVICE, + endOfStream: false, + }) }) - it('should set isBufferingTimeoutError when a fatal error event comes back from strategy', () => { - mockEventHook({ data: { state: MediaState.FATAL_ERROR }, isBufferingTimeoutError: false, code: 1, message: 'media-error-aborted' }) + it("should set isBufferingTimeoutError when a fatal error event comes back from strategy", () => { + mockEventHook({ + data: { state: MediaState.FATAL_ERROR }, + isBufferingTimeoutError: false, + code: 1, + message: "media-error-aborted", + }) - expect(callback).toHaveBeenCalledWith({ state: MediaState.FATAL_ERROR, isBufferingTimeoutError: false, code: 1, message: 'media-error-aborted', endOfStream: false }) + expect(callback).toHaveBeenCalledWith({ + state: MediaState.FATAL_ERROR, + isBufferingTimeoutError: false, + code: 1, + message: "media-error-aborted", + endOfStream: false, + }) }) - it('should return a reference to the callback passed in', () => { + it("should return a reference to the callback passed in", () => { const reference = bigscreenPlayer.registerForStateChanges(callback) expect(reference).toBe(callback) }) }) - describe('unregisterForStateChanges', () => { - it('should remove callback from stateChangeCallbacks', () => { + describe("unregisterForStateChanges", () => { + it("should remove callback from stateChangeCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + const listener1 = jest.fn() const listener2 = jest.fn() const listener3 = jest.fn() - initialiseBigscreenPlayer() - bigscreenPlayer.registerForStateChanges(listener1) bigscreenPlayer.registerForStateChanges(listener2) bigscreenPlayer.registerForStateChanges(listener3) @@ -324,15 +351,15 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should remove callback from stateChangeCallbacks when a callback removes itself', () => { + it("should remove callback from stateChangeCallbacks when a callback removes itself", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + const listener1 = jest.fn() const listener2 = jest.fn().mockImplementation(() => { bigscreenPlayer.unregisterForStateChanges(listener2) }) const listener3 = jest.fn() - initialiseBigscreenPlayer() - bigscreenPlayer.registerForStateChanges(listener1) bigscreenPlayer.registerForStateChanges(listener2) bigscreenPlayer.registerForStateChanges(listener3) @@ -345,7 +372,9 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should remove callback from stateChangeCallbacks when a callback unregisters another handler last', () => { + it("should remove callback from stateChangeCallbacks when a callback unregisters another handler last", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + const listener1 = jest.fn() const listener2 = jest.fn() const listener3 = jest.fn().mockImplementation(() => { @@ -353,8 +382,6 @@ describe('Bigscreen Player', () => { bigscreenPlayer.unregisterForStateChanges(listener2) }) - initialiseBigscreenPlayer() - bigscreenPlayer.registerForStateChanges(listener1) bigscreenPlayer.registerForStateChanges(listener2) bigscreenPlayer.registerForStateChanges(listener3) @@ -367,15 +394,15 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should remove callback from stateChangeCallbacks when a callback unregisters another handler first', () => { + it("should remove callback from stateChangeCallbacks when a callback unregisters another handler first", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + + const listener3 = jest.fn() + const listener2 = jest.fn() const listener1 = jest.fn().mockImplementation(() => { bigscreenPlayer.unregisterForStateChanges(listener2) bigscreenPlayer.unregisterForStateChanges(listener3) }) - const listener2 = jest.fn() - const listener3 = jest.fn() - - initialiseBigscreenPlayer() bigscreenPlayer.registerForStateChanges(listener1) bigscreenPlayer.registerForStateChanges(listener2) @@ -389,20 +416,20 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(1) }) - it('should remove callbacks from stateChangeCallbacks when a callback unregisters multiple handlers in different places', () => { + it("should remove callbacks from stateChangeCallbacks when a callback unregisters multiple handlers in different places", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + + const listener3 = jest.fn() + const listener2 = jest.fn() const listener1 = jest.fn().mockImplementation(() => { bigscreenPlayer.unregisterForStateChanges(listener1) bigscreenPlayer.unregisterForStateChanges(listener3) }) - const listener2 = jest.fn() - const listener3 = jest.fn() const listener4 = jest.fn().mockImplementation(() => { bigscreenPlayer.unregisterForStateChanges(listener2) bigscreenPlayer.unregisterForStateChanges(listener4) }) - initialiseBigscreenPlayer() - bigscreenPlayer.registerForStateChanges(listener1) bigscreenPlayer.registerForStateChanges(listener2) bigscreenPlayer.registerForStateChanges(listener3) @@ -417,8 +444,8 @@ describe('Bigscreen Player', () => { expect(listener4).toHaveBeenCalledTimes(1) }) - it('should only remove existing callbacks from stateChangeCallbacks', () => { - initialiseBigscreenPlayer() + it("should only remove existing callbacks from stateChangeCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn() @@ -432,24 +459,27 @@ describe('Bigscreen Player', () => { }) }) - describe('player ready callback', () => { - describe('on state change event', () => { - it('should not be called when it is a fatal error', () => { - initialiseBigscreenPlayer() + describe("player ready callback", () => { + describe("on state change event", () => { + it("should not be called when it is a fatal error", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { state: MediaState.FATAL_ERROR } }) expect(successCallback).not.toHaveBeenCalled() }) - it('should be called if playing VOD and event time is valid', () => { - initialiseBigscreenPlayer() + it("should be called if playing VOD and event time is valid", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { state: MediaState.WAITING, currentTime: 0 } }) expect(successCallback).toHaveBeenCalledTimes(1) }) - it('should be called if playing VOD with an initial start time and event time is valid', () => { - initialiseBigscreenPlayer({ initialPlaybackTime: 20 }) + it("should be called if playing VOD with an initial start time and event time is valid", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ initialPlaybackTime: 20 }) + mockEventHook({ data: { state: MediaState.WAITING, currentTime: 0 } }) expect(successCallback).not.toHaveBeenCalled() @@ -458,34 +488,34 @@ describe('Bigscreen Player', () => { expect(successCallback).toHaveBeenCalledTimes(1) }) - it('should be called if playing Live and event time is valid', () => { + it("should be called if playing Live and event time is valid", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 10, - windowEndTime: 100 - } + windowEndTime: 100, + }, }) - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) mockEventHook({ - data: - { + data: { state: MediaState.PLAYING, currentTime: 10, seekableRange: { start: 10, - end: 100 - } - } + end: 100, + }, + }, }) expect(successCallback).toHaveBeenCalledTimes(1) }) - it('after a valid state change should not be called on succesive valid state changes', () => { - initialiseBigscreenPlayer() + it("after a valid state change should not be called on succesive valid state changes", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { state: MediaState.WAITING, currentTime: 0 } }) expect(successCallback).toHaveBeenCalledTimes(1) @@ -495,8 +525,9 @@ describe('Bigscreen Player', () => { expect(successCallback).not.toHaveBeenCalled() }) - it('after a valid state change should not be called on succesive valid time updates', () => { - initialiseBigscreenPlayer() + it("after a valid state change should not be called on succesive valid time updates", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { state: MediaState.WAITING, currentTime: 0 } }) expect(successCallback).toHaveBeenCalledTimes(1) @@ -507,16 +538,17 @@ describe('Bigscreen Player', () => { }) }) - describe('on time update', () => { - it('should be called if playing VOD and current time is valid', () => { - initialiseBigscreenPlayer() + describe("on time update", () => { + it("should be called if playing VOD and current time is valid", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { currentTime: 0 }, timeUpdate: true }) expect(successCallback).toHaveBeenCalledTimes(1) }) - it('should be called if playing VOD with an initial start time and current time is valid', () => { - initialiseBigscreenPlayer({ initialPlaybackTime: 20 }) + it("should be called if playing VOD with an initial start time and current time is valid", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ initialPlaybackTime: 20 }) mockEventHook({ data: { currentTime: 0 }, timeUpdate: true }) expect(successCallback).not.toHaveBeenCalled() @@ -525,33 +557,34 @@ describe('Bigscreen Player', () => { expect(successCallback).toHaveBeenCalledTimes(1) }) - it('should be called if playing Live and current time is valid', () => { + it("should be called if playing Live and current time is valid", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 10, - windowEndTime: 100 - } + windowEndTime: 100, + }, }) - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) mockEventHook({ - data: - { + data: { currentTime: 10, seekableRange: { start: 10, - end: 100 - } + end: 100, + }, }, - timeUpdate: true + timeUpdate: true, }) expect(successCallback).toHaveBeenCalledTimes(1) }) - it('after a valid time update should not be called on succesive valid time updates', () => { - initialiseBigscreenPlayer() + it("after a valid time update should not be called on succesive valid time updates", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { currentTime: 0 }, timeUpdate: true }) expect(successCallback).toHaveBeenCalledTimes(1) @@ -561,8 +594,9 @@ describe('Bigscreen Player', () => { expect(successCallback).not.toHaveBeenCalled() }) - it('after a valid time update should not be called on succesive valid state changes', () => { - initialiseBigscreenPlayer() + it("after a valid time update should not be called on succesive valid state changes", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockEventHook({ data: { currentTime: 0 }, timeUpdate: true }) expect(successCallback).toHaveBeenCalledTimes(1) @@ -574,10 +608,10 @@ describe('Bigscreen Player', () => { }) }) - describe('registerForTimeUpdates', () => { - it('should call the callback when we get a timeupdate event from the strategy', () => { + describe("registerForTimeUpdates", () => { + it("should call the callback when we get a timeupdate event from the strategy", () => { const callback = jest.fn() - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.registerForTimeUpdates(callback) expect(callback).not.toHaveBeenCalled() @@ -587,9 +621,10 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ currentTime: 60, endOfStream: false }) }) - it('returns a reference to the callback passed in', () => { + it("returns a reference to the callback passed in", () => { const callback = jest.fn() - initialiseBigscreenPlayer() + + bigscreenPlayer = initialiseBigscreenPlayer() const reference = bigscreenPlayer.registerForTimeUpdates(callback) @@ -597,9 +632,9 @@ describe('Bigscreen Player', () => { }) }) - describe('unregisterForTimeUpdates', () => { - it('should remove callback from timeUpdateCallbacks', () => { - initialiseBigscreenPlayer() + describe("unregisterForTimeUpdates", () => { + it("should remove callback from timeUpdateCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn() @@ -620,8 +655,8 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should remove callback from timeUpdateCallbacks when a callback removes itself', () => { - initialiseBigscreenPlayer() + it("should remove callback from timeUpdateCallbacks when a callback removes itself", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn().mockImplementation(() => { @@ -641,8 +676,8 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should only remove existing callbacks from timeUpdateCallbacks', () => { - initialiseBigscreenPlayer() + it("should only remove existing callbacks from timeUpdateCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn() @@ -656,10 +691,12 @@ describe('Bigscreen Player', () => { }) }) - describe('registerForSubtitleChanges', () => { - it('should call the callback when subtitles are turned on/off', () => { + describe("registerForSubtitleChanges", () => { + it("should call the callback when subtitles are turned on/off", () => { const callback = jest.fn() - initialiseBigscreenPlayer() + + bigscreenPlayer = initialiseBigscreenPlayer() + bigscreenPlayer.registerForSubtitleChanges(callback) expect(callback).not.toHaveBeenCalled() @@ -673,19 +710,20 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ enabled: false }) }) - it('returns a reference to the callback supplied', () => { + it("returns a reference to the callback supplied", () => { const callback = jest.fn() - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() + const reference = bigscreenPlayer.registerForSubtitleChanges(callback) expect(reference).toBe(callback) }) }) - describe('unregisterForSubtitleChanges', () => { - it('should remove callback from subtitleCallbacks', () => { - initialiseBigscreenPlayer() + describe("unregisterForSubtitleChanges", () => { + it("should remove callback from subtitleCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn() @@ -706,8 +744,8 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should remove callback from subtitleCallbacks when a callback removes itself', () => { - initialiseBigscreenPlayer() + it("should remove callback from subtitleCallbacks when a callback removes itself", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn().mockImplementation(() => { @@ -727,8 +765,8 @@ describe('Bigscreen Player', () => { expect(listener3).toHaveBeenCalledTimes(2) }) - it('should only remove existing callbacks from subtitleCallbacks', () => { - initialiseBigscreenPlayer() + it("should only remove existing callbacks from subtitleCallbacks", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const listener1 = jest.fn() const listener2 = jest.fn() @@ -742,38 +780,42 @@ describe('Bigscreen Player', () => { }) }) - describe('setCurrentTime', () => { - it('should setCurrentTime on the strategy/playerComponent', () => { - initialiseBigscreenPlayer() + describe("setCurrentTime", () => { + it("should setCurrentTime on the strategy/playerComponent", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setCurrentTime(60) expect(mockPlayerComponentInstance.setCurrentTime).toHaveBeenCalledWith(60) }) - it('should not set current time on the strategy/playerComponent if bigscreen player is not initialised', () => { - bigscreenPlayer.setCurrentTime(60) - + it("should not set the current time if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() + bigscreenPlayer = bigscreenPlayer.setCurrentTime(60) expect(mockPlayerComponentInstance.setCurrentTime).not.toHaveBeenCalled() }) - it('should set endOfStream to true when seeking to the end of a simulcast', () => { + it("should set endOfStream to true when seeking to the end of a simulcast", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 10, - windowEndTime: 100 - } + windowEndTime: 100, + }, }) - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) const callback = jest.fn() const endOfStreamWindow = bigscreenPlayerData.time.windowEndTime - 2 bigscreenPlayer.registerForTimeUpdates(callback) - mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: bigscreenPlayerData.time.windowStartTime, end: bigscreenPlayerData.time.windowEndTime }) + mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ + start: bigscreenPlayerData.time.windowStartTime, + end: bigscreenPlayerData.time.windowEndTime, + }) mockPlayerComponentInstance.getCurrentTime.mockReturnValue(endOfStreamWindow) bigscreenPlayer.setCurrentTime(endOfStreamWindow) @@ -783,23 +825,26 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith({ currentTime: endOfStreamWindow, endOfStream: true }) }) - it('should set endOfStream to false when seeking into a simulcast', () => { + it("should set endOfStream to false when seeking into a simulcast", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 10, - windowEndTime: 100 - } + windowEndTime: 100, + }, }) - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) const callback = jest.fn() bigscreenPlayer.registerForTimeUpdates(callback) const middleOfStreamWindow = bigscreenPlayerData.time.windowEndTime / 2 - mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: bigscreenPlayerData.time.windowStartTime, end: bigscreenPlayerData.time.windowEndTime }) + mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ + start: bigscreenPlayerData.time.windowStartTime, + end: bigscreenPlayerData.time.windowEndTime, + }) mockPlayerComponentInstance.getCurrentTime.mockReturnValue(middleOfStreamWindow) bigscreenPlayer.setCurrentTime(middleOfStreamWindow) @@ -810,210 +855,248 @@ describe('Bigscreen Player', () => { }) }) - describe('Playback Rate', () => { - it('should setPlaybackRate on the strategy/playerComponent', () => { - initialiseBigscreenPlayer() + describe("Playback Rate", () => { + it("should setPlaybackRate on the strategy/playerComponent", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setPlaybackRate(2) expect(mockPlayerComponentInstance.setPlaybackRate).toHaveBeenCalledWith(2) }) - it('should not set playback rate if playerComponent is not initialised', () => { - bigscreenPlayer.setPlaybackRate(2) - + it("should not set playback rate if playback component is not initialised", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() + bigscreenPlayer = bigscreenPlayer.setPlaybackRate(2) expect(mockPlayerComponentInstance.setPlaybackRate).not.toHaveBeenCalled() }) - it('should call through to get the playback rate when requested', () => { - initialiseBigscreenPlayer() + it("should call through to get the playback rate when requested", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + mockPlayerComponentInstance.getPlaybackRate.mockReturnValue(1.5) const rate = bigscreenPlayer.getPlaybackRate() expect(mockPlayerComponentInstance.getPlaybackRate).toHaveBeenCalled() - expect(rate).toEqual(1.5) + expect(rate).toBe(1.5) }) - it('should not get playback rate if playerComponent is not initialised', () => { - bigscreenPlayer.getPlaybackRate() - + it("does not get playback rate if playback component is not initialised", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() + bigscreenPlayer = bigscreenPlayer.getPlaybackRate() expect(mockPlayerComponentInstance.getPlaybackRate).not.toHaveBeenCalled() }) }) - describe('getCurrentTime', () => { - it('should return the current time from the strategy', () => { - initialiseBigscreenPlayer() + describe("getCurrentTime", () => { + it("should return the current time from the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockPlayerComponentInstance.getCurrentTime.mockReturnValue(10) expect(bigscreenPlayer.getCurrentTime()).toBe(10) }) - it('should return 0 if bigscreenPlayer is not initialised', () => { + it("returns `0` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.getCurrentTime()).toBe(0) }) }) - describe('getMediaKind', () => { - it('should return the media kind', () => { - initialiseBigscreenPlayer({ mediaKind: 'audio' }) + describe("getMediaKind", () => { + it("should return the media kind", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ mediaKind: "audio" }) - expect(bigscreenPlayer.getMediaKind()).toBe('audio') + expect(bigscreenPlayer.getMediaKind()).toBe("audio") }) }) - describe('getWindowType', () => { - it('should return the window type', () => { - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + describe("getWindowType", () => { + it("should return the window type", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) expect(bigscreenPlayer.getWindowType()).toBe(WindowTypes.SLIDING) }) }) - describe('getSeekableRange', () => { - it('should return the seekable range from the strategy', () => { - initialiseBigscreenPlayer() + describe("getSeekableRange", () => { + it("should return the seekable range from the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 10 }) - expect(bigscreenPlayer.getSeekableRange().start).toEqual(0) - expect(bigscreenPlayer.getSeekableRange().end).toEqual(10) + expect(bigscreenPlayer.getSeekableRange().start).toBe(0) + expect(bigscreenPlayer.getSeekableRange().end).toBe(10) }) - it('should return an empty object when bigscreen player has not been initialised', () => { + it("returns `{}` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.getSeekableRange()).toEqual({}) }) }) - describe('isAtLiveEdge', () => { - it('should return false when playing on demand content', () => { - initialiseBigscreenPlayer() + describe("isAtLiveEdge", () => { + it("should return false when playing on demand content", () => { + bigscreenPlayer = initialiseBigscreenPlayer() - expect(bigscreenPlayer.isPlayingAtLiveEdge()).toEqual(false) + expect(bigscreenPlayer.isPlayingAtLiveEdge()).toBe(false) }) - it('should return false when bigscreen-player has not been initialised', () => { - expect(bigscreenPlayer.isPlayingAtLiveEdge()).toEqual(false) + it("returns `false` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() + bigscreenPlayer = expect(bigscreenPlayer.isPlayingAtLiveEdge()).toBe(false) }) - it('should return true when playing live and current time is within tolerance of seekable range end', () => { - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + it("should return true when playing live and current time is within tolerance of seekable range end", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) mockPlayerComponentInstance.getCurrentTime.mockReturnValue(100) mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 105 }) - expect(bigscreenPlayer.isPlayingAtLiveEdge()).toEqual(true) + expect(bigscreenPlayer.isPlayingAtLiveEdge()).toBe(true) }) - it('should return false when playing live and current time is outside the tolerance of seekable range end', () => { - initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) + it("should return false when playing live and current time is outside the tolerance of seekable range end", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.SLIDING }) mockPlayerComponentInstance.getCurrentTime.mockReturnValue(95) mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 105 }) - expect(bigscreenPlayer.isPlayingAtLiveEdge()).toEqual(false) + expect(bigscreenPlayer.isPlayingAtLiveEdge()).toBe(false) }) }) - describe('getLiveWindowData', () => { - it('should return undefined values when windowType is static', () => { - initialiseBigscreenPlayer({ windowType: WindowTypes.STATIC }) + describe("getLiveWindowData", () => { + it("should return undefined values when windowType is static", () => { + bigscreenPlayer = initialiseBigscreenPlayer({ windowType: WindowTypes.STATIC }) expect(bigscreenPlayer.getLiveWindowData()).toEqual({}) }) - it('should return liveWindowData when the windowType is sliding and manifest is loaded', () => { + it("should return liveWindowData when the windowType is sliding and manifest is loaded", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 1, - windowEndTime: 2 - } + windowEndTime: 2, + }, }) - const initialisationData = { windowType: WindowTypes.SLIDING, serverDate: new Date(), initialPlaybackTime: new Date().getTime() } - initialiseBigscreenPlayer(initialisationData) + const initialisationData = { + windowType: WindowTypes.SLIDING, + serverDate: new Date(), + initialPlaybackTime: Date.now(), + } + + bigscreenPlayer = initialiseBigscreenPlayer(initialisationData) - expect(bigscreenPlayer.getLiveWindowData()).toEqual({ windowStartTime: 1, windowEndTime: 2, serverDate: initialisationData.serverDate, initialPlaybackTime: initialisationData.initialPlaybackTime }) + expect(bigscreenPlayer.getLiveWindowData()).toEqual({ + windowStartTime: 1, + windowEndTime: 2, + serverDate: initialisationData.serverDate, + initialPlaybackTime: initialisationData.initialPlaybackTime, + }) }) - it('should return a subset of liveWindowData when the windowType is sliding and time block is provided', () => { - const initialisationData = { windowType: WindowTypes.SLIDING, windowStartTime: 1, windowEndTime: 2, initialPlaybackTime: new Date().getTime() } - initialiseBigscreenPlayer(initialisationData) + it("should return a subset of liveWindowData when the windowType is sliding and time block is provided", () => { + const initialisationData = { + windowType: WindowTypes.SLIDING, + windowStartTime: 1, + windowEndTime: 2, + initialPlaybackTime: Date.now(), + } + + bigscreenPlayer = initialiseBigscreenPlayer(initialisationData) - expect(bigscreenPlayer.getLiveWindowData()).toEqual({ serverDate: undefined, windowStartTime: 1, windowEndTime: 2, initialPlaybackTime: initialisationData.initialPlaybackTime }) + expect(bigscreenPlayer.getLiveWindowData()).toEqual({ + serverDate: undefined, + windowStartTime: 1, + windowEndTime: 2, + initialPlaybackTime: initialisationData.initialPlaybackTime, + }) }) }) - describe('getDuration', () => { - it('should get the duration from the strategy', () => { - initialiseBigscreenPlayer() + describe("getDuration", () => { + it("should get the duration from the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockPlayerComponentInstance.getDuration.mockReturnValue(10) - expect(bigscreenPlayer.getDuration()).toEqual(10) + expect(bigscreenPlayer.getDuration()).toBe(10) }) - it('should return undefined when not initialised', () => { + it("returns `undefined` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.getDuration()).toBeUndefined() }) }) - describe('isPaused', () => { - it('should get the paused state from the strategy', () => { - initialiseBigscreenPlayer() + describe("isPaused", () => { + it("should get the paused state from the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockPlayerComponentInstance.isPaused.mockReturnValue(true) expect(bigscreenPlayer.isPaused()).toBe(true) }) - it('should return true if bigscreenPlayer has not been initialised', () => { + it("returns `true` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.isPaused()).toBe(true) }) }) - describe('isEnded', () => { - it('should get the ended state from the strategy', () => { - initialiseBigscreenPlayer() + describe("isEnded", () => { + it("should get the ended state from the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockPlayerComponentInstance.isEnded.mockReturnValue(true) expect(bigscreenPlayer.isEnded()).toBe(true) }) - it('should return false if bigscreenPlayer has not been initialised', () => { + it("returns `false` if media is not mounted", () => { + forceMediaSourcesConstructionFailure = true + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.isEnded()).toBe(false) }) }) - describe('play', () => { - it('should call play on the strategy', () => { - initialiseBigscreenPlayer() + describe("play", () => { + it("should call play on the strategy", () => { + bigscreenPlayer = initialiseBigscreenPlayer() - bigscreenPlayer.play() + bigscreenPlayer = bigscreenPlayer.play() expect(mockPlayerComponentInstance.play).toHaveBeenCalledWith() }) }) - describe('pause', () => { - it('should call pause on the strategy', () => { + describe("pause", () => { + it("should call pause on the strategy", () => { const opts = { disableAutoResume: true } - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.pause(opts) - expect(mockPlayerComponentInstance.pause).toHaveBeenCalledWith(expect.objectContaining({ disableAutoResume: true })) + expect(mockPlayerComponentInstance.pause).toHaveBeenCalledWith( + expect.objectContaining({ disableAutoResume: true }) + ) }) - it('should set pauseTrigger to an app pause if user pause is false', () => { + it("should set pauseTrigger to an app pause if user pause is false", () => { const opts = { userPause: false } - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() const callback = jest.fn() @@ -1026,10 +1109,10 @@ describe('Bigscreen Player', () => { expect(callback).toHaveBeenCalledWith(expect.objectContaining({ trigger: PauseTriggers.APP })) }) - it('should set pauseTrigger to a user pause if user pause is true', () => { + it("should set pauseTrigger to a user pause if user pause is true", () => { const opts = { userPause: true } - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() const callback = jest.fn() @@ -1043,9 +1126,9 @@ describe('Bigscreen Player', () => { }) }) - describe('setSubtitlesEnabled', () => { - it('should turn subtitles on/off when a value is passed in', () => { - initialiseBigscreenPlayer() + describe("setSubtitlesEnabled", () => { + it("should turn subtitles on/off when a value is passed in", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setSubtitlesEnabled(true) expect(mockSubtitlesInstance.enable).toHaveBeenCalledTimes(1) @@ -1055,22 +1138,22 @@ describe('Bigscreen Player', () => { expect(mockSubtitlesInstance.disable).toHaveBeenCalledTimes(1) }) - it('should show subtitles when called with true', () => { - initialiseBigscreenPlayer() + it("should show subtitles when called with true", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setSubtitlesEnabled(true) expect(mockSubtitlesInstance.show).toHaveBeenCalledTimes(1) }) - it('should hide subtitleswhen called with false', () => { - initialiseBigscreenPlayer() + it("should hide subtitleswhen called with false", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setSubtitlesEnabled(false) expect(mockSubtitlesInstance.hide).toHaveBeenCalledTimes(1) }) - it('should not show subtitles when resized', () => { - initialiseBigscreenPlayer() + it("should not show subtitles when resized", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockResizer.isResized.mockReturnValue(true) bigscreenPlayer.setSubtitlesEnabled(true) @@ -1078,8 +1161,8 @@ describe('Bigscreen Player', () => { expect(mockSubtitlesInstance.show).not.toHaveBeenCalled() }) - it('should not hide subtitles when resized', () => { - initialiseBigscreenPlayer() + it("should not hide subtitles when resized", () => { + bigscreenPlayer = initialiseBigscreenPlayer() mockResizer.isResized.mockReturnValue(true) bigscreenPlayer.setSubtitlesEnabled(true) @@ -1088,9 +1171,9 @@ describe('Bigscreen Player', () => { }) }) - describe('isSubtitlesEnabled', () => { - it('calls through to Subtitles enabled when called', () => { - initialiseBigscreenPlayer() + describe("isSubtitlesEnabled", () => { + it("calls through to Subtitles enabled when called", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.isSubtitlesEnabled() @@ -1098,9 +1181,9 @@ describe('Bigscreen Player', () => { }) }) - describe('isSubtitlesAvailable', () => { - it('calls through to Subtitles available when called', () => { - initialiseBigscreenPlayer() + describe("isSubtitlesAvailable", () => { + it("calls through to Subtitles available when called", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.isSubtitlesAvailable() @@ -1108,9 +1191,9 @@ describe('Bigscreen Player', () => { }) }) - describe('customiseSubtitles', () => { - it('passes through custom styles to Subtitles customise', () => { - initialiseBigscreenPlayer() + describe("customiseSubtitles", () => { + it("passes through custom styles to Subtitles customise", () => { + bigscreenPlayer = initialiseBigscreenPlayer() const customStyleObj = { size: 0.7 } bigscreenPlayer.customiseSubtitles(customStyleObj) @@ -1118,10 +1201,10 @@ describe('Bigscreen Player', () => { }) }) - describe('renderSubtitleExample', () => { - it('calls Subtitles renderExample with correct values', () => { - initialiseBigscreenPlayer() - const exampleUrl = '' + describe("renderSubtitleExample", () => { + it("calls Subtitles renderExample with correct values", () => { + bigscreenPlayer = initialiseBigscreenPlayer() + const exampleUrl = "" const customStyleObj = { size: 0.7 } const safePosititon = { left: 30, top: 0 } bigscreenPlayer.renderSubtitleExample(exampleUrl, customStyleObj, safePosititon) @@ -1130,120 +1213,120 @@ describe('Bigscreen Player', () => { }) }) - describe('clearSubtitleExample', () => { - it('calls Subtitles clearExample', () => { - initialiseBigscreenPlayer() + describe("clearSubtitleExample", () => { + it("calls Subtitles clearExample", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.clearSubtitleExample() expect(mockSubtitlesInstance.clearExample).toHaveBeenCalledTimes(1) }) }) - describe('setTransportControlsPosition', () => { - it('should call through to Subtitles setPosition function', () => { - initialiseBigscreenPlayer() + describe("setTransportControlsPosition", () => { + it("should call through to Subtitles setPosition function", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.setTransportControlsPosition() expect(mockSubtitlesInstance.setPosition).toHaveBeenCalledTimes(1) }) }) - describe('resize', () => { - it('calls resizer with correct values', () => { - initialiseBigscreenPlayer() + describe("resize", () => { + it("calls resizer with correct values", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.resize(10, 10, 160, 90, 100) expect(mockResizer.resize).toHaveBeenCalledWith(playbackElement, 10, 10, 160, 90, 100) }) - it('hides subtitles when resized', () => { - initialiseBigscreenPlayer() + it("hides subtitles when resized", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.resize(10, 10, 160, 90, 100) expect(mockSubtitlesInstance.hide).toHaveBeenCalledTimes(1) }) }) - describe('clearResize', () => { - it('calls resizers clear function', () => { - initialiseBigscreenPlayer() + describe("clearResize", () => { + it("calls resizers clear function", () => { + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.clearResize() expect(mockResizer.clear).toHaveBeenCalledWith(playbackElement) }) - it('shows subtitles if subtitles are enabled', () => { + it("shows subtitles if subtitles are enabled", () => { mockSubtitlesInstance.enabled.mockReturnValue(true) - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.clearResize() expect(mockSubtitlesInstance.show).toHaveBeenCalledTimes(1) }) - it('hides subtitles if subtitles are disabled', () => { + it("hides subtitles if subtitles are disabled", () => { mockSubtitlesInstance.enabled.mockReturnValue(false) - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.clearResize() expect(mockSubtitlesInstance.hide).toHaveBeenCalledTimes(1) }) }) - describe('canSeek', () => { - it('should return true when in VOD playback', () => { - initialiseBigscreenPlayer() + describe("canSeek", () => { + it("should return true when in VOD playback", () => { + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.canSeek()).toBe(true) }) - describe('live', () => { - it('should return true when it can seek', () => { + describe("live", () => { + it("should return true when it can seek", () => { mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 60 }) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canSeek()).toBe(true) }) - it('should return false when seekable range is infinite', () => { + it("should return false when seekable range is infinite", () => { mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: Infinity }) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canSeek()).toBe(false) }) - it('should return false when window length less than four minutes', () => { + it("should return false when window length less than four minutes", () => { setupManifestData({ - transferFormat: 'dash', + transferFormat: "dash", time: { windowStartTime: 0, windowEndTime: 239999, - correction: 0 - } + correction: 0, + }, }) mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 60 }) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canSeek()).toBe(false) }) - it('should return false when device does not support seeking', () => { + it("should return false when device does not support seeking", () => { mockPlayerComponentInstance.getSeekableRange.mockReturnValue({ start: 0, end: 60 }) - jest.spyOn(PlayerComponent, 'getLiveSupport').mockReturnValue(LiveSupport.PLAYABLE) + jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.PLAYABLE) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canSeek()).toBe(false) @@ -1251,47 +1334,47 @@ describe('Bigscreen Player', () => { }) }) - describe('canPause', () => { - it('VOD should return true', () => { - initialiseBigscreenPlayer() + describe("canPause", () => { + it("VOD should return true", () => { + bigscreenPlayer = initialiseBigscreenPlayer() expect(bigscreenPlayer.canPause()).toBe(true) }) - describe('LIVE', () => { - it('should return true when it can pause', () => { - jest.spyOn(PlayerComponent, 'getLiveSupport').mockReturnValue(LiveSupport.RESTARTABLE) + describe("LIVE", () => { + it("should return true when it can pause", () => { + jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.RESTARTABLE) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canPause()).toBe(true) }) - it('should be false when window length less than four minutes', () => { + it("should be false when window length less than four minutes", () => { setupManifestData({ transferFormat: TransferFormats.DASH, time: { windowStartTime: 0, windowEndTime: 239999, - correction: 0 - } + correction: 0, + }, }) - jest.spyOn(PlayerComponent, 'getLiveSupport').mockReturnValue(LiveSupport.RESTARTABLE) + jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.RESTARTABLE) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canPause()).toBe(false) }) - it('should return false when device does not support pausing', () => { - jest.spyOn(PlayerComponent, 'getLiveSupport').mockReturnValue(LiveSupport.PLAYABLE) + it("should return false when device does not support pausing", () => { + jest.spyOn(PlayerComponent, "getLiveSupport").mockReturnValue(LiveSupport.PLAYABLE) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.canPause()).toBe(false) @@ -1299,89 +1382,89 @@ describe('Bigscreen Player', () => { }) }) - describe('convertVideoTimeSecondsToEpochMs', () => { - it('converts video time to epoch time when windowStartTime is supplied', () => { + describe("convertVideoTimeSecondsToEpochMs", () => { + it("converts video time to epoch time when windowStartTime is supplied", () => { setupManifestData({ time: { windowStartTime: 4200, - windowEndTime: 150000000 - } + windowEndTime: 150000000, + }, }) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) expect(bigscreenPlayer.convertVideoTimeSecondsToEpochMs(1000)).toBe(4200 + 1000000) }) - it('does not convert video time to epoch time when windowStartTime is not supplied', () => { + it("does not convert video time to epoch time when windowStartTime is not supplied", () => { setupManifestData({ time: { windowStartTime: undefined, - windowEndTime: undefined - } + windowEndTime: undefined, + }, }) - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() - expect(bigscreenPlayer.convertVideoTimeSecondsToEpochMs(1000)).toBe(null) + expect(bigscreenPlayer.convertVideoTimeSecondsToEpochMs(1000)).toBeNull() }) }) - describe('covertEpochMsToVideoTimeSeconds', () => { - it('converts epoch time to video time when windowStartTime is available', () => { + describe("covertEpochMsToVideoTimeSeconds", () => { + it("converts epoch time to video time when windowStartTime is available", () => { // windowStartTime - 16 January 2019 12:00:00 // windowEndTime - 16 January 2019 14:00:00 setupManifestData({ time: { windowStartTime: 1547640000000, - windowEndTime: 1547647200000 - } + windowEndTime: 1547647200000, + }, }) - initialiseBigscreenPlayer({ - windowType: WindowTypes.SLIDING + bigscreenPlayer = initialiseBigscreenPlayer({ + windowType: WindowTypes.SLIDING, }) // Time to convert - 16 January 2019 13:00:00 - one hour (3600 seconds) expect(bigscreenPlayer.convertEpochMsToVideoTimeSeconds(1547643600000)).toBe(3600) }) - it('does not convert epoch time to video time when windowStartTime is not available', () => { + it("does not convert epoch time to video time when windowStartTime is not available", () => { setupManifestData({ time: { windowStartTime: undefined, - windowEndTime: undefined - } + windowEndTime: undefined, + }, }) - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() - expect(bigscreenPlayer.convertEpochMsToVideoTimeSeconds(1547643600000)).toBe(null) + expect(bigscreenPlayer.convertEpochMsToVideoTimeSeconds(1547643600000)).toBeNull() }) }) - describe('registerPlugin', () => { - it('should register a specific plugin', () => { + describe("registerPlugin", () => { + it("should register a specific plugin", () => { const mockPlugin = { - onError: jest.fn() + onError: jest.fn(), } - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.registerPlugin(mockPlugin) expect(Plugins.registerPlugin).toHaveBeenCalledWith(mockPlugin) }) }) - describe('unregister plugin', () => { - it('should remove a specific plugin', () => { + describe("unregister plugin", () => { + it("should remove a specific plugin", () => { const mockPlugin = { - onError: jest.fn() + onError: jest.fn(), } - initialiseBigscreenPlayer() + bigscreenPlayer = initialiseBigscreenPlayer() bigscreenPlayer.unregisterPlugin(mockPlugin) @@ -1389,37 +1472,11 @@ describe('Bigscreen Player', () => { }) }) - describe('getDebugLogs', () => { + describe("getDebugLogs", () => { it('should call "retrieve" on the Chronicle', () => { - jest.spyOn(Chronicle, 'retrieve') - bigscreenPlayer.getDebugLogs() + jest.spyOn(Chronicle, "retrieve") + BigscreenPlayer.getDebugLogs() expect(Chronicle.retrieve).toHaveBeenCalledTimes(1) }) }) - - // describe('mock', () => { - // afterEach(() => { - // bigscreenPlayer.unmock() - // }) - - // it('should return a mock object with jasmine spies on the same interface as the main api', () => { - // initialiseBigscreenPlayer() - - // const moduleKeys = Object.keys(bigscreenPlayer) - // bigscreenPlayer.mockJasmine() - // const mockKeys = Object.keys(bigscreenPlayer) - - // expect(mockKeys).toEqual(expect.objectContaining(moduleKeys)) - // }) - - // it('should return a mock object on the same interface as the main api', () => { - // initialiseBigscreenPlayer() - - // const moduleKeys = Object.keys(bigscreenPlayer) - // bigscreenPlayer.mock() - // const mockKeys = Object.keys(bigscreenPlayer) - - // expect(mockKeys).toEqual(expect.objectContaining(moduleKeys)) - // }) - // }) }) diff --git a/src/typedefs.js b/src/typedefs.js deleted file mode 100644 index 3755040e..00000000 --- a/src/typedefs.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Data required for playback - * @typedef {Object} BigscreenPlayerData - * @property {Object} media - * @property {String} media.type - source type e.g 'application/dash+xml' - * @property {String} media.mimeType - mimeType e.g 'video/mp4' - * @property {String} media.kind - 'video' or 'audio' - * @property {String} media.captionsUrl - 'Location for a captions file' - * @property {MediaUrl[]} media.urls - Media urls to use - * @property {Date} serverDate - Date object with server time offset - */ - -/** - * @typedef {Object} MediaUrl - * @property {String} url - media endpoint - * @property {String} cdn - identifier for the endpoint - */ - -/** - * - * @typedef {object} InitCallbacks - * @property {function} [callbacks.onSuccess] - Called after Bigscreen Player is initialised - * @property {function} [callbacks.onError] - Called when an error occurs during initialisation - */