Client: handle multiple file resolutions
authorChocobozzz <florian.bigard@gmail.com>
Fri, 6 Oct 2017 08:40:09 +0000 (10:40 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 6 Oct 2017 09:03:09 +0000 (11:03 +0200)
20 files changed:
client/config/webpack.video-embed.js
client/src/app/videos/shared/video.model.ts
client/src/app/videos/video-watch/index.ts
client/src/app/videos/video-watch/video-magnet.component.html
client/src/app/videos/video-watch/video-watch.component.ts
client/src/app/videos/video-watch/webtorrent.service.ts [deleted file]
client/src/app/videos/videos.module.ts
client/src/assets/player/peertube-videojs-plugin.ts [new file with mode: 0644]
client/src/assets/player/video-renderer.ts [new file with mode: 0644]
client/src/sass/video-js-custom.scss
client/src/standalone/videos/embed.scss
client/src/standalone/videos/embed.ts
config/test-1.yaml
config/test-3.yaml
config/test-4.yaml
config/test-5.yaml
config/test-6.yaml
config/test.yaml
server/models/video/video.ts
server/tests/api/multiple-pods.ts

index a04d5be8b57240997f1eb104420ae4bfca3e1541..fe40194cf3a010fd5c9b35262e8865ecc1f2f27e 100644 (file)
@@ -8,7 +8,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin')
 const PurifyCSSPlugin = require('purifycss-webpack')
 
 module.exports = function (options) {
-  const isProd = options.env === 'production'
+  const isProd = options && options.env === 'production'
 
   const configuration = {
     entry: {
index 17f41059d0be37778002a07d487d7971d708769f..b315e59b184451990927505140fe070d7e14e91e 100644 (file)
@@ -1,5 +1,6 @@
 import { Video as VideoServerModel, VideoFile } from '../../../../../shared'
 import { User } from '../../shared'
+import { VideoResolution } from '../../../../../shared/models/videos/video-resolution.enum'
 
 export class Video implements VideoServerModel {
   author: string
@@ -116,11 +117,19 @@ export class Video implements VideoServerModel {
     return (this.nsfw && (!user || user.displayNSFW === false))
   }
 
-  getDefaultMagnetUri () {
+  getAppropriateMagnetUri (actualDownloadSpeed = 0) {
     if (this.files === undefined || this.files.length === 0) return ''
+    if (this.files.length === 1) return this.files[0].magnetUri
 
-    // TODO: choose the original file
-    return this.files[0].magnetUri
+    // Find first video that is good for our download speed (remember they are sorted)
+    let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
+
+    // If the download speed is too bad, return the lowest resolution we have
+    if (betterResolutionFile === undefined) {
+      betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P)
+    }
+
+    return betterResolutionFile.magnetUri
   }
 
   patch (values: Object) {
index 6e35262d309a91b9196ca94853fe63d32526b185..1058724690177d1bfd91545f95a497913d91b520 100644 (file)
@@ -2,4 +2,3 @@ export * from './video-magnet.component'
 export * from './video-share.component'
 export * from './video-report.component'
 export * from './video-watch.component'
-export * from './webtorrent.service'
index 5b0324e37926d1aa653b3c7a6a0f93f241c4df2a..484280c454516f1dedae6bdc41c7418bac24ceee 100644 (file)
       </div>
 
       <div class="modal-body">
-        <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.getDefaultMagnetUri()" />
+        <div *ngFor="let file of video.files">
+          <label>{{ file.resolutionLabel }}</label>
+          <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
+        </div>
       </div>
     </div>
   </div>
index f5a47199d197030ab0a72390ae8be9453311a481..dbe391fff54e3497beb633fa402fd8a5cd18fe8f 100644 (file)
@@ -4,6 +4,8 @@ import { Observable } from 'rxjs/Observable'
 import { Subscription } from 'rxjs/Subscription'
 
 import videojs from 'video.js'
+import '../../../assets/player/peertube-videojs-plugin'
+
 import { MetaService } from '@ngx-meta/core'
 import { NotificationsService } from 'angular2-notifications'
 
@@ -13,7 +15,7 @@ import { VideoShareComponent } from './video-share.component'
 import { VideoReportComponent } from './video-report.component'
 import { Video, VideoService } from '../shared'
 import { WebTorrentService } from './webtorrent.service'
-import { UserVideoRateType, VideoRateType, UserVideoRate } from '../../../../../shared'
+import { UserVideoRateType, VideoRateType } from '../../../../../shared'
 
 @Component({
   selector: 'my-video-watch',
@@ -21,8 +23,6 @@ import { UserVideoRateType, VideoRateType, UserVideoRate } from '../../../../../
   styleUrls: [ './video-watch.component.scss' ]
 })
 export class VideoWatchComponent implements OnInit, OnDestroy {
-  private static LOADTIME_TOO_LONG = 20000
-
   @ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent
   @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
   @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
@@ -38,20 +38,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   video: Video = null
   videoNotFound = false
 
-  private errorTimer: number
   private paramsSub: Subscription
-  private errorsSub: Subscription
-  private torrentInfosInterval: number
 
   constructor (
     private elementRef: ElementRef,
-    private ngZone: NgZone,
     private route: ActivatedRoute,
     private router: Router,
     private videoService: VideoService,
     private confirmService: ConfirmService,
     private metaService: MetaService,
-    private webTorrentService: WebTorrentService,
     private authService: AuthService,
     private notificationsService: NotificationsService
   ) {}
@@ -68,81 +63,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
         }
       )
     })
-
-    this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
-
-    const videojsOptions = {
-      controls: true,
-      autoplay: true
-    }
-
-    const self = this
-    videojs(this.playerElement, videojsOptions, function () {
-      self.player = this
-    })
-
-    this.errorsSub = this.webTorrentService.errors.subscribe(err => this.handleError(err))
   }
 
   ngOnDestroy () {
     // Remove WebTorrent stuff
     console.log('Removing video from webtorrent.')
-    window.clearInterval(this.torrentInfosInterval)
-    window.clearTimeout(this.errorTimer)
-
-    if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) {
-      this.webTorrentService.remove(this.video.getDefaultMagnetUri())
-    }
 
     // Remove player
     videojs(this.playerElement).dispose()
 
     // Unsubscribe subscriptions
     this.paramsSub.unsubscribe()
-    this.errorsSub.unsubscribe()
-  }
-
-  loadVideo () {
-    // Reset the error
-    this.error = false
-    // We are loading the video
-    this.loading = true
-
-    console.log('Adding ' + this.video.getDefaultMagnetUri() + '.')
-
-    // The callback might never return if there are network issues
-    // So we create a timer to inform the user the load is abnormally long
-    this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG)
-
-    const torrent = this.webTorrentService.add(this.video.getDefaultMagnetUri(), torrent => {
-      // Clear the error timer
-      window.clearTimeout(this.errorTimer)
-      // Maybe the error was fired by the timer, so reset it
-      this.error = false
-
-      // We are not loading the video anymore
-      this.loading = false
-
-      console.log('Added ' + this.video.getDefaultMagnetUri() + '.')
-      torrent.files[0].renderTo(this.playerElement, (err) => {
-        if (err) {
-          this.notificationsService.error('Error', 'Cannot append the file in the video element.')
-          console.error(err)
-        }
-
-        // Hack to "simulate" src link in video.js >= 6
-        // If no, we can't play the video after pausing it
-        // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
-        (this.player as any).src = () => true
-
-        this.player.play()
-      })
-
-      this.runInProgress(torrent)
-    })
-
-    torrent.on('error', err => this.handleError(err))
-    torrent.on('warning', err => this.handleError(err))
   }
 
   setLike () {
@@ -295,8 +226,36 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           return this.router.navigate([ '/videos/list' ])
         }
 
