From bf5685f0b7b1f23a1a3a972fc4d66061f31f9510 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 26 Feb 2018 09:55:23 +0100 Subject: [PATCH] Add fallback to HTTP --- .../+video-watch/video-watch.component.ts | 6 +- .../assets/player/peertube-videojs-plugin.ts | 109 +++++++++++++----- client/src/assets/player/video-renderer.ts | 55 ++++++--- client/src/sass/video-js-custom.scss | 23 +++- 4 files changed, 140 insertions(+), 53 deletions(-) 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 d04d50310..c9bfa7ffb 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -272,6 +272,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private handleError (err: any) { const errorMessage: string = typeof err === 'string' ? err : err.message + if (!errorMessage) return + let message = '' if (errorMessage.indexOf('http error') !== -1) { @@ -353,9 +355,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.zone.runOutsideAngular(() => { videojs(this.playerElement, videojsOptions, function () { self.player = this - this.on('customError', (event, data) => { - self.handleError(data.err) - }) + this.on('customError', (event, data) => self.handleError(data.err)) }) }) } else { diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts index 01a630cb6..618d77cbe 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/peertube-videojs-plugin.ts @@ -1,6 +1,5 @@ // Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher -import { VideoService } from '@app/shared/video/video.service' import * as videojs from 'video.js' import * as WebTorrent from 'webtorrent' import { VideoFile } from '../../../../shared/models/videos/video.model' @@ -147,12 +146,12 @@ Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) class WebTorrentButton extends Button { createEl () { const div = document.createElement('div') - const subDiv = document.createElement('div') - div.appendChild(subDiv) + const subDivWebtorrent = document.createElement('div') + div.appendChild(subDivWebtorrent) const downloadIcon = document.createElement('span') downloadIcon.classList.add('icon', 'icon-download') - subDiv.appendChild(downloadIcon) + subDivWebtorrent.appendChild(downloadIcon) const downloadSpeedText = document.createElement('span') downloadSpeedText.classList.add('download-speed-text') @@ -161,11 +160,11 @@ class WebTorrentButton extends Button { const downloadSpeedUnit = document.createElement('span') downloadSpeedText.appendChild(downloadSpeedNumber) downloadSpeedText.appendChild(downloadSpeedUnit) - subDiv.appendChild(downloadSpeedText) + subDivWebtorrent.appendChild(downloadSpeedText) const uploadIcon = document.createElement('span') uploadIcon.classList.add('icon', 'icon-upload') - subDiv.appendChild(uploadIcon) + subDivWebtorrent.appendChild(uploadIcon) const uploadSpeedText = document.createElement('span') uploadSpeedText.classList.add('upload-speed-text') @@ -174,34 +173,56 @@ class WebTorrentButton extends Button { const uploadSpeedUnit = document.createElement('span') uploadSpeedText.appendChild(uploadSpeedNumber) uploadSpeedText.appendChild(uploadSpeedUnit) - subDiv.appendChild(uploadSpeedText) + subDivWebtorrent.appendChild(uploadSpeedText) const peersText = document.createElement('span') - peersText.textContent = ' peers' peersText.classList.add('peers-text') const peersNumber = document.createElement('span') peersNumber.classList.add('peers-number') - subDiv.appendChild(peersNumber) - subDiv.appendChild(peersText) + subDivWebtorrent.appendChild(peersNumber) + subDivWebtorrent.appendChild(peersText) - div.className = 'vjs-webtorrent' + div.className = 'vjs-peertube' // Hide the stats before we get the info - subDiv.className = 'vjs-webtorrent-hidden' + subDivWebtorrent.className = 'vjs-peertube-hidden' + + const subDivHttp = document.createElement('div') + subDivHttp.className = 'vjs-peertube-hidden' + const subDivHttpText = document.createElement('span') + subDivHttpText.classList.add('peers-number') + subDivHttpText.textContent = 'HTTP' + const subDivFallbackText = document.createElement('span') + subDivFallbackText.classList.add('peers-text') + subDivFallbackText.textContent = ' fallback' + + subDivHttp.appendChild(subDivHttpText) + subDivHttp.appendChild(subDivFallbackText) + div.appendChild(subDivHttp) this.player_.peertube().on('torrentInfo', (event, data) => { + // We are in HTTP fallback + if (!data) { + subDivHttp.className = 'vjs-peertube-displayed' + subDivWebtorrent.className = 'vjs-peertube-hidden' + + return + } + const downloadSpeed = bytes(data.downloadSpeed) const uploadSpeed = bytes(data.uploadSpeed) const numPeers = data.numPeers - downloadSpeedNumber.textContent = downloadSpeed[0] - downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] + downloadSpeedNumber.textContent = downloadSpeed[ 0 ] + downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] - uploadSpeedNumber.textContent = uploadSpeed[0] - uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] + uploadSpeedNumber.textContent = uploadSpeed[ 0 ] + uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] peersNumber.textContent = numPeers + peersText.textContent = ' peers' - subDiv.className = 'vjs-webtorrent-displayed' + subDivHttp.className = 'vjs-peertube-hidden' + subDivWebtorrent.className = 'vjs-peertube-displayed' }) return div @@ -225,6 +246,7 @@ class PeerTubePlugin extends Plugin { private videoDuration: number private videoViewInterval private torrentInfoInterval + private savePlayerSrcFunction: Function constructor (player: videojs.Player, options: PeertubePluginOptions) { super(player, options) @@ -237,12 +259,11 @@ class PeerTubePlugin extends Plugin { this.videoViewUrl = options.videoViewUrl this.videoDuration = options.videoDuration + 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 = function () { - return true - } + this.player.src = () => true this.playerElement = options.playerElement @@ -284,6 +305,10 @@ class PeerTubePlugin extends Plugin { return } + // Do not display error to user because we will have multiple fallbacks + this.disableErrorDisplay() + this.player.src = () => true + const previousVideoFile = this.currentVideoFile this.currentVideoFile = videoFile @@ -295,7 +320,7 @@ class PeerTubePlugin extends Plugin { const options = { autoplay: true, controls: true } renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { - if (err) return this.handleError(err) + if (err) return this.fallbackToHttp() this.renderer = renderer if (!this.player.paused()) { @@ -347,7 +372,8 @@ class PeerTubePlugin extends Plugin { flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) { - if (destroyRenderer === true) this.renderer.destroy() + if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy() + webtorrent.remove(videoFile.magnetUri) console.log('Removed ' + videoFile.magnetUri) } @@ -390,13 +416,17 @@ class PeerTubePlugin extends Plugin { private runTorrentInfoScheduler () { this.torrentInfoInterval = setInterval(() => { - if (this.torrent !== undefined) { - this.trigger('torrentInfo', { - downloadSpeed: this.torrent.downloadSpeed, - numPeers: this.torrent.numPeers, - uploadSpeed: this.torrent.uploadSpeed - }) - } + // Not initialized yet + if (this.torrent === undefined) return + + // Http fallback + if (this.torrent === null) return this.trigger('torrentInfo', false) + + return this.trigger('torrentInfo', { + downloadSpeed: this.torrent.downloadSpeed, + numPeers: this.torrent.numPeers, + uploadSpeed: this.torrent.uploadSpeed + }) }, 1000) } @@ -433,8 +463,29 @@ class PeerTubePlugin extends Plugin { return fetch(this.videoViewUrl, { method: 'POST' }) } + private fallbackToHttp () { + this.flushVideoFile(this.currentVideoFile, true) + this.torrent = null + + // Enable error display now this is our last fallback + this.player.one('error', () => this.enableErrorDisplay()) + + const httpUrl = this.currentVideoFile.fileUrl + this.player.src = this.savePlayerSrcFunction + this.player.src(httpUrl) + this.player.play() + } + private handleError (err: Error | string) { return this.player.trigger('customError', { err }) } + + private enableErrorDisplay () { + this.player.addClass('vjs-error-display-enabled') + } + + private disableErrorDisplay () { + this.player.removeClass('vjs-error-display-enabled') + } } videojsUntyped.registerPlugin('peertube', PeerTubePlugin) diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/video-renderer.ts index bda40b11d..174676ffa 100644 --- a/client/src/assets/player/video-renderer.ts +++ b/client/src/assets/player/video-renderer.ts @@ -1,8 +1,8 @@ // 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 { extname } from 'path' import * as videostream from 'videostream' const VIDEOSTREAM_EXTS = [ @@ -27,7 +27,7 @@ function renderVideo ( return renderMedia(file, elem, opts, callback) } -function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) { +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 @@ -41,18 +41,33 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca function useVideostream () { prepareElem() - preparedElem.addEventListener('error', fallbackToMediaSource) + preparedElem.addEventListener('error', function onError () { + preparedElem.removeEventListener('error', onError) + + return fallbackToMediaSource() + }) preparedElem.addEventListener('loadstart', onLoadStart) return videostream(file, preparedElem) } - function useMediaSource () { + function useMediaSource (useVP9 = false) { + const codecs = getCodec(file.name, useVP9) + prepareElem() - preparedElem.addEventListener('error', callback) + preparedElem.addEventListener('error', function onError(err) { + // Try with vp9 before returning an error + if (codecs.indexOf('vp8') !== -1) { + preparedElem.removeEventListener('error', onError) + + return fallbackToMediaSource(true) + } + + return callback(err) + }) preparedElem.addEventListener('loadstart', onLoadStart) const wrapper = new MediaElementWrapper(preparedElem) - const writable = wrapper.createWriteStream(getCodec(file.name)) + const writable = wrapper.createWriteStream(codecs) file.createReadStream().pipe(writable) if (currentTime) preparedElem.currentTime = currentTime @@ -60,10 +75,11 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca return wrapper } - function fallbackToMediaSource () { - preparedElem.removeEventListener('error', fallbackToMediaSource) + function fallbackToMediaSource (useVP9 = false) { + if (useVP9 === true) console.log('Falling back to media source with VP9 enabled.') + else console.log('Falling back to media source..') - useMediaSource() + useMediaSource(useVP9) } function prepareElem () { @@ -96,16 +112,19 @@ function validateFile (file) { } } -function getCodec (name: string) { +function getCodec (name: string, useVP9 = false) { 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="opus, vorbis, vp8"' - }[ext] + if (ext === '.mp4') { + return 'video/mp4; codecs="avc1.640029, mp4a.40.5"' + } + + if (ext === '.webm') { + if (useVP9 === true) return 'video/webm; codecs="vp9, opus"' + + return 'video/webm; codecs="vp8, vorbis"' + } + + return undefined } export { diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss index ee6b9219b..ee8f25949 100644 --- a/client/src/sass/video-js-custom.scss +++ b/client/src/sass/video-js-custom.scss @@ -154,17 +154,17 @@ $control-bar-height: 34px; } } - .vjs-webtorrent { + .vjs-peertube { width: 100%; line-height: $control-bar-height; text-align: right; padding-right: 60px; - .vjs-webtorrent-displayed { + .vjs-peertube-displayed { display: block; } - .vjs-webtorrent-hidden { + .vjs-peertube-hidden { display: none; } @@ -424,3 +424,20 @@ $control-bar-height: 34px; } } +// Error display disabled +.vjs-error:not(.vjs-error-display-enabled) { + .vjs-error-display { + display: none; + } + + .vjs-loading-spinner { + display: block; + } +} + +// Error display enabled +.vjs-error.vjs-error-display-enabled { + .vjs-error-display { + display: block; + } +}