]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Refractor videojs player
authorChocobozzz <me@florianbigard.com>
Wed, 23 Jan 2019 14:36:45 +0000 (15:36 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 11 Feb 2019 08:13:02 +0000 (09:13 +0100)
Add fake p2p-media-loader plugin

24 files changed:
client/package.json
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/assets/player/p2p-media-loader-plugin.ts [new file with mode: 0644]
client/src/assets/player/peertube-player-manager.ts [new file with mode: 0644]
client/src/assets/player/peertube-player.ts [deleted file]
client/src/assets/player/peertube-plugin.ts [new file with mode: 0644]
client/src/assets/player/peertube-videojs-typings.ts
client/src/assets/player/resolution-menu-item.ts [deleted file]
client/src/assets/player/videojs-components/p2p-info-button.ts [moved from client/src/assets/player/webtorrent-info-button.ts with 91% similarity]
client/src/assets/player/videojs-components/peertube-link-button.ts [moved from client/src/assets/player/peertube-link-button.ts with 87% similarity]
client/src/assets/player/videojs-components/peertube-load-progress-bar.ts [moved from client/src/assets/player/peertube-load-progress-bar.ts with 85% similarity]
client/src/assets/player/videojs-components/resolution-menu-button.ts [moved from client/src/assets/player/resolution-menu-button.ts with 56% similarity]
client/src/assets/player/videojs-components/resolution-menu-item.ts [new file with mode: 0644]
client/src/assets/player/videojs-components/settings-menu-button.ts [moved from client/src/assets/player/settings-menu-button.ts with 98% similarity]
client/src/assets/player/videojs-components/settings-menu-item.ts [moved from client/src/assets/player/settings-menu-item.ts with 97% similarity]
client/src/assets/player/videojs-components/theater-button.ts [moved from client/src/assets/player/theater-button.ts with 87% similarity]
client/src/assets/player/webtorrent-plugin.ts [moved from client/src/assets/player/peertube-videojs-plugin.ts with 73% similarity]
client/src/assets/player/webtorrent/peertube-chunk-store.ts [moved from client/src/assets/player/peertube-chunk-store.ts with 100% similarity]
client/src/assets/player/webtorrent/video-renderer.ts [moved from client/src/assets/player/video-renderer.ts with 100% similarity]
client/src/standalone/videos/embed.ts
client/src/tsconfig.app.json
client/yarn.lock
scripts/dev/server.sh
server/middlewares/csp.ts

index 31fc778876fb76a77c8fdaaada38ae3231e947f1..9da7c102575211bd4a42d6dad5d3de56d678817e 100644 (file)
@@ -85,6 +85,7 @@
     "@ngx-loading-bar/router": "^3.0.0",
     "@ngx-meta/core": "^6.0.0-rc.1",
     "@ngx-translate/i18n-polyfill": "^1.0.0",
+    "@streamroot/videojs-hlsjs-plugin": "^1.0.7",
     "@types/core-js": "^2.5.0",
     "@types/jasmine": "^2.8.7",
     "@types/jasminewd2": "^2.0.3",
     "ngx-qrcode2": "^0.0.9",
     "node-sass": "^4.9.3",
     "npm-font-source-sans-pro": "^1.0.2",
+    "p2p-media-loader-hlsjs": "^0.3.0",
     "path-browserify": "^1.0.0",
     "primeng": "^7.0.0",
     "process": "^0.11.10",
     "typescript": "3.1.6",
     "video.js": "^7",
     "videojs-contextmenu-ui": "^5.0.0",
+    "videojs-contrib-quality-levels": "^2.0.9",
     "videojs-dock": "^2.0.2",
     "videojs-hotkeys": "^0.2.21",
     "webpack-bundle-analyzer": "^3.0.2",
index ee504bc58c8219320c9e8c656703e1c8d33b5ab3..6e38af19593a0b9f085ee7002b3a5ac1d0b48741 100644 (file)
@@ -7,14 +7,9 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp
 import { MetaService } from '@ngx-meta/core'
 import { Notifier, ServerService } from '@app/core'
 import { forkJoin, Subscription } from 'rxjs'
-// FIXME: something weird with our path definition in tsconfig and typings
-// @ts-ignore
-import videojs from 'video.js'
-import 'videojs-hotkeys'
 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
 import * as WebTorrent from 'webtorrent'
 import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
-import '../../../assets/player/peertube-videojs-plugin'
 import { AuthService, ConfirmService } from '../../core'
 import { RestExtractor, VideoBlacklistService } from '../../shared'
 import { VideoDetails } from '../../shared/video/video-details.model'
@@ -24,12 +19,11 @@ import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
 import { VideoBlacklistComponent } from './modal/video-blacklist.component'
 import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
-import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { environment } from '../../../environments/environment'
-import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
 import { VideoCaptionService } from '@app/shared/video-caption'
 import { MarkdownService } from '@app/shared/renderer'
+import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager'
 
 @Component({
   selector: 'my-video-watch',
@@ -46,7 +40,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
   @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
 
-  player: videojs.Player
+  player: any
   playerElement: HTMLVideoElement
   userRating: UserVideoRateType = null
   video: VideoDetails = null
@@ -61,7 +55,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   remoteServerDown = false
   hotkeys: Hotkey[]
 
-  private videojsLocaleLoaded = false
   private paramsSub: Subscription
 
   constructor (
@@ -402,41 +395,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
       src: environment.apiUrl + c.captionPath
     }))
 
-    const videojsOptions = getVideojsOptions({
-      autoplay: this.isAutoplay(),
-      inactivityTimeout: 2500,
-      videoFiles: this.video.files,
-      videoCaptions: playerCaptions,
-      playerElement: this.playerElement,
-      videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
-      videoDuration: this.video.duration,
-      enableHotkeys: true,
-      peertubeLink: false,
-      poster: this.video.previewUrl,
-      startTime,
-      subtitle: urlOptions.subtitle,
-      theaterMode: true,
-      language: this.localeId,
-
-      userWatching: this.user && this.user.videosHistoryEnabled === true ? {
-        url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
-        authorizationHeader: this.authService.getRequestHeaderValue()
-      } : undefined
-    })
+    const options = {
+      common: {
+        autoplay: this.isAutoplay(),
+        playerElement: this.playerElement,
+        videoDuration: this.video.duration,
+        enableHotkeys: true,
+        inactivityTimeout: 2500,
+        poster: this.video.previewUrl,
+        startTime,
+
+        theaterMode: true,
+        captions: videoCaptions.length !== 0,
+        peertubeLink: false,
+
+        videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
+        embedUrl: this.video.embedUrl,
+
+        language: this.localeId,
+
+        subtitle: urlOptions.subtitle,
 
-    if (this.videojsLocaleLoaded === false) {
-      await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId)
-      this.videojsLocaleLoaded = true
+        userWatching: this.user && this.user.videosHistoryEnabled === true ? {
+          url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
+          authorizationHeader: this.authService.getRequestHeaderValue()
+        } : undefined,
+
+        serverUrl: environment.apiUrl,
+
+        videoCaptions: playerCaptions
+      },
+
+      webtorrent: {
+        videoFiles: this.video.files
+      }
     }
 
-    const self = this
     this.zone.runOutsideAngular(async () => {
-      videojs(this.playerElement, videojsOptions, function (this: videojs.Player) {
-        self.player = this
-        this.on('customError', ({ err }: { err: any }) => self.handleError(err))
-
-        addContextMenu(self.player, self.video.embedUrl)
-      })
+      this.player = await PeertubePlayerManager.initialize('webtorrent', options)
+      this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
     })
 
     this.setVideoDescriptionHTML()
diff --git a/client/src/assets/player/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader-plugin.ts
new file mode 100644 (file)
index 0000000..6d07a2c
--- /dev/null
@@ -0,0 +1,33 @@
+// FIXME: something weird with our path definition in tsconfig and typings
+// @ts-ignore
+import * as videojs from 'video.js'
+import { P2PMediaLoaderPluginOptions, VideoJSComponentInterface } from './peertube-videojs-typings'
+
+// videojs-hlsjs-plugin needs videojs in window
+window['videojs'] = videojs
+import '@streamroot/videojs-hlsjs-plugin'
+
+import { initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
+
+// import { Events } from '../p2p-media-loader/p2p-media-loader-core/lib'
+
+const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
+class P2pMediaLoaderPlugin extends Plugin {
+
+  constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) {
+    super(player, options)
+
+    initVideoJsContribHlsJsPlayer(player)
+
+    console.log(options)
+
+    player.src({
+      type: options.type,
+      src: options.src
+    })
+  }
+
+}
+
+videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
+export { P2pMediaLoaderPlugin }
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
new file mode 100644 (file)
index 0000000..9155c06
--- /dev/null
@@ -0,0 +1,388 @@
+import { VideoFile } from '../../../../shared/models/videos'
+// @ts-ignore
+import * as videojs from 'video.js'
+import 'videojs-hotkeys'
+import 'videojs-dock'
+import 'videojs-contextmenu-ui'
+import 'videojs-contrib-quality-levels'
+import './peertube-plugin'
+import './videojs-components/peertube-link-button'
+import './videojs-components/resolution-menu-button'
+import './videojs-components/settings-menu-button'
+import './videojs-components/p2p-info-button'
+import './videojs-components/peertube-load-progress-bar'
+import './videojs-components/theater-button'
+import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
+import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
+import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
+import { Engine } from 'p2p-media-loader-hlsjs'
+
+// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
+videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
+// Change Captions to Subtitles/CC
+videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
+// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
+videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
+
+type PlayerMode = 'webtorrent' | 'p2p-media-loader'
+
+type WebtorrentOptions = {
+  videoFiles: VideoFile[]
+}
+
+type P2PMediaLoaderOptions = {
+  playlistUrl: string
+}
+
+type CommonOptions = {
+  playerElement: HTMLVideoElement
+
+  autoplay: boolean
+  videoDuration: number
+  enableHotkeys: boolean
+  inactivityTimeout: number
+  poster: string
+  startTime: number | string
+
+  theaterMode: boolean
+  captions: boolean
+  peertubeLink: boolean
+
+  videoViewUrl: string
+  embedUrl: string
+
+  language?: string
+  controls?: boolean
+  muted?: boolean
+  loop?: boolean
+  subtitle?: string
+
+  videoCaptions: VideoJSCaption[]
+
+  userWatching?: UserWatching
+
+  serverUrl: string
+}
+
+export type PeertubePlayerManagerOptions = {
+  common: CommonOptions,
+  webtorrent?: WebtorrentOptions,
+  p2pMediaLoader?: P2PMediaLoaderOptions
+}
+
+export class PeertubePlayerManager {
+
+  private static videojsLocaleCache: { [ path: string ]: any } = {}
+
+  static getServerTranslations (serverUrl: string, locale: string) {
+    const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
+    // It is the default locale, nothing to translate
+    if (!path) return Promise.resolve(undefined)
+
+    return fetch(path + '/server.json')
+      .then(res => res.json())
+      .catch(err => {
+        console.error('Cannot get server translations', err)
+        return undefined
+      })
+  }
+
+  static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
+    if (mode === 'webtorrent') await import('./webtorrent-plugin')
+    if (mode === 'p2p-media-loader') await import('./p2p-media-loader-plugin')
+
+    const videojsOptions = this.getVideojsOptions(mode, options)
+
+    await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language)
+
+    const self = this
+    return new Promise(res => {
+      videojs(options.common.playerElement, videojsOptions, function (this: any) {
+        const player = this
+
+        self.addContextMenu(mode, player, options.common.embedUrl)
+
+        return res(player)
+      })
+    })
+  }
+
+  private static loadLocaleInVideoJS (serverUrl: string, locale: string) {
+    const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
+    // It is the default locale, nothing to translate
+    if (!path) return Promise.resolve(undefined)
+
+    let p: Promise<any>
+
+    if (PeertubePlayerManager.videojsLocaleCache[path]) {
+      p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path])
+    } else {
+      p = fetch(path + '/player.json')
+        .then(res => res.json())
+        .then(json => {
+          PeertubePlayerManager.videojsLocaleCache[path] = json
+          return json
+        })
+        .catch(err => {
+          console.error('Cannot get player translations', err)
+          return undefined
+        })
+    }
+
+    const completeLocale = getCompleteLocale(locale)
+    return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
+  }
+
+  private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
+    const commonOptions = options.common
+    const webtorrentOptions = options.webtorrent
+    const p2pMediaLoaderOptions = options.p2pMediaLoader
+
+    const plugins: VideoJSPluginOptions = {
+      peertube: {
+        autoplay: commonOptions.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
+        videoViewUrl: commonOptions.videoViewUrl,
+        videoDuration: commonOptions.videoDuration,
+        startTime: commonOptions.startTime,
+        userWatching: commonOptions.userWatching,
+        subtitle: commonOptions.subtitle,
+        videoCaptions: commonOptions.videoCaptions
+      }
+    }
+
+    if (p2pMediaLoaderOptions) {
+      const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
+        type: 'application/x-mpegURL',
+        src: p2pMediaLoaderOptions.playlistUrl
+      }
+
+      const config = {
+        segments: {
+          swarmId: 'swarm' // TODO: choose swarm id
+        }
+      }
+      const streamrootHls = {
+        html5: {
+          hlsjsConfig: {
+            liveSyncDurationCount: 7,
+            loader: new Engine(config).createLoaderClass()
+          }
+        }
+      }
+
+      Object.assign(plugins, { p2pMediaLoader, streamrootHls })
+    }
+
+    if (webtorrentOptions) {
+      const webtorrent = {
+        autoplay: commonOptions.autoplay,
+        videoDuration: commonOptions.videoDuration,
+        playerElement: commonOptions.playerElement,
+        videoFiles: webtorrentOptions.videoFiles
+      }
+      Object.assign(plugins, { webtorrent })
+    }
+
+    const videojsOptions = {
+      // We don't use text track settings for now
+      textTrackSettings: false,
+      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
+
+      poster: commonOptions.poster,
+      autoplay: false,
+      inactivityTimeout: commonOptions.inactivityTimeout,
+      playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
+      plugins,
+      controlBar: {
+        children: this.getControlBarChildren(mode, {
+          captions: commonOptions.captions,
+          peertubeLink: commonOptions.peertubeLink,
+          theaterMode: commonOptions.theaterMode
+        })
+      }
+    }
+
+    if (commonOptions.enableHotkeys === true) {
+      Object.assign(videojsOptions.plugins, {
+        hotkeys: {
+          enableVolumeScroll: false,
+          enableModifiersForNumbers: false,
+
+          fullscreenKey: function (event: KeyboardEvent) {
+            // fullscreen with the f key or Ctrl+Enter
+            return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
+          },
+
+          seekStep: function (event: KeyboardEvent) {
+            // mimic VLC seek behavior, and default to 5 (original value is 5).
+            if (event.ctrlKey && event.altKey) {
+              return 5 * 60
+            } else if (event.ctrlKey) {
+              return 60
+            } else if (event.altKey) {
+              return 10
+            } else {
+              return 5
+            }
+          },
+
+          customKeys: {
+            increasePlaybackRateKey: {
+              key: function (event: KeyboardEvent) {
+                return event.key === '>'
+              },
+              handler: function (player: videojs.Player) {
+                player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
+              }
+            },
+            decreasePlaybackRateKey: {
+              key: function (event: KeyboardEvent) {
+                return event.key === '<'
+              },
+              handler: function (player: videojs.Player) {
+                player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
+              }
+            },
+            frameByFrame: {
+              key: function (event: KeyboardEvent) {
+                return event.key === '.'
+              },
+              handler: function (player: videojs.Player) {
+                player.pause()
+                // Calculate movement distance (assuming 30 fps)
+                const dist = 1 / 30
+                player.currentTime(player.currentTime() + dist)
+              }
+            }
+          }
+        }
+      })
+    }
+
+    if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
+      Object.assign(videojsOptions, { language: commonOptions.language })
+    }
+
+    return videojsOptions
+  }
+
+  private static getControlBarChildren (mode: PlayerMode, options: {
+    peertubeLink: boolean
+    theaterMode: boolean,
+    captions: 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 = {
+      'playToggle': {},
+      'currentTimeDisplay': {},
+      'timeDivider': {},
+      'durationDisplay': {},
+      'liveDisplay': {},
+
+      'flexibleWidthSpacer': {},
+      'progressControl': {
+        children: {
+          'seekBar': {
+            children: {
+              [loadProgressBar]: {},
+              'mouseTimeDisplay': {},
+              'playProgressBar': {}
+            }
+          }
+        }
+      },
+
+      'p2PInfoButton': {},
+
+      'muteToggle': {},
+      'volumeControl': {},
+
+      'settingsButton': {
+        setup: {
+          maxHeightOffset: 40
+        },
+        entries: settingEntries
+      }
+    }
+
+    if (options.peertubeLink === true) {
+      Object.assign(children, {
+        'peerTubeLinkButton': {}
+      })
+    }
+
+    if (options.theaterMode === true) {
+      Object.assign(children, {
+        'theaterButton': {}
+      })
+    }
+
+    Object.assign(children, {
+      'fullscreenToggle': {}
+    })
+
+    return children
+  }
+
+  private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) {
+    const content = [
+      {
+        label: player.localize('Copy the video URL'),
+        listener: function () {
+          copyToClipboard(buildVideoLink())
+        }
+      },
+      {
+        label: player.localize('Copy the video URL at the current time'),
+        listener: function () {
+          const player = this as videojs.Player
+          copyToClipboard(buildVideoLink(player.currentTime()))
+        }
+      },
+      {
+        label: player.localize('Copy embed code'),
+        listener: () => {
+          copyToClipboard(buildVideoEmbed(videoEmbedUrl))
+        }
+      }
+    ]
+
+    if (mode === 'webtorrent') {
+      content.push({
+        label: player.localize('Copy magnet URI'),
+        listener: function () {
+          const player = this as videojs.Player
+          copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri)
+        }
+      })
+    }
+
+    player.contextmenuUI({ content })
+  }
+
+  private static getLocalePath (serverUrl: string, locale: string) {
+    const completeLocale = getCompleteLocale(locale)
+
+    if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
+
+    return serverUrl + '/client/locales/' + completeLocale
+  }
+}
+
+// ############################################################################
+
+export {
+  videojs
+}
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts
deleted file mode 100644 (file)
index 2de6d7f..0000000
+++ /dev/null
@@ -1,300 +0,0 @@
-import { VideoFile } from '../../../../shared/models/videos'
-
-import 'videojs-hotkeys'
-import 'videojs-dock'
-import 'videojs-contextmenu-ui'
-import './peertube-link-button'
-import './resolution-menu-button'
-import './settings-menu-button'
-import './webtorrent-info-button'
-import './peertube-videojs-plugin'
-import './peertube-load-progress-bar'
-import './theater-button'
-import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
-import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
-import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
-
-// FIXME: something weird with our path definition in tsconfig and typings
-// @ts-ignore
-import { Player } from 'video.js'
-
-// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
-videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
-// Change Captions to Subtitles/CC
-videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
-// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
-videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
-
-function getVideojsOptions (options: {
-  autoplay: boolean
-  playerElement: HTMLVideoElement
-  videoViewUrl: string
-  videoDuration: number
-  videoFiles: VideoFile[]
-  enableHotkeys: boolean
-  inactivityTimeout: number
-  peertubeLink: boolean
-  poster: string
-  startTime: number | string
-  theaterMode: boolean
-  videoCaptions: VideoJSCaption[]
-
-  language?: string
-  controls?: boolean
-  muted?: boolean
-  loop?: boolean
-  subtitle?: string
-
-  userWatching?: UserWatching
-}) {
-  const videojsOptions = {
-    // We don't use text track settings for now
-    textTrackSettings: false,
-    controls: options.controls !== undefined ? options.controls : true,
-    loop: options.loop !== undefined ? options.loop : false,
-
-    muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage
-
-    poster: options.poster,
-    autoplay: false,
-    inactivityTimeout: options.inactivityTimeout,
-    playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
-    plugins: {
-      peertube: {
-        autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
-        videoCaptions: options.videoCaptions,
-        videoFiles: options.videoFiles,
-        playerElement: options.playerElement,
-        videoViewUrl: options.videoViewUrl,
-        videoDuration: options.videoDuration,
-        startTime: options.startTime,
-        userWatching: options.userWatching,
-        subtitle: options.subtitle
-      }
-    },
-    controlBar: {
-      children: getControlBarChildren(options)
-    }
-  }
-
-  if (options.enableHotkeys === true) {
-    Object.assign(videojsOptions.plugins, {
-      hotkeys: {
-        enableVolumeScroll: false,
-        enableModifiersForNumbers: false,
-
-        fullscreenKey: function (event: KeyboardEvent) {
-          // fullscreen with the f key or Ctrl+Enter
-          return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
-        },
-
-        seekStep: function (event: KeyboardEvent) {
-          // mimic VLC seek behavior, and default to 5 (original value is 5).
-          if (event.ctrlKey && event.altKey) {
-            return 5 * 60
-          } else if (event.ctrlKey) {
-            return 60
-          } else if (event.altKey) {
-            return 10
-          } else {
-            return 5
-          }
-        },
-
-        customKeys: {
-          increasePlaybackRateKey: {
-            key: function (event: KeyboardEvent) {
-              return event.key === '>'
-            },
-            handler: function (player: Player) {
-              player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
-            }
-          },
-          decreasePlaybackRateKey: {
-            key: function (event: KeyboardEvent) {
-              return event.key === '<'
-            },
-            handler: function (player: Player) {
-              player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
-            }
-          },
-          frameByFrame: {
-            key: function (event: KeyboardEvent) {
-              return event.key === '.'
-            },
-            handler: function (player: Player) {
-              player.pause()
-              // Calculate movement distance (assuming 30 fps)
-              const dist = 1 / 30
-              player.currentTime(player.currentTime() + dist)
-            }
-          }
-        }
-      }
-    })
-  }
-
-  if (options.language && !isDefaultLocale(options.language)) {
-    Object.assign(videojsOptions, { language: options.language })
-  }
-
-  return videojsOptions
-}
-
-function getControlBarChildren (options: {
-  peertubeLink: boolean
-  theaterMode: boolean,
-  videoCaptions: VideoJSCaption[]
-}) {
-  const settingEntries = []
-
-  // Keep an order
-  settingEntries.push('playbackRateMenuButton')
-  if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton')
-  settingEntries.push('resolutionMenuButton')
-
-  const children = {
-    'playToggle': {},
-    'currentTimeDisplay': {},
-    'timeDivider': {},
-    'durationDisplay': {},
-    'liveDisplay': {},
-
-    'flexibleWidthSpacer': {},
-    'progressControl': {
-      children: {
-        'seekBar': {
-          children: {
-            'peerTubeLoadProgressBar': {},
-            'mouseTimeDisplay': {},
-            'playProgressBar': {}
-          }
-        }
-      }
-    },
-
-    'webTorrentButton': {},
-
-    'muteToggle': {},
-    'volumeControl': {},
-
-    'settingsButton': {
-      setup: {
-        maxHeightOffset: 40
-      },
-      entries: settingEntries
-    }
-  }
-
-  if (options.peertubeLink === true) {
-    Object.assign(children, {
-      'peerTubeLinkButton': {}
-    })
-  }
-
-  if (options.theaterMode === true) {
-    Object.assign(children, {
-      'theaterButton': {}
-    })
-  }
-
-  Object.assign(children, {
-    'fullscreenToggle': {}
-  })
-
-  return children
-}
-
-function addContextMenu (player: any, videoEmbedUrl: string) {
-  player.contextmenuUI({
-    content: [
-      {
-        label: player.localize('Copy the video URL'),
-        listener: function () {
-          copyToClipboard(buildVideoLink())
-        }
-      },
-      {
-        label: player.localize('Copy the video URL at the current time'),
-        listener: function () {
-          const player = this as Player
-          copyToClipboard(buildVideoLink(player.currentTime()))
-        }
-      },
-      {
-        label: player.localize('Copy embed code'),
-        listener: () => {
-          copyToClipboard(buildVideoEmbed(videoEmbedUrl))
-        }
-      },
-      {
-        label: player.localize('Copy magnet URI'),
-        listener: function () {
-          const player = this as Player
-          copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri)
-        }
-      }
-    ]
-  })
-}
-
-function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) {
-  const path = getLocalePath(serverUrl, locale)
-  // It is the default locale, nothing to translate
-  if (!path) return Promise.resolve(undefined)
-
-  let p: Promise<any>
-
-  if (loadLocaleInVideoJS.cache[path]) {
-    p = Promise.resolve(loadLocaleInVideoJS.cache[path])
-  } else {
-    p = fetch(path + '/player.json')
-      .then(res => res.json())
-      .then(json => {
-        loadLocaleInVideoJS.cache[path] = json
-        return json
-      })
-      .catch(err => {
-        console.error('Cannot get player translations', err)
-        return undefined
-      })
-  }
-
-  const completeLocale = getCompleteLocale(locale)
-  return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
-}
-namespace loadLocaleInVideoJS {
-  export const cache: { [ path: string ]: any } = {}
-}
-
-function getServerTranslations (serverUrl: string, locale: string) {
-  const path = getLocalePath(serverUrl, locale)
-  // It is the default locale, nothing to translate
-  if (!path) return Promise.resolve(undefined)
-
-  return fetch(path + '/server.json')
-    .then(res => res.json())
-    .catch(err => {
-      console.error('Cannot get server translations', err)
-      return undefined
-    })
-}
-
-// ############################################################################
-
-export {
-  getServerTranslations,
-  loadLocaleInVideoJS,
-  getVideojsOptions,
-  addContextMenu
-}
-
-// ############################################################################
-
-function getLocalePath (serverUrl: string, locale: string) {
-  const completeLocale = getCompleteLocale(locale)
-
-  if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
-
-  return serverUrl + '/client/locales/' + completeLocale
-}
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
new file mode 100644 (file)
index 0000000..0bd6076
--- /dev/null
@@ -0,0 +1,219 @@
+// FIXME: something weird with our path definition in tsconfig and typings
+// @ts-ignore
+import * as videojs from 'video.js'
+import './videojs-components/settings-menu-button'
+import { PeerTubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { isMobile, timeToInt } from './utils'
+import {
+  getStoredLastSubtitle,
+  getStoredMute,
+  getStoredVolume,
+  saveLastSubtitle,
+  saveMuteInStore,
+  saveVolumeInStore
+} from './peertube-player-local-storage'
+
+const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
+class PeerTubePlugin extends Plugin {
+  private readonly autoplay: boolean = false
+  private readonly startTime: number = 0
+  private readonly videoViewUrl: string
+  private readonly videoDuration: number
+  private readonly CONSTANTS = {
+    USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
+  }
+
+  private player: any
+  private videoCaptions: VideoJSCaption[]
+  private defaultSubtitle: string
+
+  private videoViewInterval: any
+  private userWatchingVideoInterval: any
+  private qualityObservationTimer: any
+
+  constructor (player: videojs.Player, options: PeerTubePluginOptions) {
+    super(player, options)
+
+    this.startTime = timeToInt(options.startTime)
+    this.videoViewUrl = options.videoViewUrl
+    this.videoDuration = options.videoDuration
+    this.videoCaptions = options.videoCaptions
+
+    if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
+
+    this.player.ready(() => {
+      const playerOptions = this.player.options_
+
+      const volume = getStoredVolume()
+      if (volume !== undefined) this.player.volume(volume)
+
+      const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
+      if (muted !== undefined) this.player.muted(muted)
+
+      this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
+
+      this.player.on('volumechange', () => {
+        saveVolumeInStore(this.player.volume())
+        saveMuteInStore(this.player.muted())
+      })
+
+      this.player.textTracks().on('change', () => {
+        const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
+          return t.kind === 'captions' && t.mode === 'showing'
+        })
+
+        if (!showing) {
+          saveLastSubtitle('off')
+          return
+        }
+
+        saveLastSubtitle(showing.language)
+      })
+
+      this.player.on('sourcechange', () => this.initCaptions())
+
+      this.player.duration(options.videoDuration)
+
+      this.initializePlayer()
+      this.runViewAdd()
+
+      if (options.userWatching) this.runUserWatchVideo(options.userWatching)
+    })
+  }
+
+  dispose () {
+    clearTimeout(this.qualityObservationTimer)
+
+    clearInterval(this.videoViewInterval)
+
+    if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
+  }
+
+  private initializePlayer () {
+    if (isMobile()) this.player.addClass('vjs-is-mobile')
+
+    this.initSmoothProgressBar()
+
+    this.initCaptions()
+
+    this.alterInactivity()
+  }
+
+  private runViewAdd () {
+    this.clearVideoViewInterval()
+
+    // After 30 seconds (or 3/4 of the video), add a view to the video
+    let minSecondsToView = 30
+
+    if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
+
+    let secondsViewed = 0
+    this.videoViewInterval = setInterval(() => {
+      if (this.player && !this.player.paused()) {
+        secondsViewed += 1
+
+        if (secondsViewed > minSecondsToView) {
+          this.clearVideoViewInterval()
+
+          this.addViewToVideo().catch(err => console.error(err))
+        }
+      }
+    }, 1000)
+  }
+
+  private runUserWatchVideo (options: UserWatching) {
+    let lastCurrentTime = 0
+
+    this.userWatchingVideoInterval = setInterval(() => {
+      const currentTime = Math.floor(this.player.currentTime())
+
+      if (currentTime - lastCurrentTime >= 1) {
+        lastCurrentTime = currentTime
+
+        this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
+          .catch(err => console.error('Cannot notify user is watching.', err))
+      }
+    }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
+  }
+
+  private clearVideoViewInterval () {
+    if (this.videoViewInterval !== undefined) {
+      clearInterval(this.videoViewInterval)
+      this.videoViewInterval = undefined
+    }
+  }
+
+  private addViewToVideo () {
+    if (!this.videoViewUrl) return Promise.resolve(undefined)
+
+    return fetch(this.videoViewUrl, { method: 'POST' })
+  }
+
+  private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
+    const body = new URLSearchParams()
+    body.append('currentTime', currentTime.toString())
+
+    const headers = new Headers({ 'Authorization': authorizationHeader })
+
+    return fetch(url, { method: 'PUT', body, headers })
+  }
+
+  private alterInactivity () {
+    let saveInactivityTimeout: number
+
+    const disableInactivity = () => {
+      saveInactivityTimeout = this.player.options_.inactivityTimeout
+      this.player.options_.inactivityTimeout = 0
+    }
+    const enableInactivity = () => {
+      this.player.options_.inactivityTimeout = saveInactivityTimeout
+    }
+
+    const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
+
+    this.player.controlBar.on('mouseenter', () => disableInactivity())
+    settingsDialog.on('mouseenter', () => disableInactivity())
+    this.player.controlBar.on('mouseleave', () => enableInactivity())
+    settingsDialog.on('mouseleave', () => enableInactivity())
+  }
+
+  private initCaptions () {
+    for (const caption of this.videoCaptions) {
+      this.player.addRemoteTextTrack({
+        kind: 'captions',
+        label: caption.label,
+        language: caption.language,
+        id: caption.language,
+        src: caption.src,
+        default: this.defaultSubtitle === caption.language
+      }, false)
+    }
+
+    this.player.trigger('captionsChanged')
+  }
+
+  // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
+  private initSmoothProgressBar () {
+    const SeekBar = videojsUntyped.getComponent('SeekBar')
+    SeekBar.prototype.getPercent = function getPercent () {
+      // Allows for smooth scrubbing, when player can't keep up.
+      // const time = (this.player_.scrubbing()) ?
+      //   this.player_.getCache().currentTime :
+      //   this.player_.currentTime()
+      const time = this.player_.currentTime()
+      const percent = time / this.player_.duration()
+      return percent >= 1 ? 1 : percent
+    }
+    SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
+      let newTime = this.calculateDistance(event) * this.player_.duration()
+      if (newTime === this.player_.duration()) {
+        newTime = newTime - 0.1
+      }
+      this.player_.currentTime(newTime)
+      this.update()
+    }
+  }
+}
+
+videojs.registerPlugin('peertube', PeerTubePlugin)
+export { PeerTubePlugin }
index 634c7fdc9df0e38484c31cebe5ed2b0b1b721c49..060ea4dce4d49f96de16ed4d4cb0794e19befc4e 100644 (file)
@@ -3,11 +3,13 @@
 import * as videojs from 'video.js'
 
 import { VideoFile } from '../../../../shared/models/videos/video.model'
