aboutsummaryrefslogblamecommitdiffhomepage
path: root/client/src/assets/player/stats/stats-card.ts
blob: e76a81a74e6bedac7cd287fbd5e4f3fa71260af4 (plain) (tree)
1
2
3
4
5
6
7
8
9
                              
                                                  
                                                                                         
                                

                                                             


                                         
                     

 






                             
 

                               




                                                   
 
                           
 

                          
 







                                           
 


                                             
                                       








                                                                                                 
                                                                              

                                        
                             








                                                                                                              
                                                                                              










                                                                                           

                                        

   
           
                                          
                                                   
           
                                                        
                                  
                                                          




                                                           
       
                       


           
                                      

                                         
 
                              





















































































                                                                                                                       
                                                       









                                                                                                 
                                                                                                                       






                                                                                
                                                                               
                                                                                                                      
 
















































                                                                                                          







                                                 
import videojs from 'video.js'
import { secondsToTime } from '@shared/core-utils'
import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
import { bytes } from '../utils'

interface StatsCardOptions extends videojs.ComponentOptions {
  videoUUID: string
  videoIsLive: boolean
  mode: 'webtorrent' | 'p2p-media-loader'
  p2pEnabled: boolean
}

interface PlayerNetworkInfo {
  downloadSpeed?: string
  uploadSpeed?: string
  totalDownloaded?: string
  totalUploaded?: string
  numPeers?: number
  averageBandwidth?: string

  downloadedFromServer?: string
  downloadedFromPeers?: string
}

const Component = videojs.getComponent('Component')
class StatsCard extends Component {
  options_: StatsCardOptions

  container: HTMLDivElement

  list: HTMLDivElement
  closeButton: HTMLElement

  updateInterval: any

  mode: 'webtorrent' | 'p2p-media-loader'

  metadataStore: any = {}

  intervalMs = 300
  playerNetworkInfo: PlayerNetworkInfo = {}

  createEl () {
    const container = super.createEl('div', {
      className: 'vjs-stats-content',
      innerHTML: this.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

    this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
      if (!data) return // HTTP fallback

      this.mode = 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(data.bandwidthEstimate).join(' ') + '/s'

      if (data.source === 'p2p-media-loader') {
        this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
        this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
      }
    })

    return container
  }

  toggle () {
    if (this.updateInterval) this.hide()
    else this.show()
  }

  show () {
    this.container.style.display = 'block'
    this.updateInterval = setInterval(async () => {
      try {
        const options = this.mode === 'p2p-media-loader'
          ? this.buildHLSOptions()
          : await this.buildWebTorrentOptions() // Default

        this.list.innerHTML = this.getListTemplate(options)
      } catch (err) {
        console.error('Cannot update stats.', err)
        clearInterval(this.updateInterval)
      }
    }, this.intervalMs)
  }

  hide () {
    clearInterval(this.updateInterval)
    this.container.style.display = 'none'
  }

  private 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 = `${Math.round(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 servers · ${playerNetworkInfo.downloadedFromPeers} from peers`
      : undefined

    const bufferProgress = progress !== undefined
      ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
      : undefined

    return `
      ${this.buildElement(player.localize('Player mode'), this.mode || 'HTTP')}
      ${this.buildElement(player.localize('P2P'), player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))}

      ${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)

export {
  StatsCard,
  StatsCardOptions
}