2
1

peertube-player-manager.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import '@peertube/videojs-contextmenu'
  2. import './shared/upnext/end-card'
  3. import './shared/upnext/upnext-plugin'
  4. import './shared/stats/stats-card'
  5. import './shared/stats/stats-plugin'
  6. import './shared/bezels/bezels-plugin'
  7. import './shared/peertube/peertube-plugin'
  8. import './shared/resolutions/peertube-resolutions-plugin'
  9. import './shared/control-bar/next-previous-video-button'
  10. import './shared/control-bar/p2p-info-button'
  11. import './shared/control-bar/peertube-link-button'
  12. import './shared/control-bar/peertube-load-progress-bar'
  13. import './shared/control-bar/theater-button'
  14. import './shared/settings/resolution-menu-button'
  15. import './shared/settings/resolution-menu-item'
  16. import './shared/settings/settings-dialog'
  17. import './shared/settings/settings-menu-button'
  18. import './shared/settings/settings-menu-item'
  19. import './shared/settings/settings-panel'
  20. import './shared/settings/settings-panel-child'
  21. import './shared/playlist/playlist-plugin'
  22. import './shared/mobile/peertube-mobile-plugin'
  23. import './shared/mobile/peertube-mobile-buttons'
  24. import './shared/hotkeys/peertube-hotkeys-plugin'
  25. import './shared/metrics/metrics-plugin'
  26. import videojs from 'video.js'
  27. import { logger } from '@root-helpers/logger'
  28. import { PluginsManager } from '@root-helpers/plugins-manager'
  29. import { isMobile } from '@root-helpers/web-browser'
  30. import { saveAverageBandwidth } from './peertube-player-local-storage'
  31. import { ManagerOptionsBuilder } from './shared/manager-options'
  32. import { TranslationsManager } from './translations-manager'
  33. import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode, PlayerNetworkInfo } from './types'
  34. // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
  35. (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
  36. const CaptionsButton = videojs.getComponent('CaptionsButton') as any
  37. // Change Captions to Subtitles/CC
  38. CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
  39. // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
  40. CaptionsButton.prototype.label_ = ' '
  41. export class PeertubePlayerManager {
  42. private static playerElementClassName: string
  43. private static onPlayerChange: (player: videojs.Player) => void
  44. private static alreadyPlayed = false
  45. private static pluginsManager: PluginsManager
  46. private static videojsDecodeErrors = 0
  47. private static p2pMediaLoaderModule: any
  48. static initState () {
  49. this.alreadyPlayed = false
  50. }
  51. static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
  52. this.pluginsManager = options.pluginsManager
  53. this.onPlayerChange = onPlayerChange
  54. this.playerElementClassName = options.common.playerElement.className
  55. if (mode === 'webtorrent') await import('./shared/webtorrent/webtorrent-plugin')
  56. if (mode === 'p2p-media-loader') {
  57. const [ p2pMediaLoaderModule ] = await Promise.all([
  58. import('@peertube/p2p-media-loader-hlsjs'),
  59. import('./shared/p2p-media-loader/p2p-media-loader-plugin')
  60. ])
  61. this.p2pMediaLoaderModule = p2pMediaLoaderModule
  62. }
  63. await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
  64. return this.buildPlayer(mode, options)
  65. }
  66. private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> {
  67. const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
  68. const videojsOptions = await this.pluginsManager.runHook(
  69. 'filter:internal.player.videojs.options.result',
  70. videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed)
  71. )
  72. const self = this
  73. return new Promise(res => {
  74. videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
  75. const player = this
  76. let alreadyFallback = false
  77. const handleError = () => {
  78. if (alreadyFallback) return
  79. alreadyFallback = true
  80. if (mode === 'p2p-media-loader') {
  81. self.tryToRecoverHLSError(player.error(), player, options)
  82. } else {
  83. self.maybeFallbackToWebTorrent(mode, player, options)
  84. }
  85. }
  86. player.one('error', () => handleError())
  87. player.one('play', () => {
  88. self.alreadyPlayed = true
  89. })
  90. self.addContextMenu(videojsOptionsBuilder, player, options.common)
  91. if (isMobile()) player.peertubeMobile()
  92. if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin()
  93. if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden')
  94. player.bezels()
  95. player.stats({
  96. videoUUID: options.common.videoUUID,
  97. videoIsLive: options.common.isLive,
  98. mode,
  99. p2pEnabled: options.common.p2pEnabled
  100. })
  101. player.on('p2pInfo', (_, data: PlayerNetworkInfo) => {
  102. if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return
  103. saveAverageBandwidth(data.bandwidthEstimate)
  104. })
  105. const offlineNotificationElem = document.createElement('div')
  106. offlineNotificationElem.classList.add('vjs-peertube-offline-notification')
  107. offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work')
  108. const handleOnline = () => {
  109. player.el().removeChild(offlineNotificationElem)
  110. logger.info('The browser is online')
  111. }
  112. const handleOffline = () => {
  113. player.el().appendChild(offlineNotificationElem)
  114. logger.info('The browser is offline')
  115. }
  116. window.addEventListener('online', handleOnline)
  117. window.addEventListener('offline', handleOffline)
  118. player.on('dispose', () => {
  119. window.removeEventListener('online', handleOnline)
  120. window.removeEventListener('offline', handleOffline)
  121. })
  122. return res(player)
  123. })
  124. })
  125. }
  126. private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) {
  127. if (err.code === 3) { // Decode error
  128. // Display a notification to user
  129. if (this.videojsDecodeErrors === 0) {
  130. options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.'))
  131. }
  132. if (this.videojsDecodeErrors === 20) {
  133. this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
  134. return
  135. }
  136. logger.info('Fast forwarding HLS to recover from an error.')
  137. this.videojsDecodeErrors++
  138. options.common.startTime = currentPlayer.currentTime() + 2
  139. options.common.autoplay = true
  140. this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
  141. const newPlayer = await this.buildPlayer('p2p-media-loader', options)
  142. this.onPlayerChange(newPlayer)
  143. } else {
  144. this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
  145. }
  146. }
  147. private static async maybeFallbackToWebTorrent (
  148. currentMode: PlayerMode,
  149. currentPlayer: videojs.Player,
  150. options: PeertubePlayerManagerOptions
  151. ) {
  152. if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') {
  153. currentPlayer.peertube().displayFatalError()
  154. return
  155. }
  156. logger.info('Fallback to webtorrent.')
  157. this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
  158. await import('./shared/webtorrent/webtorrent-plugin')
  159. const newPlayer = await this.buildPlayer('webtorrent', options)
  160. this.onPlayerChange(newPlayer)
  161. }
  162. private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) {
  163. const newVideoElement = document.createElement('video')
  164. newVideoElement.className = this.playerElementClassName
  165. // VideoJS wraps our video element inside a div
  166. let currentParentPlayerElement = commonOptions.playerElement.parentNode
  167. // Fix on IOS, don't ask me why
  168. if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode
  169. currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
  170. commonOptions.playerElement = newVideoElement
  171. commonOptions.onPlayerElementChange(newVideoElement)
  172. player.dispose()
  173. return newVideoElement
  174. }
  175. private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
  176. const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
  177. player.contextmenuUI(options)
  178. }
  179. }
  180. // ############################################################################
  181. export {
  182. videojs
  183. }