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