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}p${level?.attrs['FRAME-RATE'] || ''}`
149 const buffer = this.timeRangesToString(this.player().buffered())
154 if (this.options_.videoIsLive) {
155 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
157 progress = this.player().bufferedPercent()
161 playerNetworkInfo: this.playerNetworkInfo,
170 private async buildWebTorrentOptions () {
171 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
173 if (!this.metadataStore[videoFile.fileUrl]) {
174 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
177 const metadata = this.metadataStore[videoFile.fileUrl]
179 let colorSpace = 'unknown'
180 let codecs = 'unknown'
182 if (metadata?.streams[0]) {
183 const stream = metadata.streams[0]
185 colorSpace = stream['color_space'] !== 'unknown'
186 ? stream['color_space']
189 codecs = stream['codec_name'] || 'avc1'
192 const resolution = videoFile?.resolution.label + videoFile?.fps
193 const buffer = this.timeRangesToString(this.player().buffered())
194 const progress = this.player_.webtorrent().getTorrent()?.progress
197 playerNetworkInfo: this.playerNetworkInfo,
206 private populateInfoBlocks () {
207 this.playerMode = this.buildInfoRow(this.player().localize('Player mode'))
208 this.p2p = this.buildInfoRow(this.player().localize('P2P'))
209 this.uuid = this.buildInfoRow(this.player().localize('Video UUID'))
210 this.viewport = this.buildInfoRow(this.player().localize('Viewport / Frames'))
211 this.resolution = this.buildInfoRow(this.player().localize('Resolution'))
212 this.volume = this.buildInfoRow(this.player().localize('Volume'))
213 this.codecs = this.buildInfoRow(this.player().localize('Codecs'))
214 this.color = this.buildInfoRow(this.player().localize('Color'))
215 this.connection = this.buildInfoRow(this.player().localize('Connection Speed'))
217 this.network = this.buildInfoRow(this.player().localize('Network Activity'))
218 this.transferred = this.buildInfoRow(this.player().localize('Total Transfered'))
219 this.download = this.buildInfoRow(this.player().localize('Download Breakdown'))
221 this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress'))
222 this.bufferState = this.buildInfoRow(this.player().localize('Buffer State'))
224 this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency'))
226 this.infoListEl.appendChild(this.playerMode.root)
227 this.infoListEl.appendChild(this.p2p.root)
228 this.infoListEl.appendChild(this.uuid.root)
229 this.infoListEl.appendChild(this.viewport.root)
230 this.infoListEl.appendChild(this.resolution.root)
231 this.infoListEl.appendChild(this.volume.root)
232 this.infoListEl.appendChild(this.codecs.root)
233 this.infoListEl.appendChild(this.color.root)
234 this.infoListEl.appendChild(this.connection.root)
235 this.infoListEl.appendChild(this.network.root)
236 this.infoListEl.appendChild(this.transferred.root)
237 this.infoListEl.appendChild(this.download.root)
238 this.infoListEl.appendChild(this.bufferProgress.root)
239 this.infoListEl.appendChild(this.bufferState.root)
240 this.infoListEl.appendChild(this.liveLatency.root)
243 private populateInfoValues (options: {
244 playerNetworkInfo: PlayerNetworkInfo
253 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
254 const player = this.player()
256 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
257 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
258 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
259 const pr = (window.devicePixelRatio || 1).toFixed(2)
260 const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
262 const duration = player.duration()
264 let volume = `${Math.round(player.volume() * 100)}`
265 if (player.muted()) volume += ' (muted)'
267 const networkActivity = playerNetworkInfo.downloadSpeed
268 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
271 const totalTransferred = playerNetworkInfo.totalDownloaded
272 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
274 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
275 ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
278 const bufferProgress = progress !== undefined
279 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
282 this.setInfoValue(this.playerMode, this.mode || 'HTTP')
283 this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))
284 this.setInfoValue(this.uuid, this.options_.videoUUID)
286 this.setInfoValue(this.viewport, frames)
287 this.setInfoValue(this.resolution, resolution)
288 this.setInfoValue(this.volume, volume)
289 this.setInfoValue(this.codecs, codecs)
290 this.setInfoValue(this.color, colorSpace)
291 this.setInfoValue(this.connection, playerNetworkInfo.averageBandwidth)
293 this.setInfoValue(this.network, networkActivity)
294 this.setInfoValue(this.transferred, totalTransferred)
295 this.setInfoValue(this.download, downloadBreakdown)
297 this.setInfoValue(this.bufferProgress, bufferProgress)
298 this.setInfoValue(this.bufferState, buffer)
300 this.setInfoValue(this.liveLatency, latency)
303 private setInfoValue (el: InfoElement, value: string) {
305 el.root.style.display = 'none'
309 el.root.style.display = 'block'
311 if (el.value.innerHTML === value) return
312 el.value.innerHTML = value
315 private buildInfoRow (labelText: string, valueHTML?: string) {
316 const root = videojs.dom.createEl('div') as HTMLElement
317 root.style.display = 'none'
319 const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
320 const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement
322 root.appendChild(label)
323 root.appendChild(value)
325 return { root, value }
328 private timeRangesToString (r: videojs.TimeRange) {
331 for (let i = 0; i < r.length; i++) {
332 const start = Math.floor(r.start(i))
333 const end = Math.floor(r.end(i))
335 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
342 videojs.registerComponent('StatsCard', StatsCard)