From ff563914bb10728301a24fb9e548c9efb62387eb Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Mon, 12 Apr 2021 10:26:30 +0200 Subject: [PATCH] add stats videojs plugin --- client/src/assets/player/images/info.svg | 1 + .../player/peertube-player-local-storage.ts | 1 + .../assets/player/peertube-player-manager.ts | 15 ++ .../assets/player/peertube-videojs-typings.ts | 3 + client/src/assets/player/stats/stats-card.ts | 184 ++++++++++++++++++ .../src/assets/player/stats/stats-plugin.ts | 31 +++ client/src/sass/player/context-menu.scss | 4 +- client/src/sass/player/index.scss | 1 + client/src/sass/player/stats.scss | 42 ++++ 9 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 client/src/assets/player/images/info.svg create mode 100644 client/src/assets/player/stats/stats-card.ts create mode 100644 client/src/assets/player/stats/stats-plugin.ts create mode 100644 client/src/sass/player/stats.scss diff --git a/client/src/assets/player/images/info.svg b/client/src/assets/player/images/info.svg new file mode 100644 index 000000000..bd1d9c6ca --- /dev/null +++ b/client/src/assets/player/images/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts index cf2cfb472..80aceb239 100644 --- a/client/src/assets/player/peertube-player-local-storage.ts +++ b/client/src/assets/player/peertube-player-local-storage.ts @@ -45,6 +45,7 @@ function saveTheaterInStore (enabled: boolean) { } function saveAverageBandwidth (value: number) { + /** used to choose the most fitting resolution */ return setLocalStorage('average-bandwidth', value.toString()) } diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index ed82e0496..62dff8285 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -4,6 +4,8 @@ import 'videojs-contextmenu-pt' import 'videojs-contrib-quality-levels' import './upnext/end-card' import './upnext/upnext-plugin' +import './stats/stats-card' +import './stats/stats-plugin' import './bezels/bezels-plugin' import './peertube-plugin' import './videojs-components/next-previous-video-button' @@ -170,6 +172,11 @@ export class PeertubePlayerManager { self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) player.bezels() + player.stats({ + videoUUID: options.common.videoUUID, + videoIsLive: options.common.isLive, + mode + }) return res(player) }) @@ -538,6 +545,14 @@ export class PeertubePlayerManager { }) } + items.push({ + icon: 'info', + label: player.localize('Stats for nerds'), + listener: () => { + player.stats().show() + } + }) + return items.map(i => ({ ...i, label: `` + i.label diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 4a6c80247..cf92e5f08 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -7,6 +7,7 @@ import { PlayerMode } from './peertube-player-manager' import { PeerTubePlugin } from './peertube-plugin' import { PlaylistPlugin } from './playlist/playlist-plugin' import { EndCardOptions } from './upnext/end-card' +import { StatsCardOptions } from './stats/stats-card' import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' declare module 'video.js' { @@ -36,6 +37,8 @@ declare module 'video.js' { bezels (): void + stats (options?: Partial): any + qualityLevels (): QualityLevels textTracks (): TextTrackList & { diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts new file mode 100644 index 000000000..278899b72 --- /dev/null +++ b/client/src/assets/player/stats/stats-card.ts @@ -0,0 +1,184 @@ +import videojs from 'video.js' +import { PlayerNetworkInfo } from '../peertube-videojs-typings' +import { getAverageBandwidthInStore } from '../peertube-player-local-storage' +import { bytes } from '../utils' + +interface StatsCardOptions extends videojs.ComponentOptions { + 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 ` +
+
${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 ` + +
+ ` +} + +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() + + constructor (player: videojs.Player, options: StatsCardOptions) { + super(player, options) + } + + createEl () { + const container = super.createEl('div', { + className: 'vjs-stats-content', + innerHTML: getMainTemplate() + }) as HTMLDivElement + this.container = container + this.container.style.display = 'none' + + this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement + this.closeButton.onclick = () => this.hide() + + this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement + + console.log(this.player_.qualityLevels()) + + this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { + if (!data) return // HTTP fallback + + this.source = data.source + + const p2pStats = data.p2p + const httpStats = data.http + + this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ') + this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ') + 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(' ') + + if (data.source === 'p2p-media-loader') { + this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') + this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') + } + }) + + return container + } + + toggle () { + this.update + ? this.hide() + : this.show() + } + + show (options?: StatsCardOptions) { + if (options) this.options_ = options + + let metadata = {} + + this.container.style.display = 'block' + this.update = 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) + } + }, this.interval) + } + + hide () { + clearInterval(this.update) + this.container.style.display = 'none' + } +} + +videojs.registerComponent('StatsCard', StatsCard) + +export { + StatsCard, + StatsCardOptions +} diff --git a/client/src/assets/player/stats/stats-plugin.ts b/client/src/assets/player/stats/stats-plugin.ts new file mode 100644 index 000000000..3402e7861 --- /dev/null +++ b/client/src/assets/player/stats/stats-plugin.ts @@ -0,0 +1,31 @@ +import videojs from 'video.js' +import { StatsCard, StatsCardOptions } from './stats-card' + +const Plugin = videojs.getPlugin('plugin') + +class StatsForNerdsPlugin extends Plugin { + private statsCard: StatsCard + + constructor (player: videojs.Player, options: Partial = {}) { + const settings = { + ...options + } + + super(player) + + this.player.ready(() => { + player.addClass('vjs-stats-for-nerds') + }) + + this.statsCard = new StatsCard(player, options) + + player.addChild(this.statsCard, settings) + } + + show (options?: StatsCardOptions) { + this.statsCard.show(options) + } +} + +videojs.registerPlugin('stats', StatsForNerdsPlugin) +export { StatsForNerdsPlugin } diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index df78916c6..6bc66af0c 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss @@ -8,7 +8,7 @@ $context-menu-width: 350px; .video-js .vjs-contextmenu-ui-menu { position: absolute; - background-color: rgba(0, 0, 0, 0.5); + background-color: $primary-background-color; padding: 8px 0; border-radius: 4px; width: $context-menu-width; @@ -42,7 +42,7 @@ $context-menu-width: 350px; mask-size: cover; margin-right: 0.8rem !important; - $icons: 'link-2', 'repeat', 'code', 'tick-white'; + $icons: 'link-2', 'repeat', 'code', 'tick-white', 'info'; @each $icon in $icons { &[class$="-#{$icon}"] { diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss index fe92ce5e0..502ee33ff 100644 --- a/client/src/sass/player/index.scss +++ b/client/src/sass/player/index.scss @@ -6,3 +6,4 @@ @import './upnext'; @import './bezels.scss'; @import './playlist.scss'; +@import './stats.scss'; diff --git a/client/src/sass/player/stats.scss b/client/src/sass/player/stats.scss new file mode 100644 index 000000000..953f6032a --- /dev/null +++ b/client/src/sass/player/stats.scss @@ -0,0 +1,42 @@ +@import './_player-variables'; + +$stats-width: 420px; +$contextmenu-background-color: rgba(0, 0, 0, 0.6); + +.video-js { + + .vjs-stats-content { + position: absolute; + background-color: $contextmenu-background-color; + padding: 5px 0; + border-radius: 4px; + width: $stats-width; + min-width: 27em; + max-width: calc(100vw - 20px); + left: 10px; + top: 10px; + z-index: 64; + font-size: 12px; + line-height: 1.2; + + @include transition(opacity 0.1s); + } + + .vjs-stats-close { + cursor: pointer; + position: absolute; + right: 3px; + top: 3px; + padding: 0; + } + + .vjs-stats-list > div > div { + display: inline-block; + font-weight: 600; + padding: 0 .5em; + text-align: right; + width: 11.5em; + white-space: nowrap; + } + +}