diff options
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/app/videos/+video-watch/video-watch.component.ts | 6 | ||||
-rw-r--r-- | client/src/assets/player/peertube-videojs-plugin.ts | 109 | ||||
-rw-r--r-- | client/src/assets/player/video-renderer.ts | 55 | ||||
-rw-r--r-- | 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 { | |||
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 | ||
3 | import { VideoService } from '@app/shared/video/video.service' | ||
4 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
5 | import * as WebTorrent from 'webtorrent' | 4 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
@@ -147,12 +146,12 @@ Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) | |||
147 | class WebTorrentButton extends Button { | 146 | class 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 | } |
440 | videojsUntyped.registerPlugin('peertube', PeerTubePlugin) | 491 | 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 @@ | |||
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 | ||
4 | import { extname } from 'path' | ||
5 | import * as MediaElementWrapper from 'mediasource' | 4 | import * as MediaElementWrapper from 'mediasource' |
5 | import { extname } from 'path' | ||
6 | import * as videostream from 'videostream' | 6 | import * as videostream from 'videostream' |
7 | 7 | ||
8 | const VIDEOSTREAM_EXTS = [ | 8 | const 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 | ||
30 | function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) { | 30 | function 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 | ||
99 | function getCodec (name: string) { | 115 | function 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 | ||
111 | export { | 130 | 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; | |||
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 | } | ||