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'
7 interface StatsCardOptions extends videojs.ComponentOptions {
10 mode: 'webtorrent' | 'p2p-media-loader'
14 interface PlayerNetworkInfo {
15 downloadSpeed?: string
17 totalDownloaded?: string
18 totalUploaded?: string
20 averageBandwidth?: string
22 downloadedFromServer?: string
23 downloadedFromPeers?: string
26 interface InfoElement {
31 const Component = videojs.getComponent('Component')
32 class StatsCard extends Component {
33 options_: StatsCardOptions
37 mode: 'webtorrent' | 'p2p-media-loader'
39 metadataStore: any = {}
42 playerNetworkInfo: PlayerNetworkInfo = {}
44 private containerEl: HTMLDivElement
45 private infoListEl: HTMLDivElement
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
57 private network: InfoElement
58 private transferred: InfoElement
59 private download: InfoElement
61 private bufferProgress: InfoElement
62 private bufferState: InfoElement
64 private liveLatency: InfoElement
67 this.containerEl = videojs.dom.createEl('div', {
68 className: 'vjs-stats-content'
70 this.containerEl.style.display = 'none'
72 this.infoListEl = videojs.dom.createEl('div', {
73 className: 'vjs-stats-list'
76 const closeButton = videojs.dom.createEl('button', {
77 className: 'vjs-stats-close',
81 }, { 'aria-label': 'Close stats' }) as HTMLElement
82 closeButton.onclick = () => this.hide()
84 this.containerEl.appendChild(closeButton)
85 this.containerEl.appendChild(this.infoListEl)
87 this.populateInfoBlocks()
89 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
90 if (!data) return // HTTP fallback
92 this.mode = data.source
94 const p2pStats = data.p2p
95 const httpStats = data.http
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'
104 if (data.source === 'p2p-media-loader') {
105 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
106 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
110 return this.containerEl
114 if (this.updateInterval) this.hide()
119 this.containerEl.style.display = 'block'
121 this.updateInterval = setInterval(async () => {
123 const options = this.mode === 'p2p-media-loader'
124 ? this.buildHLSOptions()
125 : await this.buildWebTorrentOptions() // Default
127 this.populateInfoValues(options)
129 logger.error('Cannot update stats.', err)
130 clearInterval(this.updateInterval)
136 clearInterval(this.updateInterval)
137 this.containerEl.style.display = 'none'
140 private buildHLSOptions () {
141 const p2pMediaLoader = this.player_.p2pMediaLoader()
142 const level = p2pMediaLoader.getCurrentLevel()
144 const codecs = level?.videoCodec || level?.audioCodec
145 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
148 const resolution = level?.height
149 ? `${level.height}p${level?.attrs['FRAME-RATE'] || ''}`
152 const buffer = this.timeRangesToString(this.player().buffered())
157 if (this.options_.videoIsLive) {
158 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
160 progress = this.player().bufferedPercent()
164 playerNetworkInfo: this.playerNetworkInfo,
173 private async buildWebTorrentOptions () {
174 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
176 if (!this.metadataStore[videoFile.fileUrl]) {
177 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
180 const metadata = this.metadataStore[videoFile.fileUrl]
182 let colorSpace = 'unknown'
183 let codecs = 'unknown'
185 if (metadata?.streams[0]) {
186 const stream = metadata.streams[0]
188 colorSpace = stream['color_space'] !== 'unknown'
189 ? stream['color_space']
192 codecs = stream['codec_name'] || 'avc1'
195 const resolution = videoFile?.resolution.label + videoFile?.fps
196 const buffer = this.timeRangesToString(this.player().buffered())
197 const progress = this.player_.webtorrent().getTorrent()?.progress
200 playerNetworkInfo: this.playerNetworkInfo,
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'))
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'))
224 this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress'))
225 this.bufferState = this.buildInfoRow(this.player().localize('Buffer State'))
227 this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency'))
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)
246 private populateInfoValues (options: {
247 playerNetworkInfo: PlayerNetworkInfo
256 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
257 const player = this.player()
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()
268 let volume = `${Math.round(player.volume() * 100)}`
269 if (player.muted()) volume += player.localize(' (muted)')
271 const networkActivity = playerNetworkInfo.downloadSpeed
272 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
275 const totalTransferred = playerNetworkInfo.totalDownloaded
276 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
278 const { downloadedFromServer, downloadedFromPeers } = playerNetworkInfo
279 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
280 ? player.localize('{1} from servers ยท {2} from peers', [ downloadedFromServer, downloadedFromPeers ])
283 const bufferProgress = progress !== undefined
284 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
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)
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)
298 this.setInfoValue(this.network, networkActivity)
299 this.setInfoValue(this.transferred, totalTransferred)
300 this.setInfoValue(this.download, downloadBreakdown)
302 this.setInfoValue(this.bufferProgress, bufferProgress)
303 this.setInfoValue(this.bufferState, buffer)
305 this.setInfoValue(this.liveLatency, latency)
308 private setInfoValue (el: InfoElement, value: string) {
310 el.root.style.display = 'none'
314 el.root.style.display = 'block'
316 if (el.value.innerHTML === value) return
317 el.value.innerHTML = value
320 private buildInfoRow (labelText: string, valueHTML?: string) {
321 const root = videojs.dom.createEl('div') as HTMLElement
322 root.style.display = 'none'
324 const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
325 const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement
327 root.appendChild(label)
328 root.appendChild(value)
330 return { root, value }
333 private timeRangesToString (r: videojs.TimeRange) {
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))
340 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
347 videojs.registerComponent('StatsCard', StatsCard)