]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Reorganize player manager options builder
authorChocobozzz <me@florianbigard.com>
Mon, 14 Mar 2022 10:16:54 +0000 (11:16 +0100)
committerChocobozzz <me@florianbigard.com>
Mon, 14 Mar 2022 10:36:38 +0000 (11:36 +0100)
client/src/assets/player/index.ts
client/src/assets/player/manager-options/control-bar-options-builder.ts [new file with mode: 0644]
client/src/assets/player/manager-options/hls-options-builder.ts [new file with mode: 0644]
client/src/assets/player/manager-options/manager-options-builder.ts [new file with mode: 0644]
client/src/assets/player/manager-options/manager-options.model.ts [new file with mode: 0644]
client/src/assets/player/manager-options/webtorrent-options-builder.ts [new file with mode: 0644]
client/src/assets/player/p2p-media-loader/hls-plugin.ts
client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/peertube-player-options-builder.ts [deleted file]
client/src/assets/player/peertube-videojs-typings.ts

index e2a6ccf24c8dca823b815bf4447581b4eb7a1b9a..92270476d75c75c3a6321f2a34087d98071e5de1 100644 (file)
@@ -1,2 +1,2 @@
 export * from './peertube-player-manager'
-export * from './peertube-player-options-builder'
+export * from './manager-options/manager-options.model'
diff --git a/client/src/assets/player/manager-options/control-bar-options-builder.ts b/client/src/assets/player/manager-options/control-bar-options-builder.ts
new file mode 100644 (file)
index 0000000..54e61c5
--- /dev/null
@@ -0,0 +1,132 @@
+import { NextPreviousVideoButtonOptions, PeerTubeLinkButtonOptions } from '../peertube-videojs-typings'
+import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model'
+
+export class ControlBarOptionsBuilder {
+  private options: CommonOptions
+
+  constructor (
+    globalOptions: PeertubePlayerManagerOptions,
+    private mode: PlayerMode
+  ) {
+    this.options = globalOptions.common
+  }
+
+  getChildrenOptions () {
+    const children = {}
+
+    if (this.options.previousVideo) {
+      Object.assign(children, this.getPreviousVideo())
+    }
+
+    Object.assign(children, { playToggle: {} })
+
+    if (this.options.nextVideo) {
+      Object.assign(children, this.getNextVideo())
+    }
+
+    Object.assign(children, {
+      currentTimeDisplay: {},
+      timeDivider: {},
+      durationDisplay: {},
+      liveDisplay: {},
+
+      flexibleWidthSpacer: {},
+
+      ...this.getProgressControl(),
+
+      p2PInfoButton: {
+        p2pEnabled: this.options.p2pEnabled
+      },
+
+      muteToggle: {},
+      volumeControl: {},
+
+      settingsButton: this.getSettingsButton()
+    })
+
+    if (this.options.peertubeLink === true) {
+      Object.assign(children, {
+        peerTubeLinkButton: { shortUUID: this.options.videoShortUUID } as PeerTubeLinkButtonOptions
+      })
+    }
+
+    if (this.options.theaterButton === true) {
+      Object.assign(children, {
+        theaterButton: {}
+      })
+    }
+
+    Object.assign(children, {
+      fullscreenToggle: {}
+    })
+
+    return children
+  }
+
+  private getSettingsButton () {
+    const settingEntries: string[] = []
+
+    settingEntries.push('playbackRateMenuButton')
+
+    if (this.options.captions === true) settingEntries.push('captionsButton')
+
+    settingEntries.push('resolutionMenuButton')
+
+    return {
+      settingsButton: {
+        setup: {
+          maxHeightOffset: 40
+        },
+        entries: settingEntries
+      }
+    }
+  }
+
+  private getProgressControl () {
+    const loadProgressBar = this.mode === 'webtorrent'
+      ? 'peerTubeLoadProgressBar'
+      : 'loadProgressBar'
+
+    return {
+      progressControl: {
+        children: {
+          seekBar: {
+            children: {
+              [loadProgressBar]: {},
+              mouseTimeDisplay: {},
+              playProgressBar: {}
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private getPreviousVideo () {
+    const buttonOptions: NextPreviousVideoButtonOptions = {
+      type: 'previous',
+      handler: this.options.previousVideo,
+      isDisabled: () => {
+        if (!this.options.hasPreviousVideo) return false
+
+        return !this.options.hasPreviousVideo()
+      }
+    }
+
+    return { previousVideoButton: buttonOptions }
+  }
+
+  private getNextVideo () {
+    const buttonOptions: NextPreviousVideoButtonOptions = {
+      type: 'next',
+      handler: this.options.nextVideo,
+      isDisabled: () => {
+        if (!this.options.hasNextVideo) return false
+
+        return !this.options.hasNextVideo()
+      }
+    }
+
+    return { nextVideoButton: buttonOptions }
+  }
+}
diff --git a/client/src/assets/player/manager-options/hls-options-builder.ts b/client/src/assets/player/manager-options/hls-options-builder.ts
new file mode 100644 (file)
index 0000000..9de2356
--- /dev/null
@@ -0,0 +1,192 @@
+import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
+import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
+import { LiveVideoLatencyMode } from '@shared/models'
+import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
+import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
+import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator'
+import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
+import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../peertube-videojs-typings'
+import { getRtcConfig } from '../utils'
+import { PeertubePlayerManagerOptions } from './manager-options.model'
+
+export class HLSOptionsBuilder {
+
+  constructor (
+    private options: PeertubePlayerManagerOptions,
+    private p2pMediaLoaderModule?: any
+  ) {
+
+  }
+
+  getPluginOptions () {
+    const commonOptions = this.options.common
+
+    const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
+
+    const p2pMediaLoaderConfig = this.getP2PMediaLoaderOptions(redundancyUrlManager)
+    const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader
+
+    const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
+      redundancyUrlManager,
+      type: 'application/x-mpegURL',
+      startTime: commonOptions.startTime,
+      src: this.options.p2pMediaLoader.playlistUrl,
+      loader
+    }
+
+    const hlsjs = {
+      levelLabelHandler: (level: { height: number, width: number }) => {
+        const resolution = Math.min(level.height || 0, level.width || 0)
+
+        const file = this.options.p2pMediaLoader.videoFiles.find(f => f.resolution.id === resolution)
+        // We don't have files for live videos
+        if (!file) return level.height
+
+        let label = file.resolution.label
+        if (file.fps >= 50) label += file.fps
+
+        return label
+      },
+      html5: {
+        hlsjsConfig: this.getHLSJSOptions(loader)
+      }
+    }
+
+    return { p2pMediaLoader, hlsjs }
+  }
+
+  // ---------------------------------------------------------------------------
+
+  private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings {
+    let consumeOnly = false
+    if ((navigator as any)?.connection?.type === 'cellular') {
+      console.log('We are on a cellular connection: disabling seeding.')
+      consumeOnly = true
+    }
+
+    const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce
+                                                 .filter(t => t.startsWith('ws'))
+
+    const specificLiveOrVODOptions = this.options.common.isLive
+      ? this.getP2PMediaLoaderLiveOptions()
+      : this.getP2PMediaLoaderVODOptions()
+
+    return {
+      loader: {
+
+        trackerAnnounce,
+        rtcConfig: getRtcConfig(),
+
+        simultaneousHttpDownloads: 1,
+        httpFailedSegmentTimeout: 1000,
+
+        segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
+        segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
+
+        useP2P: this.options.common.p2pEnabled,
+        consumeOnly,
+
+        ...specificLiveOrVODOptions
+      },
+      segments: {
+        swarmId: this.options.p2pMediaLoader.playlistUrl,
+        forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority
+      }
+    }
+  }
+
+  private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
+    const base = {
+      requiredSegmentsPriority: 1
+    }
+
+    const latencyMode = this.options.common.liveOptions.latencyMode
+
+    switch (latencyMode) {
+      case LiveVideoLatencyMode.SMALL_LATENCY:
+        return {
+          ...base,
+
+          useP2P: false,
+          httpDownloadProbability: 1
+        }
+
+      case LiveVideoLatencyMode.HIGH_LATENCY:
+        return base
+
+      default:
+        return base
+    }
+  }
+
+  private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
+    return {
+      requiredSegmentsPriority: 3,
+
+      cachedSegmentExpiration: 86400000,
+      cachedSegmentsCount: 100,
+
+      httpDownloadMaxPriority: 9,
+      httpDownloadProbability: 0.06,
+      httpDownloadProbabilitySkipIfNoPeers: true,
+
+      p2pDownloadMaxPriority: 50
+    }
+  }
+
+  // ---------------------------------------------------------------------------
+
+  private getHLSJSOptions (loader: P2PMediaLoader) {
+    const specificLiveOrVODOptions = this.options.common.isLive
+      ? this.getHLSLiveOptions()
+      : this.getHLSVODOptions()
+
+    const base = {
+      capLevelToPlayerSize: true,
+      autoStartLoad: false,
+
+      loader,
+
+      ...specificLiveOrVODOptions
+    }
+
+    const averageBandwidth = getAverageBandwidthInStore()
+    if (!averageBandwidth) return base
+
+    return {
+      ...base,
+
+      abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
+      startLevel: -1,
+      testBandwidth: false,
+      debug: false
+    }
+  }
+
+  private getHLSLiveOptions () {
+    const latencyMode = this.options.common.liveOptions.latencyMode
+
+    switch (latencyMode) {
+      case LiveVideoLatencyMode.SMALL_LATENCY:
+        return {
+          liveSyncDurationCount: 2
+        }
+
+      case LiveVideoLatencyMode.HIGH_LATENCY:
+        return {
+          liveSyncDurationCount: 10
+        }
+
+      default:
+        return {
+          liveSyncDurationCount: 5
+        }
+    }
+  }
+
+  private getHLSVODOptions () {
+    return {
+      liveSyncDurationCount: 5
+    }
+  }
+}
diff --git a/client/src/assets/player/manager-options/manager-options-builder.ts b/client/src/assets/player/manager-options/manager-options-builder.ts
new file mode 100644 (file)
index 0000000..14bdb5d
--- /dev/null
@@ -0,0 +1,168 @@
+import videojs from 'video.js'
+import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
+import { isDefaultLocale } from '@shared/core-utils/i18n'
+import { copyToClipboard } from '../../../root-helpers/utils'
+import { VideoJSPluginOptions } from '../peertube-videojs-typings'
+import { buildVideoOrPlaylistEmbed, isIOS, isSafari } from '../utils'
+import { ControlBarOptionsBuilder } from './control-bar-options-builder'
+import { HLSOptionsBuilder } from './hls-options-builder'
+import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model'
+import { WebTorrentOptionsBuilder } from './webtorrent-options-builder'
+
+export class ManagerOptionsBuilder {
+
+  constructor (
+    private mode: PlayerMode,
+    private options: PeertubePlayerManagerOptions,
+    private p2pMediaLoaderModule?: any
+  ) {
+
+  }
+
+  getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
+    const commonOptions = this.options.common
+
+    let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
+    const html5 = {
+      preloadTextTracks: false
+    }
+
+    const plugins: VideoJSPluginOptions = {
+      peertube: {
+        mode: this.mode,
+        autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
+        videoViewUrl: commonOptions.videoViewUrl,
+        videoDuration: commonOptions.videoDuration,
+        userWatching: commonOptions.userWatching,
+        subtitle: commonOptions.subtitle,
+        videoCaptions: commonOptions.videoCaptions,
+        stopTime: commonOptions.stopTime,
+        isLive: commonOptions.isLive,
+        videoUUID: commonOptions.videoUUID
+      }
+    }
+
+    if (commonOptions.playlist) {
+      plugins.playlist = commonOptions.playlist
+    }
+
+    if (this.mode === 'p2p-media-loader') {
+      const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule)
+
+      Object.assign(plugins, hlsOptionsBuilder.getPluginOptions())
+    } else if (this.mode === 'webtorrent') {
+      const webtorrentOptionsBuilder = new WebTorrentOptionsBuilder(this.options, this.getAutoPlayValue(autoplay, alreadyPlayed))
+
+      Object.assign(plugins, webtorrentOptionsBuilder.getPluginOptions())
+
+      // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
+      autoplay = false
+    }
+
+    const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode)
+
+    const videojsOptions = {
+      html5,
+
+      // We don't use text track settings for now
+      textTrackSettings: false as any, // FIXME: typings
+      controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
+      loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
+
+      muted: commonOptions.muted !== undefined
+        ? commonOptions.muted
+        : undefined, // Undefined so the player knows it has to check the local storage
+
+      autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
+
+      poster: commonOptions.poster,
+      inactivityTimeout: commonOptions.inactivityTimeout,
+      playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
+
+      plugins,
+
+      controlBar: {
+        children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings
+      }
+    }
+
+    if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
+      Object.assign(videojsOptions, { language: commonOptions.language })
+    }
+
+    return videojsOptions
+  }
+
+  private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
+    if (autoplay !== true) return autoplay
+
+    // On first play, disable autoplay to avoid issues
+    // But if the player already played videos, we can safely autoplay next ones
+    if (isIOS() || isSafari()) {
+      return alreadyPlayed ? 'play' : false
+    }
+
+    return 'play'
+  }
+
+  getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
+    const content = () => {
+      const isLoopEnabled = player.options_['loop']
+
+      const items = [
+        {
+          icon: 'repeat',
+          label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
+          listener: function () {
+            player.options_['loop'] = !isLoopEnabled
+          }
+        },
+        {
+          label: player.localize('Copy the video URL'),
+          listener: function () {
+            copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
+          }
+        },
+        {
+          label: player.localize('Copy the video URL at the current time'),
+          listener: function (this: videojs.Player) {
+            const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
+
+            copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
+          }
+        },
+        {
+          icon: 'code',
+          label: player.localize('Copy embed code'),
+          listener: () => {
+            copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
+          }
+        }
+      ]
+
+      if (this.mode === 'webtorrent') {
+        items.push({
+          label: player.localize('Copy magnet URI'),
+          listener: function (this: videojs.Player) {
+            copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
+          }
+        })
+      }
+
+      items.push({
+        icon: 'info',
+        label: player.localize('Stats for nerds'),
+        listener: () => {
+          player.stats().show()
+        }
+      })
+
+      return items.map(i => ({
+        ...i,
+        label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
+      }))
+    }
+
+    return { content }
+  }
+}
diff --git a/client/src/assets/player/manager-options/manager-options.model.ts b/client/src/assets/player/manager-options/manager-options.model.ts
new file mode 100644 (file)
index 0000000..0b0f8b4
--- /dev/null
@@ -0,0 +1,84 @@
+import { PluginsManager } from '@root-helpers/plugins-manager'
+import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
+import { PlaylistPluginOptions, UserWatching, VideoJSCaption } from '../peertube-videojs-typings'
+
+export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
+
+export type WebtorrentOptions = {
+  videoFiles: VideoFile[]
+}
+
+export type P2PMediaLoaderOptions = {
+  playlistUrl: string
+  segmentsSha256Url: string
+  trackerAnnounce: string[]
+  redundancyBaseUrls: string[]
+  videoFiles: VideoFile[]
+}
+
+export interface CustomizationOptions {
+  startTime: number | string
+  stopTime: number | string
+
+  controls?: boolean
+  muted?: boolean
+  loop?: boolean
+  subtitle?: string
+  resume?: string
+
+  peertubeLink: boolean
+}
+
+export interface CommonOptions extends CustomizationOptions {
+  playerElement: HTMLVideoElement
+  onPlayerElementChange: (element: HTMLVideoElement) => void
+
+  autoplay: boolean
+  p2pEnabled: boolean
+
+  nextVideo?: () => void
+  hasNextVideo?: () => boolean
+
+  previousVideo?: () => void
+  hasPreviousVideo?: () => boolean
+
+  playlist?: PlaylistPluginOptions
+
+  videoDuration: number
+  enableHotkeys: boolean
+  inactivityTimeout: number
+  poster: string
+
+  theaterButton: boolean
+  captions: boolean
+
+  videoViewUrl: string
+  embedUrl: string
+  embedTitle: string
+
+  isLive: boolean
+  liveOptions?: {
+    latencyMode: LiveVideoLatencyMode
+  }
+
+  language?: string
+
+  videoCaptions: VideoJSCaption[]
+
+  videoUUID: string
+  videoShortUUID: string
+
+  userWatching?: UserWatching
+
+  serverUrl: string
+
+  errorNotifier: (message: string) => void
+}
+
+export type PeertubePlayerManagerOptions = {
+  common: CommonOptions
+  webtorrent: WebtorrentOptions
+  p2pMediaLoader?: P2PMediaLoaderOptions
+
+  pluginsManager: PluginsManager
+}
diff --git a/client/src/assets/player/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/manager-options/webtorrent-options-builder.ts
new file mode 100644 (file)
index 0000000..303940b
--- /dev/null
@@ -0,0 +1,36 @@
+import { PeertubePlayerManagerOptions } from './manager-options.model'
+
+export class WebTorrentOptionsBuilder {
+
+  constructor (
+    private options: PeertubePlayerManagerOptions,
+    private autoPlayValue: any
+  ) {
+
+  }
+
+  getPluginOptions () {
+    const commonOptions = this.options.common
+    const webtorrentOptions = this.options.webtorrent
+    const p2pMediaLoaderOptions = this.options.p2pMediaLoader
+
+    const autoplay = this.autoPlayValue === 'play'
+
+    const webtorrent = {
+      autoplay,
+
+      playerRefusedP2P: commonOptions.p2pEnabled === false,
+      videoDuration: commonOptions.videoDuration,
+      playerElement: commonOptions.playerElement,
+
+      videoFiles: webtorrentOptions.videoFiles.length !== 0
+        ? webtorrentOptions.videoFiles
+        // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
+        : p2pMediaLoaderOptions?.videoFiles || [],
+
+      startTime: commonOptions.startTime
+    }
+
+    return { webtorrent }
+  }
+}
index ae31bcfe176fc78ece41142170c7c36c9df4843a..ccee2d90ffc0e1730b13a22961a772b0d1bf3249 100644 (file)
@@ -24,7 +24,7 @@ const registerSourceHandler = function (vjs: typeof videojs) {
   const html5 = vjs.getTech('Html5')
 
   if (!html5) {
-    console.error('Not supported version if video.js')
+    console.error('No Hml5 tech found in videojs')
     return
   }
 
index f8e5e2d6baf69a45c5ba2a6fac087b3e678a1376..1d7a39b4e72b68f090805a5434aeb8ace0a28368 100644 (file)
@@ -111,9 +111,7 @@ class P2pMediaLoaderPlugin extends Plugin {
   private initializePlugin () {
     initHlsJsPlayer(this.hlsjs)
 
-    // FIXME: typings
-    const options = (this.player.tech(true).options_ as any)
-    this.p2pEngine = options.hlsjsConfig.loader.getEngine()
+    this.p2pEngine = this.options.loader.getEngine()
 
     this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
       console.error('Segment error.', segment, err)
index 81ddb8814bc9aeb673b0eb442f6e8f04c1c29f14..ddb521a52f311a90cf18acd9fedc30c2a8709a07 100644 (file)
@@ -10,22 +10,23 @@ import './control-bar/next-previous-video-button'
 import './control-bar/p2p-info-button'
 import './control-bar/peertube-link-button'
 import './control-bar/peertube-load-progress-bar'
-import './control-bar/resolution-menu-button'
-import './control-bar/resolution-menu-item'
-import './control-bar/settings-dialog'
-import './control-bar/settings-menu-button'
-import './control-bar/settings-menu-item'
-import './control-bar/settings-panel'
-import './control-bar/settings-panel-child'
 import './control-bar/theater-button'
+import './settings/resolution-menu-button'
+import './settings/resolution-menu-item'
+import './settings/settings-dialog'
+import './settings/settings-menu-button'
+import './settings/settings-menu-item'
+import './settings/settings-panel'
+import './settings/settings-panel-child'
 import './playlist/playlist-plugin'
 import './mobile/peertube-mobile-plugin'
 import './mobile/peertube-mobile-buttons'
 import './hotkeys/peertube-hotkeys-plugin'
 import videojs from 'video.js'
 import { PluginsManager } from '@root-helpers/plugins-manager'
+import { ManagerOptionsBuilder } from './manager-options/manager-options-builder'
+import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options/manager-options.model'
 import { saveAverageBandwidth } from './peertube-player-local-storage'
-import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder'
 import { PlayerNetworkInfo } from './peertube-videojs-typings'
 import { TranslationsManager } from './translations-manager'
 import { isMobile } from './utils'
@@ -75,7 +76,7 @@ export class PeertubePlayerManager {
   }
 
   private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> {
-    const videojsOptionsBuilder = new PeertubePlayerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
+    const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
 
     const videojsOptions = await this.pluginsManager.runHook(
       'filter:internal.player.videojs.options.result',
@@ -198,7 +199,7 @@ export class PeertubePlayerManager {
     return newVideoElement
   }
 
-  private static addContextMenu (optionsBuilder: PeertubePlayerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
+  private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
     const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
 
     player.contextmenuUI(options)
diff --git a/client/src/assets/player/peertube-player-options-builder.ts b/client/src/assets/player/peertube-player-options-builder.ts
deleted file mode 100644 (file)
index c9cbbbf..0000000
+++ /dev/null
@@ -1,577 +0,0 @@
-import videojs from 'video.js'
-import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
-import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
-import { PluginsManager } from '@root-helpers/plugins-manager'
-import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
-import { isDefaultLocale } from '@shared/core-utils/i18n'
-import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
-import { copyToClipboard } from '../../root-helpers/utils'
-import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
-import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
-import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
-import { getAverageBandwidthInStore } from './peertube-player-local-storage'
-import {
-  NextPreviousVideoButtonOptions,
-  P2PMediaLoaderPluginOptions,
-  PeerTubeLinkButtonOptions,
-  PlaylistPluginOptions,
-  UserWatching,
-  VideoJSCaption,
-  VideoJSPluginOptions
-} from './peertube-videojs-typings'
-import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
-
-export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
-
-export type WebtorrentOptions = {
-  videoFiles: VideoFile[]
-}
-
-export type P2PMediaLoaderOptions = {
-  playlistUrl: string
-  segmentsSha256Url: string
-  trackerAnnounce: string[]
-  redundancyBaseUrls: string[]
-  videoFiles: VideoFile[]
-}
-
-export interface CustomizationOptions {
-  startTime: number | string
-  stopTime: number | string
-
-  controls?: boolean
-  muted?: boolean
-  loop?: boolean
-  subtitle?: string
-  resume?: string
-
-  peertubeLink: boolean
-}
-
-export interface CommonOptions extends CustomizationOptions {
-  playerElement: HTMLVideoElement
-  onPlayerElementChange: (element: HTMLVideoElement) => void
-
-  autoplay: boolean
-  p2pEnabled: boolean
-
-  nextVideo?: () => void
-  hasNextVideo?: () => boolean
-
-  previousVideo?: () => void
-  hasPreviousVideo?: () => boolean
-
-  playlist?: PlaylistPluginOptions
-
-  videoDuration: number
-  enableHotkeys: boolean
-  inactivityTimeout: number
-  poster: string
-
-  theaterButton: boolean
-  captions: boolean
-
-  videoViewUrl: string
-  embedUrl: string
-  embedTitle: string
-
-  isLive: boolean
-  liveOptions?: {
-    latencyMode: LiveVideoLatencyMode
-  }
-
-  language?: string
-
-  videoCaptions: VideoJSCaption[]
-
-  videoUUID: string
-  videoShortUUID: string
-
-  userWatching?: UserWatching
-
-  serverUrl: string
-
-  errorNotifier: (message: string) => void
-}
-
-export type PeertubePlayerManagerOptions = {
-  common: CommonOptions
-  webtorrent: WebtorrentOptions
-  p2pMediaLoader?: P2PMediaLoaderOptions
-
-  pluginsManager: PluginsManager
-}
-
-export class PeertubePlayerOptionsBuilder {
-
-  constructor (
-    private mode: PlayerMode,
-    private options: PeertubePlayerManagerOptions,
-    private p2pMediaLoaderModule?: any
-  ) {
-
-  }
-
-  getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
-    const commonOptions = this.options.common
-    const isHLS = this.mode === 'p2p-media-loader'
-
-    let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
-    const html5 = {
-      preloadTextTracks: false
-    }
-
-    const plugins: VideoJSPluginOptions = {
-      peertube: {
-        mode: this.mode,
-        autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
-        videoViewUrl: commonOptions.videoViewUrl,
-        videoDuration: commonOptions.videoDuration,
-        userWatching: commonOptions.userWatching,
-        subtitle: commonOptions.subtitle,
-        videoCaptions: commonOptions.videoCaptions,
-        stopTime: commonOptions.stopTime,
-        isLive: commonOptions.isLive,
-        videoUUID: commonOptions.videoUUID
-      }
-    }
-
-    if (commonOptions.playlist) {
-      plugins.playlist = commonOptions.playlist
-    }
-
-    if (isHLS) {
-      const { hlsjs } = this.addP2PMediaLoaderOptions(plugins)
-
-      Object.assign(html5, hlsjs.html5)
-    }
-
-    if (this.mode === 'webtorrent') {
-      this.addWebTorrentOptions(plugins, alreadyPlayed)
-
-      // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
-      autoplay = false
-    }
-
-    const videojsOptions = {
-      html5,
-
-      // We don't use text track settings for now
-      textTrackSettings: false as any, // FIXME: typings
-      controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
-      loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
-
-      muted: commonOptions.muted !== undefined
-        ? commonOptions.muted
-        : undefined, // Undefined so the player knows it has to check the local storage
-
-      autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
-
-      poster: commonOptions.poster,
-      inactivityTimeout: commonOptions.inactivityTimeout,
-      playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
-
-      plugins,
-
-      controlBar: {
-        children: this.getControlBarChildren(this.mode, {
-          videoShortUUID: commonOptions.videoShortUUID,
-          p2pEnabled: commonOptions.p2pEnabled,
-
-          captions: commonOptions.captions,
-          peertubeLink: commonOptions.peertubeLink,
-          theaterButton: commonOptions.theaterButton,
-
-          nextVideo: commonOptions.nextVideo,
-          hasNextVideo: commonOptions.hasNextVideo,
-
-          previousVideo: commonOptions.previousVideo,
-          hasPreviousVideo: commonOptions.hasPreviousVideo
-        }) as any // FIXME: typings
-      }
-    }
-
-    if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
-      Object.assign(videojsOptions, { language: commonOptions.language })
-    }
-
-    return videojsOptions
-  }
-
-  private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) {
-    const p2pMediaLoaderOptions = this.options.p2pMediaLoader
-    const commonOptions = this.options.common
-
-    const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
-
-    const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
-      redundancyUrlManager,
-      type: 'application/x-mpegURL',
-      startTime: commonOptions.startTime,
-      src: p2pMediaLoaderOptions.playlistUrl
-    }
-
-    const p2pMediaLoaderConfig: HlsJsEngineSettings = {
-      loader: this.getP2PMediaLoaderOptions(redundancyUrlManager),
-      segments: {
-        swarmId: p2pMediaLoaderOptions.playlistUrl
-      }
-    }
-
-    const hlsjs = {
-      levelLabelHandler: (level: { height: number, width: number }) => {
-        const resolution = Math.min(level.height || 0, level.width || 0)
-
-        const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
-        // We don't have files for live videos
-        if (!file) return level.height
-
-        let label = file.resolution.label
-        if (file.fps >= 50) label += file.fps
-
-        return label
-      },
-      html5: {
-        hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig)
-      }
-    }
-
-    const toAssign = { p2pMediaLoader, hlsjs }
-    Object.assign(plugins, toAssign)
-
-    return toAssign
-  }
-
-  private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): Partial<HybridLoaderSettings> {
-    let consumeOnly = false
-    if ((navigator as any)?.connection?.type === 'cellular') {
-      console.log('We are on a cellular connection: disabling seeding.')
-      consumeOnly = true
-    }
-
-    const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce
-                                                 .filter(t => t.startsWith('ws'))
-
-    const specificLiveOrVODOptions = this.options.common.isLive
-      ? this.getP2PMediaLoaderLiveOptions()
-      : this.getP2PMediaLoaderVODOptions()
-
-    return {
-      trackerAnnounce,
-      rtcConfig: getRtcConfig(),
-
-      simultaneousHttpDownloads: 1,
-      httpFailedSegmentTimeout: 1000,
-
-      segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
-      segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
-
-      useP2P: this.options.common.p2pEnabled,
-      consumeOnly,
-
-      ...specificLiveOrVODOptions
-    }
-  }
-
-  private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
-    const base = {
-      requiredSegmentsPriority: 1
-    }
-
-    const latencyMode = this.options.common.liveOptions.latencyMode
-
-    switch (latencyMode) {
-      case LiveVideoLatencyMode.SMALL_LATENCY:
-        return {
-          ...base,
-
-          useP2P: false,
-          httpDownloadProbability: 1
-        }
-
-      case LiveVideoLatencyMode.HIGH_LATENCY:
-        return base
-
-      default:
-        return base
-    }
-  }
-
-  private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
-    return {
-      requiredSegmentsPriority: 3,
-
-      cachedSegmentExpiration: 86400000,
-      cachedSegmentsCount: 100,
-
-      httpDownloadMaxPriority: 9,
-      httpDownloadProbability: 0.06,
-      httpDownloadProbabilitySkipIfNoPeers: true,
-
-      p2pDownloadMaxPriority: 50
-    }
-  }
-
-  private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
-    const specificLiveOrVODOptions = this.options.common.isLive
-      ? this.getHLSLiveOptions()
-      : this.getHLSVODOptions()
-
-    const base = {
-      capLevelToPlayerSize: true,
-      autoStartLoad: false,
-
-      loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(),
-
-      ...specificLiveOrVODOptions
-    }
-
-    const averageBandwidth = getAverageBandwidthInStore()
-    if (!averageBandwidth) return base
-
-    return {
-      ...base,
-
-      abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
-      startLevel: -1,
-      testBandwidth: false,
-      debug: false
-    }
-  }
-
-  private getHLSLiveOptions () {
-    const latencyMode = this.options.common.liveOptions.latencyMode
-
-    switch (latencyMode) {
-      case LiveVideoLatencyMode.SMALL_LATENCY:
-        return {
-          liveSyncDurationCount: 2
-        }
-
-      case LiveVideoLatencyMode.HIGH_LATENCY:
-        return {
-          liveSyncDurationCount: 10
-        }
-
-      default:
-        return {
-          liveSyncDurationCount: 5
-        }
-    }
-  }
-
-  private getHLSVODOptions () {
-    return {
-      liveSyncDurationCount: 5
-    }
-  }
-
-  private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
-    const commonOptions = this.options.common
-    const webtorrentOptions = this.options.webtorrent
-    const p2pMediaLoaderOptions = this.options.p2pMediaLoader
-
-    const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play'
-
-    const webtorrent = {
-      autoplay,
-
-      playerRefusedP2P: commonOptions.p2pEnabled === false,
-      videoDuration: commonOptions.videoDuration,
-      playerElement: commonOptions.playerElement,
-
-      videoFiles: webtorrentOptions.videoFiles.length !== 0
-        ? webtorrentOptions.videoFiles
-        // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
-        : p2pMediaLoaderOptions?.videoFiles || [],
-
-      startTime: commonOptions.startTime
-    }
-
-    Object.assign(plugins, { webtorrent })
-  }
-
-  private getControlBarChildren (mode: PlayerMode, options: {
-    p2pEnabled: boolean
-    videoShortUUID: string
-
-    peertubeLink: boolean
-    theaterButton: boolean
-    captions: boolean
-
-    nextVideo?: () => void
-    hasNextVideo?: () => boolean
-
-    previousVideo?: () => void
-    hasPreviousVideo?: () => boolean
-  }) {
-    const settingEntries = []
-    const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
-
-    // Keep an order
-    settingEntries.push('playbackRateMenuButton')
-    if (options.captions === true) settingEntries.push('captionsButton')
-    settingEntries.push('resolutionMenuButton')
-
-    const children = {}
-
-    if (options.previousVideo) {
-      const buttonOptions: NextPreviousVideoButtonOptions = {
-        type: 'previous',
-        handler: options.previousVideo,
-        isDisabled: () => {
-          if (!options.hasPreviousVideo) return false
-
-          return !options.hasPreviousVideo()
-        }
-      }
-
-      Object.assign(children, {
-        previousVideoButton: buttonOptions
-      })
-    }
-
-    Object.assign(children, { playToggle: {} })
-
-    if (options.nextVideo) {
-      const buttonOptions: NextPreviousVideoButtonOptions = {
-        type: 'next',
-        handler: options.nextVideo,
-        isDisabled: () => {
-          if (!options.hasNextVideo) return false
-
-          return !options.hasNextVideo()
-        }
-      }
-
-      Object.assign(children, {
-        nextVideoButton: buttonOptions
-      })
-    }
-
-    Object.assign(children, {
-      currentTimeDisplay: {},
-      timeDivider: {},
-      durationDisplay: {},
-      liveDisplay: {},
-
-      flexibleWidthSpacer: {},
-      progressControl: {
-        children: {
-          seekBar: {
-            children: {
-              [loadProgressBar]: {},
-              mouseTimeDisplay: {},
-              playProgressBar: {}
-            }
-          }
-        }
-      },
-
-      p2PInfoButton: {
-        p2pEnabled: options.p2pEnabled
-      },
-
-      muteToggle: {},
-      volumeControl: {},
-
-      settingsButton: {
-        setup: {
-          maxHeightOffset: 40
-        },
-        entries: settingEntries
-      }
-    })
-
-    if (options.peertubeLink === true) {
-      Object.assign(children, {
-        peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
-      })
-    }
-
-    if (options.theaterButton === true) {
-      Object.assign(children, {
-        theaterButton: {}
-      })
-    }
-
-    Object.assign(children, {
-      fullscreenToggle: {}
-    })
-
-    return children
-  }
-
-  private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
-    if (autoplay !== true) return autoplay
-
-    // On first play, disable autoplay to avoid issues
-    // But if the player already played videos, we can safely autoplay next ones
-    if (isIOS() || isSafari()) {
-      return alreadyPlayed ? 'play' : false
-    }
-
-    return 'play'
-  }
-
-  getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
-    const content = () => {
-      const isLoopEnabled = player.options_['loop']
-
-      const items = [
-        {
-          icon: 'repeat',
-          label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
-          listener: function () {
-            player.options_['loop'] = !isLoopEnabled
-          }
-        },
-        {
-          label: player.localize('Copy the video URL'),
-          listener: function () {
-            copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
-          }
-        },
-        {
-          label: player.localize('Copy the video URL at the current time'),
-          listener: function (this: videojs.Player) {
-            const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
-
-            copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
-          }
-        },
-        {
-          icon: 'code',
-          label: player.localize('Copy embed code'),
-          listener: () => {
-            copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
-          }
-        }
-      ]
-
-      if (this.mode === 'webtorrent') {
-        items.push({
-          label: player.localize('Copy magnet URI'),
-          listener: function (this: videojs.Player) {
-            copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
-          }
-        })
-      }
-
-      items.push({
-        icon: 'info',
-        label: player.localize('Stats for nerds'),
-        listener: () => {
-          player.stats().show()
-        }
-      })
-
-      return items.map(i => ({
-        ...i,
-        label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
-      }))
-    }
-
-    return { content }
-  }
-}
index 09996f75d8f4f5a6f6c504662c84b6f26cb5ec11..fcaa8a9c359eb817dfaa7509b48dbba3718b55c1 100644 (file)
@@ -1,11 +1,12 @@
 import { HlsConfig, Level } from 'hls.js'
 import videojs from 'video.js'
+import { Engine } from '@peertube/p2p-media-loader-hlsjs'
 import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
 import { PeerTubeDockPluginOptions } from './dock/peertube-dock-plugin'
+import { PlayerMode } from './manager-options/manager-options.model'
 import { Html5Hlsjs } from './p2p-media-loader/hls-plugin'
 import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
 import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
-import { PlayerMode } from './peertube-player-options-builder'
 import { PeerTubePlugin } from './peertube-plugin'
 import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin'
 import { PlaylistPlugin } from './playlist/playlist-plugin'
@@ -154,6 +155,12 @@ type P2PMediaLoaderPluginOptions = {
   src: string
 
   startTime: number | string
+
+  loader: P2PMediaLoader
+}
+
+export type P2PMediaLoader = {
+  getEngine(): Engine
 }
 
 type VideoJSPluginOptions = {