From aa8b6df4a51c82eb91e6fd71a090b2128098af6b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 6 Oct 2017 10:40:09 +0200 Subject: [PATCH] Client: handle multiple file resolutions --- client/config/webpack.video-embed.js | 2 +- client/src/app/videos/shared/video.model.ts | 15 +- client/src/app/videos/video-watch/index.ts | 1 - .../video-watch/video-magnet.component.html | 5 +- .../video-watch/video-watch.component.ts | 121 +++------ .../videos/video-watch/webtorrent.service.ts | 29 --- client/src/app/videos/videos.module.ts | 6 +- .../assets/player/peertube-videojs-plugin.ts | 238 ++++++++++++++++++ client/src/assets/player/video-renderer.ts | 119 +++++++++ client/src/sass/video-js-custom.scss | 32 ++- client/src/standalone/videos/embed.scss | 8 +- client/src/standalone/videos/embed.ts | 91 ++----- config/test-1.yaml | 3 + config/test-3.yaml | 3 + config/test-4.yaml | 3 + config/test-5.yaml | 3 + config/test-6.yaml | 3 + config/test.yaml | 4 + server/models/video/video.ts | 33 ++- server/tests/api/multiple-pods.ts | 10 +- 20 files changed, 509 insertions(+), 220 deletions(-) delete mode 100644 client/src/app/videos/video-watch/webtorrent.service.ts create mode 100644 client/src/assets/player/peertube-videojs-plugin.ts create mode 100644 client/src/assets/player/video-renderer.ts diff --git a/client/config/webpack.video-embed.js b/client/config/webpack.video-embed.js index a04d5be8b..fe40194cf 100644 --- a/client/config/webpack.video-embed.js +++ b/client/config/webpack.video-embed.js @@ -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: { diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts index 17f41059d..b315e59b1 100644 --- a/client/src/app/videos/shared/video.model.ts +++ b/client/src/app/videos/shared/video.model.ts @@ -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) { diff --git a/client/src/app/videos/video-watch/index.ts b/client/src/app/videos/video-watch/index.ts index 6e35262d3..105872469 100644 --- a/client/src/app/videos/video-watch/index.ts +++ b/client/src/app/videos/video-watch/index.ts @@ -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' diff --git a/client/src/app/videos/video-watch/video-magnet.component.html b/client/src/app/videos/video-watch/video-magnet.component.html index 5b0324e37..484280c45 100644 --- a/client/src/app/videos/video-watch/video-magnet.component.html +++ b/client/src/app/videos/video-watch/video-magnet.component.html @@ -10,7 +10,10 @@ diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts index f5a47199d..dbe391fff 100644 --- a/client/src/app/videos/video-watch/video-watch.component.ts +++ b/client/src/app/videos/video-watch/video-watch.component.ts @@ -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 index 8819e17d4..000000000 --- a/client/src/app/videos/video-watch/webtorrent.service.ts +++ /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() - - 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 - } -} diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts index 7d2451de7..bc86118cc 100644 --- a/client/src/app/videos/videos.module.ts +++ b/client/src/app/videos/videos.module.ts @@ -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 index 000000000..090cc53ba --- /dev/null +++ b/client/src/assets/player/peertube-videojs-plugin.ts @@ -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 index 000000000..8baa42533 --- /dev/null +++ b/client/src/assets/player/video-renderer.ts @@ -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 +} diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss index eb5b8f869..4e3aceaab 100644 --- a/client/src/sass/video-js-custom.scss +++ b/client/src/sass/video-js-custom.scss @@ -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, diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index 938a6e48c..b76f09677 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss @@ -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; } diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 0698344b0..f2f339bcc 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -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) }) }) diff --git a/config/test-1.yaml b/config/test-1.yaml index 4e9f29435..d9b4d2b1a 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -26,3 +26,6 @@ user: signup: limit: 4 + +transcoding: + enabled: false diff --git a/config/test-3.yaml b/config/test-3.yaml index a29225a44..291b43edc 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -20,3 +20,6 @@ storage: admin: email: 'admin3@example.com' + +transcoding: + enabled: false diff --git a/config/test-4.yaml b/config/test-4.yaml index da93e128d..6f80939fc 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -20,3 +20,6 @@ storage: admin: email: 'admin4@example.com' + +transcoding: + enabled: false diff --git a/config/test-5.yaml b/config/test-5.yaml index f95e25eb8..0b5eab72e 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -20,3 +20,6 @@ storage: admin: email: 'admin5@example.com' + +transcoding: + enabled: false diff --git a/config/test-6.yaml b/config/test-6.yaml index 87d054439..5d33e45b9 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -20,3 +20,6 @@ storage: admin: email: 'admin6@example.com' + +transcoding: + enabled: false diff --git a/config/test.yaml b/config/test.yaml index 1a08d5ed1..feecb7883 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -10,3 +10,7 @@ database: signup: enabled: true + +transcoding: + enabled: true + threads: 4 diff --git a/server/models/video/video.ts b/server/models/video/video.ts index b4a2b0c95..c376d769e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 } diff --git a/server/tests/api/multiple-pods.ts b/server/tests/api/multiple-pods.ts index c43793b30..08fa73aa2 100644 --- a/server/tests/api/multiple-pods.ts +++ b/server/tests/api/multiple-pods.ts @@ -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) -- 2.41.0