From 4e11d8f3ca65bb1ffd0f42fa1521eabc5f3d7713 Mon Sep 17 00:00:00 2001 From: Chocobozzz <me@florianbigard.com> Date: Tue, 27 Apr 2021 15:50:29 +0200 Subject: Cleanup stats for nerds --- client/src/assets/player/stats/stats-card.ts | 307 ++++++++++++++++--------- client/src/assets/player/stats/stats-plugin.ts | 6 +- 2 files changed, 200 insertions(+), 113 deletions(-) (limited to 'client/src/assets/player/stats') diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts index 278899b72..f66766089 100644 --- a/client/src/assets/player/stats/stats-card.ts +++ b/client/src/assets/player/stats/stats-card.ts @@ -1,103 +1,42 @@ 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) @@ -106,7 +45,7 @@ class StatsCard extends Component { 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' @@ -116,12 +55,10 @@ class StatsCard extends Component { 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 @@ -131,7 +68,7 @@ class StatsCard extends Component { 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(' ') @@ -143,37 +80,187 @@ class StatsCard extends Component { } 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) diff --git a/client/src/assets/player/stats/stats-plugin.ts b/client/src/assets/player/stats/stats-plugin.ts index 3402e7861..8aad80e8a 100644 --- a/client/src/assets/player/stats/stats-plugin.ts +++ b/client/src/assets/player/stats/stats-plugin.ts @@ -6,7 +6,7 @@ const Plugin = videojs.getPlugin('plugin') class StatsForNerdsPlugin extends Plugin { private statsCard: StatsCard - constructor (player: videojs.Player, options: Partial<StatsCardOptions> = {}) { + constructor (player: videojs.Player, options: StatsCardOptions) { const settings = { ...options } @@ -22,8 +22,8 @@ class StatsForNerdsPlugin extends Plugin { player.addChild(this.statsCard, settings) } - show (options?: StatsCardOptions) { - this.statsCard.show(options) + show () { + this.statsCard.show() } } -- cgit v1.2.3