1 import videojs from 'video.js'
2 import { PlayerNetworkInfo } from '../peertube-videojs-typings'
3 import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
4 import { bytes } from '../utils'
6 interface StatsCardOptions extends videojs.ComponentOptions {
9 mode?: 'webtorrent' | 'p2p-media-loader'
12 function getListTemplate (
13 options: StatsCardOptions,
14 player: videojs.Player,
16 playerNetworkInfo?: any
20 const { playerNetworkInfo, videoFile, progress } = args
22 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
23 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
24 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
25 const pr = (window.devicePixelRatio || 1).toFixed(2)
26 const colorspace = videoFile?.metadata?.streams[0]['color_space'] !== "unknown"
27 ? videoFile?.metadata?.streams[0]['color_space']
32 <div>${player.localize('Video UUID')}</div>
33 <span>${options.videoUUID || ''}</span>
36 <div>Viewport / ${player.localize('Frames')}</div>
37 <span>${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}</span>
39 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
40 <div>${player.localize('Resolution')}</div>
41 <span>${videoFile?.resolution.label + videoFile?.fps}</span>
44 <div>${player.localize('Volume')}</div>
45 <span>${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''}</span>
47 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
48 <div>${player.localize('Codecs')}</div>
49 <span>${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'}</span>
51 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
52 <div>${player.localize('Color')}</div>
53 <span>${colorspace || 'bt709'}</span>
55 <div${playerNetworkInfo.averageBandwidth !== undefined ? '' : ' style="display: none;"'}>
56 <div>${player.localize('Connection Speed')}</div>
57 <span>${playerNetworkInfo.averageBandwidth}</span>
59 <div${playerNetworkInfo.downloadSpeed !== undefined ? '' : ' style="display: none;"'}>
60 <div>${player.localize('Network Activity')}</div>
61 <span>${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑</span>
63 <div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
64 <div>${player.localize('Total Transfered')}</div>
65 <span>${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑</span>
67 <div${playerNetworkInfo.downloadedFromServer ? '' : ' style="display: none;"'}>
68 <div>${player.localize('Download Breakdown')}</div>
69 <span>${playerNetworkInfo.downloadedFromServer} from server ยท ${playerNetworkInfo.downloadedFromPeers} from peers</span>
71 <div${progress !== undefined && videoFile !== undefined ? '' : ' style="display: none;"'}>
72 <div>${player.localize('Buffer Health')}</div>
73 <span>${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s)</span>
75 <div style="display: none;"> <!-- TODO: implement live latency measure -->
76 <div>${player.localize('Live Latency')}</div>
82 function getMainTemplate () {
84 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
85 <div class="vjs-stats-list"></div>
89 const Component = videojs.getComponent('Component')
90 class StatsCard extends Component {
91 options_: StatsCardOptions
92 container: HTMLDivElement
94 closeButton: HTMLElement
99 playerNetworkInfo: any = {}
100 statsForNerdsEvents = new videojs.EventTarget()
102 constructor (player: videojs.Player, options: StatsCardOptions) {
103 super(player, options)
107 const container = super.createEl('div', {
108 className: 'vjs-stats-content',
109 innerHTML: getMainTemplate()
111 this.container = container
112 this.container.style.display = 'none'
114 this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
115 this.closeButton.onclick = () => this.hide()
117 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
119 console.log(this.player_.qualityLevels())
121 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
122 if (!data) return // HTTP fallback
124 this.source = data.source
126 const p2pStats = data.p2p
127 const httpStats = data.http
129 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
130 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
131 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
132 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
133 this.playerNetworkInfo.numPeers = p2pStats.numPeers
134 this.playerNetworkInfo.averageBandwidth = bytes(getAverageBandwidthInStore() || p2pStats.downloaded + httpStats.downloaded).join(' ')
136 if (data.source === 'p2p-media-loader') {
137 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
138 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
151 show (options?: StatsCardOptions) {
152 if (options) this.options_ = options
156 this.container.style.display = 'block'
157 this.update = setInterval(async () => {
159 if (this.source === 'webtorrent') {
160 const progress = this.player_.webtorrent().getTorrent()?.progress
161 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
162 videoFile.metadata = metadata[videoFile.fileUrl] = videoFile.metadata || metadata[videoFile.fileUrl] || videoFile.metadataUrl && await fetch(videoFile.metadataUrl).then(res => res.json())
163 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo, videoFile, progress })
165 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo })
168 clearInterval(this.update)
174 clearInterval(this.update)
175 this.container.style.display = 'none'
179 videojs.registerComponent('StatsCard', StatsCard)