+        this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
+
+        const videojsOptions = {
+          controls: true,
+          autoplay: true,
+          plugins: {
+            peertube: {
+              videoFiles: this.video.files,
+              playerElement: this.playerElement,
+              autoplay: true,
+              peerTubeLink: false
+            }
+          }
+        }
+
+        const self = this
+        videojs(this.playerElement, videojsOptions, function () {
+          self.player = this
+          this.on('customError', (event, data) => {
+            self.handleError(data.err)
+          })
+
+          this.on('torrentInfo', (event, data) => {
+            self.downloadSpeed = data.downloadSpeed
+            self.numPeers = data.numPeers
+            self.uploadSpeed = data.uploadSpeed
+          })
+        })
+
         this.setOpenGraphTags()
-        this.loadVideo()
         this.checkUserRating()
       }
     )
@@ -318,11 +277,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.video.dislikes += dislikesToIncrement
   }
 
-  private loadTooLong () {
-    this.error = true
-    console.error('The video load seems to be abnormally long.')
-  }
-
   private setOpenGraphTags () {
     this.metaService.setTitle(this.video.name)
 
@@ -343,15 +297,4 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.metaService.setTag('og:url', window.location.href)
     this.metaService.setTag('url', window.location.href)
   }
-
-  private runInProgress (torrent: any) {
-    // Refresh each second
-    this.torrentInfosInterval = window.setInterval(() => {
-      this.ngZone.run(() => {
-        this.downloadSpeed = torrent.downloadSpeed
-        this.numPeers = torrent.numPeers
-        this.uploadSpeed = torrent.uploadSpeed
-      })
-    }, 1000)
-  }
 }
