1 import videojs from 'video.js'
2 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
3 import { bytes, secondsToTime } from '../utils'
5 interface StatsCardOptions extends videojs.ComponentOptions {
8 mode: 'webtorrent' | 'p2p-media-loader'
11 interface PlayerNetworkInfo {
12 downloadSpeed?: string
14 totalDownloaded?: string
15 totalUploaded?: string
17 averageBandwidth?: string
19 downloadedFromServer?: string
20 downloadedFromPeers?: string
23 const Component = videojs.getComponent('Component')
24 class StatsCard extends Component {
25 options_: StatsCardOptions
27 container: HTMLDivElement
30 closeButton: HTMLElement
34 mode: 'webtorrent' | 'p2p-media-loader'
36 metadataStore: any = {}
39 playerNetworkInfo: PlayerNetworkInfo = {}
41 constructor (player: videojs.Player, options: StatsCardOptions) {
42 super(player, options)
46 const container = super.createEl('div', {
47 className: 'vjs-stats-content',
48 innerHTML: this.getMainTemplate()
50 this.container = container
51 this.container.style.display = 'none'
53 this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
54 this.closeButton.onclick = () => this.hide()
56 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
58 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
59 if (!data) return // HTTP fallback
61 this.mode = data.source
63 const p2pStats = data.p2p
64 const httpStats = data.http
66 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
67 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
68 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
69 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
70 this.playerNetworkInfo.numPeers = p2pStats.numPeers
71 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
73 if (data.source === 'p2p-media-loader') {
74 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
75 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
89 this.container.style.display = 'block'
90 this.updateInterval = setInterval(async () => {
92 const options = this.mode === 'webtorrent'
93 ? await this.buildWebTorrentOptions()
94 : await this.buildHLSOptions()
96 this.list.innerHTML = this.getListTemplate(options)
98 console.error('Cannot update stats.', err)
99 clearInterval(this.updateInterval)
105 clearInterval(this.updateInterval)
106 this.container.style.display = 'none'
109 private async buildHLSOptions () {
110 const p2pMediaLoader = this.player_.p2pMediaLoader()
111 const level = p2pMediaLoader.getCurrentLevel()
113 const codecs = level?.videoCodec || level?.audioCodec
114 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
117 const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
118 const buffer = this.timeRangesToString(this.player().buffered())
123 if (this.options_.videoIsLive) {
124 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
126 progress = this.player().bufferedPercent()
130 playerNetworkInfo: this.playerNetworkInfo,
139 private async buildWebTorrentOptions () {
140 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
142 if (!this.metadataStore[videoFile.fileUrl]) {
143 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
146 const metadata = this.metadataStore[videoFile.fileUrl]
148 let colorSpace = 'unknown'
149 let codecs = 'unknown'
151 if (metadata?.streams[0]) {
152 const stream = metadata.streams[0]
154 colorSpace = stream['color_space'] !== 'unknown'
155 ? stream['color_space']
158 codecs = stream['codec_name'] || 'avc1'
161 const resolution = videoFile?.resolution.label + videoFile?.fps
162 const buffer = this.timeRangesToString(this.player().buffered())
163 const progress = this.player_.webtorrent().getTorrent()?.progress
166 playerNetworkInfo: this.playerNetworkInfo,
175 private getListTemplate (options: {
176 playerNetworkInfo: PlayerNetworkInfo
185 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
186 const player = this.player()
188 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
189 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
190 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
191 const pr = (window.devicePixelRatio || 1).toFixed(2)
192 const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
194 const duration = player.duration()
196 let volume = `${player.volume() * 100}`
197 if (player.muted()) volume += ' (muted)'
199 const networkActivity = playerNetworkInfo.downloadSpeed
200 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
203 const totalTransferred = playerNetworkInfo.totalDownloaded
204 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
206 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
207 ? `${playerNetworkInfo.downloadedFromServer} from server ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
210 const bufferProgress = progress !== undefined
211 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
215 ${this.buildElement(player.localize('Player mode'), this.options_.mode)}
217 ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
219 ${this.buildElement(player.localize('Viewport / Frames'), frames)}
221 ${this.buildElement(player.localize('Resolution'), resolution)}
223 ${this.buildElement(player.localize('Volume'), volume)}
225 ${this.buildElement(player.localize('Codecs'), codecs)}
226 ${this.buildElement(player.localize('Color'), colorSpace)}
228 ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)}
230 ${this.buildElement(player.localize('Network Activity'), networkActivity)}
231 ${this.buildElement(player.localize('Total Transfered'), totalTransferred)}
232 ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)}
234 ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)}
235 ${this.buildElement(player.localize('Buffer State'), buffer)}
237 ${this.buildElement(player.localize('Live Latency'), latency)}
241 private getMainTemplate () {
243 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
244 <div class="vjs-stats-list"></div>
248 private buildElement (label: string, value?: string) {
249 if (!value) return ''
251 return `<div><div>${label}</div><span>${value}</span></div>`
254 private timeRangesToString (r: videojs.TimeRange) {
257 for (let i = 0; i < r.length; i++) {
258 const start = Math.floor(r.start(i))
259 const end = Math.floor(r.end(i))
261 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
268 videojs.registerComponent('StatsCard', StatsCard)