Cleanup stats for nerds

This commit is contained in:
Chocobozzz 2021-04-27 15:50:29 +02:00 committed by Chocobozzz
parent ff563914bb
commit 4e11d8f3ca
6 changed files with 248 additions and 120 deletions

View File

@ -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 videojs from 'video.js'
import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' 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 { timeToInt } from '../utils'
import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
import * as Hlsjs from 'hls.js/dist/hls.light.js'
registerConfigPlugin(videojs) registerConfigPlugin(videojs)
registerSourceHandler(videojs) registerSourceHandler(videojs)
@ -36,6 +36,9 @@ class P2pMediaLoaderPlugin extends Plugin {
private networkInfoInterval: any private networkInfoInterval: any
private hlsjsCurrentLevel: number
private hlsjsLevels: Hlsjs.Level[]
constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) { constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
super(player) super(player)
@ -84,6 +87,16 @@ class P2pMediaLoaderPlugin extends Plugin {
clearInterval(this.networkInfoInterval) 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 () { getHLSJS () {
return this.hlsjs return this.hlsjs
} }
@ -140,6 +153,14 @@ class P2pMediaLoaderPlugin extends Plugin {
this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
this.p2pEngine.on(Events.PeerClose, () => 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(() => { this.networkInfoInterval = setInterval(() => {
const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
@ -166,7 +187,8 @@ class P2pMediaLoaderPlugin extends Plugin {
numPeers: this.statsP2PBytes.numPeers, numPeers: this.statsP2PBytes.numPeers,
downloaded: this.statsP2PBytes.totalDownload, downloaded: this.statsP2PBytes.totalDownload,
uploaded: this.statsP2PBytes.totalUpload uploaded: this.statsP2PBytes.totalUpload
} },
bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8
} as PlayerNetworkInfo) } as PlayerNetworkInfo)
}, this.CONSTANTS.INFO_SCHEDULER) }, this.CONSTANTS.INFO_SCHEDULER)
} }

View File

