]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/shared/stats/stats-card.ts
Merge branch 'release/4.2.0' into develop
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / stats / stats-card.ts
CommitLineData
ff563914 1import videojs from 'video.js'
15a7eafb 2import { secondsToTime } from '@shared/core-utils'
57d65032
C
3import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
4import { bytes } from '../common'
ff563914
RK
5
6interface StatsCardOptions extends videojs.ComponentOptions {
4e11d8f3
C
7 videoUUID: string
8 videoIsLive: boolean
9 mode: 'webtorrent' | 'p2p-media-loader'
95765067 10 p2pEnabled: boolean
ff563914
RK
11}
12
4e11d8f3
C
13interface PlayerNetworkInfo {
14 downloadSpeed?: string
15 uploadSpeed?: string
16 totalDownloaded?: string
17 totalUploaded?: string
18 numPeers?: number
19 averageBandwidth?: string
ff563914 20
4e11d8f3
C
21 downloadedFromServer?: string
22 downloadedFromPeers?: string
ff563914
RK
23}
24
68e72ba9
C
25interface InfoElement {
26 root: HTMLElement
27 value: HTMLElement
28}
29
ff563914
RK
30const Component = videojs.getComponent('Component')
31class StatsCard extends Component {
32 options_: StatsCardOptions
4e11d8f3 33
4e11d8f3
C
34 updateInterval: any
35
36 mode: 'webtorrent' | 'p2p-media-loader'
37
38 metadataStore: any = {}
39
40 intervalMs = 300
41 playerNetworkInfo: PlayerNetworkInfo = {}
ff563914 42
68e72ba9
C
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
ff563914 65 createEl () {
68e72ba9
C
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'
ff563914 73 }) as HTMLDivElement
ff563914 74
68e72ba9
C
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()
ff563914 82
68e72ba9
C
83 this.containerEl.appendChild(closeButton)
84 this.containerEl.appendChild(this.infoListEl)
85
86 this.populateInfoBlocks()
ff563914 87
4e11d8f3 88 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
ff563914
RK
89 if (!data) return // HTTP fallback
90
4e11d8f3 91 this.mode = data.source
ff563914
RK
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
4e11d8f3 101 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
ff563914
RK
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
68e72ba9 109 return this.containerEl
ff563914
RK
110 }
111
112 toggle () {
9df52d66
C
113 if (this.updateInterval) this.hide()
114 else this.show()
ff563914
RK
115 }
116
4e11d8f3 117 show () {
68e72ba9
C
118 this.containerEl.style.display = 'block'
119
4e11d8f3 120 this.updateInterval = setInterval(async () => {
ff563914 121 try {
db0159c7 122 const options = this.mode === 'p2p-media-loader'
98ab5dc8 123 ? this.buildHLSOptions()
db0159c7 124 : await this.buildWebTorrentOptions() // Default
4e11d8f3 125
68e72ba9 126 this.populateInfoValues(options)
4e11d8f3
C
127 } catch (err) {
128 console.error('Cannot update stats.', err)
129 clearInterval(this.updateInterval)
ff563914 130 }
4e11d8f3 131 }, this.intervalMs)
ff563914
RK
132 }
133
134 hide () {
4e11d8f3 135 clearInterval(this.updateInterval)
68e72ba9 136 this.containerEl.style.display = 'none'
ff563914 137 }
4e11d8f3 138
98ab5dc8 139 private buildHLSOptions () {
4e11d8f3
C
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
68e72ba9
C
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: {
4e11d8f3
C
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
b76db2ff 263 let volume = `${Math.round(player.volume() * 100)}`
4e11d8f3
C
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
95765067 274 ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
4e11d8f3
C
275 : undefined
276
277 const bufferProgress = progress !== undefined
278 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
279 : undefined
280
68e72ba9
C
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)
4e11d8f3 284
68e72ba9
C
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)
4e11d8f3 291
68e72ba9
C
292 this.setInfoValue(this.network, networkActivity)
293 this.setInfoValue(this.transferred, totalTransferred)
294 this.setInfoValue(this.download, downloadBreakdown)
4e11d8f3 295
68e72ba9
C
296 this.setInfoValue(this.bufferProgress, bufferProgress)
297 this.setInfoValue(this.bufferState, buffer)
4e11d8f3 298
68e72ba9
C
299 this.setInfoValue(this.liveLatency, latency)
300 }
4e11d8f3 301
68e72ba9
C
302 private setInfoValue (el: InfoElement, value: string) {
303 if (!value) {
304 el.root.style.display = 'none'
305 return
306 }
4e11d8f3 307
68e72ba9 308 el.root.style.display = 'block'
4e11d8f3 309
68e72ba9
C
310 if (el.value.innerHTML === value) return
311 el.value.innerHTML = value
4e11d8f3
C
312 }
313
68e72ba9
C
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
4e11d8f3 320
68e72ba9
C
321 root.appendChild(label)
322 root.appendChild(value)
4e11d8f3 323
68e72ba9 324 return { root, value }
4e11d8f3
C
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 }
ff563914
RK
339}
340
341videojs.registerComponent('StatsCard', StatsCard)
342
343export {
344 StatsCard,
345 StatsCardOptions
346}