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

                                                                         

                                                             

                      
                                        
                     

 






                             
 

                               

 




                       


                                                   
 

                     
                                        




                                           
 





















                                     
                                                                                   
 
               






                                                    
                        
 






                                                        
 



                                                 
 
                                                   
                             



                                 




                                                                                                                             

                                               
                                                                                                
                                                                                           
                                                                                               
       

     
                                                               
 
                           

   


                                                               
                                                                



                   
             

                                        

   
           

                                            
                                                   
           
                                                        
                                  
                                                        
 
                                        
                     
                                                 
                                          
       
                       


           
                                      
                                           
   
 
                              






                                                                 
                                    
                                                             

                 




















                                                                    

                                                                   









                                                                                                        
                                 









                                                                   
                                                                   
                                                   










                                                





































                                                                                      









                                                                                                    

                                                                           





                                                                                            


                                                                                                                      

                                      
                                                       
                                                             




                                                                                             







                                                                       
                                                                    
                                                                                                            





                                                                                



                                                                                     
                                                         
 





                                                                          
 


                                                         
 

                                                          
 

                                                
 




                                                         
 
                                   
 

                                            

   





                                                                                       
 

                           
 
                          













                                                                   







                                                 
import videojs from 'video.js'
import { logger } from '@root-helpers/logger'
import { secondsToTime } from '@shared/core-utils'
import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
import { bytes } from '../common'

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

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

  downloadedFromServer?: string
  downloadedFromPeers?: string
}

