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