@ -9,6 +9,7 @@ import { PlaylistPlugin } from './playlist/playlist-plugin'
import { EndCardOptions } from './upnext/end-card' import { EndCardOptions } from './upnext/end-card'
import { StatsCardOptions } from './stats/stats-card' import { StatsCardOptions } from './stats/stats-card'
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
import { StatsForNerdsPlugin } from './stats/stats-plugin'
declare module 'video.js' { declare module 'video.js' {
@ -37,7 +38,7 @@ declare module 'video.js' {
bezels (): void bezels (): void
stats (options?: Partial<StatsCardOptions>): any stats (options?: StatsCardOptions): StatsForNerdsPlugin
qualityLevels (): QualityLevels qualityLevels (): QualityLevels
@ -198,6 +199,9 @@ type PlayerNetworkInfo = {
uploaded: number uploaded: number
numPeers: number numPeers: number
} }
// In bytes
bandwidthEstimate: number
} }
type PlaylistItemOptions = { type PlaylistItemOptions = {

View File

@ -1,103 +1,42 @@
import videojs from 'video.js' import videojs from 'video.js'
import { PlayerNetworkInfo } from '../peertube-videojs-typings' import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
import { getAverageBandwidthInStore } from '../peertube-player-local-storage' import { bytes, secondsToTime } from '../utils'
import { bytes } from '../utils'
interface StatsCardOptions extends videojs.ComponentOptions { interface StatsCardOptions extends videojs.ComponentOptions {
videoUUID?: string, videoUUID: string
videoIsLive?: boolean, videoIsLive: boolean
mode?: 'webtorrent' | 'p2p-media-loader' mode: 'webtorrent' | 'p2p-media-loader'
} }
function getListTemplate ( interface PlayerNetworkInfo {
options: StatsCardOptions, downloadSpeed?: string
player: videojs.Player, uploadSpeed?: string
args: { totalDownloaded?: string
playerNetworkInfo?: any totalUploaded?: string
videoFile?: any numPeers?: number
progress?: number averageBandwidth?: string
}) {
const { playerNetworkInfo, videoFile, progress } = args
const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality() downloadedFromServer?: string
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) downloadedFromPeers?: string
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} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;</span>
</div>
<div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
<div>${player.localize('Total Transfered')}</div>
<span>${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;</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>
`
}
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>
`
} }
const Component = videojs.getComponent('Component') const Component = videojs.getComponent('Component')
class StatsCard extends Component { class StatsCard extends Component {
options_: StatsCardOptions options_: StatsCardOptions
container: HTMLDivElement container: HTMLDivElement
list: HTMLDivElement list: HTMLDivElement
closeButton: HTMLElement closeButton: HTMLElement
update: any
source: any
interval = 300 updateInterval: any
playerNetworkInfo: any = {}
statsForNerdsEvents = new videojs.EventTarget() mode: 'webtorrent' | 'p2p-media-loader'
metadataStore: any = {}
intervalMs = 300
playerNetworkInfo: PlayerNetworkInfo = {}
constructor (player: videojs.Player, options: StatsCardOptions) { constructor (player: videojs.Player, options: StatsCardOptions) {
super(player, options) super(player, options)
@ -106,7 +45,7 @@ class StatsCard extends Component {
createEl () { createEl () {
const container = super.createEl('div', { const container = super.createEl('div', {
className: 'vjs-stats-content', className: 'vjs-stats-content',
innerHTML: getMainTemplate() innerHTML: this.getMainTemplate()
}) as HTMLDivElement }) as HTMLDivElement
this.container = container this.container = container
this.container.style.display = 'none' this.container.style.display = 'none'
@ -116,12 +55,10 @@ class StatsCard extends Component {
this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
console.log(this.player_.qualityLevels()) this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
if (!data) return // HTTP fallback if (!data) return // HTTP fallback
this.source = data.source this.mode = data.source
const p2pStats = data.p2p const p2pStats = data.p2p
const httpStats = data.http const httpStats = data.http
@ -131,7 +68,7 @@ class StatsCard extends Component {
this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ') this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
this.playerNetworkInfo.numPeers = p2pStats.numPeers 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') { if (data.source === 'p2p-media-loader') {
this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
@ -143,37 +80,187 @@ class StatsCard extends Component {
} }
toggle () { toggle () {
this.update this.updateInterval
? this.hide() ? this.hide()
: this.show() : this.show()
} }
show (options?: StatsCardOptions) { show () {
if (options) this.options_ = options
let metadata = {}
this.container.style.display = 'block' this.container.style.display = 'block'
this.update = setInterval(async () => { this.updateInterval = setInterval(async () => {
try { try {
if (this.source === 'webtorrent') { const options = this.mode === 'webtorrent'
const progress = this.player_.webtorrent().getTorrent()?.progress ? await this.buildWebTorrentOptions()
const videoFile = this.player_.webtorrent().getCurrentVideoFile() : await this.buildHLSOptions()
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 }) this.list.innerHTML = this.getListTemplate(options)
} else { } catch (err) {
this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo }) console.error('Cannot update stats.', err)
clearInterval(this.updateInterval)
} }
} catch (e) { }, this.intervalMs)
clearInterval(this.update)
}
}, this.interval)
} }
hide () { hide () {
clearInterval(this.update) clearInterval(this.updateInterval)
this.container.style.display = 'none' 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} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;`
: undefined
const totalTransferred = playerNetworkInfo.totalDownloaded
? `${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;`
: 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) videojs.registerComponent('StatsCard', StatsCard)

View File

@ -6,7 +6,7 @@ const Plugin = videojs.getPlugin('plugin')
class StatsForNerdsPlugin extends Plugin { class StatsForNerdsPlugin extends Plugin {
private statsCard: StatsCard private statsCard: StatsCard
constructor (player: videojs.Player, options: Partial<StatsCardOptions> = {}) { constructor (player: videojs.Player, options: StatsCardOptions) {
const settings = { const settings = {
...options ...options
} }
@ -22,8 +22,8 @@ class StatsForNerdsPlugin extends Plugin {
player.addChild(this.statsCard, settings) player.addChild(this.statsCard, settings)
} }
show (options?: StatsCardOptions) { show () {
this.statsCard.show(options) this.statsCard.show()
} }
} }

View File

@ -506,7 +506,8 @@ class WebTorrentPlugin extends Plugin {
uploadSpeed: this.torrent.uploadSpeed, uploadSpeed: this.torrent.uploadSpeed,
downloaded: this.torrent.downloaded, downloaded: this.torrent.downloaded,
uploaded: this.torrent.uploaded uploaded: this.torrent.uploaded
} },
bandwidthEstimate: this.webtorrent.downloadSpeed
} as PlayerNetworkInfo) } as PlayerNetworkInfo)
}, this.CONSTANTS.INFO_SCHEDULER) }, this.CONSTANTS.INFO_SCHEDULER)
} }

View File

@ -36,7 +36,21 @@ const playerKeys = {
'From servers: ': 'From servers: ', 'From servers: ': 'From servers: ',
'From peers: ': 'From peers: ', 'From peers: ': 'From peers: ',
'Normal mode': 'Normal mode', '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) Object.assign(playerKeys, videojs)