1 import videojs from 'video.js'
2 import { secondsToTime } from '@shared/core-utils'
3 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
4 import { bytes } from '../common'
6 interface StatsCardOptions extends videojs.ComponentOptions {
9 mode: 'webtorrent' | 'p2p-media-loader'
13 interface PlayerNetworkInfo {
14 downloadSpeed?: string
16 totalDownloaded?: string
17 totalUploaded?: string
19 averageBandwidth?: string
21 downloadedFromServer?: string
22 downloadedFromPeers?: string
25 interface InfoElement {
30 const Component = videojs.getComponent('Component')
31 class StatsCard extends Component {
32 options_: StatsCardOptions
36 mode: 'webtorrent' | 'p2p-media-loader'
38 metadataStore: any = {}
41 playerNetworkInfo: PlayerNetworkInfo = {}
43 private containerEl: HTMLDivElement
44 private infoListEl: HTMLDivElement
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
56 private network: InfoElement
57 private transferred: InfoElement
58 private download: InfoElement
60 private bufferProgress: InfoElement
61 private bufferState: InfoElement
63 private liveLatency: InfoElement
66 this.containerEl = videojs.dom.createEl('div', {
67 className: 'vjs-stats-content'
69 this.containerEl.style.display = 'none'
71 this.infoListEl = videojs.dom.createEl('div', {
72 className: 'vjs-stats-list'
75 const closeButton = videojs.dom.createEl('button', {
76 className: 'vjs-stats-close',
80 }, { 'aria-label': 'Close stats' }) as HTMLElement
81 closeButton.onclick = () => this.hide()
83 this.containerEl.appendChild(closeButton)
84 this.containerEl.appendChild(this.infoListEl)
86 this.populateInfoBlocks()
88 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
89 if (!data) return // HTTP fallback
91 this.mode = data.source
93 const p2pStats = data.p2p
94 const httpStats = data.http
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
101 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
103 if (data.source === 'p2p-media-loader') {
104 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
105 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
109 return this.containerEl
113 if (this.updateInterval) this.hide()
118 this.containerEl.style.display = 'block'
120 this.updateInterval = setInterval(async () => {
122 const options = this.mode === 'p2p-media-loader'
123 ? this.buildHLSOptions()
124 : await this.buildWebTorrentOptions() // Default
126 this.populateInfoValues(options)
128 console.error('Cannot update stats.', err)
129 clearInterval(this.updateInterval)
135 clearInterval(this.updateInterval)
136 this.containerEl.style.display = 'none'
139 private buildHLSOptions () {
140 const p2pMediaLoader = this.player_.p2pMediaLoader()
141 const level = p2pMediaLoader.getCurrentLevel()
143 const codecs = level?.videoCodec || level?.audioCodec
144 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
147 const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
148 const buffer = this.timeRangesToString(this.player().buffered())
153 if (this.options_.videoIsLive) {
154 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
156 progress = this.player().bufferedPercent()
160 playerNetworkInfo: this.playerNetworkInfo,
169 private async buildWebTorrentOptions () {
170 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
172 if (!this.metadataStore[videoFile.fileUrl]) {
173 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
176 const metadata = this.metadataStore[videoFile.fileUrl]
178 let colorSpace = 'unknown'
179 let codecs = 'unknown'
181 if (metadata?.streams[0]) {
182 const stream = metadata.streams[0]
184 colorSpace = stream['color_space'] !== 'unknown'
185 ? stream['color_space']
188 codecs = stream['codec_name'] || 'avc1'
191 const resolution = videoFile?.resolution.label + videoFile?.fps
192 const buffer = this.timeRangesToString(this.player().buffered())
193 const progress = this.player_.webtorrent().getTorrent()?.progress
196 playerNetworkInfo: this.playerNetworkInfo,
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'))
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'))
220 this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress'))
221 this.bufferState = this.buildInfoRow(this.player().localize('Buffer State'))
223 this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency'))
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)
242 private populateInfoValues (options: {
243 playerNetworkInfo: PlayerNetworkInfo
252 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
253 const player = this.player()
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}`
261 const duration = player.duration()
263 let volume = `${Math.round(player.volume() * 100)}`
264 if (player.muted()) volume += ' (muted)'
266 const networkActivity = playerNetworkInfo.downloadSpeed
267 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
270 const totalTransferred = playerNetworkInfo.totalDownloaded
271 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
273 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
274 ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
277 const bufferProgress = progress !== undefined
278 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
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)
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)
292 this.setInfoValue(this.network, networkActivity)
293 this.setInfoValue(this.transferred, totalTransferred)
294 this.setInfoValue(this.download, downloadBreakdown)
296 this.setInfoValue(this.bufferProgress, bufferProgress)
297 this.setInfoValue(this.bufferState, buffer)
299 this.setInfoValue(this.liveLatency, latency)
302 private setInfoValue (el: InfoElement, value: string) {
304 el.root.style.display = 'none'
308 el.root.style.display = 'block'
310 if (el.value.innerHTML === value) return
311 el.value.innerHTML = value
314 private buildInfoRow (labelText: string, valueHTML?: string) {
315 const root = videojs.dom.createEl('div') as HTMLElement
316 root.style.display = 'none'
318 const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
319 const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement
321 root.appendChild(label)
322 root.appendChild(value)
324 return { root, value }
327 private timeRangesToString (r: videojs.TimeRange) {
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))
334 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
341 videojs.registerComponent('StatsCard', StatsCard)