From 4e11d8f3ca65bb1ffd0f42fa1521eabc5f3d7713 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 27 Apr 2021 15:50:29 +0200 Subject: [PATCH] Cleanup stats for nerds --- .../p2p-media-loader-plugin.ts | 30 +- .../assets/player/peertube-videojs-typings.ts | 6 +- client/src/assets/player/stats/stats-card.ts | 307 +++++++++++------- .../src/assets/player/stats/stats-plugin.ts | 6 +- .../player/webtorrent/webtorrent-plugin.ts | 3 +- scripts/i18n/create-custom-files.ts | 16 +- 6 files changed, 248 insertions(+), 120 deletions(-) diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index e97925ab5..4275a5e5e 100644 --- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts @@ -1,10 +1,10 @@ +import * as Hlsjs from 'hls.js/dist/hls.light.js' +import { Events, Segment } from 'p2p-media-loader-core' +import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' import videojs from 'video.js' import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' -import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' -import { Events, Segment } from 'p2p-media-loader-core' import { timeToInt } from '../utils' import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' -import * as Hlsjs from 'hls.js/dist/hls.light.js' registerConfigPlugin(videojs) registerSourceHandler(videojs) @@ -36,6 +36,9 @@ class P2pMediaLoaderPlugin extends Plugin { private networkInfoInterval: any + private hlsjsCurrentLevel: number + private hlsjsLevels: Hlsjs.Level[] + constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) { super(player) @@ -84,6 +87,16 @@ class P2pMediaLoaderPlugin extends Plugin { clearInterval(this.networkInfoInterval) } + getCurrentLevel () { + return this.hlsjsLevels.find(l => l.level === this.hlsjsCurrentLevel) + } + + getLiveLatency () { + return undefined as number + // FIXME: Use latency when hls >= V1 + // return this.hlsjs.latency + } + getHLSJS () { return this.hlsjs } @@ -140,6 +153,14 @@ class P2pMediaLoaderPlugin extends Plugin { this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) + this.hlsjs.on(Hlsjs.Events.MANIFEST_PARSED, (_e, manifest) => { + this.hlsjsCurrentLevel = manifest.firstLevel + this.hlsjsLevels = manifest.levels + }) + this.hlsjs.on(Hlsjs.Events.LEVEL_LOADED, (_e, level) => { + this.hlsjsCurrentLevel = level.levelId || (level as any).id + }) + this.networkInfoInterval = setInterval(() => { const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) @@ -166,7 +187,8 @@ class P2pMediaLoaderPlugin extends Plugin { numPeers: this.statsP2PBytes.numPeers, downloaded: this.statsP2PBytes.totalDownload, uploaded: this.statsP2PBytes.totalUpload - } + }, + bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8 } as PlayerNetworkInfo) }, this.CONSTANTS.INFO_SCHEDULER) } diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index cf92e5f08..8afb424a7 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -9,6 +9,7 @@ import { PlaylistPlugin } from './playlist/playlist-plugin' import { EndCardOptions } from './upnext/end-card' import { StatsCardOptions } from './stats/stats-card' import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' +import { StatsForNerdsPlugin } from './stats/stats-plugin' declare module 'video.js' { @@ -37,7 +38,7 @@ declare module 'video.js' { bezels (): void - stats (options?: Partial): any + stats (options?: StatsCardOptions): StatsForNerdsPlugin qualityLevels (): QualityLevels @@ -198,6 +199,9 @@ type PlayerNetworkInfo = { uploaded: number numPeers: number } + + // In bytes + bandwidthEstimate: number } type PlaylistItemOptions = { 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 +interface PlayerNetworkInfo { + downloadSpeed?: string + uploadSpeed?: string + totalDownloaded?: string + totalUploaded?: string + numPeers?: number + averageBandwidth?: string - 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 ` -
-
${player.localize('Video UUID')}
- ${options.videoUUID || ''} -
-
-
Viewport / ${player.localize('Frames')}
- ${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames} -
- -
${player.localize('Resolution')}
- ${videoFile?.resolution.label + videoFile?.fps} - -
-
${player.localize('Volume')}
- ${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''} -
- -
${player.localize('Codecs')}
- ${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'} - - -
${player.localize('Color')}
- ${colorspace || 'bt709'} - - -
${player.localize('Connection Speed')}
- ${playerNetworkInfo.averageBandwidth} - - -
${player.localize('Network Activity')}
- ${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑ - - -
${player.localize('Total Transfered')}
- ${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑ - - -
${player.localize('Download Breakdown')}
- ${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers - - -
${player.localize('Buffer Health')}
- ${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s) - -
-
${player.localize('Live Latency')}
- -
- ` -} - -function getMainTemplate () { - return ` - -
- ` + 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 ` + +
+ ` + } + + private buildElement (label: string, value?: string) { + if (!value) return '' + + return `
${label}
${value}
` + } + + 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 = {}) { + 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() } } diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index e557fe722..6f5fbe4c9 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts @@ -506,7 +506,8 @@ class WebTorrentPlugin extends Plugin { uploadSpeed: this.torrent.uploadSpeed, downloaded: this.torrent.downloaded, uploaded: this.torrent.uploaded - } + }, + bandwidthEstimate: this.webtorrent.downloadSpeed } as PlayerNetworkInfo) }, this.CONSTANTS.INFO_SCHEDULER) } diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index d4d5b44f0..81b6e3388 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts @@ -36,7 +36,21 @@ const playerKeys = { 'From servers: ': 'From servers: ', 'From peers: ': 'From peers: ', 'Normal mode': 'Normal mode', - 'Theater mode': 'Theater mode' + 'Stats for nerds': 'Stats for nerds', + 'Theater mode': 'Theater mode', + 'Video UUID': 'Video UUID', + 'Viewport / Frames': 'Viewport / Frames', + 'Resolution': 'Resolution', + 'Volume': 'Volume', + 'Codecs': 'Codecs', + 'Color': 'Color', + 'Connection Speed': 'Connection Speed', + 'Network Activity': 'Network Activity', + 'Total Transfered': 'Total Transfered', + 'Download Breakdown': 'Download Breakdown', + 'Buffer Progress': 'Buffer Progress', + 'Buffer State': 'Buffer State', + 'Live Latency': 'Live Latency' } Object.assign(playerKeys, videojs)