diff --git a/client/src/app/videos/video-watch/webtorrent.service.ts b/client/src/app/videos/video-watch/webtorrent.service.ts
deleted file mode 100644 (file)
index 8819e17..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Injectable } from '@angular/core'
-import { Subject } from 'rxjs/Subject'
-
-import * as WebTorrent from 'webtorrent'
-
-@Injectable()
-export class WebTorrentService {
-  errors = new Subject<string | Error>()
-
-  private client: WebTorrent.Instance
-
-  constructor () {
-    this.client = new WebTorrent({ dht: false })
-
-    this.client.on('error', err => this.errors.next(err))
-  }
-
-  add (magnetUri: string, callback: (torrent: WebTorrent.Torrent) => any) {
-    return this.client.add(magnetUri, callback)
-  }
-
-  remove (magnetUri: string) {
-    return this.client.remove(magnetUri)
-  }
-
-  has (magnetUri: string) {
-    return this.client.get(magnetUri) !== null
-  }
-}
index 7d2451de720f57107f3b13feb15d37e41fcdc09b..bc86118cc56ce9d5bbe58f1e92a6b1f30238b504 100644 (file)
@@ -10,8 +10,7 @@ import {
   VideoWatchComponent,
   VideoMagnetComponent,
   VideoReportComponent,
-  VideoShareComponent,
-  WebTorrentService
+  VideoShareComponent
 } from './video-watch'
 import { VideoService } from './shared'
 import { SharedModule } from '../shared'
