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) {
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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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;
}
}