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