diff options
Diffstat (limited to 'client/src/assets/player/stats')
-rw-r--r-- | client/src/assets/player/stats/stats-card.ts | 271 | ||||
-rw-r--r-- | client/src/assets/player/stats/stats-plugin.ts | 31 |
2 files changed, 0 insertions, 302 deletions
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts deleted file mode 100644 index e76a81a74..000000000 --- a/client/src/assets/player/stats/stats-card.ts +++ /dev/null | |||
@@ -1,271 +0,0 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { secondsToTime } from '@shared/core-utils' | ||
3 | import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings' | ||
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 | p2pEnabled: boolean | ||
11 | } | ||
12 | |||
13 | interface PlayerNetworkInfo { | ||
14 | downloadSpeed?: string | ||
15 | uploadSpeed?: string | ||
16 | totalDownloaded?: string | ||
17 | totalUploaded?: string | ||
18 | numPeers?: number | ||
19 | averageBandwidth?: string | ||
20 | |||
21 | downloadedFromServer?: string | ||
22 | downloadedFromPeers?: string | ||
23 | } | ||
24 | |||
25 | const Component = videojs.getComponent('Component') | ||
26 | class StatsCard extends Component { | ||
27 | options_: StatsCardOptions | ||
28 | |||
29 | container: HTMLDivElement | ||
30 | |||
31 | list: HTMLDivElement | ||
32 | closeButton: HTMLElement | ||
33 | |||
34 | updateInterval: any | ||
35 | |||
36 | mode: 'webtorrent' | 'p2p-media-loader' | ||
37 | |||
38 | metadataStore: any = {} | ||
39 | |||
40 | intervalMs = 300 | ||
41 | playerNetworkInfo: PlayerNetworkInfo = {} | ||
42 | |||
43 | createEl () { | ||
44 | const container = super.createEl('div', { | ||
45 | className: 'vjs-stats-content', | ||
46 | innerHTML: this.getMainTemplate() | ||
47 | }) as HTMLDivElement | ||
48 | this.container = container | ||
49 | this.container.style.display = 'none' | ||
50 | |||
51 | this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement | ||
52 | this.closeButton.onclick = () => this.hide() | ||
53 | |||
54 | this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement | ||
55 | |||
56 | this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => { | ||
57 | if (!data) return // HTTP fallback | ||
58 | |||
59 | this.mode = data.source | ||
60 | |||
61 | const p2pStats = data.p2p | ||
62 | const httpStats = data.http | ||
63 | |||
64 | this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ') | ||
65 | this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ') | ||
66 | this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') | ||
67 | this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ') | ||
68 | this.playerNetworkInfo.numPeers = p2pStats.numPeers | ||
69 | this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s' | ||
70 | |||
71 | if (data.source === 'p2p-media-loader') { | ||
72 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') | ||
73 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | return container | ||
78 | } | ||
79 | |||
80 | toggle () { | ||
81 | if (this.updateInterval) this.hide() | ||
82 | else this.show() | ||
83 | } | ||
84 | |||
85 | show () { | ||
86 | this.container.style.display = 'block' | ||
87 | this.updateInterval = setInterval(async () => { | ||
88 | try { | ||
89 | const options = this.mode === 'p2p-media-loader' | ||
90 | ? this.buildHLSOptions() | ||
91 | : await this.buildWebTorrentOptions() // Default | ||
92 | |||
93 | this.list.innerHTML = this.getListTemplate(options) | ||
94 | } catch (err) { | ||
95 | console.error('Cannot update stats.', err) | ||
96 | clearInterval(this.updateInterval) | ||
97 | } | ||
98 | }, this.intervalMs) | ||
99 | } | ||
100 | |||
101 | hide () { | ||
102 | clearInterval(this.updateInterval) | ||
103 | this.container.style.display = 'none' | ||
104 | } | ||
105 | |||
106 | private buildHLSOptions () { | ||
107 | const p2pMediaLoader = this.player_.p2pMediaLoader() | ||
108 | const level = p2pMediaLoader.getCurrentLevel() | ||
109 | |||
110 | const codecs = level?.videoCodec || level?.audioCodec | ||
111 | ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}` | ||
112 | : undefined | ||
113 | |||
114 | const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}` | ||
115 | const buffer = this.timeRangesToString(this.player().buffered()) | ||
116 | |||
117 | let progress: number | ||
118 | let latency: string | ||
119 | |||
120 | if (this.options_.videoIsLive) { | ||
121 | latency = secondsToTime(p2pMediaLoader.getLiveLatency()) | ||
122 | } else { | ||
123 | progress = this.player().bufferedPercent() | ||
124 | } | ||
125 | |||
126 | return { | ||
127 | playerNetworkInfo: this.playerNetworkInfo, | ||
128 | resolution, | ||
129 | codecs, | ||
130 | buffer, | ||
131 | latency, | ||
132 | progress | ||
133 | } | ||
134 | } | ||
135 | |||
136 | private async buildWebTorrentOptions () { | ||
137 | const videoFile = this.player_.webtorrent().getCurrentVideoFile() | ||
138 | |||
139 | if (!this.metadataStore[videoFile.fileUrl]) { | ||
140 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) | ||
141 | } | ||
142 | |||
143 | const metadata = this.metadataStore[videoFile.fileUrl] | ||
144 | |||
145 | let colorSpace = 'unknown' | ||
146 | let codecs = 'unknown' | ||
147 | |||
148 | if (metadata?.streams[0]) { | ||
149 | const stream = metadata.streams[0] | ||
150 | |||
151 | colorSpace = stream['color_space'] !== 'unknown' | ||
152 | ? stream['color_space'] | ||
153 | : 'bt709' | ||
154 | |||
155 | codecs = stream['codec_name'] || 'avc1' | ||
156 | } | ||
157 | |||
158 | const resolution = videoFile?.resolution.label + videoFile?.fps | ||
159 | const buffer = this.timeRangesToString(this.player().buffered()) | ||
160 | const progress = this.player_.webtorrent().getTorrent()?.progress | ||
161 | |||
162 | return { | ||
163 | playerNetworkInfo: this.playerNetworkInfo, | ||
164 | progress, | ||
165 | colorSpace, | ||
166 | codecs, | ||
167 | resolution, | ||
168 | buffer | ||
169 | } | ||
170 | } | ||
171 | |||
172 | private getListTemplate (options: { | ||
173 | playerNetworkInfo: PlayerNetworkInfo | ||
174 | progress: number | ||
175 | codecs: string | ||
176 | resolution: string | ||
177 | buffer: string | ||
178 | |||
179 | latency?: string | ||
180 | colorSpace?: string | ||
181 | }) { | ||
182 | const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options | ||
183 | const player = this.player() | ||
184 | |||
185 | const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality() | ||
186 | const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) | ||
187 | const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) | ||
188 | const pr = (window.devicePixelRatio || 1).toFixed(2) | ||
189 | const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}` | ||
190 | |||
191 | const duration = player.duration() | ||
192 | |||
193 | let volume = `${Math.round(player.volume() * 100)}` | ||
194 | if (player.muted()) volume += ' (muted)' | ||
195 | |||
196 | const networkActivity = playerNetworkInfo.downloadSpeed | ||
197 | ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑` | ||
198 | : undefined | ||
199 | |||
200 | const totalTransferred = playerNetworkInfo.totalDownloaded | ||
201 | ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑` | ||
202 | : undefined | ||
203 | const downloadBreakdown = playerNetworkInfo.downloadedFromServer | ||
204 | ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers` | ||
205 | : undefined | ||
206 | |||
207 | const bufferProgress = progress !== undefined | ||
208 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` | ||
209 | : undefined | ||
210 | |||
211 | return ` | ||
212 | ${this.buildElement(player.localize('Player mode'), this.mode || 'HTTP')} | ||
213 | ${this.buildElement(player.localize('P2P'), player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))} | ||
214 | |||
215 | ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)} | ||
216 | |||
217 | ${this.buildElement(player.localize('Viewport / Frames'), frames)} | ||
218 | |||
219 | ${this.buildElement(player.localize('Resolution'), resolution)} | ||
220 | |||
221 | ${this.buildElement(player.localize('Volume'), volume)} | ||
222 | |||
223 | ${this.buildElement(player.localize('Codecs'), codecs)} | ||
224 | ${this.buildElement(player.localize('Color'), colorSpace)} | ||
225 | |||
226 | ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)} | ||
227 | |||
228 | ${this.buildElement(player.localize('Network Activity'), networkActivity)} | ||
229 | ${this.buildElement(player.localize('Total Transfered'), totalTransferred)} | ||
230 | ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)} | ||
231 | |||
232 | ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)} | ||
233 | ${this.buildElement(player.localize('Buffer State'), buffer)} | ||
234 | |||
235 | ${this.buildElement(player.localize('Live Latency'), latency)} | ||
236 | ` | ||
237 | } | ||
238 | |||
239 | private getMainTemplate () { | ||
240 | return ` | ||
241 | <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button> | ||
242 | <div class="vjs-stats-list"></div> | ||
243 | ` | ||
244 | } | ||
245 | |||
246 | private buildElement (label: string, value?: string) { | ||
247 | if (!value) return '' | ||
248 | |||
249 | return `<div><div>${label}</div><span>${value}</span></div>` | ||
250 | } | ||
251 | |||
252 | private timeRangesToString (r: videojs.TimeRange) { | ||
253 | let result = '' | ||
254 | |||
255 | for (let i = 0; i < r.length; i++) { | ||
256 | const start = Math.floor(r.start(i)) | ||
257 | const end = Math.floor(r.end(i)) | ||
258 | |||
259 | result += `[${secondsToTime(start)}, ${secondsToTime(end)}] ` | ||
260 | } | ||
261 | |||
262 | return result | ||
263 | } | ||
264 | } | ||
265 | |||
266 | videojs.registerComponent('StatsCard', StatsCard) | ||
267 | |||
268 | export { | ||
269 | StatsCard, | ||
270 | StatsCardOptions | ||
271 | } | ||
diff --git a/client/src/assets/player/stats/stats-plugin.ts b/client/src/assets/player/stats/stats-plugin.ts deleted file mode 100644 index 8aad80e8a..000000000 --- a/client/src/assets/player/stats/stats-plugin.ts +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
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: 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 () { | ||
26 | this.statsCard.show() | ||
27 | } | ||
28 | } | ||
29 | |||
30 | videojs.registerPlugin('stats', StatsForNerdsPlugin) | ||
31 | export { StatsForNerdsPlugin } | ||