interface InfoElement {
  root: HTMLElement
  value: HTMLElement
}

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

  updateInterval: any

  mode: 'web-video' | 'p2p-media-loader'

  metadataStore: any = {}

  intervalMs = 300
  playerNetworkInfo: PlayerNetworkInfo = {}

  private containerEl: HTMLDivElement
  private infoListEl: HTMLDivElement

  private playerMode: InfoElement
  private p2p: InfoElement
  private uuid: InfoElement
  private viewport: InfoElement
  private resolution: InfoElement
  private volume: InfoElement
  private codecs: InfoElement
  private color: InfoElement
  private connection: InfoElement

  private network: InfoElement
  private transferred: InfoElement
  private download: InfoElement

  private bufferProgress: InfoElement
  private bufferState: InfoElement

  private liveLatency: InfoElement

  private onNetworkInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void

  createEl () {
    this.containerEl = videojs.dom.createEl('div', {
      className: 'vjs-stats-content'
    }) as HTMLDivElement
    this.containerEl.style.display = 'none'

    this.infoListEl = videojs.dom.createEl('div', {
      className: 'vjs-stats-list'
    }) as HTMLDivElement

    const closeButton = videojs.dom.createEl('button', {
      className: 'vjs-stats-close',
      tabindex: '0',
      title: 'Close stats',
      innerText: '[x]'
    }, { 'aria-label': 'Close stats' }) as HTMLElement
    closeButton.onclick = () => this.hide()

    this.containerEl.appendChild(closeButton)
    this.containerEl.appendChild(this.infoListEl)

    this.populateInfoBlocks()

    this.onNetworkInfoHandler = (_event, data) => {
      this.mode = data.source

      const p2pStats = data.p2p
      const httpStats = data.http

      this.playerNetworkInfo.downloadSpeed = bytes((p2pStats?.downloadSpeed || 0) + (httpStats.downloadSpeed || 0)).join(' ')
      this.playerNetworkInfo.uploadSpeed = bytes(p2pStats?.uploadSpeed || 0).join(' ')
      this.playerNetworkInfo.totalDownloaded = bytes((p2pStats?.downloaded || 0) + httpStats.downloaded).join(' ')
      this.playerNetworkInfo.totalUploaded = bytes(p2pStats?.uploaded || 0).join(' ')
      this.playerNetworkInfo.numPeers = p2pStats?.peersWithWebSeed

      if (data.source === 'p2p-media-loader') {
        this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
        this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
        this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats?.downloaded || 0).join(' ')
      }
    }

    this.player().on('network-info', this.onNetworkInfoHandler)

    return this.containerEl
  }

  dispose () {
    if (this.updateInterval) clearInterval(this.updateInterval)

    this.player().off('network-info', this.onNetworkInfoHandler)

    super.dispose()
  }

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

  show () {
    this.containerEl.style.display = 'block'

    this.updateInterval = setInterval(async () => {
      try {
        const options = this.mode === 'p2p-media-loader'
          ? this.buildHLSOptions()
          : await this.buildWebVideoOptions() // Default

        this.populateInfoValues(options)
      } catch (err) {
        logger.error('Cannot update stats.', err)
        clearInterval(this.updateInterval)
      }
    }, this.intervalMs)
  }

  hide () {
    clearInterval(this.updateInterval)
    this.containerEl.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
      ? `${level.height}p${level?.attrs['FRAME-RATE'] || ''}`
      : undefined

    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 buildWebVideoOptions () {
    const videoFile = this.player_.webVideo().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_.bufferedPercent()

    return {
      playerNetworkInfo: this.playerNetworkInfo,
      progress,
      colorSpace,
      codecs,
      resolution,
      buffer
    }
  }

  private populateInfoBlocks () {
    this.playerMode = this.buildInfoRow(this.player().localize('Player mode'))
    this.p2p = this.buildInfoRow(this.player().localize('P2P'))
    this.uuid = this.buildInfoRow(this.player().localize('Video UUID'))
    this.viewport = this.buildInfoRow(this.player().localize('Viewport / Frames'))
    this.resolution = this.buildInfoRow(this.player().localize('Resolution'))
    this.volume = this.buildInfoRow(this.player().localize('Volume'))
    this.codecs = this.buildInfoRow(this.player().localize('Codecs'))
    this.color = this.buildInfoRow(this.player().localize('Color'))
    this.connection = this.buildInfoRow(this.player().localize('Connection Speed'))

    this.network = this.buildInfoRow(this.player().localize('Network Activity'))
    this.transferred = this.buildInfoRow(this.player().localize('Total Transfered'))
    this.download = this.buildInfoRow(this.player().localize('Download Breakdown'))

    this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress'))
    this.bufferState = this.buildInfoRow(this.player().localize('Buffer State'))

    this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency'))

    this.infoListEl.appendChild(this.playerMode.root)
    this.infoListEl.appendChild(this.p2p.root)
    this.infoListEl.appendChild(this.uuid.root)
    this.infoListEl.appendChild(this.viewport.root)
    this.infoListEl.appendChild(this.resolution.root)
    this.infoListEl.appendChild(this.volume.root)
    this.infoListEl.appendChild(this.codecs.root)
    this.infoListEl.appendChild(this.color.root)
    this.infoListEl.appendChild(this.connection.root)
    this.infoListEl.appendChild(this.network.root)
    this.infoListEl.appendChild(this.transferred.root)
    this.infoListEl.appendChild(this.download.root)
    this.infoListEl.appendChild(this.bufferProgress.root)
    this.infoListEl.appendChild(this.bufferState.root)
    this.infoListEl.appendChild(this.liveLatency.root)
  }

  private populateInfoValues (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 { downloadedFromServer, downloadedFromPeers } = playerNetworkInfo

    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 vp = `${vw}x${vh}*${pr}`
    const { droppedVideoFrames, totalVideoFrames } = videoQuality
    const frames = player.localize('{1} / {2} dropped of {3}', [ vp, droppedVideoFrames + '', totalVideoFrames + '' ])
    const duration = player.duration()

    let volume = `${Math.round(player.volume() * 100)}`
    if (player.muted()) volume += player.localize(' (muted)')

    const networkActivity = playerNetworkInfo.downloadSpeed
      ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
      : undefined

    let totalTransferred = playerNetworkInfo.totalDownloaded
      ? `${playerNetworkInfo.totalDownloaded} ⇓`
      : ''

    if (playerNetworkInfo.totalUploaded) {
      totalTransferred += `/ ${playerNetworkInfo.totalUploaded} ⇑`
    }

    const downloadBreakdown = playerNetworkInfo.downloadedFromServer
      ? player.localize('{1} from servers · {2} from peers', [ downloadedFromServer, downloadedFromPeers ])
      : undefined

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

    const p2pEnabled = this.options_.p2pEnabled && this.mode === 'p2p-media-loader'

    this.setInfoValue(this.playerMode, this.mode)
    this.setInfoValue(this.p2p, player.localize(p2pEnabled ? 'enabled' : 'disabled'))
    this.setInfoValue(this.uuid, this.options_.videoUUID)

    this.setInfoValue(this.viewport, frames)
    this.setInfoValue(this.resolution, resolution)
    this.setInfoValue(this.volume, volume)
    this.setInfoValue(this.codecs, codecs)
    this.setInfoValue(this.color, colorSpace)
    this.setInfoValue(this.connection, playerNetworkInfo.averageBandwidth)

    this.setInfoValue(this.network, networkActivity)
    this.setInfoValue(this.transferred, totalTransferred)
    this.setInfoValue(this.download, downloadBreakdown)

    this.setInfoValue(this.bufferProgress, bufferProgress)
    this.setInfoValue(this.bufferState, buffer)

    this.setInfoValue(this.liveLatency, latency)
  }

  private setInfoValue (el: InfoElement, value: string) {
    if (!value) {
      el.root.style.display = 'none'
      return
    }

    el.root.style.display = 'block'

    if (el.value.innerHTML === value) return
    el.value.innerHTML = value
  }

  private buildInfoRow (labelText: string, valueHTML?: string) {
    const root = videojs.dom.createEl('div') as HTMLElement
    root.style.display = 'none'

    const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement
    const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement

    root.appendChild(label)
    root.appendChild(value)

    return { root, 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)

export {
  StatsCard,
  StatsCardOptions
}