@@ -47,8 +46,7 @@ import { SharedModule } from '../shared'
   ],
 
   providers: [
-    VideoService,
-    WebTorrentService
+    VideoService
   ]
 })
 export class VideosModule { }
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
new file mode 100644 (file)
index 0000000..090cc53
--- /dev/null
@@ -0,0 +1,238 @@
+// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher
+
+import videojs, { Player } from 'video.js'
+import * as WebTorrent from 'webtorrent'
+
+import { renderVideo } from './video-renderer'
+import { VideoFile } from '../../../../shared'
+
+// videojs typings don't have some method we need
+const videojsUntyped = videojs as any
+const webtorrent = new WebTorrent({ dht: false })
+
+const MenuItem = videojsUntyped.getComponent('MenuItem')
+const ResolutionMenuItem = videojsUntyped.extend(MenuItem, {
+  constructor: function (player: Player, options) {
+    options.selectable = true
+    MenuItem.call(this, player, options)
+
+    const currentResolution = this.player_.getCurrentResolution()
+    this.selected(this.options_.id === currentResolution)
+  },
+
+  handleClick: function (event) {
+    MenuItem.prototype.handleClick.call(this, event)
+    this.player_.updateResolution(this.options_.id)
+  }
+})
+MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
+
+const MenuButton = videojsUntyped.getComponent('MenuButton')
+const ResolutionMenuButton = videojsUntyped.extend(MenuButton, {
+  constructor: function (player, options) {
+    this.label = document.createElement('span')
+    options.label = 'Quality'
+
+    MenuButton.call(this, player, options)
+    this.el().setAttribute('aria-label', 'Quality')
+    this.controlText('Quality')
+
+    videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label')
+    this.el().appendChild(this.label)
+
+    player.on('videoFileUpdate', videojs.bind(this, this.update))
+  },
+
+  createItems: function () {
+    const menuItems = []
+    for (const videoFile of this.player_.videoFiles) {
+      menuItems.push(new ResolutionMenuItem(
+        this.player_,
+        {
+          id: videoFile.resolution,
+          label: videoFile.resolutionLabel,
+          src: videoFile.magnetUri,
+          selected: videoFile.resolution === this.currentSelection
+        })
+      )
+    }
+
+    return menuItems
+  },
+
+  update: function () {
+    this.label.innerHTML = this.player_.getCurrentResolutionLabel()
+    return MenuButton.prototype.update.call(this)
+  },
+
+  buildCSSClass: function () {
+    return MenuButton.prototype.buildCSSClass.call(this) + ' vjs-resolution-button'
+  }
+})
+MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
+
+const Button = videojsUntyped.getComponent('Button')
+const PeertubeLinkButton = videojsUntyped.extend(Button, {
+  constructor: function (player) {
+    Button.apply(this, arguments)
+    this.player = player
+  },
+
+  createEl: function () {
+    const link = document.createElement('a')
+    link.href = window.location.href.replace('embed', 'watch')
+    link.innerHTML = 'PeerTube'
+    link.title = 'Go to the video page'
+    link.className = 'vjs-peertube-link'
+    link.target = '_blank'
+
+    return link
+  },
+
+  handleClick: function () {
+    this.player.pause()
+  }
+})
+Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
+
+type PeertubePluginOptions = {
+  videoFiles: VideoFile[]
+  playerElement: HTMLVideoElement
+  autoplay: boolean
+  peerTubeLink: boolean
+}
+const peertubePlugin = function (options: PeertubePluginOptions) {
+  const player = this
+  let currentVideoFile: VideoFile = undefined
+  const playerElement = options.playerElement
+  player.videoFiles = options.videoFiles
+
+  // 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
+  player.src = function () {
+    return true
+  }
+
+  player.getCurrentResolution = function () {
+    return currentVideoFile ? currentVideoFile.resolution : -1
+  }
+
+  player.getCurrentResolutionLabel = function () {
+    return currentVideoFile ? currentVideoFile.resolutionLabel : ''
+  }
+
+  player.updateVideoFile = function (videoFile: VideoFile, done: () => void) {
+    if (done === undefined) {
+      done = () => { /* empty */ }
+    }
+
+    // Pick the first one
+    if (videoFile === undefined) {
+      videoFile = player.videoFiles[0]
+    }
+
+    // Don't add the same video file once again
+    if (currentVideoFile !== undefined && currentVideoFile.magnetUri === videoFile.magnetUri) {
+      return
+    }
+
+    const previousVideoFile = currentVideoFile
+    currentVideoFile = videoFile
+
+    console.log('Adding ' + videoFile.magnetUri + '.')
+    player.torrent = webtorrent.add(videoFile.magnetUri, torrent => {
+      console.log('Added ' + videoFile.magnetUri + '.')
+
+      this.flushVideoFile(previousVideoFile)
+
+      const options = { autoplay: true, controls: true }
+      renderVideo(torrent.files[0], playerElement, options,(err, renderer) => {
+        if (err) return handleError(err)
+
+        this.renderer = renderer
+        player.play()
+
+        return done()
+      })
+    })
+
+    player.torrent.on('error', err => handleError(err))
+    player.torrent.on('warning', err => handleError(err))
+
+    player.trigger('videoFileUpdate')
+
+    return player
+  }
+
+  player.updateResolution = function (resolution) {
+    // Remember player state
+    const currentTime = player.currentTime()
+    const isPaused = player.paused()
+
+    // Hide bigPlayButton
+    if (!isPaused && this.player_.options_.bigPlayButton) {
+      this.player_.bigPlayButton.hide()
+    }
+
+    const newVideoFile = player.videoFiles.find(f => f.resolution === resolution)
+    player.updateVideoFile(newVideoFile, () => {
+      player.currentTime(currentTime)
+      player.handleTechSeeked_()
+    })
+  }
+
+  player.flushVideoFile = function (videoFile: VideoFile, destroyRenderer = true) {
+    if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) {
+      if (destroyRenderer === true) this.renderer.destroy()
+      webtorrent.remove(videoFile.magnetUri)
+    }
+  }
+
+  player.ready(function () {
+    const controlBar = player.controlBar
+
+    const menuButton = new ResolutionMenuButton(player, options)
+    const fullscreenElement = controlBar.fullscreenToggle.el()
+    controlBar.resolutionSwitcher = controlBar.el().insertBefore(menuButton.el(), fullscreenElement)
+    controlBar.resolutionSwitcher.dispose = function () {
+      this.parentNode.removeChild(this)
+    }
+
+    player.dispose = function () {
+      // Don't need to destroy renderer, video player will be destroyed
+      player.flushVideoFile(currentVideoFile, false)
+    }
+
+    if (options.peerTubeLink === true) {
+      const peerTubeLinkButton = new PeertubeLinkButton(player)
+      controlBar.peerTubeLink = controlBar.el().insertBefore(peerTubeLinkButton.el(), fullscreenElement)
+
+      controlBar.peerTubeLink.dispose = function () {
+        this.parentNode.removeChild(this)
+      }
+    }
+
+    if (options.autoplay === true) {
+      player.updateVideoFile()
+    } else {
+      player.one('play', () => player.updateVideoFile())
+    }
+
+    setInterval(() => {
+      if (player.torrent !== undefined) {
+        player.trigger('torrentInfo', {
+          downloadSpeed: player.torrent.downloadSpeed,
+          numPeers: player.torrent.numPeers,
+          uploadSpeed: player.torrent.uploadSpeed
+        })
+      }
+    }, 1000)
+  })
+
+  function handleError (err: Error|string) {
+    return player.trigger('customError', { err })
+  }
+}
+
+videojsUntyped.registerPlugin('peertube', peertubePlugin)
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/video-renderer.ts
new file mode 100644 (file)
index 0000000..8baa425
--- /dev/null
@@ -0,0 +1,119 @@
+// Thanks: https://github.com/feross/render-media
+// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed
+
+import { extname } from 'path'
+import * as MediaElementWrapper from 'mediasource'
+import * as videostream from 'videostream'
+
+const VIDEOSTREAM_EXTS = [
+  '.m4a',
+  '.m4v',
+  '.mp4'
+]
+
+type RenderMediaOptions = {
+  controls: boolean
+  autoplay: boolean
+}
+
+function renderVideo (
+  file,
+  elem: HTMLVideoElement,
+  opts: RenderMediaOptions,
+  callback: (err: Error, renderer: any) => void
+) {
+  validateFile(file)
+
+  return renderMedia(file, elem, opts, callback)
+}
+
+function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) {
+  const extension = extname(file.name).toLowerCase()
+  let preparedElem = undefined
+  let currentTime = 0
+  let renderer
+
+  if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
+    renderer = useVideostream()
+  } else {
+    renderer = useMediaSource()
+  }
+
+  function useVideostream () {
+    prepareElem()
+    preparedElem.addEventListener('error', fallbackToMediaSource)
+    preparedElem.addEventListener('loadstart', onLoadStart)
+    preparedElem.addEventListener('canplay', onCanPlay)
+    return videostream(file, preparedElem)
+  }
+
+  function useMediaSource () {
+    prepareElem()
+    preparedElem.addEventListener('error', callback)
+    preparedElem.addEventListener('loadstart', onLoadStart)
+    preparedElem.addEventListener('canplay', onCanPlay)
+
+    const wrapper = new MediaElementWrapper(preparedElem)
+    const writable = wrapper.createWriteStream(getCodec(file.name))
+    file.createReadStream().pipe(writable)
+
+    if (currentTime) preparedElem.currentTime = currentTime
+
+    return wrapper
+  }
+
+  function fallbackToMediaSource () {
+    preparedElem.removeEventListener('error', fallbackToMediaSource)
+    preparedElem.removeEventListener('canplay', onCanPlay)
+
+    useMediaSource()
+  }
+
+  function prepareElem () {
+    if (preparedElem === undefined) {
+      preparedElem = elem
+
+      preparedElem.addEventListener('progress', function () {
+        currentTime = elem.currentTime
+      })
+    }
+  }
+
+  function onLoadStart () {
+    preparedElem.removeEventListener('loadstart', onLoadStart)
+    if (opts.autoplay) preparedElem.play()
+  }
+
+  function onCanPlay () {
+    preparedElem.removeEventListener('canplay', onCanPlay)
+    callback(null, renderer)
+  }
+}
+
+function validateFile (file) {
+  if (file == null) {
+    throw new Error('file cannot be null or undefined')
+  }
+  if (typeof file.name !== 'string') {
+    throw new Error('missing or invalid file.name property')
+  }
+  if (typeof file.createReadStream !== 'function') {
+    throw new Error('missing or invalid file.createReadStream property')
+  }
+}
+
+function getCodec (name: string) {
+  const ext = extname(name).toLowerCase()
+  return {
+    '.m4a': 'audio/mp4; codecs="mp4a.40.5"',
+    '.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
+    '.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"',
+    '.mp3': 'audio/mpeg',
+    '.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
+    '.webm': 'video/webm; codecs="vorbis, vp8"'
+  }[ext]
+}
+
+export {
+  renderVideo
+}
index eb5b8f8693ddd7e1da16b7bacc3cc787d30ebcdb..4e3aceaab97c56201ec22e7022fa748477279682 100644 (file)
@@ -1,3 +1,33 @@
+// Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files
+.vjs-resolution-button-label {
+  font-size: 1em;
+  line-height: 3em;
+  position: absolute;
+  top: 0;
+  left: -1px;
+  width: 100%;
+  height: 100%;
+  text-align: center;
+  box-sizing: inherit;
+}
+
+.vjs-resolution-button {
+  outline: 0 !important;
+
+  .vjs-menu {
+    .vjs-menu-content {
+      width: 4em;
+      left: 50%; /* Center the menu, in it's parent */
+      margin-left: -2em; /* half of width, to center */
+    }
+
+    li {
+      text-transform: none;
+      font-size: 1em;
+    }
+  }
+}
+
 // Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin
 
 // Video JS Sublime Skin
