]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/assets/player/peertube-videojs-plugin.ts
add webtorrent opt-out settings
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / peertube-videojs-plugin.ts
index e268510fa0ff74cfe6d561ee4106d790e5b82a13..90ca8f9faaeb2df12956cc1b74f6a69c6d3f5dbd 100644 (file)
@@ -3,11 +3,12 @@ import * as WebTorrent from 'webtorrent'
 import { VideoFile } from '../../../../shared/models/videos/video.model'
 import { renderVideo } from './video-renderer'
 import './settings-menu-button'
-import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
-import { isMobile, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
+import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
+import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
 import * as CacheChunkStore from 'cache-chunk-store'
 import { PeertubeChunkStore } from './peertube-chunk-store'
 import {
+  getStoredWebTorrentPolicy,
   getAverageBandwidthInStore,
   getStoredMute,
   getStoredVolume,
@@ -32,7 +33,8 @@ 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
+    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
   }
 
   private readonly webtorrent = new WebTorrent({
@@ -54,15 +56,21 @@ class PeerTubePlugin extends Plugin {
   private player: any
   private currentVideoFile: VideoFile
   private torrent: WebTorrent.Torrent
+  private videoCaptions: VideoJSCaption[]
+
   private renderer
   private fakeRenderer
+  private destoyingFakeRenderer = false
+
   private autoResolution = true
   private forbidAutoResolution = false
   private isAutoResolutionObservation = false
+  private playerRefusedP2P = false
 
   private videoViewInterval
   private torrentInfoInterval
   private autoQualityInterval
+  private userWatchingVideoInterval
   private addTorrentDelay
   private qualityObservationTimer
   private runAutoQualitySchedulerTimer
@@ -75,17 +83,13 @@ class PeerTubePlugin extends Plugin {
     // Disable auto play on iOS
     this.autoplay = options.autoplay && this.isIOS() === false
 
-    this.startTime = options.startTime
+    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
-    // Hack to "simulate" src link in video.js >= 6
-    // Without this, we can't play the video after pausing it
-    // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
-    this.player.src = () => true
-
     this.playerElement = options.playerElement
 
     if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
@@ -95,16 +99,17 @@ class PeerTubePlugin extends Plugin {
       if (volume !== undefined) this.player.volume(volume)
       const muted = getStoredMute()
       if (muted !== undefined) this.player.muted(muted)
+      this.playerRefusedP2P = getStoredWebTorrentPolicy() || false
 
       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
-        this.runAutoQualitySchedulerTimer = setTimeout(() => {
-          this.runAutoQualityScheduler()
-        }, this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
+        this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
       })
     })
 
@@ -123,6 +128,8 @@ class PeerTubePlugin extends Plugin {
     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)
 
@@ -165,6 +172,9 @@ class PeerTubePlugin extends Plugin {
     // Do not display error to user because we will have multiple fallback
     this.disableErrorDisplay()
 
+    // Hack to "simulate" src link in video.js >= 6
+    // Without this, we can't play the video after pausing it
+    // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
     this.player.src = () => true
     const oldPlaybackRate = this.player.playbackRate()
 
@@ -179,7 +189,66 @@ class PeerTubePlugin extends Plugin {
     this.trigger('videoFileUpdate')
   }
 
-  addTorrent (
+  updateResolution (resolutionId: number, delay = 0) {
+    // Remember player state
+    const currentTime = this.player.currentTime()
+    const isPaused = this.player.paused()
+
+    // Remove poster to have black background
+    this.playerElement.poster = ''
+
+    // Hide bigPlayButton
+    if (!isPaused) {
+      this.player.bigPlayButton.hide()
+    }
+
+    const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
+    const options = {
+      forcePlay: false,
+      delay,
+      seek: currentTime + (delay / 1000)
+    }
+    this.updateVideoFile(newVideoFile, options)
+  }
+
+  flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
+    if (videoFile !== undefined && this.webtorrent.get(videoFile.magnetUri)) {
+      if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy()
+
+      this.webtorrent.remove(videoFile.magnetUri)
+      console.log('Removed ' + videoFile.magnetUri)
+    }
+  }
+
+  isAutoResolutionOn () {
+    return this.autoResolution
+  }
+
+  enableAutoResolution () {
+    this.autoResolution = true
+    this.trigger('autoResolutionUpdate')
+  }
+
+  disableAutoResolution (forbid = false) {
+    if (forbid === true) this.forbidAutoResolution = true
+
+    this.autoResolution = false
+    this.trigger('autoResolutionUpdate')
+  }
+
+  isAutoResolutionForbidden () {
+    return this.forbidAutoResolution === true
+  }
+
+  getCurrentVideoFile () {
+    return this.currentVideoFile
+  }
+
+  getTorrent () {
+    return this.torrent
+  }
+
+  private addTorrent (
     magnetOrTorrentUrl: string,
     previousVideoFile: VideoFile,
     options: {
@@ -203,26 +272,15 @@ class PeerTubePlugin extends Plugin {
 
       if (oldTorrent) {
         // Pause the old torrent
-        oldTorrent.pause()
-        // Pause does not remove actual peers (in particular the webseed peer)
-        oldTorrent.removePeer(oldTorrent['ws'])
+        this.stopTorrent(oldTorrent)
 
         // We use a fake renderer so we download correct pieces of the next file
-        if (options.delay) {
-          const fakeVideoElem = document.createElement('video')
-          renderVideo(torrent.files[0], fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => {
-            this.fakeRenderer = renderer
-
-            if (err) console.error('Cannot render new torrent in fake video element.', err)
-
-            // Load the future file at the correct time
-            fakeVideoElem.currentTime = this.player.currentTime() + (options.delay / 2000)
-          })
-        }
+        if (options.delay) this.renderFileInFakeElement(torrent.files[ 0 ], options.delay)
       }
 
       // Render the video in a few seconds? (on resolution change for example, we wait some seconds of the new video resolution)
       this.addTorrentDelay = setTimeout(() => {
+        // We don't need the fake renderer anymore
         this.destroyFakeRenderer()
 
         const paused = this.player.paused()
@@ -230,16 +288,19 @@ class PeerTubePlugin extends Plugin {
         this.flushVideoFile(previousVideoFile)
 
         const renderVideoOptions = { autoplay: false, controls: true }
-        renderVideo(torrent.files[0], this.playerElement, renderVideoOptions,(err, renderer) => {
+        renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
           this.renderer = renderer
 
-          if (err) return this.fallbackToHttp(done)
+          console.log('value this.playerRefusedP2P', this.playerRefusedP2P)
+          if (err || this.playerRefusedP2P) return this.fallbackToHttp(done)
 
           return this.tryToPlay(err => {
             if (err) return done(err)
 
             if (options.seek) this.seek(options.seek)
             if (options.forcePlay === false && paused === true) this.player.pause()
+
+            return done(err)
           })
         })
       }, options.delay || 0)
@@ -260,71 +321,17 @@ class PeerTubePlugin extends Plugin {
       // Magnet hash is not up to date with the torrent file, add directly the torrent file
       if (err.message.indexOf('incorrect info hash') !== -1) {
         console.error('Incorrect info hash detected, falling back to torrent file.')
-        const options = { forcePlay: true }
-        return this.addTorrent(this.torrent['xs'], previousVideoFile, options, done)
+        const newOptions = { forcePlay: true, seek: options.seek }
+        return this.addTorrent(this.torrent[ 'xs' ], previousVideoFile, newOptions, done)
       }
 
-      return console.warn(err)
-    })
-  }
-
-  updateResolution (resolutionId: number, delay = 0) {
-    // Remember player state
-    const currentTime = this.player.currentTime()
-    const isPaused = this.player.paused()
-
-    // Remove poster to have black background
-    this.playerElement.poster = ''
-
-    // Hide bigPlayButton
-    if (!isPaused) {
-      this.player.bigPlayButton.hide()
-    }
-
-    const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
-    const options = {
-      forcePlay: false,
-      delay,
-      seek: currentTime + (delay / 1000)
-    }
-    this.updateVideoFile(newVideoFile, options)
-  }
-
-  flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
-    if (videoFile !== undefined && this.webtorrent.get(videoFile.magnetUri)) {
-      if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy()
-
-      this.webtorrent.remove(videoFile.magnetUri)
-      console.log('Removed ' + videoFile.magnetUri)
-    }
-  }
-
-  isAutoResolutionOn () {
-    return this.autoResolution
-  }
-
-  enableAutoResolution () {
-    this.autoResolution = true
-    this.trigger('autoResolutionUpdate')
-  }
-
-  disableAutoResolution (forbid = false) {
-    if (forbid === true) this.forbidAutoResolution = true
-
-    this.autoResolution = false
-    this.trigger('autoResolutionUpdate')
-  }
-
-  isAutoResolutionForbidden () {
-    return this.forbidAutoResolution === true
-  }
-
-  getCurrentVideoFile () {
-    return this.currentVideoFile
-  }
+      // Remote instance is down
+      if (err.message.indexOf('from xs param') !== -1) {
+        this.handleError(err)
+      }
 
-  getTorrent () {
-    return this.torrent
+      console.warn(err)
+    })
   }
 
   private tryToPlay (done?: Function) {
@@ -365,18 +372,34 @@ class PeerTubePlugin extends Plugin {
 
     if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed()
 
-    // Filter videos we can play according to our bandwidth
-    const filteredFiles = this.videoFiles.filter(f => {
-      const fileBitrate = (f.size / this.videoDuration)
-      let threshold = fileBitrate
-
-      // If this is for a higher resolution or an initial load: add a margin
-      if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) {
-        threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100)
+    // Limit resolution according to player height
+    const playerHeight = this.playerElement.offsetHeight as number
+
+    // We take the first resolution just above the player height
+    // Example: player height is 530px, we want the 720p file instead of 480p
+    let maxResolution = this.videoFiles[0].resolution.id
+    for (let i = this.videoFiles.length - 1; i >= 0; i--) {
+      const resolutionId = this.videoFiles[i].resolution.id
+      if (resolutionId >= playerHeight) {
+        maxResolution = resolutionId
+        break
       }
+    }
 
-      return averageDownloadSpeed > threshold
-    })
+    // Filter videos we can play according to our screen resolution and bandwidth
+    const filteredFiles = this.videoFiles
+                              .filter(f => f.resolution.id <= maxResolution)
+                              .filter(f => {
+                                const fileBitrate = (f.size / this.videoDuration)
+                                let threshold = fileBitrate
+
+                                // If this is for a higher resolution or an initial load: add a margin
+                                if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) {
+                                  threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100)
+                                }
+
+                                return averageDownloadSpeed > threshold
+                              })
 
     // If the download speed is too bad, return the lowest resolution we have
     if (filteredFiles.length === 0) return videoFileMinByResolution(this.videoFiles)
@@ -403,27 +426,29 @@ class PeerTubePlugin extends Plugin {
 
     this.initSmoothProgressBar()
 
+    this.initCaptions()
+
     this.alterInactivity()
 
     if (this.autoplay === true) {
       this.player.posterImage.hide()
 
-      this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
-    } else {
-      // Don't try on iOS that does not support MediaSource
-      if (this.isIOS()) {
-        this.currentVideoFile = this.pickAverageVideoFile()
-        return this.fallbackToHttp(undefined, false)
-      }
+      return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
+    }
 
-      // Proxy first play
-      const oldPlay = this.player.play.bind(this.player)
-      this.player.play = () => {
-        this.player.addClass('vjs-has-big-play-button-clicked')
-        this.player.play = oldPlay
+    // Don't try on iOS that does not support MediaSource
+    if (this.isIOS()) {
+      this.currentVideoFile = this.pickAverageVideoFile()
+      return this.fallbackToHttp(undefined, false)
+    }
 
-        this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
-      }
+    // Proxy first play
+    const oldPlay = this.player.play.bind(this.player)
+    this.player.play = () => {
+      this.player.addClass('vjs-has-big-play-button-clicked')
+      this.player.play = oldPlay
+
+      this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
     }
   }
 
@@ -509,6 +534,21 @@ class PeerTubePlugin extends Plugin {
     }, 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)
@@ -522,6 +562,15 @@ class PeerTubePlugin extends Plugin {
     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 (done?: Function, play = true) {
     this.disableAutoResolution(true)
 
@@ -580,8 +629,33 @@ class PeerTubePlugin extends Plugin {
     return this.videoFiles[Math.floor(this.videoFiles.length / 2)]
   }
 
+  private stopTorrent (torrent: WebTorrent.Torrent) {
+    torrent.pause()
+    // Pause does not remove actual peers (in particular the webseed peer)
+    torrent.removePeer(torrent[ 'ws' ])
+  }
+
+  private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) {
+    this.destoyingFakeRenderer = false
+
+    const fakeVideoElem = document.createElement('video')
+    renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => {
+      this.fakeRenderer = renderer
+
+      // The renderer returns an error when we destroy it, so skip them
+      if (this.destoyingFakeRenderer === false && err) {
+        console.error('Cannot render new torrent in fake video element.', err)
+      }
+
+      // Load the future file at the correct time (in delay MS - 2 seconds)
+      fakeVideoElem.currentTime = this.player.currentTime() + (delay - 2000)
+    })
+  }
+
   private destroyFakeRenderer () {
     if (this.fakeRenderer) {
+      this.destoyingFakeRenderer = true
+
       if (this.fakeRenderer.destroy) {
         try {
           this.fakeRenderer.destroy()
@@ -593,6 +667,18 @@ 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
+      }, false)
+    }
+  }
+
   // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
   private initSmoothProgressBar () {
     const SeekBar = videojsUntyped.getComponent('SeekBar')