aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2021-04-12 10:26:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-04-28 09:05:44 +0200
commitff563914bb10728301a24fb9e548c9efb62387eb (patch)
treec998ad721f134404d3510cff2906e88ea03d8301
parent0979075453b380fa8e3694db3f460e822f046c35 (diff)
downloadPeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.tar.gz
PeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.tar.zst
PeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.zip
add stats videojs plugin
-rw-r--r--client/src/assets/player/images/info.svg1
-rw-r--r--client/src/assets/player/peertube-player-local-storage.ts1
-rw-r--r--client/src/assets/player/peertube-player-manager.ts15
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts3
-rw-r--r--client/src/assets/player/stats/stats-card.ts184
-rw-r--r--client/src/assets/player/stats/stats-plugin.ts31
-rw-r--r--client/src/sass/player/context-menu.scss4
-rw-r--r--client/src/sass/player/index.scss1
-rw-r--r--client/src/sass/player/stats.scss42
9 files changed, 280 insertions, 2 deletions
diff --git a/client/src/assets/player/images/info.svg b/client/src/assets/player/images/info.svg
new file mode 100644
index 000000000..bd1d9c6ca
--- /dev/null
+++ b/client/src/assets/player/images/info.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> \ No newline at end of file
diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts
index cf2cfb472..80aceb239 100644
--- a/client/src/assets/player/peertube-player-local-storage.ts
+++ b/client/src/assets/player/peertube-player-local-storage.ts
@@ -45,6 +45,7 @@ function saveTheaterInStore (enabled: boolean) {
45} 45}
46 46
47function saveAverageBandwidth (value: number) { 47function saveAverageBandwidth (value: number) {
48 /** used to choose the most fitting resolution */
48 return setLocalStorage('average-bandwidth', value.toString()) 49 return setLocalStorage('average-bandwidth', value.toString())
49} 50}
50 51
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index ed82e0496..62dff8285 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -4,6 +4,8 @@ import 'videojs-contextmenu-pt'
4import 'videojs-contrib-quality-levels' 4import 'videojs-contrib-quality-levels'
5import './upnext/end-card' 5import './upnext/end-card'
6import './upnext/upnext-plugin' 6import './upnext/upnext-plugin'
7import './stats/stats-card'
8import './stats/stats-plugin'
7import './bezels/bezels-plugin' 9import './bezels/bezels-plugin'
8import './peertube-plugin' 10import './peertube-plugin'
9import './videojs-components/next-previous-video-button' 11import './videojs-components/next-previous-video-button'
@@ -170,6 +172,11 @@ export class PeertubePlayerManager {
170 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) 172 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
171 173
172 player.bezels() 174 player.bezels()
175 player.stats({
176 videoUUID: options.common.videoUUID,
177 videoIsLive: options.common.isLive,
178 mode
179 })
173 180
174 return res(player) 181 return res(player)
175 }) 182 })
@@ -538,6 +545,14 @@ export class PeertubePlayerManager {
538 }) 545 })
539 } 546 }
540 547
548 items.push({
549 icon: 'info',
550 label: player.localize('Stats for nerds'),
551 listener: () => {
552 player.stats().show()
553 }
554 })
555
541 return items.map(i => ({ 556 return items.map(i => ({
542 ...i, 557 ...i,
543 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label 558 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index 4a6c80247..cf92e5f08 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -7,6 +7,7 @@ import { PlayerMode } from './peertube-player-manager'
7import { PeerTubePlugin } from './peertube-plugin' 7import { PeerTubePlugin } from './peertube-plugin'
8import { PlaylistPlugin } from './playlist/playlist-plugin' 8import { PlaylistPlugin } from './playlist/playlist-plugin'
9import { EndCardOptions } from './upnext/end-card' 9import { EndCardOptions } from './upnext/end-card'
10import { StatsCardOptions } from './stats/stats-card'
10import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' 11import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
11 12
12declare module 'video.js' { 13declare module 'video.js' {
@@ -36,6 +37,8 @@ declare module 'video.js' {
36 37
37 bezels (): void 38 bezels (): void
38 39
40 stats (options?: Partial<StatsCardOptions>): any
41
39 qualityLevels (): QualityLevels 42 qualityLevels (): QualityLevels
40 43
41 textTracks (): TextTrackList & { 44 textTracks (): TextTrackList & {
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts
new file mode 100644
index 000000000..278899b72
--- /dev/null
+++ b/client/src/assets/player/stats/stats-card.ts
@@ -0,0 +1,184 @@
1import videojs from 'video.js'
2import { PlayerNetworkInfo } from '../peertube-videojs-typings'
3import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
4import { bytes } from '../utils'
5
6interface StatsCardOptions extends videojs.ComponentOptions {
7 videoUUID?: string,
8 videoIsLive?: boolean,
9 mode?: 'webtorrent' | 'p2p-media-loader'
10}
11
12function getListTemplate (
13 options: StatsCardOptions,
14 player: videojs.Player,
15 args: {
16 playerNetworkInfo?: any
17 videoFile?: any
18 progress?: number
19 }) {
20 const { playerNetworkInfo, videoFile, progress } = args
21
22 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
23 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
24 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
25 const pr = (window.devicePixelRatio || 1).toFixed(2)
26 const colorspace = videoFile?.metadata?.streams[0]['color_space'] !== "unknown"
27 ? videoFile?.metadata?.streams[0]['color_space']
28 : undefined
29
30 return `
31 <div>
32 <div>${player.localize('Video UUID')}</div>
33 <span>${options.videoUUID || ''}</span>
34 </div>
35 <div>
36 <div>Viewport / ${player.localize('Frames')}</div>
37 <span>${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}</span>
38 </div>
39 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
40 <div>${player.localize('Resolution')}</div>
41 <span>${videoFile?.resolution.label + videoFile?.fps}</span>
42 </div>
43 <div>
44 <div>${player.localize('Volume')}</div>
45 <span>${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''}</span>
46 </div>
47 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
48 <div>${player.localize('Codecs')}</div>
49 <span>${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'}</span>
50 </div>
51 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
52 <div>${player.localize('Color')}</div>
53 <span>${colorspace || 'bt709'}</span>
54 </div>
55 <div${playerNetworkInfo.averageBandwidth !== undefined ? '' : ' style="display: none;"'}>
56 <div>${player.localize('Connection Speed')}</div>
57 <span>${playerNetworkInfo.averageBandwidth}</span>
58 </div>
59 <div${playerNetworkInfo.downloadSpeed !== undefined ? '' : ' style="display: none;"'}>
60 <div>${player.localize('Network Activity')}</div>
61 <span>${playerNetworkInfo.downloadSpeed} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;</span>
62 </div>
63 <div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
64 <div>${player.localize('Total Transfered')}</div>
65 <span>${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;</span>
66 </div>
67 <div${playerNetworkInfo.downloadedFromServer ? '' : ' style="display: none;"'}>
68 <div>${player.localize('Download Breakdown')}</div>
69 <span>${playerNetworkInfo.downloadedFromServer} from server ยท ${playerNetworkInfo.downloadedFromPeers} from peers</span>
70 </div>
71 <div${progress !== undefined && videoFile !== undefined ? '' : ' style="display: none;"'}>
72 <div>${player.localize('Buffer Health')}</div>
73 <span>${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s)</span>
74 </div>
75 <div style="display: none;"> <!-- TODO: implement live latency measure -->
76 <div>${player.localize('Live Latency')}</div>
77 <span></span>
78 </div>
79 `
80}
81
82function getMainTemplate () {
83 return `
84 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
85 <div class="vjs-stats-list"></div>
86 `
87}
88
89const Component = videojs.getComponent('Component')
90class StatsCard extends Component {
91 options_: StatsCardOptions
92 container: HTMLDivElement
93 list: HTMLDivElement
94 closeButton: HTMLElement
95 update: any
96 source: any
97
98 interval = 300
99 playerNetworkInfo: any = {}
100 statsForNerdsEvents = new videojs.EventTarget()
101
102 constructor (player: videojs.Player, options: StatsCardOptions) {
103 super(player, options)
104 }
105
106 createEl () {
107 const container = super.createEl('div', {
108 className: 'vjs-stats-content',
109 innerHTML: getMainTemplate()
110 }) as HTMLDivElement
111 this.container = container
112 this.container.style.display = 'none'
113
114 this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
115 this.closeButton.onclick = () => this.hide()
116
117 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
118
119 console.log(this.player_.qualityLevels())
120
121 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
122 if (!data) return // HTTP fallback
123
124 this.source = data.source
125
126 const p2pStats = data.p2p
127 const httpStats = data.http
128
129 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
130 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
131 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
132 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
133 this.playerNetworkInfo.numPeers = p2pStats.numPeers
134 this.playerNetworkInfo.averageBandwidth = bytes(getAverageBandwidthInStore() || p2pStats.downloaded + httpStats.downloaded).join(' ')
135
136 if (data.source === 'p2p-media-loader') {
137 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
138 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
139 }
140 })
141
142 return container
143 }
144
145 toggle () {
146 this.update
147 ? this.hide()
148 : this.show()
149 }
150
151 show (options?: StatsCardOptions) {
152 if (options) this.options_ = options
153
154 let metadata = {}
155
156 this.container.style.display = 'block'
157 this.update = setInterval(async () => {
158 try {
159 if (this.source === 'webtorrent') {
160 const progress = this.player_.webtorrent().getTorrent()?.progress
161 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
162 videoFile.metadata = metadata[videoFile.fileUrl] = videoFile.metadata || metadata[videoFile.fileUrl] || videoFile.metadataUrl && await fetch(videoFile.metadataUrl).then(res => res.json())
163 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo, videoFile, progress })
164 } else {
165 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo })
166 }
167 } catch (e) {
168 clearInterval(this.update)
169 }
170 }, this.interval)
171 }
172
173 hide () {
174 clearInterval(this.update)
175 this.container.style.display = 'none'
176 }
177}
178
179videojs.registerComponent('StatsCard', StatsCard)
180
181export {
182 StatsCard,
183 StatsCardOptions
184}
diff --git a/client/src/assets/player/stats/stats-plugin.ts b/client/src/assets/player/stats/stats-plugin.ts
new file mode 100644
index 000000000..3402e7861
--- /dev/null
+++ b/client/src/assets/player/stats/stats-plugin.ts
@@ -0,0 +1,31 @@
1import videojs from 'video.js'
2import { StatsCard, StatsCardOptions } from './stats-card'
3
4const Plugin = videojs.getPlugin('plugin')
5
6class StatsForNerdsPlugin extends Plugin {
7 private statsCard: StatsCard
8
9 constructor (player: videojs.Player, options: Partial<StatsCardOptions> = {}) {
10 const settings = {
11 ...options
12 }
13
14 super(player)
15
16 this.player.ready(() => {
17 player.addClass('vjs-stats-for-nerds')
18 })
19
20 this.statsCard = new StatsCard(player, options)
21
22 player.addChild(this.statsCard, settings)
23 }
24
25 show (options?: StatsCardOptions) {
26 this.statsCard.show(options)
27 }
28}
29
30videojs.registerPlugin('stats', StatsForNerdsPlugin)
31export { StatsForNerdsPlugin }
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss
index df78916c6..6bc66af0c 100644
--- a/client/src/sass/player/context-menu.scss
+++ b/client/src/sass/player/context-menu.scss
@@ -8,7 +8,7 @@ $context-menu-width: 350px;
8 8
9.video-js .vjs-contextmenu-ui-menu { 9.video-js .vjs-contextmenu-ui-menu {
10 position: absolute; 10 position: absolute;
11 background-color: rgba(0, 0, 0, 0.5); 11 background-color: $primary-background-color;
12 padding: 8px 0; 12 padding: 8px 0;
13 border-radius: 4px; 13 border-radius: 4px;
14 width: $context-menu-width; 14 width: $context-menu-width;
@@ -42,7 +42,7 @@ $context-menu-width: 350px;
42 mask-size: cover; 42 mask-size: cover;
43 margin-right: 0.8rem !important; 43 margin-right: 0.8rem !important;
44 44
45 $icons: 'link-2', 'repeat', 'code', 'tick-white'; 45 $icons: 'link-2', 'repeat', 'code', 'tick-white', 'info';
46 46
47 @each $icon in $icons { 47 @each $icon in $icons {
48 &[class$="-#{$icon}"] { 48 &[class$="-#{$icon}"] {
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss
index fe92ce5e0..502ee33ff 100644
--- a/client/src/sass/player/index.scss
+++ b/client/src/sass/player/index.scss
@@ -6,3 +6,4 @@
6@import './upnext'; 6@import './upnext';
7@import './bezels.scss'; 7@import './bezels.scss';
8@import './playlist.scss'; 8@import './playlist.scss';
9@import './stats.scss';
diff --git a/client/src/sass/player/stats.scss b/client/src/sass/player/stats.scss
new file mode 100644
index 000000000..953f6032a
--- /dev/null
+++ b/client/src/sass/player/stats.scss
@@ -0,0 +1,42 @@
1@import './_player-variables';
2
3$stats-width: 420px;
4$contextmenu-background-color: rgba(0, 0, 0, 0.6);
5
6.video-js {
7
8 .vjs-stats-content {
9 position: absolute;
10 background-color: $contextmenu-background-color;
11 padding: 5px 0;
12 border-radius: 4px;
13 width: $stats-width;
14 min-width: 27em;
15 max-width: calc(100vw - 20px);
16 left: 10px;
17 top: 10px;
18 z-index: 64;
19 font-size: 12px;
20 line-height: 1.2;
21
22 @include transition(opacity 0.1s);
23 }
24
25 .vjs-stats-close {
26 cursor: pointer;
27 position: absolute;
28 right: 3px;
29 top: 3px;
30 padding: 0;
31 }
32
33 .vjs-stats-list > div > div {
34 display: inline-block;
35 font-weight: 600;
36 padding: 0 .5em;
37 text-align: right;
38 width: 11.5em;
39 white-space: nowrap;
40 }
41
42}