@@ -210,7 +240,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
     width: 6em;
     position: absolute;
     right: 0;
-    margin-right: 30px;
+    margin-right: 65px;
 }
 
 .vjs-sublime-skin .vjs-volume-menu-button .vjs-menu-content,
index 938a6e48ceafe87dd2b1fffa728e6745499289fc..b76f096775cead75fc01c3e9da3b8d97bc602bf0 100644 (file)
@@ -29,7 +29,11 @@ html, body {
   line-height: 2.20;
   transition: all .4s;
   position: relative;
-  right: 6px;
+  right: 8px;
+}
+
+.vjs-resolution-button-label {
+  left: -7px;
 }
 
 .vjs-peertube-link:hover {
@@ -38,5 +42,5 @@ html, body {
 
 // Fix volume panel because we added a new component (PeerTube link)
 .vjs-volume-panel {
-  margin-right: 90px !important;
+  margin-right: 130px !important;
 }
index 0698344b014023afdf341032efcb5c1e3e2b6403..f2f339bcca0899f7de1efd53b7d0c917f6ff752e 100644 (file)
@@ -1,14 +1,11 @@
 import './embed.scss'
 
 import videojs from 'video.js'
+import '../../assets/player/peertube-videojs-plugin'
 import 'videojs-dock/dist/videojs-dock.es.js'
-import * as WebTorrent from 'webtorrent'
 import { Video } from '../../../../shared'
 
-// videojs typings don't have some method we need
-const videojsUntyped = videojs as any
-
-function loadVideoInfos (videoId: string, callback: (err: Error, res?: Video) => void) {
+function loadVideoInfo (videoId: string, callback: (err: Error, res?: Video) => void) {
   const xhttp = new XMLHttpRequest()
   xhttp.onreadystatechange = function () {
     if (this.readyState === 4 && this.status === 200) {
@@ -24,84 +21,36 @@ function loadVideoInfos (videoId: string, callback: (err: Error, res?: Video) =>
   xhttp.send()
 }
 
-function loadVideoTorrent (magnetUri: string, player: videojs.Player) {
-  console.log('Loading video ' + videoId)
-  const client = new WebTorrent()
-
-  console.log('Adding magnet ' + magnetUri)
-  client.add(magnetUri, torrent => {
-    const file = torrent.files[0]
-
-    file.renderTo('video', err => {
-      if (err) {
-        console.error(err)
-        return
-      }
-
-      // Hack to "simulate" src link in video.js >= 6
-      // If no, we can't play the video after pausing it
-      // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
-      (player as any).src = () => true
-
-      player.play()
-    })
-  })
-}
-
 const urlParts = window.location.href.split('/')
 const videoId = urlParts[urlParts.length - 1]
 
-loadVideoInfos(videoId, (err, videoInfos) => {
+loadVideoInfo(videoId, (err, videoInfo) => {
   if (err) {
     console.error(err)
     return
   }
 
-  let magnetUri = ''
-  if (videoInfos.files !== undefined && videoInfos.files.length !== 0) {
-    magnetUri = videoInfos.files[0].magnetUri
+  const videoElement = document.getElementById('video-container') as HTMLVideoElement
+  const previewUrl = window.location.origin + videoInfo.previewPath
+  videoElement.poster = previewUrl
+
+  const videojsOptions = {
+    controls: true,
+    autoplay: false,
+    plugins: {
+      peertube: {
+        videoFiles: videoInfo.files,
+        playerElement: videoElement,
+        autoplay: false,
+        peerTubeLink: true
+      }
+    }
   }
-
-  const videoContainer = document.getElementById('video-container') as HTMLVideoElement
-  const previewUrl = window.location.origin + videoInfos.previewPath
-  videoContainer.poster = previewUrl
-
-  videojs('video-container', { controls: true, autoplay: false }, function () {
+  videojs('video-container', videojsOptions, function () {
     const player = this
 
-    const Button = videojsUntyped.getComponent('Button')
-    const peertubeLinkButton = videojsUntyped.extend(Button, {
-      constructor: function () {
-        Button.apply(this, arguments)
-      },
-
-      createEl: function () {
-        const link = document.createElement('a')
-        link.href = window.location.href.replace('embed', 'watch')
-        link.innerHTML = 'PeerTube'
-        link.title = 'Go to the video page'
-        link.className = 'vjs-peertube-link'
-        link.target = '_blank'
-
-        return link
-      },
-
-      handleClick: function () {
-        player.pause()
-      }
-    })
-    videojsUntyped.registerComponent('PeerTubeLinkButton', peertubeLinkButton)
-
-    const controlBar = player.getChild('controlBar')
-    const addedLink = controlBar.addChild('PeerTubeLinkButton', {})
-    controlBar.el().insertBefore(addedLink.el(), controlBar.fullscreenToggle.el())
-
     player.dock({
-      title: videoInfos.name
+      title: videoInfo.name
     })
-
-    document.querySelector('.vjs-big-play-button').addEventListener('click', () => {
-      loadVideoTorrent(magnetUri, player)
-    }, false)
   })
 })
index 4e9f294353466b37d5356e14cfcb091736810b83..d9b4d2b1a46a900d645fa1f112c823c37a4f90b2 100644 (file)
@@ -26,3 +26,6 @@ user:
 
 signup:
   limit: 4
+
+transcoding:
+  enabled: false
index a29225a44bcf4986356e4f7e1e924bdae8cc659a..291b43edc7926450845478082c1dd1e543df2312 100644 (file)
@@ -20,3 +20,6 @@ storage:
 
 admin:
   email: 'admin3@example.com'
+
+transcoding:
+  enabled: false
index da93e128de3b2a6eae224c8516f9cebeed579006..6f80939fc1782c9667aae5b724bea0a9915643e9 100644 (file)
@@ -20,3 +20,6 @@ storage:
 
 admin:
   email: 'admin4@example.com'
+
+transcoding:
+  enabled: false
index f95e25eb8d3457e06f37da9b886d2678987342b7..0b5eab72e6b5b30a08d9af967f8d50e61b0267bb 100644 (file)
@@ -20,3 +20,6 @@ storage:
 
 admin:
   email: 'admin5@example.com'
+
+transcoding:
+  enabled: false
index 87d0544392f277bc43d3fe90214f3a140ab0fd25..5d33e45b9ae566521159ced8a396fdca9031e63b 100644 (file)
@@ -20,3 +20,6 @@ storage:
 
 admin:
   email: 'admin6@example.com'
+
+transcoding:
+  enabled: false
index 1a08d5ed1f011348deecd9b1423d4cd1b629a150..feecb7883e6226652e56dfdc278704ddf97dc653 100644 (file)
@@ -10,3 +10,7 @@ database:
 
 signup:
   enabled: true
+
+transcoding:
+  enabled: true
+  threads: 4
index b4a2b0c95f86b1d374191a2c4b3881f280e43c4c..c376d769ed989ca7bdb56d77fafe672afd661085 100644 (file)
@@ -477,19 +477,26 @@ toFormattedJSON = function (this: VideoInstance) {
     files: []
   }
 
-  this.VideoFiles.forEach(videoFile => {
-    let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
-    if (!resolutionLabel) resolutionLabel = 'Unknown'
-
-    const videoFileJson = {
-      resolution: videoFile.resolution,
-      resolutionLabel,
-      magnetUri: this.generateMagnetUri(videoFile),
-      size: videoFile.size
-    }
-
-    json.files.push(videoFileJson)
-  })
+  // Format and sort video files
+  json.files = this.VideoFiles
+                   .map(videoFile => {
+                     let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
+                     if (!resolutionLabel) resolutionLabel = 'Unknown'
+
+                     const videoFileJson = {
+                       resolution: videoFile.resolution,
+                       resolutionLabel,
+                       magnetUri: this.generateMagnetUri(videoFile),
+                       size: videoFile.size
+                     }
+
+                     return videoFileJson
+                   })
+                   .sort((a, b) => {
+                     if (a.resolution < b.resolution) return 1
+                     if (a.resolution === b.resolution) return 0
+                     return -1
+                   })
 
   return json
 }
index c43793b303511c28e1830e79cecb1f3ef466cb36..08fa73aa2df5387729586bea2a277c60b7d3644d 100644 (file)
@@ -195,27 +195,27 @@ describe('Test multiple pods', function () {
         const originalFile = video.files.find(f => f.resolution === 0)
         expect(originalFile).not.to.be.undefined
         expect(originalFile.resolutionLabel).to.equal('original')
-        expect(originalFile.size).to.equal(711327)
+        expect(originalFile.size).to.be.above(700000).and.below(720000)
 
         const file240p = video.files.find(f => f.resolution === 240)
         expect(file240p).not.to.be.undefined
         expect(file240p.resolutionLabel).to.equal('240p')
-        expect(file240p.size).to.equal(139953)
+        expect(file240p.size).to.be.above(130000).and.below(150000)
 
         const file360p = video.files.find(f => f.resolution === 360)
         expect(file360p).not.to.be.undefined
         expect(file360p.resolutionLabel).to.equal('360p')
-        expect(file360p.size).to.equal(169926)
+        expect(file360p.size).to.be.above(160000).and.below(180000)
 
         const file480p = video.files.find(f => f.resolution === 480)
         expect(file480p).not.to.be.undefined
         expect(file480p.resolutionLabel).to.equal('480p')
-        expect(file480p.size).to.equal(206758)
+        expect(file480p.size).to.be.above(200000).and.below(220000)
 
         const file720p = video.files.find(f => f.resolution === 720)
         expect(file720p).not.to.be.undefined
         expect(file720p.resolutionLabel).to.equal('720p')
-        expect(file720p.size).to.equal(314913)
+        expect(file720p.size).to.be.above(310000).and.below(320000)
 
         const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath)
         expect(test).to.equal(true)