]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/shared/stats/stats-card.ts
Optimize improve stat card rendering in player
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / stats / stats-card.ts
1 import videojs from 'video.js'
2 import { secondsToTime } from '@shared/core-utils'
3 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
4 import { bytes } from '../common'
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 interface InfoElement {
26 root: HTMLElement
27 value: HTMLElement
28 }
29
30 const Component = videojs.getComponent('Component')
31 class StatsCard extends Component {
32 options_: StatsCardOptions
33
34 updateInterval: any
35
36 mode: 'webtorrent' | 'p2p-media-loader'
37
38 metadataStore: any = {}
39
40 intervalMs = 300
41 playerNetworkInfo: PlayerNetworkInfo = {}
42
43 private containerEl: HTMLDivElement
44 private infoListEl: HTMLDivElement
45
46 private playerMode: InfoElement
47 private p2p: InfoElement
48 private uuid: InfoElement
49 private viewport: InfoElement
50 private resolution: InfoElement
51 private volume: InfoElement
52 private codecs: InfoElement
53 private color: InfoElement
54 private connection: InfoElement
55
56 private network: InfoElement
57 private transferred: InfoElement
58 private download: InfoElement
59
60 private bufferProgress: InfoElement
61 private bufferState: InfoElement
62
63 private liveLatency: InfoElement
64
65 createEl () {
66 this.containerEl = videojs.dom.createEl('div', {
67 className: 'vjs-stats-content'
68 }) as HTMLDivElement
69 this.containerEl.style.display = 'none'
70
71 this.infoListEl = videojs.dom.createEl('div', {
72 className: 'vjs-stats-list'
73 }) as HTMLDivElement
74
75 const closeButton = videojs.dom.createEl('button', {
76 className: 'vjs-stats-close',
77 tabindex: '0',
78 title: 'Close stats',
79 innerText: '[x]'
80 }, { 'aria-label': 'Close stats' }) as HTMLElement
81 closeButton.onclick = () => this.hide()
82
83 this.containerEl.appendChild(closeButton)
84 this.containerEl.appendChild(this.infoListEl)
85
86 this.populateInfoBlocks()
87
88 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
89 if (!data) return // HTTP fallback
90
91 this.mode = data.source
92
93 const p2pStats = data.p2p
94 const httpStats = data.http
95
96 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
97 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
98 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
99 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
100 this.playerNetworkInfo.numPeers = p2pStats.numPeers
101 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
102
103 if (data.source === 'p2p-media-loader') {
104 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
105 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
106 }
107 })
108
109 return this.containerEl
110 }
111
112 toggle () {
113 if (this.updateInterval) this.hide()
114 else this.show()
115 }
116
117 show () {
118 this.containerEl.style.display = 'block'
119
120 this.updateInterval = setInterval(async () => {
121 try {
122 const options = this.mode === 'p2p-media-loader'
123 ? this.buildHLSOptions()
124 : await this.buildWebTorrentOptions() // Default
125
126 this.populateInfoValues(options)
127 } catch (err) {
128 console.error('Cannot update stats.', err)
129 clearInterval(this.updateInterval)
130 }
131 }, this.intervalMs)
132 }
133
134 hide () {
135 clearInterval(this.updateInterval)
136 this.containerEl.style.display = 'none'
137 }
138
139 private buildHLSOptions () {
140 const p2pMediaLoader = this.player_.p2pMediaLoader()
141 const level = p2pMediaLoader.getCurrentLevel()
142
143 const codecs = level?.videoCodec || level?.audioCodec
144 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
145 : undefined
146
147 const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
148 const buffer = this.timeRangesToString(this.player().buffered())
149
150 let progress: number
151 let latency: string
152
153 if (this.options_.videoIsLive) {
154 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
155 } else {
156 progress = this.player().bufferedPercent()
157 }
158
159 return {
160 playerNetworkInfo: this.playerNetworkInfo,
161 resolution,
162 codecs,
163 buffer,
164 latency,
165 progress
166 }
167 }
168
169 private async buildWebTorrentOptions () {
170 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
171
172 if (!this.metadataStore[videoFile.fileUrl]) {
173 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
174 }
175
176 const metadata = this.metadataStore[videoFile.fileUrl]
177
178 let colorSpace = 'unknown'
179 let codecs = 'unknown'
180
181 if (metadata?.streams[0]) {
182 const stream = metadata.streams[0]
183
184 colorSpace = stream['color_space'] !== 'unknown'
185 ? stream['color_space']
186 : 'bt709'
187
188 codecs = stream['codec_name'] || 'avc1'
189 }
190
191 const resolution = videoFile?.resolution.label + videoFile?.fps
192 const buffer = this.timeRangesToString(this.player().buffered())
193 const progress = this.player_.webtorrent().getTorrent()?.progress
194
195 return {
196 playerNetworkInfo: this.playerNetworkInfo,
197 progress,
198 colorSpace,
199 codecs,
200 resolution,
201 buffer
202 }
203 }
204
205 private populateInfoBlocks () {
206 this.playerMode = this.buildInfoRow(this.player().localize('Player mode'))
207 this.p2p = this.buildInfoRow(this.player().localize('P2P'))
208 this.uuid = this.buildInfoRow(this.player().localize('Video UUID'))
209 this.viewport = this.buildInfoRow(this.player().localize('Viewport / Frames'))
210 this.resolution = this.buildInfoRow(this.player().localize('Resolution'))
211 this.volume = this.buildInfoRow(this.player().localize('Volume'))
212 this.codecs = this.buildInfoRow(this.player().localize('Codecs'))
213 this.color = this.buildInfoRow(this.player().localize('Color'))
214 this.connection = this.buildInfoRow(this.player().localize('Connection Speed'))
215
216 this.network = this.buildInfoRow(this.player().localize('Network Activity'))
217 this.transferred = this.buildInfoRow(this.player().localize('Total Transfered'))
218 this.download = this.buildInfoRow(this.player().localize('Download Breakdown'))
219
220 this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress'))
221 this.bufferState = this.buildInfoRow(this.player().localize('Buffer State'))
222
223 this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency'))
224
225 this.infoListEl.appendChild(this.playerMode.root)
226 this.infoListEl.appendChild(this.p2p.root)
227 this.infoListEl.appendChild(this.uuid.root)
228 this.infoListEl.appendChild(this.viewport.root)
229 this.infoListEl.appendChild(this.resolution.root)
230 this.infoListEl.appendChild(this.volume.root)
231 this.infoListEl.appendChild(this.codecs.root)
232 this.infoListEl.appendChild(this.color.root)
233 this.infoListEl.appendChild(this.connection.root)
234 this.infoListEl.appendChild(this.network.root)
235 this.infoListEl.appendChild(this.transferred.root)
236 this.infoListEl.appendChild(this.download.root)
237 this.infoListEl.appendChild(this.bufferProgress.root)
238 this.infoListEl.appendChild(this.bufferState.root)
239 this.infoListEl.appendChild(this.liveLatency.root)
240 }
241
242 private populateInfoValues (options: {
243 playerNetworkInfo: PlayerNetworkInfo
244 progress: number
245 codecs: string
246 resolution: string
247 buffer: string
248
249 latency?: string
250 colorSpace?: string
251 }) {
252 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
253 const player = this.player()
254
255 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
256 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
257 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
258 const pr = (window.devicePixelRatio || 1).toFixed(2)
259 const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
260
261 const duration = player.duration()
262
263 let volume = `${Math.round(player.volume() * 100)}`
264 if (player.muted()) volume += ' (muted)'
265
266 const networkActivity = playerNetworkInfo.downloadSpeed
267 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
268 : undefined
269
270 const totalTransferred = playerNetworkInfo.totalDownloaded
271 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
272 : undefined
273 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
274 ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
275 : undefined
276
277 const bufferProgress = progress !== undefined
278 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
279 : undefined
280
281 this.setInfoValue(this.playerMode, this.mode || 'HTTP')
282 this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))
283 this.setInfoValue(this.uuid, this.options_.videoUUID)
284
285 this.setInfoValue(this.viewport, frames)
286 this.setInfoValue(this.resolution, resolution)
287 this.setInfoValue(this.volume, volume)
288 this.setInfoValue(this.codecs, codecs)
289 this.setInfoValue(this.color, colorSpace)
290 this.setInfoValue(this.connection, playerNetworkInfo.averageBandwidth)
291
292 this.setInfoValue(this.network, networkActivity)
293 this.setInfoValue(this.transferred, totalTransferred)
294 this.setInfoValue(this.download, downloadBreakdown)
295
296 this.setInfoValue(this.bufferProgress, bufferProgress)
297 this.setInfoValue(this.bufferState, buffer)
298
299 this.setInfoValue(this.liveLatency, latency)
300 }
301
302 private setInfoValue (el: InfoElement, value: string) {
303 if (!value) {
304 el.root.style.display = 'none'
305 return
306 }
307
308 el.root.style.display = 'block'
309
310 if (el.value.innerHTML === value) return
311 el.value.innerHTML = value
312 }
313
314 private buildInfoRow (labelText: string, valueHTML?: string) {
315 const root = videojs.dom.createEl('div') as HTMLElement
316 root.style.display = 'none'
317
318 const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
319 const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement
320
321 root.appendChild(label)
322 root.appendChild(value)
323
324 return { root, value }
325 }
326
327 private timeRangesToString (r: videojs.TimeRange) {
328 let result = ''
329
330 for (let i = 0; i < r.length; i++) {
331 const start = Math.floor(r.start(i))
332 const end = Math.floor(r.end(i))
333
334 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
335 }
336
337 return result
338 }
339 }
340
341 videojs.registerComponent('StatsCard', StatsCard)
342
343 export {
344 StatsCard,
345 StatsCardOptions
346 }