]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
add stats videojs plugin
authorRigel Kent <sendmemail@rigelk.eu>
Mon, 12 Apr 2021 08:26:30 +0000 (10:26 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 28 Apr 2021 07:05:44 +0000 (09:05 +0200)
client/src/assets/player/images/info.svg [new file with mode: 0644]
client/src/assets/player/peertube-player-local-storage.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/peertube-videojs-typings.ts
client/src/assets/player/stats/stats-card.ts [new file with mode: 0644]
client/src/assets/player/stats/stats-plugin.ts [new file with mode: 0644]
client/src/sass/player/context-menu.scss
client/src/sass/player/index.scss
client/src/sass/player/stats.scss [new file with mode: 0644]

diff --git a/client/src/assets/player/images/info.svg b/client/src/assets/player/images/info.svg
new file mode 100644 (file)
index 0000000..bd1d9c6
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
\ No newline at end of file
index cf2cfb472e6b4bbdf80e41e1eecd810847f5ea40..80aceb2395801e0b9490df264230dfa151284fa4 100644 (file)
@@ -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())
 }
 
index ed82e0496595611024ac9fe0703d0aa5bd4bc0bd..62dff82859320aa1dc8b6e041a850ee022d9922b 100644 (file)
@@ -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: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
index 4a6c8024767cd20f15c45a1e941ba60315879fcf..cf92e5f08a4b626fcb8b31be11e17d2380eb45f9 100644 (file)
@@ -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<StatsCardOptions>): 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 (file)
index 0000000..278899b
--- /dev/null
@@ -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 `
+    <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')
+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 (file)
index 0000000..3402e78
--- /dev/null
@@ -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<StatsCardOptions> = {}) {
+    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 }
index df78916c6605dee6a0c9a05ca690babd368acffe..6bc66af0cdde629642e9da4ed6402c1d777c5866 100644 (file)
@@ -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}"] {
index fe92ce5e0637c7bf9c4c1434fa8da49431d7a515..502ee33ff0acf965e868dc62ff2875765f05052f 100644 (file)
@@ -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 (file)
index 0000000..953f603
--- /dev/null
@@ -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;
+  }
+
+}