aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-03-30 17:40:00 +0200
committerChocobozzz <me@florianbigard.com>2018-04-03 14:02:10 +0200
commitc6352f2c64f3c1ad54f8500f493587cdce3d33c9 (patch)
tree642a5b29b4d68ed8915e5e800232eab069303f79 /client/src/assets/player
parent6b9af1293621a81564296ead6f12f5e70eafbca2 (diff)
downloadPeerTube-c6352f2c64f3c1ad54f8500f493587cdce3d33c9.tar.gz
PeerTube-c6352f2c64f3c1ad54f8500f493587cdce3d33c9.tar.zst
PeerTube-c6352f2c64f3c1ad54f8500f493587cdce3d33c9.zip
Improve player
Add a settings dialog based on the work of Yanko Shterev (@yshterev): https://github.com/yshterev/videojs-settings-menu. Thanks!
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/images/settings.svg14
-rw-r--r--client/src/assets/player/images/tick.svg12
-rw-r--r--client/src/assets/player/peertube-link-button.ts20
-rw-r--r--client/src/assets/player/peertube-player.ts96
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts288
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts33
-rw-r--r--client/src/assets/player/resolution-menu-button.ts68
-rw-r--r--client/src/assets/player/resolution-menu-item.ts31
-rw-r--r--client/src/assets/player/settings-menu-button.ts285
-rw-r--r--client/src/assets/player/settings-menu-item.ts313
-rw-r--r--client/src/assets/player/utils.ts72
-rw-r--r--client/src/assets/player/webtorrent-info-button.ts101
12 files changed, 1100 insertions, 233 deletions
diff --git a/client/src/assets/player/images/settings.svg b/client/src/assets/player/images/settings.svg
new file mode 100644
index 000000000..c663087b7
--- /dev/null
+++ b/client/src/assets/player/images/settings.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>settings</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
8 <g id="Artboard-4" transform="translate(-796.000000, -159.000000)" stroke="#fff" stroke-width="2">
9 <g id="38" transform="translate(796.000000, 159.000000)">
10 <path d="M7.20852293,4.3800958 C8.05442158,3.84706631 8.99528987,3.45099725 10,3.22301642 L10,1.99980749 C10,1.44762906 10.4433532,1 11.0093689,1 L12.9906311,1 C13.5480902,1 14,1.44371665 14,1.99980749 L14,3.22301642 C15.0047101,3.45099725 15.9455784,3.84706631 16.7914771,4.3800958 L17.6569904,3.5145825 C18.0474395,3.12413339 18.6774591,3.12110988 19.0776926,3.52134344 L20.4786566,4.92230738 C20.8728396,5.31649045 20.8786331,5.94979402 20.4854175,6.34300963 L19.6199042,7.20852293 C20.1529337,8.05442158 20.5490027,8.99528987 20.7769836,10 L22.0001925,10 C22.5523709,10 23,10.4433532 23,11.0093689 L23,12.9906311 C23,13.5480902 22.5562834,14 22.0001925,14 L20.7769836,14 C20.5490027,15.0047101 20.1529337,15.9455784 19.6199042,16.7914771 L20.4854175,17.6569904 C20.8758666,18.0474395 20.8788901,18.6774591 20.4786566,19.0776926 L19.0776926,20.4786566 C18.6835095,20.8728396 18.050206,20.8786331 17.6569904,20.4854175 L16.7914771,19.6199042 C15.9455784,20.1529337 15.0047101,20.5490027 14,20.7769836 L14,22.0001925 C14,22.5523709 13.5566468,23 12.9906311,23 L11.0093689,23 C10.4519098,23 10,22.5562834 10,22.0001925 L10,20.7769836 C8.99528987,20.5490027 8.05442158,20.1529337 7.20852293,19.6199042 L6.34300963,20.4854175 C5.95256051,20.8758666 5.32254093,20.8788901 4.92230738,20.4786566 L3.52134344,19.0776926 C3.12716036,18.6835095 3.12136689,18.050206 3.5145825,17.6569904 L4.3800958,16.7914771 C3.84706631,15.9455784 3.45099725,15.0047101 3.22301642,14 L1.99980749,14 C1.44762906,14 1,13.5566468 1,12.9906311 L1,11.0093689 C1,10.4519098 1.44371665,10 1.99980749,10 L3.22301642,10 C3.45099725,8.99528987 3.84706631,8.05442158 4.3800958,7.20852293 L3.5145825,6.34300963 C3.12413339,5.95256051 3.12110988,5.32254093 3.52134344,4.92230738 L4.92230738,3.52134344 C5.31649045,3.12716036 5.94979402,3.12136689 6.34300963,3.5145825 L7.20852293,4.3800958 Z M12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 Z" id="Combined-Shape"></path>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/player/images/tick.svg b/client/src/assets/player/images/tick.svg
new file mode 100644
index 000000000..d329e6bfb
--- /dev/null
+++ b/client/src/assets/player/images/tick.svg
@@ -0,0 +1,12 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
5 <g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
6 <g id="8" transform="translate(356.000000, 115.000000)">
7 <path d="M21,6 L9,18" id="Path-14"></path>
8 <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path>
9 </g>
10 </g>
11 </g>
12</svg>
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/peertube-link-button.ts
new file mode 100644
index 000000000..6ead78c00
--- /dev/null
+++ b/client/src/assets/player/peertube-link-button.ts
@@ -0,0 +1,20 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
2
3const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
4class PeerTubeLinkButton extends Button {
5
6 createEl () {
7 return videojsUntyped.dom.createEl('a', {
8 href: window.location.href.replace('embed', 'watch'),
9 innerHTML: 'PeerTube',
10 title: 'Go to the video page',
11 className: 'vjs-peertube-link',
12 target: '_blank'
13 })
14 }
15
16 handleClick () {
17 this.player_.pause()
18 }
19}
20Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts
new file mode 100644
index 000000000..4ae3e71bd
--- /dev/null
+++ b/client/src/assets/player/peertube-player.ts
@@ -0,0 +1,96 @@
1import { VideoFile } from '../../../../shared/models/videos'
2
3import 'videojs-hotkeys'
4import 'videojs-dock/dist/videojs-dock.es.js'
5import './peertube-link-button'
6import './resolution-menu-button'
7import './settings-menu-button'
8import './webtorrent-info-button'
9import './peertube-videojs-plugin'
10import { videojsUntyped } from './peertube-videojs-typings'
11
12// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
13videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
14
15function getVideojsOptions (options: {
16 autoplay: boolean,
17 playerElement: HTMLVideoElement,
18 videoViewUrl: string,
19 videoDuration: number,
20 videoFiles: VideoFile[],
21 enableHotkeys: boolean,
22 inactivityTimeout: number,
23 peertubeLink: boolean
24}) {
25 const videojsOptions = {
26 controls: true,
27 autoplay: options.autoplay,
28 inactivityTimeout: options.inactivityTimeout,
29 playbackRates: [ 0.5, 1, 1.5, 2 ],
30 plugins: {
31 peertube: {
32 videoFiles: options.videoFiles,
33 playerElement: options.playerElement,
34 videoViewUrl: options.videoViewUrl,
35 videoDuration: options.videoDuration
36 }
37 },
38 controlBar: {
39 children: getControlBarChildren(options)
40 }
41 }
42
43 if (options.enableHotkeys === true) {
44 Object.assign(videojsOptions.plugins, {
45 hotkeys: {
46 enableVolumeScroll: false
47 }
48 })
49 }
50
51 return videojsOptions
52}
53
54function getControlBarChildren (options: {
55 peertubeLink: boolean
56}) {
57 const children = {
58 'playToggle': {},
59 'currentTimeDisplay': {},
60 'timeDivider': {},
61 'durationDisplay': {},
62 'liveDisplay': {},
63
64 'flexibleWidthSpacer': {},
65 'progressControl': {},
66
67 'webTorrentButton': {},
68
69 'muteToggle': {},
70 'volumeControl': {},
71
72 'settingsButton': {
73 setup: {
74 maxHeightOffset: 40
75 },
76 entries: [
77 'resolutionMenuButton',
78 'playbackRateMenuButton'
79 ]
80 }
81 }
82
83 if (options.peertubeLink === true) {
84 Object.assign(children, {
85 'peerTubeLinkButton': {}
86 })
87 }
88
89 Object.assign(children, {
90 'fullscreenToggle': {}
91 })
92
93 return children
94}
95
96export { getVideojsOptions }
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
3import * as videojs from 'video.js' 1import * as videojs from 'video.js'
4import * as WebTorrent from 'webtorrent' 2import * as WebTorrent from 'webtorrent'
5import { VideoConstant, VideoResolution } from '../../../../shared/models/videos'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 3import { VideoFile } from '../../../../shared/models/videos/video.model'
7import { renderVideo } from './video-renderer' 4import { renderVideo } from './video-renderer'
5import './settings-menu-button'
6import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
7import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils'
8 8
9declare module 'video.js' {
10 interface Player {
11 peertube (): PeerTubePlugin
12 }
13}
14
15interface VideoJSComponentInterface {
16 _player: videojs.Player
17
18 new (player: videojs.Player, options?: any)
19
20 registerComponent (name: string, obj: any)
21}
22
23type 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
32const 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]
38function 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
46const videojsUntyped = videojs as any
47const webtorrent = new WebTorrent({ 9const 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
63const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
64class 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}
80MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
81
82const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
83class 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}
134MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
135
136const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
137class 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}
154Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
155
156class 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}
241Button.registerComponent('WebTorrentButton', WebTorrentButton)
242
243const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') 25const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin')
244class PeerTubePlugin extends Plugin { 26class 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
523videojsUntyped.registerPlugin('peertube', PeerTubePlugin) 344videojsUntyped.registerPlugin('peertube', PeerTubePlugin)
345export { PeerTubePlugin }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
new file mode 100644
index 000000000..a58fa6505
--- /dev/null
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -0,0 +1,33 @@
1import * as videojs from 'video.js'
2import { VideoFile } from '../../../../shared/models/videos/video.model'
3import { PeerTubePlugin } from './peertube-videojs-plugin'
4
5declare module 'video.js' {
6 interface Player {
7 peertube (): PeerTubePlugin
8 }
9}
10
11interface VideoJSComponentInterface {
12 _player: videojs.Player
13
14 new (player: videojs.Player, options?: any)
15
16 registerComponent (name: string, obj: any)
17}
18
19type PeertubePluginOptions = {
20 videoFiles: VideoFile[]
21 playerElement: HTMLVideoElement
22 videoViewUrl: string
23 videoDuration: number
24}
25
26// videojs typings don't have some method we need
27const videojsUntyped = videojs as any
28
29export {
30 VideoJSComponentInterface,
31 PeertubePluginOptions,
32 videojsUntyped
33}
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts
new file mode 100644
index 000000000..c927b084d
--- /dev/null
+++ b/client/src/assets/player/resolution-menu-button.ts
@@ -0,0 +1,68 @@
1import * as videojs from 'video.js'
2import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
3import { ResolutionMenuItem } from './resolution-menu-item'
4
5const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
6const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
7class ResolutionMenuButton extends MenuButton {
8 label: HTMLElement
9
10 constructor (player: videojs.Player, options) {
11 options.label = 'Quality'
12 super(player, options)
13
14 this.controlText_ = 'Quality'
15 this.player = player
16
17 player.peertube().on('videoFileUpdate', () => this.updateLabel())
18 }
19
20 createEl () {
21 const el = super.createEl()
22
23 this.labelEl_ = videojsUntyped.dom.createEl('div', {
24 className: 'vjs-resolution-value',
25 innerHTML: this.player_.peertube().getCurrentResolutionLabel()
26 })
27
28 el.appendChild(this.labelEl_)
29
30 return el
31 }
32
33 updateARIAAttributes () {
34 this.el().setAttribute('aria-label', 'Quality')
35 }
36
37 createMenu () {
38 const menu = new Menu(this.player())
39
40 for (const videoFile of this.player_.peertube().videoFiles) {
41 menu.addChild(new ResolutionMenuItem(
42 this.player_,
43 {
44 id: videoFile.resolution.id,
45 label: videoFile.resolution.label,
46 src: videoFile.magnetUri
47 })
48 )
49 }
50
51 return menu
52 }
53
54 updateLabel () {
55 if (!this.labelEl_) return
56
57 this.labelEl_.innerHTML = this.player_.peertube().getCurrentResolutionLabel()
58 }
59
60 buildCSSClass () {
61 return super.buildCSSClass() + ' vjs-resolution-button'
62 }
63
64 buildWrapperCSSClass () {
65 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
66 }
67}
68MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts
new file mode 100644
index 000000000..95e0ed1f8
--- /dev/null
+++ b/client/src/assets/player/resolution-menu-item.ts
@@ -0,0 +1,31 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
2
3const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
4class ResolutionMenuItem extends MenuItem {
5
6 constructor (player: videojs.Player, options) {
7 const currentResolutionId = player.peertube().getCurrentResolutionId()
8 options.selectable = true
9 options.selected = options.id === currentResolutionId
10
11 super(player, options)
12
13 this.label = options.label
14 this.id = options.id
15
16 player.peertube().on('videoFileUpdate', () => this.update())
17 }
18
19 handleClick (event) {
20 super.handleClick(event)
21
22 this.player_.peertube().updateResolution(this.id)
23 }
24
25 update () {
26 this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
27 }
28}
29MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
30
31export { ResolutionMenuItem }
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/settings-menu-button.ts
new file mode 100644
index 000000000..c48e1382c
--- /dev/null
+++ b/client/src/assets/player/settings-menu-button.ts
@@ -0,0 +1,285 @@
1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4import * as videojs from 'video.js'
5import { SettingsMenuItem } from './settings-menu-item'
6import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
7import { toTitleCase } from './utils'
8
9const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
10const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
11const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
12
13class SettingsButton extends Button {
14 constructor (player: videojs.Player, options) {
15 super(player, options)
16
17 this.playerComponent = player
18 this.dialog = this.playerComponent.addChild('settingsDialog')
19 this.dialogEl = this.dialog.el_
20 this.menu = null
21 this.panel = this.dialog.addChild('settingsPanel')
22 this.panelChild = this.panel.addChild('settingsPanelChild')
23
24 this.addClass('vjs-settings')
25 this.el_.setAttribute('aria-label', 'Settings Button')
26
27 // Event handlers
28 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
29 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
30 this.playerClickHandler = this.onPlayerClick.bind(this)
31 this.userInactiveHandler = this.onUserInactive.bind(this)
32
33 this.buildMenu()
34 this.bindEvents()
35
36 // Prepare dialog
37 this.player().one('play', () => this.hideDialog())
38 }
39
40 onPlayerClick (event: MouseEvent) {
41 const element = event.target as HTMLElement
42 if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
43 return
44 }
45
46 if (!this.dialog.hasClass('vjs-hidden')) {
47 this.hideDialog()
48 }
49 }
50
51 onDisposeSettingsItem (event, name: string) {
52 if (name === undefined) {
53 let children = this.menu.children()
54
55 while (children.length > 0) {
56 children[0].dispose()
57 this.menu.removeChild(children[0])
58 }
59
60 this.addClass('vjs-hidden')
61 } else {
62 let item = this.menu.getChild(name)
63
64 if (item) {
65 item.dispose()
66 this.menu.removeChild(item)
67 }
68 }
69
70 this.hideDialog()
71
72 if (this.options_.entries.length === 0) {
73 this.addClass('vjs-hidden')
74 }
75 }
76
77 onAddSettingsItem (event, data) {
78 const [ entry, options ] = data
79
80 this.addMenuItem(entry, options)
81 this.removeClass('vjs-hidden')
82 }
83
84 onUserInactive () {
85 if (!this.dialog.hasClass('vjs-hidden')) {
86 this.hideDialog()
87 }
88 }
89
90 bindEvents () {
91 this.playerComponent.on('click', this.playerClickHandler)
92 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler)
93 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler)
94 this.playerComponent.on('userinactive', this.userInactiveHandler)
95 }
96
97 buildCSSClass () {
98 return `vjs-icon-settings ${super.buildCSSClass()}`
99 }
100
101 handleClick () {
102 if (this.dialog.hasClass('vjs-hidden')) {
103 this.showDialog()
104 } else {
105 this.hideDialog()
106 }
107 }
108
109 showDialog () {
110 this.menu.el_.style.opacity = '1'
111 this.dialog.show()
112
113 this.setDialogSize(this.getComponentSize(this.menu))
114 }
115
116 hideDialog () {
117 this.dialog.hide()
118 this.setDialogSize(this.getComponentSize(this.menu))
119 this.menu.el_.style.opacity = '1'
120 this.resetChildren()
121 }
122
123 getComponentSize (element) {
124 let width: number = null
125 let height: number = null
126
127 // Could be component or just DOM element
128 if (element instanceof Component) {
129 width = element.el_.offsetWidth
130 height = element.el_.offsetHeight
131
132 // keep width/height as properties for direct use
133 element.width = width
134 element.height = height
135 } else {
136 width = element.offsetWidth
137 height = element.offsetHeight
138 }
139
140 return [ width, height ]
141 }
142
143 setDialogSize ([ width, height ]: number[]) {
144 if (typeof height !== 'number') {
145 return
146 }
147
148 let offset = this.options_.setup.maxHeightOffset
149 let maxHeight = this.playerComponent.el_.offsetHeight - offset
150
151 if (height > maxHeight) {
152 height = maxHeight
153 width += 17
154 this.panel.el_.style.maxHeight = `${height}px`
155 } else if (this.panel.el_.style.maxHeight !== '') {
156 this.panel.el_.style.maxHeight = ''
157 }
158
159 this.dialogEl.style.width = `${width}px`
160 this.dialogEl.style.height = `${height}px`
161 }
162
163 buildMenu () {
164 this.menu = new Menu(this.player())
165 this.menu.addClass('vjs-main-menu')
166 let entries = this.options_.entries
167
168 if (entries.length === 0) {
169 this.addClass('vjs-hidden')
170 this.panelChild.addChild(this.menu)
171 return
172 }
173
174 for (let entry of entries) {
175 this.addMenuItem(entry, this.options_)
176 }
177
178 this.panelChild.addChild(this.menu)
179 }
180
181 addMenuItem (entry, options) {
182 const openSubMenu = function () {
183 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
184 videojsUntyped.dom.removeClass(this.el_, 'open')
185 } else {
186 videojsUntyped.dom.addClass(this.el_, 'open')
187 }
188 }
189
190 options.name = toTitleCase(entry)
191 let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
192
193 this.menu.addChild(settingsMenuItem)
194
195 // Hide children to avoid sub menus stacking on top of each other
196 // or having multiple menus open
197 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
198
199 // Whether to add or remove selected class on the settings sub menu element
200 settingsMenuItem.on('click', openSubMenu)
201 }
202
203 resetChildren () {
204 for (let menuChild of this.menu.children()) {
205 menuChild.reset()
206 }
207 }
208
209 /**
210 * Hide all the sub menus
211 */
212 hideChildren () {
213 for (let menuChild of this.menu.children()) {
214 menuChild.hideSubMenu()
215 }
216 }
217
218}
219
220class SettingsPanel extends Component {
221 constructor (player: videojs.Player, options) {
222 super(player, options)
223 }
224
225 createEl () {
226 return super.createEl('div', {
227 className: 'vjs-settings-panel',
228 innerHTML: '',
229 tabIndex: -1
230 })
231 }
232}
233
234class SettingsPanelChild extends Component {
235 constructor (player: videojs.Player, options) {
236 super(player, options)
237 }
238
239 createEl () {
240 return super.createEl('div', {
241 className: 'vjs-settings-panel-child',
242 innerHTML: '',
243 tabIndex: -1
244 })
245 }
246}
247
248class SettingsDialog extends Component {
249 constructor (player: videojs.Player, options) {
250 super(player, options)
251 this.hide()
252 }
253
254 /**
255 * Create the component's DOM element
256 *
257 * @return {Element}
258 * @method createEl
259 */
260 createEl () {
261 const uniqueId = this.id_
262 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
263 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
264
265 return super.createEl('div', {
266 className: 'vjs-settings-dialog vjs-modal-overlay',
267 innerHTML: '',
268 tabIndex: -1
269 }, {
270 'role': 'dialog',
271 'aria-labelledby': dialogLabelId,
272 'aria-describedby': dialogDescriptionId
273 })
274 }
275
276}
277
278SettingsButton.prototype.controlText_ = 'Settings Button'
279
280Component.registerComponent('SettingsButton', SettingsButton)
281Component.registerComponent('SettingsDialog', SettingsDialog)
282Component.registerComponent('SettingsPanel', SettingsPanel)
283Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
284
285export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/settings-menu-item.ts
new file mode 100644
index 000000000..e979ae088
--- /dev/null
+++ b/client/src/assets/player/settings-menu-item.ts
@@ -0,0 +1,313 @@
1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4import { toTitleCase } from './utils'
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
9
10class SettingsMenuItem extends MenuItem {
11
12 constructor (player: videojs.Player, options, entry: string, menuButton: VideoJSComponentInterface) {
13 super(player, options)
14
15 this.settingsButton = menuButton
16 this.dialog = this.settingsButton.dialog
17 this.mainMenu = this.settingsButton.menu
18 this.panel = this.dialog.getChild('settingsPanel')
19 this.panelChild = this.panel.getChild('settingsPanelChild')
20 this.panelChildEl = this.panelChild.el_
21
22 this.size = null
23
24 // keep state of what menu type is loading next
25 this.menuToLoad = 'mainmenu'
26
27 const subMenuName = toTitleCase(entry)
28 const SubMenuComponent = videojsUntyped.getComponent(subMenuName)
29
30 if (!SubMenuComponent) {
31 throw new Error(`Component ${subMenuName} does not exist`)
32 }
33 this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this)
34
35 this.eventHandlers()
36
37 player.ready(() => {
38 this.build()
39 this.reset()
40 })
41 }
42
43 eventHandlers () {
44 this.submenuClickHandler = this.onSubmenuClick.bind(this)
45 this.transitionEndHandler = this.onTransitionEnd.bind(this)
46 }
47
48 onSubmenuClick (event) {
49 let target = null
50
51 if (event.type === 'tap') {
52 target = event.target
53 } else {
54 target = event.currentTarget
55 }
56
57 if (target.classList.contains('vjs-back-button')) {
58 this.loadMainMenu()
59 return
60 }
61
62 // To update the sub menu value on click, setTimeout is needed because
63 // updating the value is not instant
64 setTimeout(() => this.update(event), 0)
65 }
66
67 /**
68 * Create the component's DOM element
69 *
70 * @return {Element}
71 * @method createEl
72 */
73 createEl () {
74 const el = videojsUntyped.dom.createEl('li', {
75 className: 'vjs-menu-item'
76 })
77
78 this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', {
79 className: 'vjs-settings-sub-menu-title'
80 })
81
82 el.appendChild(this.settingsSubMenuTitleEl_)
83
84 this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', {
85 className: 'vjs-settings-sub-menu-value'
86 })
87
88 el.appendChild(this.settingsSubMenuValueEl_)
89
90 this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', {
91 className: 'vjs-settings-sub-menu'
92 })
93
94 return el
95 }
96
97 /**
98 * Handle click on menu item
99 *
100 * @method handleClick
101 */
102 handleClick () {
103 this.menuToLoad = 'submenu'
104 // Remove open class to ensure only the open submenu gets this class
105 videojsUntyped.dom.removeClass(this.el_, 'open')
106
107 super.handleClick()
108
109 this.mainMenu.el_.style.opacity = '0'
110 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
111 if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
112 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
113
114 // animation not played without timeout
115 setTimeout(() => {
116 this.settingsSubMenuEl_.style.opacity = '1'
117 this.settingsSubMenuEl_.style.marginRight = '0px'
118 }, 0)
119
120 this.settingsButton.setDialogSize(this.size)
121 } else {
122 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
123 }
124 }
125
126 /**
127 * Create back button
128 *
129 * @method createBackButton
130 */
131 createBackButton () {
132 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
133 button.name_ = 'BackButton'
134 button.addClass('vjs-back-button')
135 button.el_.innerHTML = this.subMenu.controlText_
136 }
137
138 /**
139 * Add/remove prefixed event listener for CSS Transition
140 *
141 * @method PrefixedEvent
142 */
143 PrefixedEvent (element, type, callback, action = 'addEvent') {
144 let prefix = ['webkit', 'moz', 'MS', 'o', '']
145
146 for (let p = 0; p < prefix.length; p++) {
147 if (!prefix[p]) {
148 type = type.toLowerCase()
149 }
150
151 if (action === 'addEvent') {
152 element.addEventListener(prefix[p] + type, callback, false)
153 } else if (action === 'removeEvent') {
154 element.removeEventListener(prefix[p] + type, callback, false)
155 }
156 }
157 }
158
159 onTransitionEnd (event) {
160 if (event.propertyName !== 'margin-right') {
161 return
162 }
163
164 if (this.menuToLoad === 'mainmenu') {
165 // hide submenu
166 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
167
168 // reset opacity to 0
169 this.settingsSubMenuEl_.style.opacity = '0'
170 }
171 }
172
173 reset () {
174 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
175 this.settingsSubMenuEl_.style.opacity = '0'
176 this.setMargin()
177 }
178
179 loadMainMenu () {
180 this.menuToLoad = 'mainmenu'
181 this.mainMenu.show()
182 this.mainMenu.el_.style.opacity = '0'
183
184 // back button will always take you to main menu, so set dialog sizes
185 this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height])
186
187 // animation not triggered without timeout (some async stuff ?!?)
188 setTimeout(() => {
189 // animate margin and opacity before hiding the submenu
190 // this triggers CSS Transition event
191 this.setMargin()
192 this.mainMenu.el_.style.opacity = '1'
193 }, 0)
194 }
195
196 build () {
197 const saveUpdateLabel = this.subMenu.updateLabel
198 this.subMenu.updateLabel = () => {
199 this.update()
200
201 saveUpdateLabel.call(this.subMenu)
202 }
203
204 this.settingsSubMenuTitleEl_.innerHTML = this.subMenu.controlText_
205 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
206 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
207 this.update()
208
209 this.createBackButton()
210 this.getSize()
211 this.bindClickEvents()
212
213 // prefixed event listeners for CSS TransitionEnd
214 this.PrefixedEvent(
215 this.settingsSubMenuEl_,
216 'TransitionEnd',
217 this.transitionEndHandler,
218 'addEvent'
219 )
220 }
221
222 update (event?: Event) {
223 let target = null
224 let subMenu = this.subMenu.name()
225
226 if (event && event.type === 'tap') {
227 target = event.target
228 } else if (event) {
229 target = event.currentTarget
230 }
231
232 // Playback rate menu button doesn't get a vjs-selected class
233 // or sets options_['selected'] on the selected playback rate.
234 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
235 if (subMenu === 'PlaybackRateMenuButton') {
236 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250)
237 } else {
238 // Loop trough the submenu items to find the selected child
239 for (let subMenuItem of this.subMenu.menu.children_) {
240 if (!(subMenuItem instanceof component)) {
241 continue
242 }
243
244 switch (subMenu) {
245 case 'SubtitlesButton':
246 case 'CaptionsButton':
247 // subtitlesButton entering default check twice and overwriting
248 // selected label in main manu
249 if (subMenuItem.hasClass('vjs-selected')) {
250 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
251 }
252 break
253
254 default:
255 // Set submenu value based on what item is selected
256 if (subMenuItem.options_.selected || subMenuItem.hasClass('vjs-selected')) {
257 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
258 }
259 }
260 }
261 }
262
263 if (target && !target.classList.contains('vjs-back-button')) {
264 this.settingsButton.hideDialog()
265 }
266 }
267
268 bindClickEvents () {
269 for (let item of this.subMenu.menu.children()) {
270 if (!(item instanceof component)) {
271 continue
272 }
273 item.on(['tap', 'click'], this.submenuClickHandler)
274 }
275 }
276
277 // save size of submenus on first init
278 // if number of submenu items change dynamically more logic will be needed
279 getSize () {
280 this.dialog.removeClass('vjs-hidden')
281 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
282 this.setMargin()
283 this.dialog.addClass('vjs-hidden')
284 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
285 }
286
287 setMargin () {
288 let [width] = this.size
289
290 this.settingsSubMenuEl_.style.marginRight = `-${width}px`
291 }
292
293 /**
294 * Hide the sub menu
295 */
296 hideSubMenu () {
297 // after removing settings item this.el_ === null
298 if (!this.el_) {
299 return
300 }
301
302 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
303 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
304 videojsUntyped.dom.removeClass(this.el_, 'open')
305 }
306 }
307
308}
309
310SettingsMenuItem.prototype.contentElType = 'button'
311videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem)
312
313export { SettingsMenuItem }
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
new file mode 100644
index 000000000..7a99dba1a
--- /dev/null
+++ b/client/src/assets/player/utils.ts
@@ -0,0 +1,72 @@
1function toTitleCase (str: string) {
2 return str.charAt(0).toUpperCase() + str.slice(1)
3}
4
5// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
6// Don't import all Angular stuff, just copy the code with shame
7const dictionaryBytes: Array<{max: number, type: string}> = [
8 { max: 1024, type: 'B' },
9 { max: 1048576, type: 'KB' },
10 { max: 1073741824, type: 'MB' },
11 { max: 1.0995116e12, type: 'GB' }
12]
13function bytes (value) {
14 const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
15 const calc = Math.floor(value / (format.max / 1024)).toString()
16
17 return [ calc, format.type ]
18}
19
20function getStoredVolume () {
21 const value = getLocalStorage('volume')
22 if (value !== null && value !== undefined) {
23 const valueNumber = parseFloat(value)
24 if (isNaN(valueNumber)) return undefined
25
26 return valueNumber
27 }
28
29 return undefined
30}
31
32function getStoredMute () {
33 const value = getLocalStorage('mute')
34 if (value !== null && value !== undefined) return value === 'true'
35
36 return undefined
37}
38
39function saveVolumeInStore (value: number) {
40 return setLocalStorage('volume', value.toString())
41}
42
43function saveMuteInStore (value: boolean) {
44 return setLocalStorage('mute', value.toString())
45}
46
47export {
48 toTitleCase,
49 getStoredVolume,
50 saveVolumeInStore,
51 saveMuteInStore,
52 getStoredMute,
53 bytes
54}
55
56// ---------------------------------------------------------------------------
57
58const KEY_PREFIX = 'peertube-videojs-'
59
60function getLocalStorage (key: string) {
61 try {
62 return localStorage.getItem(KEY_PREFIX + key)
63 } catch {
64 return undefined
65 }
66}
67
68function setLocalStorage (key: string, value: string) {
69 try {
70 localStorage.setItem(KEY_PREFIX + key, value)
71 } catch { /* empty */ }
72}
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/webtorrent-info-button.ts
new file mode 100644
index 000000000..8a79e0e50
--- /dev/null
+++ b/client/src/assets/player/webtorrent-info-button.ts
@@ -0,0 +1,101 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
2import { bytes } from './utils'
3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class WebtorrentInfoButton extends Button {
6 createEl () {
7 const div = videojsUntyped.dom.createEl('div', {
8 className: 'vjs-peertube'
9 })
10 const subDivWebtorrent = videojsUntyped.dom.createEl('div', {
11 className: 'vjs-peertube-hidden' // Hide the stats before we get the info
12 })
13 div.appendChild(subDivWebtorrent)
14
15 const downloadIcon = videojsUntyped.dom.createEl('span', {
16 className: 'icon icon-download'
17 })
18 subDivWebtorrent.appendChild(downloadIcon)
19
20 const downloadSpeedText = videojsUntyped.dom.createEl('span', {
21 className: 'download-speed-text'
22 })
23 const downloadSpeedNumber = videojsUntyped.dom.createEl('span', {
24 className: 'download-speed-number'
25 })
26 const downloadSpeedUnit = videojsUntyped.dom.createEl('span')
27 downloadSpeedText.appendChild(downloadSpeedNumber)
28 downloadSpeedText.appendChild(downloadSpeedUnit)
29 subDivWebtorrent.appendChild(downloadSpeedText)
30
31 const uploadIcon = videojsUntyped.dom.createEl('span', {
32 className: 'icon icon-upload'
33 })
34 subDivWebtorrent.appendChild(uploadIcon)
35
36 const uploadSpeedText = videojsUntyped.dom.createEl('span', {
37 className: 'upload-speed-text'
38 })
39 const uploadSpeedNumber = videojsUntyped.dom.createEl('span', {
40 className: 'upload-speed-number'
41 })
42 const uploadSpeedUnit = videojsUntyped.dom.createEl('span')
43 uploadSpeedText.appendChild(uploadSpeedNumber)
44 uploadSpeedText.appendChild(uploadSpeedUnit)
45 subDivWebtorrent.appendChild(uploadSpeedText)
46
47 const peersText = videojsUntyped.dom.createEl('span', {
48 className: 'peers-text'
49 })
50 const peersNumber = videojsUntyped.dom.createEl('span', {
51 className: 'peers-number'
52 })
53 subDivWebtorrent.appendChild(peersNumber)
54 subDivWebtorrent.appendChild(peersText)
55
56 const subDivHttp = videojsUntyped.dom.createEl('div', {
57 className: 'vjs-peertube-hidden'
58 })
59 const subDivHttpText = videojsUntyped.dom.createEl('span', {
60 className: 'peers-number',
61 textContent: 'HTTP'
62 })
63 const subDivFallbackText = videojsUntyped.dom.createEl('span', {
64 className: 'peers-text',
65 textContent: 'fallback'
66 })
67
68 subDivHttp.appendChild(subDivHttpText)
69 subDivHttp.appendChild(subDivFallbackText)
70 div.appendChild(subDivHttp)
71
72 this.player_.peertube().on('torrentInfo', (event, data) => {
73 // We are in HTTP fallback
74 if (!data) {
75 subDivHttp.className = 'vjs-peertube-displayed'
76 subDivWebtorrent.className = 'vjs-peertube-hidden'
77
78 return
79 }
80
81 const downloadSpeed = bytes(data.downloadSpeed)
82 const uploadSpeed = bytes(data.uploadSpeed)
83 const numPeers = data.numPeers
84
85 downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
86 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
87
88 uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
89 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
90
91 peersNumber.textContent = numPeers
92 peersText.textContent = ' peers'
93
94 subDivHttp.className = 'vjs-peertube-hidden'
95 subDivWebtorrent.className = 'vjs-peertube-displayed'
96 })
97
98 return div
99 }
100}
101Button.registerComponent('WebTorrentButton', WebtorrentInfoButton)