aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts282
1 files changed, 155 insertions, 127 deletions
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
index ca2b9a724..25e65abd8 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -6,6 +6,24 @@ import { VideoFile } from '../../../../shared/models/videos/video.model'
6 6
7import { renderVideo } from './video-renderer' 7import { renderVideo } from './video-renderer'
8 8
9interface VideoJSComponentInterface {
10 _player: VideoJSPlayer
11
12 new (player: VideoJSPlayer, options?: any)
13
14 registerComponent (name: string, obj: any)
15}
16
17interface VideoJSPlayer extends videojs.Player {
18 peertube (): PeerTubePlugin
19}
20
21type PeertubePluginOptions = {
22 videoFiles: VideoFile[]
23 playerElement: HTMLVideoElement
24 peerTubeLink: boolean
25}
26
9// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts 27// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
10// Don't import all Angular stuff, just copy the code with shame 28// Don't import all Angular stuff, just copy the code with shame
11const dictionaryBytes: Array<{max: number, type: string}> = [ 29const dictionaryBytes: Array<{max: number, type: string}> = [
@@ -25,42 +43,46 @@ function bytes (value) {
25const videojsUntyped = videojs as any 43const videojsUntyped = videojs as any
26const webtorrent = new WebTorrent({ dht: false }) 44const webtorrent = new WebTorrent({ dht: false })
27 45
28const MenuItem = videojsUntyped.getComponent('MenuItem') 46const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
29const ResolutionMenuItem = videojsUntyped.extend(MenuItem, { 47class ResolutionMenuItem extends MenuItem {
30 constructor: function (player: videojs.Player, options) { 48
49 constructor (player: VideoJSPlayer, options) {
31 options.selectable = true 50 options.selectable = true
32 MenuItem.call(this, player, options) 51 super(player, options)
33 52
34 const currentResolution = this.player_.getCurrentResolution() 53 const currentResolution = this.player_.peertube().getCurrentResolution()
35 this.selected(this.options_.id === currentResolution) 54 this.selected(this.options_.id === currentResolution)
36 }, 55 }
37 56
38 handleClick: function (event) { 57 handleClick (event) {
39 MenuItem.prototype.handleClick.call(this, event) 58 MenuItem.prototype.handleClick.call(this, event)
40 this.player_.updateResolution(this.options_.id) 59 this.player_.peertube().updateResolution(this.options_.id)
41 } 60 }
42}) 61}
43MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) 62MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
44 63
45const MenuButton = videojsUntyped.getComponent('MenuButton') 64const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
46const ResolutionMenuButton = videojsUntyped.extend(MenuButton, { 65class ResolutionMenuButton extends MenuButton {
47 constructor: function (player, options) { 66 label: HTMLElement
48 this.label = document.createElement('span') 67
68 constructor (player: VideoJSPlayer, options) {
49 options.label = 'Quality' 69 options.label = 'Quality'
70 super(player, options)
71
72 this.label = document.createElement('span')
50 73
51 MenuButton.call(this, player, options)
52 this.el().setAttribute('aria-label', 'Quality') 74 this.el().setAttribute('aria-label', 'Quality')
53 this.controlText('Quality') 75 this.controlText('Quality')
54 76
55 videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label') 77 videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label')
56 this.el().appendChild(this.label) 78 this.el().appendChild(this.label)
57 79
58 player.on('videoFileUpdate', videojs.bind(this, this.update)) 80 player.peertube().on('videoFileUpdate', () => this.update())
59 }, 81 }
60 82
61 createItems: function () { 83 createItems () {
62 const menuItems = [] 84 const menuItems = []
63 for (const videoFile of this.player_.videoFiles) { 85 for (const videoFile of this.player_.peertube().videoFiles) {
64 menuItems.push(new ResolutionMenuItem( 86 menuItems.push(new ResolutionMenuItem(
65 this.player_, 87 this.player_,
66 { 88 {
@@ -73,27 +95,30 @@ const ResolutionMenuButton = videojsUntyped.extend(MenuButton, {
73 } 95 }
74 96
75 return menuItems 97 return menuItems
76 }, 98 }
99
100 update () {
101 if (!this.label) return
77 102
78 update: function () { 103 this.label.innerHTML = this.player_.peertube().getCurrentResolutionLabel()
79 this.label.innerHTML = this.player_.getCurrentResolutionLabel()
80 this.hide() 104 this.hide()
81 return MenuButton.prototype.update.call(this) 105 return super.update()
82 }, 106 }
107
108 buildCSSClass () {
109 return super.buildCSSClass() + ' vjs-resolution-button'
110 }
83 111
84 buildCSSClass: function () { 112 dispose () {
85 return MenuButton.prototype.buildCSSClass.call(this) + ' vjs-resolution-button' 113 this.parentNode.removeChild(this)
86 } 114 }
87}) 115}
88MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) 116MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
89 117
90const Button = videojsUntyped.getComponent('Button') 118const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
91const PeertubeLinkButton = videojsUntyped.extend(Button, { 119class PeertubeLinkButton extends Button {
92 constructor: function (player) {
93 Button.call(this, player)
94 },
95 120
96 createEl: function () { 121 createEl () {
97 const link = document.createElement('a') 122 const link = document.createElement('a')
98 link.href = window.location.href.replace('embed', 'watch') 123 link.href = window.location.href.replace('embed', 'watch')
99 link.innerHTML = 'PeerTube' 124 link.innerHTML = 'PeerTube'
@@ -102,20 +127,20 @@ const PeertubeLinkButton = videojsUntyped.extend(Button, {
102 link.target = '_blank' 127 link.target = '_blank'
103 128
104 return link 129 return link
105 }, 130 }
106 131
107 handleClick: function () { 132 handleClick () {
108 this.player_.pause() 133 this.player_.pause()
109 } 134 }
110})
111Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
112 135
113const WebTorrentButton = videojsUntyped.extend(Button, { 136 dispose () {
114 constructor: function (player) { 137 this.parentNode.removeChild(this)
115 Button.call(this, player) 138 }
116 }, 139}
140Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
117 141
118 createEl: function () { 142class WebTorrentButton extends Button {
143 createEl () {
119 const div = document.createElement('div') 144 const div = document.createElement('div')
120 const subDiv = document.createElement('div') 145 const subDiv = document.createElement('div')
121 div.appendChild(subDiv) 146 div.appendChild(subDiv)
@@ -158,7 +183,7 @@ const WebTorrentButton = videojsUntyped.extend(Button, {
158 // Hide the stats before we get the info 183 // Hide the stats before we get the info
159 subDiv.className = 'vjs-webtorrent-hidden' 184 subDiv.className = 'vjs-webtorrent-hidden'
160 185
161 this.player_.on('torrentInfo', (event, data) => { 186 this.player_.peertube().on('torrentInfo', (event, data) => {
162 const downloadSpeed = bytes(data.downloadSpeed) 187 const downloadSpeed = bytes(data.downloadSpeed)
163 const uploadSpeed = bytes(data.uploadSpeed) 188 const uploadSpeed = bytes(data.uploadSpeed)
164 const numPeers = data.numPeers 189 const numPeers = data.numPeers
@@ -176,71 +201,89 @@ const WebTorrentButton = videojsUntyped.extend(Button, {
176 201
177 return div 202 return div
178 } 203 }
179})
180Button.registerComponent('WebTorrentButton', WebTorrentButton)
181 204
182type PeertubePluginOptions = { 205 dispose () {
183 videoFiles: VideoFile[] 206 this.parentNode.removeChild(this)
184 playerElement: HTMLVideoElement 207 }
185 autoplay: boolean
186 peerTubeLink: boolean
187} 208}
188const peertubePlugin = function (options: PeertubePluginOptions) { 209Button.registerComponent('WebTorrentButton', WebTorrentButton)
189 const player = this 210
190 let currentVideoFile: VideoFile = undefined 211const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin')
191 const playerElement = options.playerElement 212class PeerTubePlugin extends Plugin {
192 player.videoFiles = options.videoFiles 213 private player: any
193 214 private currentVideoFile: VideoFile
194 // Hack to "simulate" src link in video.js >= 6 215 private playerElement: HTMLVideoElement
195 // Without this, we can't play the video after pausing it 216 private videoFiles: VideoFile[]
196 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 217 private torrent: WebTorrent.Torrent
197 player.src = function () { 218
198 return true 219 constructor (player: VideoJSPlayer, options: PeertubePluginOptions) {
220 super(player, options)
221
222 this.videoFiles = options.videoFiles
223
224 // Hack to "simulate" src link in video.js >= 6
225 // Without this, we can't play the video after pausing it
226 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
227 this.player.src = function () {
228 return true
229 }
230
231 this.playerElement = options.playerElement
232
233 this.player.ready(() => {
234 this.initializePlayer(options)
235 this.runTorrentInfoScheduler()
236 })
237 }
238
239 dispose () {
240 // Don't need to destroy renderer, video player will be destroyed
241 this.flushVideoFile(this.currentVideoFile, false)
199 } 242 }
200 243
201 player.getCurrentResolution = function () { 244 getCurrentResolution () {
202 return currentVideoFile ? currentVideoFile.resolution : -1 245 return this.currentVideoFile ? this.currentVideoFile.resolution : -1
203 } 246 }
204 247
205 player.getCurrentResolutionLabel = function () { 248 getCurrentResolutionLabel () {
206 return currentVideoFile ? currentVideoFile.resolutionLabel : '' 249 return this.currentVideoFile ? this.currentVideoFile.resolutionLabel : ''
207 } 250 }
208 251
209 player.updateVideoFile = function (videoFile: VideoFile, done: () => void) { 252 updateVideoFile (videoFile?: VideoFile, done?: () => void) {
210 if (done === undefined) { 253 if (done === undefined) {
211 done = () => { /* empty */ } 254 done = () => { /* empty */ }
212 } 255 }
213 256
214 // Pick the first one 257 // Pick the first one
215 if (videoFile === undefined) { 258 if (videoFile === undefined) {
216 videoFile = player.videoFiles[0] 259 videoFile = this.videoFiles[0]
217 } 260 }
218 261
219 // Don't add the same video file once again 262 // Don't add the same video file once again
220 if (currentVideoFile !== undefined && currentVideoFile.magnetUri === videoFile.magnetUri) { 263 if (this.currentVideoFile !== undefined && this.currentVideoFile.magnetUri === videoFile.magnetUri) {
221 return 264 return
222 } 265 }
223 266
224 const previousVideoFile = currentVideoFile 267 const previousVideoFile = this.currentVideoFile
225 currentVideoFile = videoFile 268 this.currentVideoFile = videoFile
226 269
227 console.log('Adding ' + videoFile.magnetUri + '.') 270 console.log('Adding ' + videoFile.magnetUri + '.')
228 player.torrent = webtorrent.add(videoFile.magnetUri, torrent => { 271 this.torrent = webtorrent.add(videoFile.magnetUri, torrent => {
229 console.log('Added ' + videoFile.magnetUri + '.') 272 console.log('Added ' + videoFile.magnetUri + '.')
230 273
231 this.flushVideoFile(previousVideoFile) 274 this.flushVideoFile(previousVideoFile)
232 275
233 const options = { autoplay: true, controls: true } 276 const options = { autoplay: true, controls: true }
234 renderVideo(torrent.files[0], playerElement, options,(err, renderer) => { 277 renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => {
235 if (err) return handleError(err) 278 if (err) return this.handleError(err)
236 279
237 this.renderer = renderer 280 this.renderer = renderer
238 player.play().then(done) 281 this.player.play().then(done)
239 }) 282 })
240 }) 283 })
241 284
242 player.torrent.on('error', err => handleError(err)) 285 this.torrent.on('error', err => this.handleError(err))
243 player.torrent.on('warning', err => { 286 this.torrent.on('warning', (err: any) => {
244 // We don't support HTTP tracker but we don't care -> we use the web socket tracker 287 // We don't support HTTP tracker but we don't care -> we use the web socket tracker
245 if (err.message.indexOf('Unsupported tracker protocol: http') !== -1) return 288 if (err.message.indexOf('Unsupported tracker protocol: http') !== -1) return
246 // Users don't care about issues with WebRTC, but developers do so log it in the console 289 // Users don't care about issues with WebRTC, but developers do so log it in the console
@@ -249,103 +292,88 @@ const peertubePlugin = function (options: PeertubePluginOptions) {
249 return 292 return
250 } 293 }
251 294
252 return handleError(err) 295 return this.handleError(err)
253 }) 296 })
254 297
255 player.trigger('videoFileUpdate') 298 this.trigger('videoFileUpdate')
256
257 return player
258 } 299 }
259 300
260 player.updateResolution = function (resolution) { 301 updateResolution (resolution) {
261 // Remember player state 302 // Remember player state
262 const currentTime = player.currentTime() 303 const currentTime = this.player.currentTime()
263 const isPaused = player.paused() 304 const isPaused = this.player.paused()
264 305
265 // Hide bigPlayButton 306 // Hide bigPlayButton
266 if (!isPaused) { 307 if (!isPaused) {
267 this.player_.bigPlayButton.hide() 308 this.player.bigPlayButton.hide()
268 } 309 }
269 310
270 const newVideoFile = player.videoFiles.find(f => f.resolution === resolution) 311 const newVideoFile = this.videoFiles.find(f => f.resolution === resolution)
271 player.updateVideoFile(newVideoFile, () => { 312 this.updateVideoFile(newVideoFile, () => {
272 player.currentTime(currentTime) 313 this.player.currentTime(currentTime)
273 player.handleTechSeeked_() 314 this.player.handleTechSeeked_()
274 }) 315 })
275 } 316 }
276 317
277 player.flushVideoFile = function (videoFile: VideoFile, destroyRenderer = true) { 318 flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
278 if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) { 319 if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) {
279 if (destroyRenderer === true) this.renderer.destroy() 320 if (destroyRenderer === true) this.renderer.destroy()
280 webtorrent.remove(videoFile.magnetUri) 321 webtorrent.remove(videoFile.magnetUri)
322 console.log('Removed ' + videoFile.magnetUri)
281 } 323 }
282 } 324 }
283 325
284 player.setVideoFiles = function (files: VideoFile[]) { 326 setVideoFiles (files: VideoFile[]) {
285 player.videoFiles = files 327 this.videoFiles = files
286 328
287 player.updateVideoFile(undefined, () => player.play()) 329 this.updateVideoFile(undefined, () => this.player.play())
288 } 330 }
289 331
290 player.ready(function () { 332 private initializePlayer (options: PeertubePluginOptions) {
291 const controlBar = player.controlBar 333 const controlBar = this.player.controlBar
292 334
293 const menuButton = new ResolutionMenuButton(player, options) 335 const menuButton = new ResolutionMenuButton(this.player, options)
294 const fullscreenElement = controlBar.fullscreenToggle.el() 336 const fullscreenElement = controlBar.fullscreenToggle.el()
295 controlBar.resolutionSwitcher = controlBar.el().insertBefore(menuButton.el(), fullscreenElement) 337 controlBar.resolutionSwitcher = controlBar.el().insertBefore(menuButton.el(), fullscreenElement)
296 controlBar.resolutionSwitcher.dispose = function () {
297 this.parentNode.removeChild(this)
298 }
299
300 player.dispose = function () {
301 // Don't need to destroy renderer, video player will be destroyed
302 player.flushVideoFile(currentVideoFile, false)
303 }
304 338
305 if (options.peerTubeLink === true) { 339 if (options.peerTubeLink === true) {
306 const peerTubeLinkButton = new PeertubeLinkButton(player) 340 const peerTubeLinkButton = new PeertubeLinkButton(this.player)
307 controlBar.peerTubeLink = controlBar.el().insertBefore(peerTubeLinkButton.el(), fullscreenElement) 341 controlBar.peerTubeLink = controlBar.el().insertBefore(peerTubeLinkButton.el(), fullscreenElement)
308
309 controlBar.peerTubeLink.dispose = function () {
310 this.parentNode.removeChild(this)
311 }
312 } 342 }
313 343
314 const webTorrentButton = new WebTorrentButton(player) 344 const webTorrentButton = new WebTorrentButton(this.player)
315 controlBar.webTorrent = controlBar.el().insertBefore(webTorrentButton.el(), controlBar.progressControl.el()) 345 controlBar.webTorrent = controlBar.el().insertBefore(webTorrentButton.el(), controlBar.progressControl.el())
316 controlBar.webTorrent.dispose = function () {
317 this.parentNode.removeChild(this)
318 }
319 346
320 if (options.autoplay === true) { 347 if (this.player.options_.autoplay === true) {
321 player.updateVideoFile() 348 this.updateVideoFile()
322 } else { 349 } else {
323 player.one('play', () => { 350 this.player.one('play', () => {
324 // On firefox, we need to wait to load the video before playing 351 // On firefox, we need to wait to load the video before playing
325 if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) { 352 if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
326 player.pause() 353 this.player.pause()
327 player.updateVideoFile(undefined, () => player.play()) 354 this.updateVideoFile(undefined, () => this.player.play())
328 return 355 return
329 } 356 }
330 357
331 player.updateVideoFile(undefined) 358 this.updateVideoFile(undefined)
332 }) 359 })
333 } 360 }
361 }
334 362
363 private runTorrentInfoScheduler () {
335 setInterval(() => { 364 setInterval(() => {
336 if (player.torrent !== undefined) { 365 if (this.torrent !== undefined) {
337 player.trigger('torrentInfo', { 366 this.trigger('torrentInfo', {
338 downloadSpeed: player.torrent.downloadSpeed, 367 downloadSpeed: this.torrent.downloadSpeed,
339 numPeers: player.torrent.numPeers, 368 numPeers: this.torrent.numPeers,
340 uploadSpeed: player.torrent.uploadSpeed 369 uploadSpeed: this.torrent.uploadSpeed
341 }) 370 })
342 } 371 }
343 }, 1000) 372 }, 1000)
344 }) 373 }
345 374
346 function handleError (err: Error | string) { 375 private handleError (err: Error | string) {
347 return player.trigger('customError', { err }) 376 return this.player.trigger('customError', { err })
348 } 377 }
349} 378}
350 379videojsUntyped.registerPlugin('peertube', PeerTubePlugin)
351videojsUntyped.registerPlugin('peertube', peertubePlugin)