]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/shared/stats/stats-card.ts
Fix token injection if unlogged user
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / stats / stats-card.ts
1 import videojs from 'video.js'
2 import { logger } from '@root-helpers/logger'
3 import { secondsToTime } from '@shared/core-utils'
4 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
5 import { bytes } from '../common'
6
7 interface StatsCardOptions extends videojs.ComponentOptions {
8 videoUUID: string
9 videoIsLive: boolean
10 mode: 'webtorrent' | 'p2p-media-loader'
11 p2pEnabled: boolean
12 }
13
14 interface PlayerNetworkInfo {
15 downloadSpeed?: string
16 uploadSpeed?: string
17 totalDownloaded?: string
18 totalUploaded?: string
19 numPeers?: number
20 averageBandwidth?: string
21
22 downloadedFromServer?: string
23 downloadedFromPeers?: string
24 }
25
26 interface InfoElement {
27 root: HTMLElement
28 value: HTMLElement
29 }
30
31 const Component = videojs.getComponent('Component')
32 class StatsCard extends Component {
33 options_: StatsCardOptions
34
35 updateInterval: any
36
37 mode: 'webtorrent' | 'p2p-media-loader'
38
39 metadataStore: any = {}
40
41 intervalMs = 300
42 playerNetworkInfo: PlayerNetworkInfo = {}
43
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
66 createEl () {
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'
74 }) as HTMLDivElement
75
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()
83
84 this.containerEl.appendChild(closeButton)
85 this.containerEl.appendChild(this.infoListEl)
86
87 this.populateInfoBlocks()
88
89 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
90 if (!data) return // HTTP fallback
91
92 this.mode = data.source
93
94 const p2pStats = data.p2p
95 const httpStats = data.http
96
97 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
98 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed).join(' ')
99 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
100 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded).join(' ')
101 this.playerNetworkInfo.numPeers = p2pStats.numPeers
102 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
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
110 return this.containerEl
111 }
112
113 toggle () {
114 if (this.updateInterval) this.hide()
115 else this.show()
116 }
117
118 show () {
119 this.containerEl.style.display = 'block'
120
121 this.updateInterval = setInterval(async () => {
122 try {
123 const options = this.mode === 'p2p-media-loader'
124 ? this.buildHLSOptions()
125 : await this.buildWebTorrentOptions() // Default
126
127 this.populateInfoValues(options)
128 } catch (err) {
129 logger.error('Cannot update stats.', err)
130 clearInterval(this.updateInterval)
131 }
132 }, this.intervalMs)
133 }
134
135 hide () {
136 clearInterval(this.updateInterval)
137 this.containerEl.style.display = 'none'
138 }
139
140 private buildHLSOptions () {
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
148 const resolution = level?.height
149 ? `${level.height}p${level?.attrs['FRAME-RATE'] || ''}`
150 : undefined
151
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
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: {
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 vp = `${vw}x${vh}*${pr}`
264 const { droppedVideoFrames, totalVideoFrames } = videoQuality
265 const frames = player.localize('{1} / {2} dropped of {3}', [ vp, droppedVideoFrames + '', totalVideoFrames + '' ])
266 const duration = player.duration()
267
268 let volume = `${Math.round(player.volume() * 100)}`
269 if (player.muted()) volume += player.localize(' (muted)')
270
271 const networkActivity = playerNetworkInfo.downloadSpeed
272 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
273 : undefined
274
275 const totalTransferred = playerNetworkInfo.totalDownloaded
276 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
277 : undefined
278 const { downloadedFromServer, downloadedFromPeers } = playerNetworkInfo
279 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
280 ? player.localize('{1} from servers ยท {2} from peers', [ downloadedFromServer, downloadedFromPeers ])
281 : undefined
282
283 const bufferProgress = progress !== undefined
284 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
285 : undefined
286
287 this.setInfoValue(this.playerMode, this.mode || 'HTTP')
288 this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))
289 this.setInfoValue(this.uuid, this.options_.videoUUID)
290
291 this.setInfoValue(this.viewport, frames)
292 this.setInfoValue(this.resolution, resolution)
293 this.setInfoValue(this.volume, volume)
294 this.setInfoValue(this.codecs, codecs)
295 this.setInfoValue(this.color, colorSpace)
296 this.setInfoValue(this.connection, playerNetworkInfo.averageBandwidth)
297
298 this.setInfoValue(this.network, networkActivity)
299 this.setInfoValue(this.transferred, totalTransferred)
300 this.setInfoValue(this.download, downloadBreakdown)
301
302 this.setInfoValue(this.bufferProgress, bufferProgress)
303 this.setInfoValue(this.bufferState, buffer)
304
305 this.setInfoValue(this.liveLatency, latency)
306 }
307
308 private setInfoValue (el: InfoElement, value: string) {
309 if (!value) {
310 el.root.style.display = 'none'
311 return
312 }
313
314 el.root.style.display = 'block'
315
316 if (el.value.innerHTML === value) return
317 el.value.innerHTML = value
318 }
319
320 private buildInfoRow (labelText: string, valueHTML?: string) {
321 const root = videojs.dom.createEl('div') as HTMLElement
322 root.style.display = 'none'
323
324 const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
325 const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement
326
327 root.appendChild(label)
328 root.appendChild(value)
329
330 return { root, value }
331 }
332
333 private timeRangesToString (r: videojs.TimeRange) {
334 let result = ''
335
336 for (let i = 0; i < r.length; i++) {
337 const start = Math.floor(r.start(i))
338 const end = Math.floor(r.end(i))
339
340 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
341 }
342
343 return result
344 }
345 }
346
347 videojs.registerComponent('StatsCard', StatsCard)
348
349 export {
350 StatsCard,
351 StatsCardOptions
352 }