-import { PeerTubePlugin } from './peertube-videojs-plugin'
+import { PeerTubePlugin } from './peertube-plugin'
+import { WebTorrentPlugin } from './webtorrent-plugin'
 
 declare namespace videojs {
   interface Player {
     peertube (): PeerTubePlugin
+    webtorrent (): WebTorrentPlugin
   }
 }
 
@@ -30,26 +32,73 @@ type UserWatching = {
   authorizationHeader: string
 }
 
-type PeertubePluginOptions = {
-  videoFiles: VideoFile[]
-  playerElement: HTMLVideoElement
+type PeerTubePluginOptions = {
+  autoplay: boolean
   videoViewUrl: string
   videoDuration: number
   startTime: number | string
-  autoplay: boolean,
-  videoCaptions: VideoJSCaption[]
 
-  subtitle?: string
   userWatching?: UserWatching
+  subtitle?: string
+
+  videoCaptions: VideoJSCaption[]
+}
+
+type WebtorrentPluginOptions = {
+  playerElement: HTMLVideoElement
+
+  autoplay: boolean
+  videoDuration: number
+
+  videoFiles: VideoFile[]
+}
+
+type P2PMediaLoaderPluginOptions = {
+  type: string
+  src: string
+}
+
+type VideoJSPluginOptions = {
+  peertube: PeerTubePluginOptions
+
+  webtorrent?: WebtorrentPluginOptions
+
+  p2pMediaLoader?: P2PMediaLoaderPluginOptions
 }
 
 // videojs typings don't have some method we need
 const videojsUntyped = videojs as any
 
+type LoadedQualityData = {
+  qualitySwitchCallback: Function,
+  qualityData: {
+    video: {
+      id: number
+      label: string
+      selected: boolean
+    }[]
+  }
+}
+
+type ResolutionUpdateData = {
+  auto: boolean,
+  resolutionId: number
+}
+
+type AutoResolutionUpdateData = {
+  possible: boolean
+}
+
 export {
+  ResolutionUpdateData,
+  AutoResolutionUpdateData,
   VideoJSComponentInterface,
-  PeertubePluginOptions,
   videojsUntyped,
   VideoJSCaption,
-  UserWatching
+  UserWatching,
+  PeerTubePluginOptions,
+  WebtorrentPluginOptions,
+  P2PMediaLoaderPluginOptions,
+  VideoJSPluginOptions,
+  LoadedQualityData
 }
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts
deleted file mode 100644 (file)
index b54fd91..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-// FIXME: something weird with our path definition in tsconfig and typings
-// @ts-ignore
-import { Player } from 'video.js'
-
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-
-const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
-class ResolutionMenuItem extends MenuItem {
-
-  constructor (player: Player, options: any) {
-    const currentResolutionId = player.peertube().getCurrentResolutionId()
-    options.selectable = true
-    options.selected = options.id === currentResolutionId
-
-    super(player, options)
-
-    this.label = options.label
-    this.id = options.id
-
-    player.peertube().on('videoFileUpdate', () => this.updateSelection())
-    player.peertube().on('autoResolutionUpdate', () => this.updateSelection())
-  }
-
-  handleClick (event: any) {
-    if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return
-
-    super.handleClick(event)
-
-    // Auto resolution
-    if (this.id === -1) {
-      this.player_.peertube().enableAutoResolution()
-      return
-    }
-
-    this.player_.peertube().disableAutoResolution()
-    this.player_.peertube().updateResolution(this.id)
-  }
-
-  updateSelection () {
-    // Check if auto resolution is forbidden or not
-    if (this.id === -1) {
-      if (this.player_.peertube().isAutoResolutionForbidden()) {
-        this.addClass('disabled')
-      } else {
-        this.removeClass('disabled')
-      }
-    }
-
-    if (this.player_.peertube().isAutoResolutionOn()) {
-      this.selected(this.id === -1)
-      return
-    }
-
-    this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
-  }
-
-  getLabel () {
-    if (this.id === -1) {
-      return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
-    }
-
-    return this.label
-  }
-}
-MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
-
-export { ResolutionMenuItem }
similarity index 91%
rename from client/src/assets/player/webtorrent-info-button.ts
rename to client/src/assets/player/videojs-components/p2p-info-button.ts
index c3c1af951430de40cad59cbafd1eebbd7e29e88f..03a5d29f0b7dacbe4d9be759da7ed088a09d1cd2 100644 (file)
@@ -1,8 +1,8 @@
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { bytes } from './utils'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+import { bytes } from '../utils'
 
 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
-class WebtorrentInfoButton extends Button {
+class P2pInfoButton extends Button {
 
   createEl () {
     const div = videojsUntyped.dom.createEl('div', {
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button {
     subDivHttp.appendChild(subDivHttpText)
     div.appendChild(subDivHttp)
 
-    this.player_.peertube().on('torrentInfo', (event: any, data: any) => {
+    this.player_.on('p2pInfo', (event: any, data: any) => {
       // We are in HTTP fallback
       if (!data) {
         subDivHttp.className = 'vjs-peertube-displayed'
@@ -99,4 +99,4 @@ class WebtorrentInfoButton extends Button {
     return div
   }
 }
-Button.registerComponent('WebTorrentButton', WebtorrentInfoButton)
+Button.registerComponent('P2PInfoButton', P2pInfoButton)
similarity index 87%
rename from client/src/assets/player/peertube-link-button.ts
rename to client/src/assets/player/videojs-components/peertube-link-button.ts
index de9a49de93dbb95fb94d3113c6a96888987f94d2..fed8ea33e4b0ea8748d189774b42f5104bf270b0 100644 (file)
@@ -1,5 +1,5 @@
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { buildVideoLink } from './utils'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+import { buildVideoLink } from '../utils'
 // FIXME: something weird with our path definition in tsconfig and typings
 // @ts-ignore
 import { Player } from 'video.js'
similarity index 85%
rename from client/src/assets/player/peertube-load-progress-bar.ts
rename to client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
index af276d1b2dfc93a1fd61ea07a43b80e8adafa688..9a0e3b550109e1e174943995f25eca8d03e058fc 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
 // FIXME: something weird with our path definition in tsconfig and typings
 // @ts-ignore
 import { Player } from 'video.js'
@@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component {
   }
 
   update () {
-    const torrent = this.player().peertube().getTorrent()
+    const torrent = this.player().webtorrent().getTorrent()
     if (!torrent) return
 
     this.el_.style.width = (torrent.progress * 100) + '%'
similarity index 56%
rename from client/src/assets/player/resolution-menu-button.ts
rename to client/src/assets/player/videojs-components/resolution-menu-button.ts
index a3c1108cac2b9debd6f4f236ca20c5d34ae65164..2847de4709d92601f43beaaa56e6dcbde5edadcb 100644 (file)
@@ -2,7 +2,7 @@
 // @ts-ignore
 import { Player } from 'video.js'
 
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
 import { ResolutionMenuItem } from './resolution-menu-item'
 
 const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
@@ -14,16 +14,18 @@ class ResolutionMenuButton extends MenuButton {
     super(player, options)
     this.player = player
 
-    player.peertube().on('videoFileUpdate', () => this.updateLabel())
-    player.peertube().on('autoResolutionUpdate', () => this.updateLabel())
+    player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
+
+    if (player.webtorrent) {
+      player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0))
+    }
   }
 
   createEl () {
     const el = super.createEl()
 
     this.labelEl_ = videojsUntyped.dom.createEl('div', {
-      className: 'vjs-resolution-value',
-      innerHTML: this.buildLabelHTML()
+      className: 'vjs-resolution-value'
     })
 
     el.appendChild(this.labelEl_)
@@ -36,51 +38,45 @@ class ResolutionMenuButton extends MenuButton {
   }
 
   createMenu () {
-    const menu = new Menu(this.player_)
-    for (const videoFile of this.player_.peertube().videoFiles) {
-      let label = videoFile.resolution.label
-      if (videoFile.fps && videoFile.fps >= 50) {
-        label += videoFile.fps
-      }
+    return new Menu(this.player_)
+  }
+
+  buildCSSClass () {
+    return super.buildCSSClass() + ' vjs-resolution-button'
+  }
 
-      menu.addChild(new ResolutionMenuItem(
+  buildWrapperCSSClass () {
+    return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
+  }
+
+  private buildQualities (data: LoadedQualityData) {
+    // The automatic resolution item will need other labels
+    const labels: { [ id: number ]: string } = {}
+
+    for (const d of data.qualityData.video) {
+      this.menu.addChild(new ResolutionMenuItem(
         this.player_,
         {
-          id: videoFile.resolution.id,
-          label,
-          src: videoFile.magnetUri
+          id: d.id,
+          label: d.label,
+          selected: d.selected,
+          callback: data.qualitySwitchCallback
         })
       )
+
+      labels[d.id] = d.label
     }
 
-    menu.addChild(new ResolutionMenuItem(
+    this.menu.addChild(new ResolutionMenuItem(
       this.player_,
       {
         id: -1,
         label: this.player_.localize('Auto'),
-        src: null
+        labels,
+        callback: data.qualitySwitchCallback,
+        selected: true // By default, in auto mode
       }
     ))
-
-    return menu
-  }
-
-  updateLabel () {
-    if (!this.labelEl_) return
-
-    this.labelEl_.innerHTML = this.buildLabelHTML()
-  }
-
-  buildCSSClass () {
-    return super.buildCSSClass() + ' vjs-resolution-button'
-  }
-
-  buildWrapperCSSClass () {
-    return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
-  }
-
-  private buildLabelHTML () {
-    return this.player_.peertube().getCurrentResolutionLabel()
   }
 }
 ResolutionMenuButton.prototype.controlText_ = 'Quality'
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
new file mode 100644 (file)
index 0000000..cc1c797
--- /dev/null
@@ -0,0 +1,87 @@
+// FIXME: something weird with our path definition in tsconfig and typings
+// @ts-ignore
+import { Player } from 'video.js'
+
+import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+
+const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
+class ResolutionMenuItem extends MenuItem {
+  private readonly id: number
+  private readonly label: string
+  // Only used for the automatic item
+  private readonly labels: { [id: number]: string }
+  private readonly callback: Function
+
+  private autoResolutionPossible: boolean
+  private currentResolutionLabel: string
+
+  constructor (player: Player, options: any) {
+    options.selectable = true
+
+    super(player, options)
+
+    this.autoResolutionPossible = true
+    this.currentResolutionLabel = ''
+
+    this.label = options.label
+    this.labels = options.labels
+    this.id = options.id
+    this.callback = options.callback
+
+    if (player.webtorrent) {
+      player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
+
+      // We only want to disable the "Auto" item
+      if (this.id === -1) {
+        player.webtorrent().on('autoResolutionUpdate', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
+      }
+    }
+
+    // TODO: update on HLS change
+  }
+
+  handleClick (event: any) {
+    // Auto button disabled?
+    if (this.autoResolutionPossible === false && this.id === -1) return
+
+    super.handleClick(event)
+
+    this.callback(this.id)
+  }
+
+  updateSelection (data: ResolutionUpdateData) {
+    if (this.id === -1) {
+      this.currentResolutionLabel = this.labels[data.resolutionId]
+    }
+
+    // Automatic resolution only
+    if (data.auto === true) {
+      this.selected(this.id === -1)
+      return
+    }
+
+    this.selected(this.id === data.resolutionId)
+  }
+
+  updateAutoResolution (data: AutoResolutionUpdateData) {
+    // Check if the auto resolution is enabled or not
+    if (data.possible === false) {
+      this.addClass('disabled')
+    } else {
+      this.removeClass('disabled')
+    }
+
+    this.autoResolutionPossible = data.possible
+  }
+
+  getLabel () {
+    if (this.id === -1) {
+      return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
+    }
+
+    return this.label
+  }
+}
+MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
+
+export { ResolutionMenuItem }
similarity index 98%
rename from client/src/assets/player/settings-menu-button.ts
rename to client/src/assets/player/videojs-components/settings-menu-button.ts
index a7aefdcc3275c0a9fc61a213be57eda69f24c5d5..14cb8ba43367c52c3dc0b4236c10a95d9bc2c7fe 100644 (file)
@@ -6,8 +6,8 @@
 import * as videojs from 'video.js'
 
 import { SettingsMenuItem } from './settings-menu-item'
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { toTitleCase } from './utils'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+import { toTitleCase } from '../utils'
 
 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
 const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
similarity index 97%
rename from client/src/assets/player/settings-menu-item.ts
rename to client/src/assets/player/videojs-components/settings-menu-item.ts
index 2a3460ae542b713be5403d2208e3a773bf92ec53..b9a4302900e2a13a032624ad0fe5c41251a50fba 100644 (file)
@@ -5,8 +5,8 @@
 // @ts-ignore
 import * as videojs from 'video.js'
 
-import { toTitleCase } from './utils'
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { toTitleCase } from '../utils'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
 
 const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
 const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
@@ -220,12 +220,9 @@ class SettingsMenuItem extends MenuItem {
   }
 
   build () {
-    const saveUpdateLabel = this.subMenu.updateLabel
-    this.subMenu.updateLabel = () => {
+    this.subMenu.on('updateLabel', () => {
       this.update()
-
-      saveUpdateLabel.call(this.subMenu)
-    }
+    })
 
     this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
     this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
similarity index 87%
rename from client/src/assets/player/theater-button.ts
rename to client/src/assets/player/videojs-components/theater-button.ts
index 4f8fede3dea0d0bc072fc17ca94b8de0675a6097..1e11a95466be63c640e6ee59b144e8fef7119463 100644 (file)
@@ -2,8 +2,8 @@
 // @ts-ignore
 import * as videojs from 'video.js'
 
-import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage'
+import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
 
 const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
 class TheaterButton extends Button {
similarity index 73%
rename from client/src/assets/player/peertube-videojs-plugin.ts
rename to client/src/assets/player/webtorrent-plugin.ts
index e9fb90c61e2659265f98131a6c9d9a410aa35760..c3d990aed6697e03d9a03776156bdfe2c7ebc46d 100644 (file)
@@ -4,21 +4,16 @@ import * as videojs from 'video.js'
 
 import * as WebTorrent from 'webtorrent'
 import { VideoFile } from '../../../../shared/models/videos/video.model'
-import { renderVideo } from './video-renderer'
-import './settings-menu-button'
-import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
-import { PeertubeChunkStore } from './peertube-chunk-store'
+import { renderVideo } from './webtorrent/video-renderer'
+import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings'
+import { videoFileMaxByResolution, videoFileMinByResolution } from './utils'
+import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store'
 import {
   getAverageBandwidthInStore,
-  getStoredLastSubtitle,
   getStoredMute,
   getStoredVolume,
   getStoredWebTorrentEnabled,
-  saveAverageBandwidth,
-  saveLastSubtitle,
-  saveMuteInStore,
-  saveVolumeInStore
+  saveAverageBandwidth
 } from './peertube-player-local-storage'
 
 const CacheChunkStore = require('cache-chunk-store')
@@ -30,14 +25,13 @@ type PlayOptions = {
 }
 
 const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
-class PeerTubePlugin extends Plugin {
+class WebTorrentPlugin extends Plugin {
   private readonly playerElement: HTMLVideoElement
 
   private readonly autoplay: boolean = false
   private readonly startTime: number = 0
   private readonly savePlayerSrcFunction: Function
   private readonly videoFiles: VideoFile[]
-  private readonly videoViewUrl: string
   private readonly videoDuration: number
   private readonly CONSTANTS = {
     INFO_SCHEDULER: 1000, // Don't change this
@@ -45,8 +39,7 @@ class PeerTubePlugin extends Plugin {
     AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
     AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
     AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
-    BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth
-    USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
+    BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
   }
 
   private readonly webtorrent = new WebTorrent({
@@ -68,46 +61,37 @@ class PeerTubePlugin extends Plugin {
   private player: any
   private currentVideoFile: VideoFile
   private torrent: WebTorrent.Torrent
-  private videoCaptions: VideoJSCaption[]
-  private defaultSubtitle: string
 
   private renderer: any
   private fakeRenderer: any
   private destroyingFakeRenderer = false
 
   private autoResolution = true
-  private forbidAutoResolution = false
+  private autoResolutionPossible = true
   private isAutoResolutionObservation = false
   private playerRefusedP2P = false
 
-  private videoViewInterval: any
   private torrentInfoInterval: any
   private autoQualityInterval: any
-  private userWatchingVideoInterval: any
   private addTorrentDelay: any
   private qualityObservationTimer: any
   private runAutoQualitySchedulerTimer: any
 
   private downloadSpeeds: number[] = []
 
-  constructor (player: videojs.Player, options: PeertubePluginOptions) {
+  constructor (player: videojs.Player, options: WebtorrentPluginOptions) {
     super(player, options)
 
     // Disable auto play on iOS
     this.autoplay = options.autoplay && this.isIOS() === false
     this.playerRefusedP2P = !getStoredWebTorrentEnabled()
 
-    this.startTime = timeToInt(options.startTime)
     this.videoFiles = options.videoFiles
-    this.videoViewUrl = options.videoViewUrl
     this.videoDuration = options.videoDuration
-    this.videoCaptions = options.videoCaptions
 
     this.savePlayerSrcFunction = this.player.src
     this.playerElement = options.playerElement
 
-    if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
-
     this.player.ready(() => {
       const playerOptions = this.player.options_
 
@@ -117,33 +101,10 @@ class PeerTubePlugin extends Plugin {
       const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
       if (muted !== undefined) this.player.muted(muted)
 
-      this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
-
-      this.player.on('volumechange', () => {
-        saveVolumeInStore(this.player.volume())
-        saveMuteInStore(this.player.muted())
-      })
-
-      this.player.textTracks().on('change', () => {
-        const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
-          return t.kind === 'captions' && t.mode === 'showing'
-        })
-
-        if (!showing) {
-          saveLastSubtitle('off')
-          return
-        }
-
-        saveLastSubtitle(showing.language)
-      })
-
       this.player.duration(options.videoDuration)
 
       this.initializePlayer()
       this.runTorrentInfoScheduler()
-      this.runViewAdd()
-
-      if (options.userWatching) this.runUserWatchVideo(options.userWatching)
 
       this.player.one('play', () => {
         // Don't run immediately scheduler, wait some seconds the TCP connections are made
@@ -157,12 +118,9 @@ class PeerTubePlugin extends Plugin {
     clearTimeout(this.qualityObservationTimer)
     clearTimeout(this.runAutoQualitySchedulerTimer)
 
-    clearInterval(this.videoViewInterval)
     clearInterval(this.torrentInfoInterval)
     clearInterval(this.autoQualityInterval)
 
-    if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
-
     // Don't need to destroy renderer, video player will be destroyed
     this.flushVideoFile(this.currentVideoFile, false)
 
@@ -173,13 +131,6 @@ class PeerTubePlugin extends Plugin {
     return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1
   }
 
-  getCurrentResolutionLabel () {
-    if (!this.currentVideoFile) return ''
-
-    const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : ''
-    return this.currentVideoFile.resolution.label + fps
-  }
-
   updateVideoFile (
     videoFile?: VideoFile,
     options: {
@@ -228,7 +179,8 @@ class PeerTubePlugin extends Plugin {
       return done()
     })
 
-    this.trigger('videoFileUpdate')
+    this.changeQuality()
+    this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
   }
 
   updateResolution (resolutionId: number, delay = 0) {
@@ -262,28 +214,17 @@ class PeerTubePlugin extends Plugin {
     }
   }
 
-  isAutoResolutionOn () {
-    return this.autoResolution
-  }
-
   enableAutoResolution () {
     this.autoResolution = true
-    this.trigger('autoResolutionUpdate')
+    this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
   }
 
   disableAutoResolution (forbid = false) {
-    if (forbid === true) this.forbidAutoResolution = true
+    if (forbid === true) this.autoResolutionPossible = false
 
     this.autoResolution = false
-    this.trigger('autoResolutionUpdate')
-  }
-
-  isAutoResolutionForbidden () {
-    return this.forbidAutoResolution === true
-  }
-
-  getCurrentVideoFile () {
-    return this.currentVideoFile
+    this.trigger('autoResolutionUpdate', { possible: this.autoResolutionPossible })
+    this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
   }
 
   getTorrent () {
@@ -462,13 +403,7 @@ class PeerTubePlugin extends Plugin {
   }
 
   private initializePlayer () {
-    if (isMobile()) this.player.addClass('vjs-is-mobile')
-
-    this.initSmoothProgressBar()
-
-    this.initCaptions()
-
-    this.alterInactivity()
+    this.buildQualities()
 
     if (this.autoplay === true) {
       this.player.posterImage.hide()
@@ -491,7 +426,7 @@ class PeerTubePlugin extends Plugin {
 
       // Not initialized or in HTTP fallback
       if (this.torrent === undefined || this.torrent === null) return
-      if (this.isAutoResolutionOn() === false) return
+      if (this.autoResolution === false) return
       if (this.isAutoResolutionObservation === true) return
 
       const file = this.getAppropriateFile()
@@ -531,12 +466,12 @@ class PeerTubePlugin extends Plugin {
       if (this.torrent === undefined) return
 
       // Http fallback
-      if (this.torrent === null) return this.trigger('torrentInfo', false)
+      if (this.torrent === null) return this.player.trigger('p2pInfo', false)
 
       // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too
       if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
 
-      return this.trigger('torrentInfo', {
+      return this.player.trigger('p2pInfo', {
         downloadSpeed: this.torrent.downloadSpeed,
         numPeers: this.torrent.numPeers,
         uploadSpeed: this.torrent.uploadSpeed,
@@ -546,65 +481,6 @@ class PeerTubePlugin extends Plugin {
     }, this.CONSTANTS.INFO_SCHEDULER)
   }
 
-  private runViewAdd () {
-    this.clearVideoViewInterval()
-
-    // After 30 seconds (or 3/4 of the video), add a view to the video
-    let minSecondsToView = 30
-
-    if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
-
-    let secondsViewed = 0
-    this.videoViewInterval = setInterval(() => {
-      if (this.player && !this.player.paused()) {
-        secondsViewed += 1
-
-        if (secondsViewed > minSecondsToView) {
-          this.clearVideoViewInterval()
-
-          this.addViewToVideo().catch(err => console.error(err))
-        }
-      }
-    }, 1000)
-  }
-
-  private runUserWatchVideo (options: UserWatching) {
-    let lastCurrentTime = 0
-
-    this.userWatchingVideoInterval = setInterval(() => {
-      const currentTime = Math.floor(this.player.currentTime())
-
-      if (currentTime - lastCurrentTime >= 1) {
-        lastCurrentTime = currentTime
-
-        this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
-          .catch(err => console.error('Cannot notify user is watching.', err))
-      }
-    }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
-  }
-
-  private clearVideoViewInterval () {
-    if (this.videoViewInterval !== undefined) {
-      clearInterval(this.videoViewInterval)
-      this.videoViewInterval = undefined
-    }
-  }
-
-  private addViewToVideo () {
-    if (!this.videoViewUrl) return Promise.resolve(undefined)
-
-    return fetch(this.videoViewUrl, { method: 'POST' })
-  }
-
-  private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
-    const body = new URLSearchParams()
-    body.append('currentTime', currentTime.toString())
-
-    const headers = new Headers({ 'Authorization': authorizationHeader })
-
-    return fetch(url, { method: 'PUT', body, headers })
-  }
-
   private fallbackToHttp (options: PlayOptions, done?: Function) {
     const paused = this.player.paused()
 
@@ -620,8 +496,10 @@ class PeerTubePlugin extends Plugin {
     this.player.src = this.savePlayerSrcFunction
     this.player.src(httpUrl)
 
+    this.changeQuality()
+
     // We changed the source, so reinit captions
-    this.initCaptions()
+    this.player.trigger('sourcechange')
 
     return this.tryToPlay(err => {
       if (err && done) return done(err)
@@ -649,25 +527,6 @@ class PeerTubePlugin extends Plugin {
     return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
   }
 
-  private alterInactivity () {
-    let saveInactivityTimeout: number
-
-    const disableInactivity = () => {
-      saveInactivityTimeout = this.player.options_.inactivityTimeout
-      this.player.options_.inactivityTimeout = 0
-    }
-    const enableInactivity = () => {
-      this.player.options_.inactivityTimeout = saveInactivityTimeout
-    }
-
-    const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
-
-    this.player.controlBar.on('mouseenter', () => disableInactivity())
-    settingsDialog.on('mouseenter', () => disableInactivity())
-    this.player.controlBar.on('mouseleave', () => enableInactivity())
-    settingsDialog.on('mouseleave', () => enableInactivity())
-  }
-
   private pickAverageVideoFile () {
     if (this.videoFiles.length === 1) return this.videoFiles[0]
 
@@ -712,43 +571,70 @@ class PeerTubePlugin extends Plugin {
     }
   }
 
-  private initCaptions () {
-    for (const caption of this.videoCaptions) {
-      this.player.addRemoteTextTrack({
-        kind: 'captions',
-        label: caption.label,
-        language: caption.language,
-        id: caption.language,
-        src: caption.src,
-        default: this.defaultSubtitle === caption.language
-      }, false)
+  private buildQualities () {
+    const qualityLevelsPayload = []
+
+    for (const file of this.videoFiles) {
+      const representation = {
+        id: file.resolution.id,
+        label: this.buildQualityLabel(file),
+        height: file.resolution.id,
+        _enabled: true
+      }
+
+      this.player.qualityLevels().addQualityLevel(representation)
+
+      qualityLevelsPayload.push({
+        id: representation.id,
+        label: representation.label,
+        selected: false
+      })
     }
 
-    this.player.trigger('captionsChanged')
+    const payload: LoadedQualityData = {
+      qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d),
+      qualityData: {
+        video: qualityLevelsPayload
+      }
+    }
+    this.player.trigger('loadedqualitydata', payload)
   }
 
-  // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
-  private initSmoothProgressBar () {
-    const SeekBar = videojsUntyped.getComponent('SeekBar')
-    SeekBar.prototype.getPercent = function getPercent () {
-      // Allows for smooth scrubbing, when player can't keep up.
-      // const time = (this.player_.scrubbing()) ?
-      //   this.player_.getCache().currentTime :
-      //   this.player_.currentTime()
-      const time = this.player_.currentTime()
-      const percent = time / this.player_.duration()
-      return percent >= 1 ? 1 : percent
+  private buildQualityLabel (file: VideoFile) {
+    let label = file.resolution.label
+
+    if (file.fps && file.fps >= 50) {
+      label += file.fps
     }
-    SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
-      let newTime = this.calculateDistance(event) * this.player_.duration()
-      if (newTime === this.player_.duration()) {
-        newTime = newTime - 0.1
-      }
-      this.player_.currentTime(newTime)
-      this.update()
+
+    return label
+  }
+
+  private qualitySwitchCallback (id: number) {
+    if (id === -1) {
+      if (this.autoResolutionPossible === true) this.enableAutoResolution()
+      return
+    }
+
+    this.disableAutoResolution()
+    this.updateResolution(id)
+  }
+
+  private changeQuality () {
+    const resolutionId = this.currentVideoFile.resolution.id
+    const qualityLevels = this.player.qualityLevels()
+
+    if (resolutionId === -1) {
+      qualityLevels.selectedIndex = -1
+      return
+    }
+
+    for (let i = 0; i < qualityLevels; i++) {
+      const q = this.player.qualityLevels[i]
+      if (q.height === resolutionId) qualityLevels.selectedIndex = i
     }
   }
 }
 
-videojs.registerPlugin('peertube', PeerTubePlugin)
-export { PeerTubePlugin }
+videojs.registerPlugin('webtorrent', WebTorrentPlugin)
+export { WebTorrentPlugin }
index 54b8fb54300133ca09b635fde06edd335733e6b2..b1261c4a2e08d88455523a4dd3fe61852519f44b 100644 (file)
@@ -17,17 +17,13 @@ import 'core-js/es6/set'
 // For google bot that uses Chrome 41 and does not understand fetch
 import 'whatwg-fetch'
 
-// FIXME: something weird with our path definition in tsconfig and typings
-// @ts-ignore
-import * as vjs from 'video.js'
-
 import * as Channel from 'jschannel'
 
 import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
-import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player'
 import { PeerTubeResolution } from '../player/definitions'
 import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
 import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
+import { PeertubePlayerManager, PeertubePlayerManagerOptions } from '../../assets/player/peertube-player-manager'
 
 /**
  * Embed API exposes control of the embed player to the outside world via
@@ -73,16 +69,16 @@ class PeerTubeEmbedApi {
   }
 
   private setResolution (resolutionId: number) {
-    if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return
+    if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return
 
     // Auto resolution
     if (resolutionId === -1) {
-      this.embed.player.peertube().enableAutoResolution()
+      this.embed.player.webtorrent().enableAutoResolution()
       return
     }
 
-    this.embed.player.peertube().disableAutoResolution()
-    this.embed.player.peertube().updateResolution(resolutionId)
+    this.embed.player.webtorrent().disableAutoResolution()
+    this.embed.player.webtorrent().updateResolution(resolutionId)
   }
 
   /**
@@ -122,15 +118,17 @@ class PeerTubeEmbedApi {
 
     // PeerTube specific capabilities
 
-    this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions())
-    this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions())
+    if (this.embed.player.webtorrent) {
+      this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions())
+      this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions())
+    }
   }
 
-  private loadResolutions () {
+  private loadWebTorrentResolutions () {
     let resolutions = []
-    let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId()
+    let currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId()
 
-    for (const videoFile of this.embed.player.peertube().videoFiles) {
+    for (const videoFile of this.embed.player.webtorrent().videoFiles) {
       let label = videoFile.resolution.label
       if (videoFile.fps && videoFile.fps >= 50) {
         label += videoFile.fps
@@ -266,9 +264,8 @@ class PeerTubeEmbed {
     const urlParts = window.location.pathname.split('/')
     const videoId = urlParts[ urlParts.length - 1 ]
 
-    const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
-      loadLocaleInVideoJS(window.location.origin, vjs, navigator.language),
-      getServerTranslations(window.location.origin, navigator.language),
+    const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
+      PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language),
       this.loadVideoInfo(videoId),
       this.loadVideoCaptions(videoId)
     ])
@@ -292,43 +289,56 @@ class PeerTubeEmbed {
 
     this.loadParams()
 
-    const videojsOptions = getVideojsOptions({
-      autoplay: this.autoplay,
-      controls: this.controls,
-      muted: this.muted,
-      loop: this.loop,
-      startTime: this.startTime,
-      subtitle: this.subtitle,
-
-      videoCaptions,
-      inactivityTimeout: 1500,
-      videoViewUrl: this.getVideoUrl(videoId) + '/views',
-      playerElement: this.videoElement,
-      videoFiles: videoInfo.files,
-      videoDuration: videoInfo.duration,
-      enableHotkeys: true,
-      peertubeLink: true,
-      poster: window.location.origin + videoInfo.previewPath,
-      theaterMode: false
-    })
+    const options: PeertubePlayerManagerOptions = {
+      common: {
+        autoplay: this.autoplay,
+        controls: this.controls,
+        muted: this.muted,
+        loop: this.loop,
+        captions: videoCaptions.length !== 0,
+        startTime: this.startTime,
+        subtitle: this.subtitle,
+
+        videoCaptions,
+        inactivityTimeout: 1500,
+        videoViewUrl: this.getVideoUrl(videoId) + '/views',
+        playerElement: this.videoElement,
+        videoDuration: videoInfo.duration,
+        enableHotkeys: true,
+        peertubeLink: true,
+        poster: window.location.origin + videoInfo.previewPath,
+        theaterMode: false,
+
+        serverUrl: window.location.origin,
+        language: navigator.language,
+        embedUrl: window.location.origin + videoInfo.embedPath
+      },
+
+      webtorrent: {
+        videoFiles: videoInfo.files
+      }
+
+      // p2pMediaLoader: {
+      //   // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8'
+      //   // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8'
+      //   playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8'
+      // }
+    }
 
-    this.playerOptions = videojsOptions
-    this.player = vjs(this.videoContainerId, videojsOptions, () => {
-      this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
+    this.player = await PeertubePlayerManager.initialize('webtorrent', options)
 
-      window[ 'videojsPlayer' ] = this.player
+    this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
 
-      if (this.controls) {
-        this.player.dock({
-          title: videoInfo.name,
-          description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
-        })
-      }
+    window[ 'videojsPlayer' ] = this.player
 
-      addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
+    if (this.controls) {
+      this.player.dock({
+        title: videoInfo.name,
+        description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
+      })
+    }
 
-      this.initializeApi()
-    })
+    this.initializeApi()
   }
 
   private handleError (err: Error, translations?: { [ id: string ]: string }) {
index af7a74e9ecf326d68f9117385c3ad9f043052f9f..729eee35359d6c9c1de71d90ab1fd5c1e63094bf 100644 (file)
@@ -3,7 +3,7 @@
   "compilerOptions": {
     "outDir": "../out-tsc/app",
     "baseUrl": "./",
-    "module": "es2015",
+    "module": "esnext",
     "types": [],
     "lib": [
       "es2017",
index dee67c41429ebd02c2d7f5067c065cc38a61ff98..0698ca501c4654aa18a0f6e079082483cb163fa0 100644 (file)
     semver "5.5.1"
     semver-intersect "1.4.0"
 
+"@streamroot/videojs-hlsjs-plugin@^1.0.7":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@streamroot/videojs-hlsjs-plugin/-/videojs-hlsjs-plugin-1.0.7.tgz#581aecdf6a966162b404c60bd3ab8264eb89d334"
+  integrity sha512-7oAIOhEFxkfLOYWDfg7Oh3+OrnoTElRvUE3Jblg2B+SHmnrw4YXQnAwYJ0AHjNIBKoHnQubzZGttLaHAFJVspQ==
+
 "@types/bittorrent-protocol@*":
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.2.tgz#169e9633e1bd18e6b830d11cf42e611b1972cb83"
@@ -1445,7 +1450,7 @@ bittorrent-protocol@^3.0.0:
     unordered-array-remove "^1.0.2"
     xtend "^4.0.0"
 
-bittorrent-tracker@^9.0.0:
+bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.10.1:
   version "9.10.1"
   resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11"
   integrity sha512-n5zTL/g6Wt0rb2EnkiyiaGYhth7I/N0/xMqGUpvGX/7g1scDGBVPhJnXR8lfp3/OMj681fv40o4q/otECMtZSA==
@@ -3305,6 +3310,11 @@ events@^1.0.0:
   resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
   integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
 
+events@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"
+  integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==
+
 eventsource@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
@@ -3900,7 +3910,7 @@ genfun@^5.0.0:
   resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
   integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==
 
-get-browser-rtc@^1.0.0:
+get-browser-rtc@^1.0.0, get-browser-rtc@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9"
   integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk=
@@ -6108,6 +6118,13 @@ m3u8-parser@4.2.0:
   resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.2.0.tgz#c8e0785fd17f741f4408b49466889274a9e36447"
   integrity sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg==
 
+m3u8-parser@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.3.0.tgz#4b4e988f87b6d8b2401d209a1d17798285a9da04"
+  integrity sha512-bVbjuBMoVIgFL1vpXVIxjeaoB5TPDJRb0m5qiTdM738SGqv/LAmsnVVPlKjM4fulm/rr1XZsKM+owHm+zvqxYA==
+  dependencies:
+    global "^4.3.2"
+
 magic-string@^0.25.0:
   version "0.25.1"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e"
@@ -7214,6 +7231,26 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
   integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
 
+p2p-media-loader-core@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.3.0.tgz#75687d7d7bee835d5c6c2f17d346add2dbe43b83"
+  integrity sha512-WKB9ONdA0kDQHXr6nixIL8t0UZuTD9Pqi/BIuaTiPUGDwYXUS/Mf5YynLAUupniLkIaDYD7/jmSLWqpZUDsAyw==
+  dependencies:
+    bittorrent-tracker "^9.10.1"
+    debug "^4.1.0"
+    events "^3.0.0"
+    get-browser-rtc "^1.0.2"
+    sha.js "^2.4.11"
+
+p2p-media-loader-hlsjs@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.3.0.tgz#4ee15d4d1a23aa0322a5be2bc6c329b6c913028d"
+  integrity sha512-U7PzMG5X7CVQ15OtMPRQjW68Msu0fuw8Pp0PRznX5uK0p26tSYMT/ZYCNeYCoDg3wGgJHM+327ed3M7TRJ4lcw==
+  dependencies:
+    events "^3.0.0"
+    m3u8-parser "^4.2.0"
+    p2p-media-loader-core "^0.3.0"
+
 package-json-versionify@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17"
@@ -8699,7 +8736,7 @@ setprototypeof@1.1.0:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
   integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
 
-sha.js@^2.4.0, sha.js@^2.4.8:
+sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
   version "2.4.11"
   resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
   integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
@@ -10090,6 +10127,14 @@ videojs-contextmenu-ui@^5.0.0:
     global "^4.3.2"
     video.js "^6 || ^7"
 
+videojs-contrib-quality-levels@^2.0.9:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.9.tgz#b5d533d5092a6fc7d29eae1b43e4597d89bd527b"
+  integrity sha512-HJeaJJQdSufi9Y5T7jlyyhkeq+mWPCog86q6ypoTi66boBMMJTo2abiOSHS9KaOGAJjH72gfvrjVY5FRdjlxYA==
+  dependencies:
+    global "^4.3.2"
+    video.js "^6 || ^7"
+
 videojs-dock@^2.0.2:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.1.4.tgz#0ebd198b5d48990e3523fdc87dbfdb9fe96f804c"
index 9b8fddac6595c2c68a225a65e415bad0315f4781..b4675c57f54c3d8b3a4f609e9382f40d9291d079 100755 (executable)
@@ -4,7 +4,7 @@ set -eu
 
 if [ ! -f "./client/dist/en_US/index.html" ]; then
   echo "client/dist/en_US/index.html does not exist, compile client files..."
-  npm run build:client
+  npm run build:client -- --light
 fi
 
 npm run watch:server
index 8b919af0d1ead6e4329af09f0bba58fc886fbeba..5fa9d1ab547d13c9a00bc3cea9179b25a7057ed7 100644 (file)
@@ -16,7 +16,7 @@ const baseDirectives = Object.assign({},
     baseUri: ["'self'"],
     manifestSrc: ["'self'"],
     frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed
-    workerSrc: ["'self'"] // instead of deprecated child-src
+    workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src
   },
   CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {},
   CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}