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