diff options
Diffstat (limited to 'client/src/assets/player')
-rw-r--r-- | client/src/assets/player/peertube-videojs-plugin.ts | 282 |
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 | ||
7 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
8 | 8 | ||
9 | interface VideoJSComponentInterface { | ||
10 | _player: VideoJSPlayer | ||
11 | |||
12 | new (player: VideoJSPlayer, options?: any) | ||
13 | |||
14 | registerComponent (name: string, obj: any) | ||
15 | } | ||
16 | |||
17 | interface VideoJSPlayer extends videojs.Player { | ||
18 | peertube (): PeerTubePlugin | ||
19 | } | ||
20 | |||
21 | type 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 |
11 | const dictionaryBytes: Array<{max: number, type: string}> = [ | 29 | const dictionaryBytes: Array<{max: number, type: string}> = [ |
@@ -25,42 +43,46 @@ function bytes (value) { | |||
25 | const videojsUntyped = videojs as any | 43 | const videojsUntyped = videojs as any |
26 | const webtorrent = new WebTorrent({ dht: false }) | 44 | const webtorrent = new WebTorrent({ dht: false }) |
27 | 45 | ||
28 | const MenuItem = videojsUntyped.getComponent('MenuItem') | 46 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
29 | const ResolutionMenuItem = videojsUntyped.extend(MenuItem, { | 47 | class 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 | } |
43 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | 62 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) |
44 | 63 | ||
45 | const MenuButton = videojsUntyped.getComponent('MenuButton') | 64 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') |
46 | const ResolutionMenuButton = videojsUntyped.extend(MenuButton, { | 65 | class 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 | } |
88 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 116 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) |
89 | 117 | ||
90 | const Button = videojsUntyped.getComponent('Button') | 118 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
91 | const PeertubeLinkButton = videojsUntyped.extend(Button, { | 119 | class 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 | }) | ||
111 | Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) | ||
112 | 135 | ||
113 | const WebTorrentButton = videojsUntyped.extend(Button, { | 136 | dispose () { |
114 | constructor: function (player) { | 137 | this.parentNode.removeChild(this) |
115 | Button.call(this, player) | 138 | } |
116 | }, | 139 | } |
140 | Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) | ||
117 | 141 | ||
118 | createEl: function () { | 142 | class 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 | }) | ||
180 | Button.registerComponent('WebTorrentButton', WebTorrentButton) | ||
181 | 204 | ||
182 | type PeertubePluginOptions = { | 205 | dispose () { |
183 | videoFiles: VideoFile[] | 206 | this.parentNode.removeChild(this) |
184 | playerElement: HTMLVideoElement | 207 | } |
185 | autoplay: boolean | ||
186 | peerTubeLink: boolean | ||
187 | } | 208 | } |
188 | const peertubePlugin = function (options: PeertubePluginOptions) { | 209 | Button.registerComponent('WebTorrentButton', WebTorrentButton) |
189 | const player = this | 210 | |
190 | let currentVideoFile: VideoFile = undefined | 211 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') |
191 | const playerElement = options.playerElement | 212 | class 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 | 379 | videojsUntyped.registerPlugin('peertube', PeerTubePlugin) | |
351 | videojsUntyped.registerPlugin('peertube', peertubePlugin) | ||