diff options
Diffstat (limited to 'client/src/assets/player/stats')
-rw-r--r-- | client/src/assets/player/stats/stats-card.ts | 184 | ||||
-rw-r--r-- | client/src/assets/player/stats/stats-plugin.ts | 31 |
2 files changed, 215 insertions, 0 deletions
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 } | ||