diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-04-12 10:26:30 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-04-28 09:05:44 +0200 |
commit | ff563914bb10728301a24fb9e548c9efb62387eb (patch) | |
tree | c998ad721f134404d3510cff2906e88ea03d8301 | |
parent | 0979075453b380fa8e3694db3f460e822f046c35 (diff) | |
download | PeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.tar.gz PeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.tar.zst PeerTube-ff563914bb10728301a24fb9e548c9efb62387eb.zip |
add stats videojs plugin
-rw-r--r-- | client/src/assets/player/images/info.svg | 1 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-local-storage.ts | 1 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 15 | ||||
-rw-r--r-- | client/src/assets/player/peertube-videojs-typings.ts | 3 | ||||
-rw-r--r-- | client/src/assets/player/stats/stats-card.ts | 184 | ||||
-rw-r--r-- | client/src/assets/player/stats/stats-plugin.ts | 31 | ||||
-rw-r--r-- | client/src/sass/player/context-menu.scss | 4 | ||||
-rw-r--r-- | client/src/sass/player/index.scss | 1 | ||||
-rw-r--r-- | client/src/sass/player/stats.scss | 42 |
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 | ||
47 | function saveAverageBandwidth (value: number) { | 47 | function 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' | |||
4 | import 'videojs-contrib-quality-levels' | 4 | import 'videojs-contrib-quality-levels' |
5 | import './upnext/end-card' | 5 | import './upnext/end-card' |
6 | import './upnext/upnext-plugin' | 6 | import './upnext/upnext-plugin' |
7 | import './stats/stats-card' | ||
8 | import './stats/stats-plugin' | ||
7 | import './bezels/bezels-plugin' | 9 | import './bezels/bezels-plugin' |
8 | import './peertube-plugin' | 10 | import './peertube-plugin' |
9 | import './videojs-components/next-previous-video-button' | 11 | import './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' | |||
7 | import { PeerTubePlugin } from './peertube-plugin' | 7 | import { PeerTubePlugin } from './peertube-plugin' |
8 | import { PlaylistPlugin } from './playlist/playlist-plugin' | 8 | import { PlaylistPlugin } from './playlist/playlist-plugin' |
9 | import { EndCardOptions } from './upnext/end-card' | 9 | import { EndCardOptions } from './upnext/end-card' |
10 | import { StatsCardOptions } from './stats/stats-card' | ||
10 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | 11 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' |
11 | 12 | ||
12 | declare module 'video.js' { | 13 | declare 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PlayerNetworkInfo } from '../peertube-videojs-typings' | ||
3 | import { getAverageBandwidthInStore } from '../peertube-player-local-storage' | ||
4 | import { bytes } from '../utils' | ||
5 | |||
6 | interface StatsCardOptions extends videojs.ComponentOptions { | ||
7 | videoUUID?: string, | ||
8 | videoIsLive?: boolean, | ||
9 | mode?: 'webtorrent' | 'p2p-media-loader' | ||
10 | } | ||
11 | |||
12 | function 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} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑</span> | ||
62 | </div> | ||
63 | <div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}> | ||
64 | <div>${player.localize('Total Transfered')}</div> | ||
65 | <span>${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑</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 | |||
82 | function 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 | |||
89 | const Component = videojs.getComponent('Component') | ||
90 | class 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 | |||
179 | videojs.registerComponent('StatsCard', StatsCard) | ||
180 | |||
181 | export { | ||
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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { StatsCard, StatsCardOptions } from './stats-card' | ||
3 | |||
4 | const Plugin = videojs.getPlugin('plugin') | ||
5 | |||
6 | class 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 | |||
30 | videojs.registerPlugin('stats', StatsForNerdsPlugin) | ||
31 | export { 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 | } | ||