Add fallback to HTTP

pull/318/head
Chocobozzz 2018-02-26 09:55:23 +01:00
parent 245dc51de0
commit bf5685f0b7
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
4 changed files with 140 additions and 53 deletions

View File

@ -272,6 +272,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private handleError (err: any) { private handleError (err: any) {
const errorMessage: string = typeof err === 'string' ? err : err.message const errorMessage: string = typeof err === 'string' ? err : err.message
if (!errorMessage) return
let message = '' let message = ''
if (errorMessage.indexOf('http error') !== -1) { if (errorMessage.indexOf('http error') !== -1) {
@ -353,9 +355,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.zone.runOutsideAngular(() => { this.zone.runOutsideAngular(() => {
videojs(this.playerElement, videojsOptions, function () { videojs(this.playerElement, videojsOptions, function () {
self.player = this self.player = this
this.on('customError', (event, data) => { this.on('customError', (event, data) => self.handleError(data.err))
self.handleError(data.err)
})
}) })
}) })
} else { } else {

View File

@ -1,6 +1,5 @@
// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher // 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 videojs from 'video.js'
import * as WebTorrent from 'webtorrent' import * as WebTorrent from 'webtorrent'
import { VideoFile } from '../../../../shared/models/videos/video.model' import { VideoFile } from '../../../../shared/models/videos/video.model'
@ -147,12 +146,12 @@ Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
class WebTorrentButton extends Button { class WebTorrentButton extends Button {
createEl () { createEl () {
const div = document.createElement('div') const div = document.createElement('div')
const subDiv = document.createElement('div') const subDivWebtorrent = document.createElement('div')
div.appendChild(subDiv) div.appendChild(subDivWebtorrent)
const downloadIcon = document.createElement('span') const downloadIcon = document.createElement('span')
downloadIcon.classList.add('icon', 'icon-download') downloadIcon.classList.add('icon', 'icon-download')
subDiv.appendChild(downloadIcon) subDivWebtorrent.appendChild(downloadIcon)
const downloadSpeedText = document.createElement('span') const downloadSpeedText = document.createElement('span')
downloadSpeedText.classList.add('download-speed-text') downloadSpeedText.classList.add('download-speed-text')
@ -161,11 +160,11 @@ class WebTorrentButton extends Button {
const downloadSpeedUnit = document.createElement('span') const downloadSpeedUnit = document.createElement('span')
downloadSpeedText.appendChild(downloadSpeedNumber) downloadSpeedText.appendChild(downloadSpeedNumber)
downloadSpeedText.appendChild(downloadSpeedUnit) downloadSpeedText.appendChild(downloadSpeedUnit)
subDiv.appendChild(downloadSpeedText) subDivWebtorrent.appendChild(downloadSpeedText)
const uploadIcon = document.createElement('span') const uploadIcon = document.createElement('span')
uploadIcon.classList.add('icon', 'icon-upload') uploadIcon.classList.add('icon', 'icon-upload')
subDiv.appendChild(uploadIcon) subDivWebtorrent.appendChild(uploadIcon)
const uploadSpeedText = document.createElement('span') const uploadSpeedText = document.createElement('span')
uploadSpeedText.classList.add('upload-speed-text') uploadSpeedText.classList.add('upload-speed-text')
@ -174,34 +173,56 @@ class WebTorrentButton extends Button {
const uploadSpeedUnit = document.createElement('span') const uploadSpeedUnit = document.createElement('span')
uploadSpeedText.appendChild(uploadSpeedNumber) uploadSpeedText.appendChild(uploadSpeedNumber)
uploadSpeedText.appendChild(uploadSpeedUnit) uploadSpeedText.appendChild(uploadSpeedUnit)
subDiv.appendChild(uploadSpeedText) subDivWebtorrent.appendChild(uploadSpeedText)
const peersText = document.createElement('span') const peersText = document.createElement('span')
peersText.textContent = ' peers'
peersText.classList.add('peers-text') peersText.classList.add('peers-text')
const peersNumber = document.createElement('span') const peersNumber = document.createElement('span')
peersNumber.classList.add('peers-number') peersNumber.classList.add('peers-number')
subDiv.appendChild(peersNumber) subDivWebtorrent.appendChild(peersNumber)
subDiv.appendChild(peersText) subDivWebtorrent.appendChild(peersText)
div.className = 'vjs-webtorrent' div.className = 'vjs-peertube'
// Hide the stats before we get the info // 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) => { 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 downloadSpeed = bytes(data.downloadSpeed)
const uploadSpeed = bytes(data.uploadSpeed) const uploadSpeed = bytes(data.uploadSpeed)
const numPeers = data.numPeers const numPeers = data.numPeers
downloadSpeedNumber.textContent = downloadSpeed[0] downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
uploadSpeedNumber.textContent = uploadSpeed[0] uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
peersNumber.textContent = numPeers peersNumber.textContent = numPeers
peersText.textContent = ' peers'
subDiv.className = 'vjs-webtorrent-displayed' subDivHttp.className = 'vjs-peertube-hidden'
subDivWebtorrent.className = 'vjs-peertube-displayed'
}) })
return div return div
@ -225,6 +246,7 @@ class PeerTubePlugin extends Plugin {
private videoDuration: number private videoDuration: number
private videoViewInterval private videoViewInterval
private torrentInfoInterval private torrentInfoInterval
private savePlayerSrcFunction: Function
constructor (player: videojs.Player, options: PeertubePluginOptions) { constructor (player: videojs.Player, options: PeertubePluginOptions) {
super(player, options) super(player, options)
@ -237,12 +259,11 @@ class PeerTubePlugin extends Plugin {
this.videoViewUrl = options.videoViewUrl this.videoViewUrl = options.videoViewUrl
this.videoDuration = options.videoDuration this.videoDuration = options.videoDuration
this.savePlayerSrcFunction = this.player.src
// Hack to "simulate" src link in video.js >= 6 // Hack to "simulate" src link in video.js >= 6
// Without this, we can't play the video after pausing it // Without this, we can't play the video after pausing it
// https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
this.player.src = function () { this.player.src = () => true
return true
}
this.playerElement = options.playerElement this.playerElement = options.playerElement
@ -284,6 +305,10 @@ class PeerTubePlugin extends Plugin {
return return
} }
// Do not display error to user because we will have multiple fallbacks
this.disableErrorDisplay()
this.player.src = () => true
const previousVideoFile = this.currentVideoFile const previousVideoFile = this.currentVideoFile
this.currentVideoFile = videoFile this.currentVideoFile = videoFile
@ -295,7 +320,7 @@ class PeerTubePlugin extends Plugin {
const options = { autoplay: true, controls: true } const options = { autoplay: true, controls: true }
renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => {
if (err) return this.handleError(err) if (err) return this.fallbackToHttp()
this.renderer = renderer this.renderer = renderer
if (!this.player.paused()) { if (!this.player.paused()) {
@ -347,7 +372,8 @@ class PeerTubePlugin extends Plugin {
flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) { 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) webtorrent.remove(videoFile.magnetUri)
console.log('Removed ' + videoFile.magnetUri) console.log('Removed ' + videoFile.magnetUri)
} }
@ -390,13 +416,17 @@ class PeerTubePlugin extends Plugin {
private runTorrentInfoScheduler () { private runTorrentInfoScheduler () {
this.torrentInfoInterval = setInterval(() => { this.torrentInfoInterval = setInterval(() => {
if (this.torrent !== undefined) { // Not initialized yet
this.trigger('torrentInfo', { if (this.torrent === undefined) return
downloadSpeed: this.torrent.downloadSpeed,
numPeers: this.torrent.numPeers, // Http fallback
uploadSpeed: this.torrent.uploadSpeed 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) }, 1000)
} }
@ -433,8 +463,29 @@ class PeerTubePlugin extends Plugin {
return fetch(this.videoViewUrl, { method: 'POST' }) 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) { private handleError (err: Error | string) {
return this.player.trigger('customError', { err }) 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) videojsUntyped.registerPlugin('peertube', PeerTubePlugin)

View File

@ -1,8 +1,8 @@
// Thanks: https://github.com/feross/render-media // Thanks: https://github.com/feross/render-media
// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed // 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 MediaElementWrapper from 'mediasource'
import { extname } from 'path'
import * as videostream from 'videostream' import * as videostream from 'videostream'
const VIDEOSTREAM_EXTS = [ const VIDEOSTREAM_EXTS = [
@ -27,7 +27,7 @@ function renderVideo (
return renderMedia(file, elem, opts, callback) 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() const extension = extname(file.name).toLowerCase()
let preparedElem = undefined let preparedElem = undefined
let currentTime = 0 let currentTime = 0
@ -41,18 +41,33 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
function useVideostream () { function useVideostream () {
prepareElem() prepareElem()
preparedElem.addEventListener('error', fallbackToMediaSource) preparedElem.addEventListener('error', function onError () {
preparedElem.removeEventListener('error', onError)
return fallbackToMediaSource()
})
preparedElem.addEventListener('loadstart', onLoadStart) preparedElem.addEventListener('loadstart', onLoadStart)
return videostream(file, preparedElem) return videostream(file, preparedElem)
} }
function useMediaSource () { function useMediaSource (useVP9 = false) {
const codecs = getCodec(file.name, useVP9)
prepareElem() 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) preparedElem.addEventListener('loadstart', onLoadStart)
const wrapper = new MediaElementWrapper(preparedElem) const wrapper = new MediaElementWrapper(preparedElem)
const writable = wrapper.createWriteStream(getCodec(file.name)) const writable = wrapper.createWriteStream(codecs)
file.createReadStream().pipe(writable) file.createReadStream().pipe(writable)
if (currentTime) preparedElem.currentTime = currentTime if (currentTime) preparedElem.currentTime = currentTime
@ -60,10 +75,11 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
return wrapper return wrapper
} }
function fallbackToMediaSource () { function fallbackToMediaSource (useVP9 = false) {
preparedElem.removeEventListener('error', fallbackToMediaSource) 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 () { function prepareElem () {
@ -96,16 +112,19 @@ function validateFile (file) {
} }
} }
function getCodec (name: string) { function getCodec (name: string, useVP9 = false) {
const ext = extname(name).toLowerCase() const ext = extname(name).toLowerCase()
return { if (ext === '.mp4') {
'.m4a': 'audio/mp4; codecs="mp4a.40.5"', return 'video/mp4; codecs="avc1.640029, mp4a.40.5"'
'.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', }
'.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"',
'.mp3': 'audio/mpeg', if (ext === '.webm') {
'.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', if (useVP9 === true) return 'video/webm; codecs="vp9, opus"'
'.webm': 'video/webm; codecs="opus, vorbis, vp8"'
}[ext] return 'video/webm; codecs="vp8, vorbis"'
}
return undefined
} }
export { export {

View File

@ -154,17 +154,17 @@ $control-bar-height: 34px;
} }
} }
.vjs-webtorrent { .vjs-peertube {
width: 100%; width: 100%;
line-height: $control-bar-height; line-height: $control-bar-height;
text-align: right; text-align: right;
padding-right: 60px; padding-right: 60px;
.vjs-webtorrent-displayed { .vjs-peertube-displayed {
display: block; display: block;
} }
.vjs-webtorrent-hidden { .vjs-peertube-hidden {
display: none; 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;
}
}