aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts6
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts109
-rw-r--r--client/src/assets/player/video-renderer.ts55
-rw-r--r--client/src/sass/video-js-custom.scss23
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 {
272 272
273 private handleError (err: any) { 273 private handleError (err: any) {
274 const errorMessage: string = typeof err === 'string' ? err : err.message 274 const errorMessage: string = typeof err === 'string' ? err : err.message
275 if (!errorMessage) return
276
275 let message = '' 277 let message = ''
276 278
277 if (errorMessage.indexOf('http error') !== -1) { 279 if (errorMessage.indexOf('http error') !== -1) {
@@ -353,9 +355,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
353 this.zone.runOutsideAngular(() => { 355 this.zone.runOutsideAngular(() => {
354 videojs(this.playerElement, videojsOptions, function () { 356 videojs(this.playerElement, videojsOptions, function () {
355 self.player = this 357 self.player = this
356 this.on('customError', (event, data) => { 358 this.on('customError', (event, data) => self.handleError(data.err))
357 self.handleError(data.err)
358 })
359 }) 359 })
360 }) 360 })
361 } else { 361 } 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 @@
1// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher 1// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher
2 2
3import { VideoService } from '@app/shared/video/video.service'
4import * as videojs from 'video.js' 3import * as videojs from 'video.js'
5import * as WebTorrent from 'webtorrent' 4import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
@@ -147,12 +146,12 @@ Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
147class WebTorrentButton extends Button { 146class WebTorrentButton extends Button {
148 createEl () { 147 createEl () {
149 const div = document.createElement('div') 148 const div = document.createElement('div')
150 const subDiv = document.createElement('div') 149 const subDivWebtorrent = document.createElement('div')
151 div.appendChild(subDiv) 150 div.appendChild(subDivWebtorrent)
152 151
153 const downloadIcon = document.createElement('span') 152 const downloadIcon = document.createElement('span')
154 downloadIcon.classList.add('icon', 'icon-download') 153 downloadIcon.classList.add('icon', 'icon-download')
155 subDiv.appendChild(downloadIcon) 154 subDivWebtorrent.appendChild(downloadIcon)
156 155
157 const downloadSpeedText = document.createElement('span') 156 const downloadSpeedText = document.createElement('span')
158 downloadSpeedText.classList.add('download-speed-text') 157 downloadSpeedText.classList.add('download-speed-text')
@@ -161,11 +160,11 @@ class WebTorrentButton extends Button {
161 const downloadSpeedUnit = document.createElement('span') 160 const downloadSpeedUnit = document.createElement('span')
162 downloadSpeedText.appendChild(downloadSpeedNumber) 161 downloadSpeedText.appendChild(downloadSpeedNumber)
163 downloadSpeedText.appendChild(downloadSpeedUnit) 162 downloadSpeedText.appendChild(downloadSpeedUnit)
164 subDiv.appendChild(downloadSpeedText) 163 subDivWebtorrent.appendChild(downloadSpeedText)
165 164
166 const uploadIcon = document.createElement('span') 165 const uploadIcon = document.createElement('span')
167 uploadIcon.classList.add('icon', 'icon-upload') 166 uploadIcon.classList.add('icon', 'icon-upload')
168 subDiv.appendChild(uploadIcon) 167 subDivWebtorrent.appendChild(uploadIcon)
169 168
170 const uploadSpeedText = document.createElement('span') 169 const uploadSpeedText = document.createElement('span')
171 uploadSpeedText.classList.add('upload-speed-text') 170 uploadSpeedText.classList.add('upload-speed-text')
@@ -174,34 +173,56 @@ class WebTorrentButton extends Button {
174 const uploadSpeedUnit = document.createElement('span') 173 const uploadSpeedUnit = document.createElement('span')
175 uploadSpeedText.appendChild(uploadSpeedNumber) 174 uploadSpeedText.appendChild(uploadSpeedNumber)
176 uploadSpeedText.appendChild(uploadSpeedUnit) 175 uploadSpeedText.appendChild(uploadSpeedUnit)
177 subDiv.appendChild(uploadSpeedText) 176 subDivWebtorrent.appendChild(uploadSpeedText)
178 177
179 const peersText = document.createElement('span') 178 const peersText = document.createElement('span')
180 peersText.textContent = ' peers'
181 peersText.classList.add('peers-text') 179 peersText.classList.add('peers-text')
182 const peersNumber = document.createElement('span') 180 const peersNumber = document.createElement('span')
183 peersNumber.classList.add('peers-number') 181 peersNumber.classList.add('peers-number')
184 subDiv.appendChild(peersNumber) 182 subDivWebtorrent.appendChild(peersNumber)
185 subDiv.appendChild(peersText) 183 subDivWebtorrent.appendChild(peersText)
186 184
187 div.className = 'vjs-webtorrent' 185 div.className = 'vjs-peertube'
188 // Hide the stats before we get the info 186 // Hide the stats before we get the info
189 subDiv.className = 'vjs-webtorrent-hidden' 187 subDivWebtorrent.className = 'vjs-peertube-hidden'
188
189 const subDivHttp = document.createElement('div')
190 subDivHttp.className = 'vjs-peertube-hidden'
191 const subDivHttpText = document.createElement('span')
192 subDivHttpText.classList.add('peers-number')
193 subDivHttpText.textContent = 'HTTP'
194 const subDivFallbackText = document.createElement('span')
195 subDivFallbackText.classList.add('peers-text')
196 subDivFallbackText.textContent = ' fallback'
197
198 subDivHttp.appendChild(subDivHttpText)
199 subDivHttp.appendChild(subDivFallbackText)
200 div.appendChild(subDivHttp)
190 201
191 this.player_.peertube().on('torrentInfo', (event, data) => { 202 this.player_.peertube().on('torrentInfo', (event, data) => {
203 // We are in HTTP fallback
204 if (!data) {
205 subDivHttp.className = 'vjs-peertube-displayed'
206 subDivWebtorrent.className = 'vjs-peertube-hidden'
207
208 return
209 }
210
192 const downloadSpeed = bytes(data.downloadSpeed) 211 const downloadSpeed = bytes(data.downloadSpeed)
193 const uploadSpeed = bytes(data.uploadSpeed) 212 const uploadSpeed = bytes(data.uploadSpeed)
194 const numPeers = data.numPeers 213 const numPeers = data.numPeers
195 214
196 downloadSpeedNumber.textContent = downloadSpeed[0] 215 downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
197 downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] 216 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
198 217
199 uploadSpeedNumber.textContent = uploadSpeed[0] 218 uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
200 uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] 219 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
201 220
202 peersNumber.textContent = numPeers 221 peersNumber.textContent = numPeers
222 peersText.textContent = ' peers'
203 223
204 subDiv.className = 'vjs-webtorrent-displayed' 224 subDivHttp.className = 'vjs-peertube-hidden'
225 subDivWebtorrent.className = 'vjs-peertube-displayed'
205 }) 226 })
206 227
207 return div 228 return div
@@ -225,6 +246,7 @@ class PeerTubePlugin extends Plugin {
225 private videoDuration: number 246 private videoDuration: number
226 private videoViewInterval 247 private videoViewInterval
227 private torrentInfoInterval 248 private torrentInfoInterval
249 private savePlayerSrcFunction: Function
228 250
229 constructor (player: videojs.Player, options: PeertubePluginOptions) { 251 constructor (player: videojs.Player, options: PeertubePluginOptions) {
230 super(player, options) 252 super(player, options)
@@ -237,12 +259,11 @@ class PeerTubePlugin extends Plugin {
237 this.videoViewUrl = options.videoViewUrl 259 this.videoViewUrl = options.videoViewUrl
238 this.videoDuration = options.videoDuration 260 this.videoDuration = options.videoDuration
239 261
262 this.savePlayerSrcFunction = this.player.src
240 // Hack to "simulate" src link in video.js >= 6 263 // Hack to "simulate" src link in video.js >= 6
241 // Without this, we can't play the video after pausing it 264 // Without this, we can't play the video after pausing it
242 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 265 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
243 this.player.src = function () { 266 this.player.src = () => true
244 return true
245 }
246 267
247 this.playerElement = options.playerElement 268 this.playerElement = options.playerElement
248 269
@@ -284,6 +305,10 @@ class PeerTubePlugin extends Plugin {
284 return 305 return
285 } 306 }
286 307
308 // Do not display error to user because we will have multiple fallbacks
309 this.disableErrorDisplay()
310 this.player.src = () => true
311
287 const previousVideoFile = this.currentVideoFile 312 const previousVideoFile = this.currentVideoFile
288 this.currentVideoFile = videoFile 313 this.currentVideoFile = videoFile
289 314
@@ -295,7 +320,7 @@ class PeerTubePlugin extends Plugin {
295 320
296 const options = { autoplay: true, controls: true } 321 const options = { autoplay: true, controls: true }
297 renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { 322 renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => {
298 if (err) return this.handleError(err) 323 if (err) return this.fallbackToHttp()
299 324
300 this.renderer = renderer 325 this.renderer = renderer
301 if (!this.player.paused()) { 326 if (!this.player.paused()) {
@@ -347,7 +372,8 @@ class PeerTubePlugin extends Plugin {
347 372
348 flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { 373 flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
349 if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) { 374 if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) {
350 if (destroyRenderer === true) this.renderer.destroy() 375 if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy()
376
351 webtorrent.remove(videoFile.magnetUri) 377 webtorrent.remove(videoFile.magnetUri)
352 console.log('Removed ' + videoFile.magnetUri) 378 console.log('Removed ' + videoFile.magnetUri)
353 } 379 }
@@ -390,13 +416,17 @@ class PeerTubePlugin extends Plugin {
390 416
391 private runTorrentInfoScheduler () { 417 private runTorrentInfoScheduler () {
392 this.torrentInfoInterval = setInterval(() => { 418 this.torrentInfoInterval = setInterval(() => {
393 if (this.torrent !== undefined) { 419 // Not initialized yet
394 this.trigger('torrentInfo', { 420 if (this.torrent === undefined) return
395 downloadSpeed: this.torrent.downloadSpeed, 421
396 numPeers: this.torrent.numPeers, 422 // Http fallback
397 uploadSpeed: this.torrent.uploadSpeed 423 if (this.torrent === null) return this.trigger('torrentInfo', false)
398 }) 424
399 } 425 return this.trigger('torrentInfo', {
426 downloadSpeed: this.torrent.downloadSpeed,
427 numPeers: this.torrent.numPeers,
428 uploadSpeed: this.torrent.uploadSpeed
429 })
400 }, 1000) 430 }, 1000)
401 } 431 }
402 432
@@ -433,8 +463,29 @@ class PeerTubePlugin extends Plugin {
433 return fetch(this.videoViewUrl, { method: 'POST' }) 463 return fetch(this.videoViewUrl, { method: 'POST' })
434 } 464 }
435 465
466 private fallbackToHttp () {
467 this.flushVideoFile(this.currentVideoFile, true)
468 this.torrent = null
469
470 // Enable error display now this is our last fallback
471 this.player.one('error', () => this.enableErrorDisplay())
472
473 const httpUrl = this.currentVideoFile.fileUrl
474 this.player.src = this.savePlayerSrcFunction
475 this.player.src(httpUrl)
476 this.player.play()
477 }
478
436 private handleError (err: Error | string) { 479 private handleError (err: Error | string) {
437 return this.player.trigger('customError', { err }) 480 return this.player.trigger('customError', { err })
438 } 481 }
482
483 private enableErrorDisplay () {
484 this.player.addClass('vjs-error-display-enabled')
485 }
486
487 private disableErrorDisplay () {
488 this.player.removeClass('vjs-error-display-enabled')
489 }
439} 490}
440videojsUntyped.registerPlugin('peertube', PeerTubePlugin) 491videojsUntyped.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 @@
1// Thanks: https://github.com/feross/render-media 1// Thanks: https://github.com/feross/render-media
2// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed 2// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed
3 3
4import { extname } from 'path'
5import * as MediaElementWrapper from 'mediasource' 4import * as MediaElementWrapper from 'mediasource'
5import { extname } from 'path'
6import * as videostream from 'videostream' 6import * as videostream from 'videostream'
7 7
8const VIDEOSTREAM_EXTS = [ 8const VIDEOSTREAM_EXTS = [
@@ -27,7 +27,7 @@ function renderVideo (
27 return renderMedia(file, elem, opts, callback) 27 return renderMedia(file, elem, opts, callback)
28} 28}
29 29
30function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) { 30function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) {
31 const extension = extname(file.name).toLowerCase() 31 const extension = extname(file.name).toLowerCase()
32 let preparedElem = undefined 32 let preparedElem = undefined
33 let currentTime = 0 33 let currentTime = 0
@@ -41,18 +41,33 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
41 41
42 function useVideostream () { 42 function useVideostream () {
43 prepareElem() 43 prepareElem()
44 preparedElem.addEventListener('error', fallbackToMediaSource) 44 preparedElem.addEventListener('error', function onError () {
45 preparedElem.removeEventListener('error', onError)
46
47 return fallbackToMediaSource()
48 })
45 preparedElem.addEventListener('loadstart', onLoadStart) 49 preparedElem.addEventListener('loadstart', onLoadStart)
46 return videostream(file, preparedElem) 50 return videostream(file, preparedElem)
47 } 51 }
48 52
49 function useMediaSource () { 53 function useMediaSource (useVP9 = false) {
54 const codecs = getCodec(file.name, useVP9)
55
50 prepareElem() 56 prepareElem()
51 preparedElem.addEventListener('error', callback) 57 preparedElem.addEventListener('error', function onError(err) {
58 // Try with vp9 before returning an error
59 if (codecs.indexOf('vp8') !== -1) {
60 preparedElem.removeEventListener('error', onError)
61
62 return fallbackToMediaSource(true)
63 }
64
65 return callback(err)
66 })
52 preparedElem.addEventListener('loadstart', onLoadStart) 67 preparedElem.addEventListener('loadstart', onLoadStart)
53 68
54 const wrapper = new MediaElementWrapper(preparedElem) 69 const wrapper = new MediaElementWrapper(preparedElem)
55 const writable = wrapper.createWriteStream(getCodec(file.name)) 70 const writable = wrapper.createWriteStream(codecs)
56 file.createReadStream().pipe(writable) 71 file.createReadStream().pipe(writable)
57 72
58 if (currentTime) preparedElem.currentTime = currentTime 73 if (currentTime) preparedElem.currentTime = currentTime
@@ -60,10 +75,11 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca
60 return wrapper 75 return wrapper
61 } 76 }
62 77
63 function fallbackToMediaSource () { 78 function fallbackToMediaSource (useVP9 = false) {
64 preparedElem.removeEventListener('error', fallbackToMediaSource) 79 if (useVP9 === true) console.log('Falling back to media source with VP9 enabled.')
80 else console.log('Falling back to media source..')
65 81
66 useMediaSource() 82 useMediaSource(useVP9)
67 } 83 }
68 84
69 function prepareElem () { 85 function prepareElem () {
@@ -96,16 +112,19 @@ function validateFile (file) {
96 } 112 }
97} 113}
98 114
99function getCodec (name: string) { 115function getCodec (name: string, useVP9 = false) {
100 const ext = extname(name).toLowerCase() 116 const ext = extname(name).toLowerCase()
101 return { 117 if (ext === '.mp4') {
102 '.m4a': 'audio/mp4; codecs="mp4a.40.5"', 118 return 'video/mp4; codecs="avc1.640029, mp4a.40.5"'
103 '.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', 119 }
104 '.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"', 120
105 '.mp3': 'audio/mpeg', 121 if (ext === '.webm') {
106 '.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', 122 if (useVP9 === true) return 'video/webm; codecs="vp9, opus"'
107 '.webm': 'video/webm; codecs="opus, vorbis, vp8"' 123
108 }[ext] 124 return 'video/webm; codecs="vp8, vorbis"'
125 }
126
127 return undefined
109} 128}
110 129
111export { 130export {
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;
154 } 154 }
155 } 155 }
156 156
157 .vjs-webtorrent { 157 .vjs-peertube {
158 width: 100%; 158 width: 100%;
159 line-height: $control-bar-height; 159 line-height: $control-bar-height;
160 text-align: right; 160 text-align: right;
161 padding-right: 60px; 161 padding-right: 60px;
162 162
163 .vjs-webtorrent-displayed { 163 .vjs-peertube-displayed {
164 display: block; 164 display: block;
165 } 165 }
166 166
167 .vjs-webtorrent-hidden { 167 .vjs-peertube-hidden {
168 display: none; 168 display: none;
169 } 169 }
170 170
@@ -424,3 +424,20 @@ $control-bar-height: 34px;
424 } 424 }
425} 425}
426 426
427// Error display disabled
428.vjs-error:not(.vjs-error-display-enabled) {
429 .vjs-error-display {
430 display: none;
431 }
432
433 .vjs-loading-spinner {
434 display: block;
435 }
436}
437
438// Error display enabled
439.vjs-error.vjs-error-display-enabled {
440 .vjs-error-display {
441 display: block;
442 }
443}