import videojs from 'video.js'
-import { PlayerNetworkInfo } from '../peertube-videojs-typings'
-import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
-import { bytes } from '../utils'
+import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
+import { bytes, secondsToTime } from '../utils'
interface StatsCardOptions extends videojs.ComponentOptions {
- videoUUID?: string,
- videoIsLive?: boolean,
- mode?: 'webtorrent' | 'p2p-media-loader'
+ videoUUID: string
+ videoIsLive: boolean
+ mode: 'webtorrent' | 'p2p-media-loader'
}
-function getListTemplate (
- options: StatsCardOptions,
- player: videojs.Player,
- args: {
- playerNetworkInfo?: any
- videoFile?: any
- progress?: number
- }) {
- const { playerNetworkInfo, videoFile, progress } = args
-
- const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
- const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
- const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
- const pr = (window.devicePixelRatio || 1).toFixed(2)
- const colorspace = videoFile?.metadata?.streams[0]['color_space'] !== "unknown"
- ? videoFile?.metadata?.streams[0]['color_space']
- : undefined
-
- return `
- <div>
- <div>${player.localize('Video UUID')}</div>
- <span>${options.videoUUID || ''}</span>
- </div>
- <div>
- <div>Viewport / ${player.localize('Frames')}</div>
- <span>${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}</span>
- </div>
- <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Resolution')}</div>
- <span>${videoFile?.resolution.label + videoFile?.fps}</span>
- </div>
- <div>
- <div>${player.localize('Volume')}</div>
- <span>${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''}</span>
- </div>
- <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Codecs')}</div>
- <span>${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'}</span>
- </div>
- <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Color')}</div>
- <span>${colorspace || 'bt709'}</span>
- </div>
- <div${playerNetworkInfo.averageBandwidth !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Connection Speed')}</div>
- <span>${playerNetworkInfo.averageBandwidth}</span>
- </div>
- <div${playerNetworkInfo.downloadSpeed !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Network Activity')}</div>
- <span>${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑</span>
- </div>
- <div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Total Transfered')}</div>
- <span>${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑</span>
- </div>
- <div${playerNetworkInfo.downloadedFromServer ? '' : ' style="display: none;"'}>
- <div>${player.localize('Download Breakdown')}</div>
- <span>${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers</span>
- </div>
- <div${progress !== undefined && videoFile !== undefined ? '' : ' style="display: none;"'}>
- <div>${player.localize('Buffer Health')}</div>
- <span>${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s)</span>
- </div>
- <div style="display: none;"> <!-- TODO: implement live latency measure -->
- <div>${player.localize('Live Latency')}</div>
- <span></span>
- </div>
- `
-}
+interface PlayerNetworkInfo {
+ downloadSpeed?: string
+ uploadSpeed?: string
+ totalDownloaded?: string
+ totalUploaded?: string
+ numPeers?: number
+ averageBandwidth?: string
-function getMainTemplate () {
- return `
- <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
- <div class="vjs-stats-list"></div>
- `
+ downloadedFromServer?: string
+ downloadedFromPeers?: string
}
const Component = videojs.getComponent('Component')
class StatsCard extends Component {
options_: StatsCardOptions
+
container: HTMLDivElement
+
list: HTMLDivElement
closeButton: HTMLElement
- update: any
- source: any
- interval = 300
- playerNetworkInfo: any = {}
- statsForNerdsEvents = new videojs.EventTarget()
+ updateInterval: any
+
+ mode: 'webtorrent' | 'p2p-media-loader'
+
+ metadataStore: any = {}
+
+ intervalMs = 300
+ playerNetworkInfo: PlayerNetworkInfo = {}
constructor (player: videojs.Player, options: StatsCardOptions) {
super(player, options)
createEl () {
const container = super.createEl('div', {
className: 'vjs-stats-content',
- innerHTML: getMainTemplate()
+ innerHTML: this.getMainTemplate()
}) as HTMLDivElement
this.container = container
this.container.style.display = 'none'
this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
- console.log(this.player_.qualityLevels())
-
- this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
+ this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
if (!data) return // HTTP fallback
- this.source = data.source
+ this.mode = data.source
const p2pStats = data.p2p
const httpStats = data.http
this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
this.playerNetworkInfo.numPeers = p2pStats.numPeers
- this.playerNetworkInfo.averageBandwidth = bytes(getAverageBandwidthInStore() || p2pStats.downloaded + httpStats.downloaded).join(' ')
+ this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
if (data.source === 'p2p-media-loader') {
this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
}
toggle () {
- this.update
+ this.updateInterval
? this.hide()
: this.show()
}
- show (options?: StatsCardOptions) {
- if (options) this.options_ = options
-
- let metadata = {}
-
+ show () {
this.container.style.display = 'block'
- this.update = setInterval(async () => {
+ this.updateInterval = setInterval(async () => {
try {
- if (this.source === 'webtorrent') {
- const progress = this.player_.webtorrent().getTorrent()?.progress
- const videoFile = this.player_.webtorrent().getCurrentVideoFile()
- videoFile.metadata = metadata[videoFile.fileUrl] = videoFile.metadata || metadata[videoFile.fileUrl] || videoFile.metadataUrl && await fetch(videoFile.metadataUrl).then(res => res.json())
- this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo, videoFile, progress })
- } else {
- this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo })
- }
- } catch (e) {
- clearInterval(this.update)
+ const options = this.mode === 'webtorrent'
+ ? await this.buildWebTorrentOptions()
+ : await this.buildHLSOptions()
+
+ this.list.innerHTML = this.getListTemplate(options)
+ } catch (err) {
+ console.error('Cannot update stats.', err)
+ clearInterval(this.updateInterval)
}
- }, this.interval)
+ }, this.intervalMs)
}
hide () {
- clearInterval(this.update)
+ clearInterval(this.updateInterval)
this.container.style.display = 'none'
}
+
+ private async buildHLSOptions () {
+ const p2pMediaLoader = this.player_.p2pMediaLoader()
+ const level = p2pMediaLoader.getCurrentLevel()
+
+ const codecs = level?.videoCodec || level?.audioCodec
+ ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
+ : undefined
+
+ const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
+ const buffer = this.timeRangesToString(this.player().buffered())
+
+ let progress: number
+ let latency: string
+
+ if (this.options_.videoIsLive) {
+ latency = secondsToTime(p2pMediaLoader.getLiveLatency())
+ } else {
+ progress = this.player().bufferedPercent()
+ }
+
+ return {
+ playerNetworkInfo: this.playerNetworkInfo,
+ resolution,
+ codecs,
+ buffer,
+ latency,
+ progress
+ }
+ }
+
+ private async buildWebTorrentOptions () {
+ const videoFile = this.player_.webtorrent().getCurrentVideoFile()
+
+ if (!this.metadataStore[videoFile.fileUrl]) {
+ this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
+ }
+
+ const metadata = this.metadataStore[videoFile.fileUrl]
+
+ let colorSpace = 'unknown'
+ let codecs = 'unknown'
+
+ if (metadata?.streams[0]) {
+ const stream = metadata.streams[0]
+
+ colorSpace = stream['color_space'] !== 'unknown'
+ ? stream['color_space']
+ : 'bt709'
+
+ codecs = stream['codec_name'] || 'avc1'
+ }
+
+ const resolution = videoFile?.resolution.label + videoFile?.fps
+ const buffer = this.timeRangesToString(this.player().buffered())
+ const progress = this.player_.webtorrent().getTorrent()?.progress
+
+ return {
+ playerNetworkInfo: this.playerNetworkInfo,
+ progress,
+ colorSpace,
+ codecs,
+ resolution,
+ buffer
+ }
+ }
+
+ private getListTemplate (options: {
+ playerNetworkInfo: PlayerNetworkInfo
+ progress: number
+ codecs: string
+ resolution: string
+ buffer: string
+
+ latency?: string
+ colorSpace?: string
+ }) {
+ const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
+ const player = this.player()
+
+ const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
+ const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
+ const pr = (window.devicePixelRatio || 1).toFixed(2)
+ const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
+
+ const duration = player.duration()
+
+ let volume = `${player.volume() * 100}`
+ if (player.muted()) volume += ' (muted)'
+
+ const networkActivity = playerNetworkInfo.downloadSpeed
+ ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
+ : undefined
+
+ const totalTransferred = playerNetworkInfo.totalDownloaded
+ ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
+ : undefined
+ const downloadBreakdown = playerNetworkInfo.downloadedFromServer
+ ? `${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers`
+ : undefined
+
+ const bufferProgress = progress !== undefined
+ ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
+ : undefined
+
+ return `
+ ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
+
+ ${this.buildElement(player.localize('Viewport / Frames'), frames)}
+
+ ${this.buildElement(player.localize('Resolution'), resolution)}
+
+ ${this.buildElement(player.localize('Volume'), volume)}
+
+ ${this.buildElement(player.localize('Codecs'), codecs)}
+ ${this.buildElement(player.localize('Color'), colorSpace)}
+
+ ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)}
+
+ ${this.buildElement(player.localize('Network Activity'), networkActivity)}
+ ${this.buildElement(player.localize('Total Transfered'), totalTransferred)}
+ ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)}
+
+ ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)}
+ ${this.buildElement(player.localize('Buffer State'), buffer)}
+
+ ${this.buildElement(player.localize('Live Latency'), latency)}
+ `
+ }
+
+ private getMainTemplate () {
+ return `
+ <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
+ <div class="vjs-stats-list"></div>
+ `
+ }
+
+ private buildElement (label: string, value?: string) {
+ if (!value) return ''
+
+ return `<div><div>${label}</div><span>${value}</span></div>`
+ }
+
+ private timeRangesToString (r: videojs.TimeRange) {
+ let result = ''
+
+ for (let i = 0; i < r.length; i++) {
+ const start = Math.floor(r.start(i))
+ const end = Math.floor(r.end(i))
+
+ result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
+ }
+
+ return result
+ }
}
videojs.registerComponent('StatsCard', StatsCard)