diff options
Diffstat (limited to 'client/src/assets/player/peertube-videojs-plugin.ts')
-rw-r--r-- | client/src/assets/player/peertube-videojs-plugin.ts | 288 |
1 files changed, 55 insertions, 233 deletions
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts index 22cb27da3..c35ce12cb 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/peertube-videojs-plugin.ts | |||
@@ -1,49 +1,11 @@ | |||
1 | // Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher | ||
2 | |||
3 | import * as videojs from 'video.js' | 1 | import * as videojs from 'video.js' |
4 | import * as WebTorrent from 'webtorrent' | 2 | import * as WebTorrent from 'webtorrent' |
5 | import { VideoConstant, VideoResolution } from '../../../../shared/models/videos' | ||
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 3 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './video-renderer' | 4 | import { renderVideo } from './video-renderer' |
5 | import './settings-menu-button' | ||
6 | import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
7 | import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils' | ||
8 | 8 | ||
9 | declare module 'video.js' { | ||
10 | interface Player { | ||
11 | peertube (): PeerTubePlugin | ||
12 | } | ||
13 | } | ||
14 | |||
15 | interface VideoJSComponentInterface { | ||
16 | _player: videojs.Player | ||
17 | |||
18 | new (player: videojs.Player, options?: any) | ||
19 | |||
20 | registerComponent (name: string, obj: any) | ||
21 | } | ||
22 | |||
23 | type PeertubePluginOptions = { | ||
24 | videoFiles: VideoFile[] | ||
25 | playerElement: HTMLVideoElement | ||
26 | videoViewUrl: string | ||
27 | videoDuration: number | ||
28 | } | ||
29 | |||
30 | // https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts | ||
31 | // Don't import all Angular stuff, just copy the code with shame | ||
32 | const dictionaryBytes: Array<{max: number, type: string}> = [ | ||
33 | { max: 1024, type: 'B' }, | ||
34 | { max: 1048576, type: 'KB' }, | ||
35 | { max: 1073741824, type: 'MB' }, | ||
36 | { max: 1.0995116e12, type: 'GB' } | ||
37 | ] | ||
38 | function bytes (value) { | ||
39 | const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1] | ||
40 | const calc = Math.floor(value / (format.max / 1024)).toString() | ||
41 | |||
42 | return [ calc, format.type ] | ||
43 | } | ||
44 | |||
45 | // videojs typings don't have some method we need | ||
46 | const videojsUntyped = videojs as any | ||
47 | const webtorrent = new WebTorrent({ | 9 | const webtorrent = new WebTorrent({ |
48 | tracker: { | 10 | tracker: { |
49 | rtcConfig: { | 11 | rtcConfig: { |
@@ -60,199 +22,19 @@ const webtorrent = new WebTorrent({ | |||
60 | dht: false | 22 | dht: false |
61 | }) | 23 | }) |
62 | 24 | ||
63 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
64 | class ResolutionMenuItem extends MenuItem { | ||
65 | |||
66 | constructor (player: videojs.Player, options) { | ||
67 | options.selectable = true | ||
68 | super(player, options) | ||
69 | |||
70 | const currentResolutionId = this.player_.peertube().getCurrentResolutionId() | ||
71 | this.selected(this.options_.id === currentResolutionId) | ||
72 | } | ||
73 | |||
74 | handleClick (event) { | ||
75 | super.handleClick(event) | ||
76 | |||
77 | this.player_.peertube().updateResolution(this.options_.id) | ||
78 | } | ||
79 | } | ||
80 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
81 | |||
82 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
83 | class ResolutionMenuButton extends MenuButton { | ||
84 | label: HTMLElement | ||
85 | |||
86 | constructor (player: videojs.Player, options) { | ||
87 | options.label = 'Quality' | ||
88 | super(player, options) | ||
89 | |||
90 | this.label = document.createElement('span') | ||
91 | |||
92 | this.el().setAttribute('aria-label', 'Quality') | ||
93 | this.controlText('Quality') | ||
94 | |||
95 | videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label') | ||
96 | this.el().appendChild(this.label) | ||
97 | |||
98 | player.peertube().on('videoFileUpdate', () => this.update()) | ||
99 | } | ||
100 | |||
101 | createItems () { | ||
102 | const menuItems = [] | ||
103 | for (const videoFile of this.player_.peertube().videoFiles) { | ||
104 | menuItems.push(new ResolutionMenuItem( | ||
105 | this.player_, | ||
106 | { | ||
107 | id: videoFile.resolution.id, | ||
108 | label: videoFile.resolution.label, | ||
109 | src: videoFile.magnetUri, | ||
110 | selected: videoFile.resolution.id === this.currentSelectionId | ||
111 | }) | ||
112 | ) | ||
113 | } | ||
114 | |||
115 | return menuItems | ||
116 | } | ||
117 | |||
118 | update () { | ||
119 | if (!this.label) return | ||
120 | |||
121 | this.label.innerHTML = this.player_.peertube().getCurrentResolutionLabel() | ||
122 | this.hide() | ||
123 | return super.update() | ||
124 | } | ||
125 | |||
126 | buildCSSClass () { | ||
127 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
128 | } | ||
129 | |||
130 | buildWrapperCSSClass () { | ||
131 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
132 | } | ||
133 | } | ||
134 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | ||
135 | |||
136 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | ||
137 | class PeerTubeLinkButton extends Button { | ||
138 | |||
139 | createEl () { | ||
140 | const link = document.createElement('a') | ||
141 | link.href = window.location.href.replace('embed', 'watch') | ||
142 | link.innerHTML = 'PeerTube' | ||
143 | link.title = 'Go to the video page' | ||
144 | link.className = 'vjs-peertube-link' | ||
145 | link.target = '_blank' | ||
146 | |||
147 | return link | ||
148 | } | ||
149 | |||
150 | handleClick () { | ||
151 | this.player_.pause() | ||
152 | } | ||
153 | } | ||
154 | Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | ||
155 | |||
156 | class WebTorrentButton extends Button { | ||
157 | createEl () { | ||
158 | const div = document.createElement('div') | ||
159 | const subDivWebtorrent = document.createElement('div') | ||
160 | div.appendChild(subDivWebtorrent) | ||
161 | |||
162 | const downloadIcon = document.createElement('span') | ||
163 | downloadIcon.classList.add('icon', 'icon-download') | ||
164 | subDivWebtorrent.appendChild(downloadIcon) | ||
165 | |||
166 | const downloadSpeedText = document.createElement('span') | ||
167 | downloadSpeedText.classList.add('download-speed-text') | ||
168 | const downloadSpeedNumber = document.createElement('span') | ||
169 | downloadSpeedNumber.classList.add('download-speed-number') | ||
170 | const downloadSpeedUnit = document.createElement('span') | ||
171 | downloadSpeedText.appendChild(downloadSpeedNumber) | ||
172 | downloadSpeedText.appendChild(downloadSpeedUnit) | ||
173 | subDivWebtorrent.appendChild(downloadSpeedText) | ||
174 | |||
175 | const uploadIcon = document.createElement('span') | ||
176 | uploadIcon.classList.add('icon', 'icon-upload') | ||
177 | subDivWebtorrent.appendChild(uploadIcon) | ||
178 | |||
179 | const uploadSpeedText = document.createElement('span') | ||
180 | uploadSpeedText.classList.add('upload-speed-text') | ||
181 | const uploadSpeedNumber = document.createElement('span') | ||
182 | uploadSpeedNumber.classList.add('upload-speed-number') | ||
183 | const uploadSpeedUnit = document.createElement('span') | ||
184 | uploadSpeedText.appendChild(uploadSpeedNumber) | ||
185 | uploadSpeedText.appendChild(uploadSpeedUnit) | ||
186 | subDivWebtorrent.appendChild(uploadSpeedText) | ||
187 | |||
188 | const peersText = document.createElement('span') | ||
189 | peersText.classList.add('peers-text') | ||
190 | const peersNumber = document.createElement('span') | ||
191 | peersNumber.classList.add('peers-number') | ||
192 | subDivWebtorrent.appendChild(peersNumber) | ||
193 | subDivWebtorrent.appendChild(peersText) | ||
194 | |||
195 | div.className = 'vjs-peertube' | ||
196 | // Hide the stats before we get the info | ||
197 | subDivWebtorrent.className = 'vjs-peertube-hidden' | ||
198 | |||
199 | const subDivHttp = document.createElement('div') | ||
200 | subDivHttp.className = 'vjs-peertube-hidden' | ||
201 | const subDivHttpText = document.createElement('span') | ||
202 | subDivHttpText.classList.add('peers-number') | ||
203 | subDivHttpText.textContent = 'HTTP' | ||
204 | const subDivFallbackText = document.createElement('span') | ||
205 | subDivFallbackText.classList.add('peers-text') | ||
206 | subDivFallbackText.textContent = ' fallback' | ||
207 | |||
208 | subDivHttp.appendChild(subDivHttpText) | ||
209 | subDivHttp.appendChild(subDivFallbackText) | ||
210 | div.appendChild(subDivHttp) | ||
211 | |||
212 | this.player_.peertube().on('torrentInfo', (event, data) => { | ||
213 | // We are in HTTP fallback | ||
214 | if (!data) { | ||
215 | subDivHttp.className = 'vjs-peertube-displayed' | ||
216 | subDivWebtorrent.className = 'vjs-peertube-hidden' | ||
217 | |||
218 | return | ||
219 | } | ||
220 | |||
221 | const downloadSpeed = bytes(data.downloadSpeed) | ||
222 | const uploadSpeed = bytes(data.uploadSpeed) | ||
223 | const numPeers = data.numPeers | ||
224 | |||
225 | downloadSpeedNumber.textContent = downloadSpeed[ 0 ] | ||
226 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] | ||
227 | |||
228 | uploadSpeedNumber.textContent = uploadSpeed[ 0 ] | ||
229 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | ||
230 | |||
231 | peersNumber.textContent = numPeers | ||
232 | peersText.textContent = ' peers' | ||
233 | |||
234 | subDivHttp.className = 'vjs-peertube-hidden' | ||
235 | subDivWebtorrent.className = 'vjs-peertube-displayed' | ||
236 | }) | ||
237 | |||
238 | return div | ||
239 | } | ||
240 | } | ||
241 | Button.registerComponent('WebTorrentButton', WebTorrentButton) | ||
242 | |||
243 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') | 25 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') |
244 | class PeerTubePlugin extends Plugin { | 26 | class PeerTubePlugin extends Plugin { |
27 | private readonly playerElement: HTMLVideoElement | ||
28 | private readonly autoplay: boolean = false | ||
29 | private readonly savePlayerSrcFunction: Function | ||
245 | private player: any | 30 | private player: any |
246 | private currentVideoFile: VideoFile | 31 | private currentVideoFile: VideoFile |
247 | private playerElement: HTMLVideoElement | ||
248 | private videoFiles: VideoFile[] | 32 | private videoFiles: VideoFile[] |
249 | private torrent: WebTorrent.Torrent | 33 | private torrent: WebTorrent.Torrent |
250 | private autoplay = false | ||
251 | private videoViewUrl: string | 34 | private videoViewUrl: string |
252 | private videoDuration: number | 35 | private videoDuration: number |
253 | private videoViewInterval | 36 | private videoViewInterval |
254 | private torrentInfoInterval | 37 | private torrentInfoInterval |
255 | private savePlayerSrcFunction: Function | ||
256 | 38 | ||
257 | constructor (player: videojs.Player, options: PeertubePluginOptions) { | 39 | constructor (player: videojs.Player, options: PeertubePluginOptions) { |
258 | super(player, options) | 40 | super(player, options) |
@@ -274,10 +56,20 @@ class PeerTubePlugin extends Plugin { | |||
274 | this.playerElement = options.playerElement | 56 | this.playerElement = options.playerElement |
275 | 57 | ||
276 | this.player.ready(() => { | 58 | this.player.ready(() => { |
59 | const volume = getStoredVolume() | ||
60 | if (volume !== undefined) this.player.volume(volume) | ||
61 | const muted = getStoredMute() | ||
62 | if (muted !== undefined) this.player.muted(muted) | ||
63 | |||
277 | this.initializePlayer() | 64 | this.initializePlayer() |
278 | this.runTorrentInfoScheduler() | 65 | this.runTorrentInfoScheduler() |
279 | this.runViewAdd() | 66 | this.runViewAdd() |
280 | }) | 67 | }) |
68 | |||
69 | this.player.on('volumechange', () => { | ||
70 | saveVolumeInStore(this.player.volume()) | ||
71 | saveMuteInStore(this.player.muted()) | ||
72 | }) | ||
281 | } | 73 | } |
282 | 74 | ||
283 | dispose () { | 75 | dispose () { |
@@ -311,16 +103,19 @@ class PeerTubePlugin extends Plugin { | |||
311 | return | 103 | return |
312 | } | 104 | } |
313 | 105 | ||
314 | // Do not display error to user because we will have multiple fallbacks | 106 | // Do not display error to user because we will have multiple fallback |
315 | this.disableErrorDisplay() | 107 | this.disableErrorDisplay() |
316 | 108 | ||
317 | this.player.src = () => true | 109 | this.player.src = () => true |
318 | this.player.playbackRate(1) | 110 | const oldPlaybackRate = this.player.playbackRate() |
319 | 111 | ||
320 | const previousVideoFile = this.currentVideoFile | 112 | const previousVideoFile = this.currentVideoFile |
321 | this.currentVideoFile = videoFile | 113 | this.currentVideoFile = videoFile |
322 | 114 | ||
323 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, done) | 115 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, () => { |
116 | this.player.playbackRate(oldPlaybackRate) | ||
117 | return done() | ||
118 | }) | ||
324 | 119 | ||
325 | this.trigger('videoFileUpdate') | 120 | this.trigger('videoFileUpdate') |
326 | } | 121 | } |
@@ -337,7 +132,7 @@ class PeerTubePlugin extends Plugin { | |||
337 | renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { | 132 | renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { |
338 | this.renderer = renderer | 133 | this.renderer = renderer |
339 | 134 | ||
340 | if (err) return this.fallbackToHttp() | 135 | if (err) return this.fallbackToHttp(done) |
341 | 136 | ||
342 | if (!this.player.paused()) { | 137 | if (!this.player.paused()) { |
343 | const playPromise = this.player.play() | 138 | const playPromise = this.player.play() |
@@ -414,13 +209,17 @@ class PeerTubePlugin extends Plugin { | |||
414 | private initializePlayer () { | 209 | private initializePlayer () { |
415 | this.initSmoothProgressBar() | 210 | this.initSmoothProgressBar() |
416 | 211 | ||
212 | this.alterInactivity() | ||
213 | |||
417 | if (this.autoplay === true) { | 214 | if (this.autoplay === true) { |
418 | this.updateVideoFile(undefined, () => this.player.play()) | 215 | this.updateVideoFile(undefined, () => this.player.play()) |
419 | } else { | 216 | } else { |
420 | this.player.one('play', () => { | 217 | // Proxify first play |
421 | this.player.pause() | 218 | const oldPlay = this.player.play.bind(this.player) |
422 | this.updateVideoFile(undefined, () => this.player.play()) | 219 | this.player.play = () => { |
423 | }) | 220 | this.updateVideoFile(undefined, () => oldPlay) |
221 | this.player.play = oldPlay | ||
222 | } | ||
424 | } | 223 | } |
425 | } | 224 | } |
426 | 225 | ||
@@ -473,7 +272,7 @@ class PeerTubePlugin extends Plugin { | |||
473 | return fetch(this.videoViewUrl, { method: 'POST' }) | 272 | return fetch(this.videoViewUrl, { method: 'POST' }) |
474 | } | 273 | } |
475 | 274 | ||
476 | private fallbackToHttp () { | 275 | private fallbackToHttp (done: Function) { |
477 | this.flushVideoFile(this.currentVideoFile, true) | 276 | this.flushVideoFile(this.currentVideoFile, true) |
478 | this.torrent = null | 277 | this.torrent = null |
479 | 278 | ||
@@ -484,6 +283,8 @@ class PeerTubePlugin extends Plugin { | |||
484 | this.player.src = this.savePlayerSrcFunction | 283 | this.player.src = this.savePlayerSrcFunction |
485 | this.player.src(httpUrl) | 284 | this.player.src(httpUrl) |
486 | this.player.play() | 285 | this.player.play() |
286 | |||
287 | return done() | ||
487 | } | 288 | } |
488 | 289 | ||
489 | private handleError (err: Error | string) { | 290 | private handleError (err: Error | string) { |
@@ -498,6 +299,25 @@ class PeerTubePlugin extends Plugin { | |||
498 | this.player.removeClass('vjs-error-display-enabled') | 299 | this.player.removeClass('vjs-error-display-enabled') |
499 | } | 300 | } |
500 | 301 | ||
302 | private alterInactivity () { | ||
303 | let saveInactivityTimeout: number | ||
304 | |||
305 | const disableInactivity = () => { | ||
306 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
307 | this.player.options_.inactivityTimeout = 0 | ||
308 | } | ||
309 | const enableInactivity = () => { | ||
310 | // this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
311 | } | ||
312 | |||
313 | const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog') | ||
314 | |||
315 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
316 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
317 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
318 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
319 | } | ||
320 | |||
501 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 321 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
502 | private initSmoothProgressBar () { | 322 | private initSmoothProgressBar () { |
503 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 323 | const SeekBar = videojsUntyped.getComponent('SeekBar') |
@@ -520,4 +340,6 @@ class PeerTubePlugin extends Plugin { | |||
520 | } | 340 | } |
521 | } | 341 | } |
522 | } | 342 | } |
343 | |||
523 | videojsUntyped.registerPlugin('peertube', PeerTubePlugin) | 344 | videojsUntyped.registerPlugin('peertube', PeerTubePlugin) |
345 | export { PeerTubePlugin } | ||