<commit_before>import debug from 'debug'
import videojs from 'video.js'
import { isMobile } from '@root-helpers/web-browser'
import { timeToInt } from '@shared/core-utils'
import { VideoView, VideoViewEvent } from '@shared/models/videos'
import {
  getStoredLastSubtitle,
  getStoredMute,
  getStoredVolume,
  saveLastSubtitle,
  saveMuteInStore,
  saveVideoWatchHistory,
  saveVolumeInStore
} from '../../peertube-player-local-storage'
import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
import { SettingsButton } from '../settings/settings-menu-button'

const logger = debug('peertube:player:peertube')

const Plugin = videojs.getPlugin('plugin')

class PeerTubePlugin extends Plugin {
  private readonly videoViewUrl: string
  private readonly authorizationHeader: string

  private readonly videoUUID: string
  private readonly startTime: number

  private readonly CONSTANTS = {
    USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
  }

  private videoCaptions: VideoJSCaption[]
  private defaultSubtitle: string

  private videoViewInterval: any

  private menuOpened = false
  private mouseInControlBar = false
  private mouseInSettings = false
  private readonly initialInactivityTimeout: number

  constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
    super(player)

    this.videoViewUrl = options.videoViewUrl
    this.authorizationHeader = options.authorizationHeader
    this.videoUUID = options.videoUUID
    this.startTime = timeToInt(options.startTime)

    this.videoCaptions = options.videoCaptions
    this.initialInactivityTimeout = this.player.options_.inactivityTimeout

    if (options.autoplay) this.player.addClass('vjs-has-autoplay')

    this.player.on('autoplay-failure', () => {
      this.player.removeClass('vjs-has-autoplay')
    })

    this.player.ready(() => {
      const playerOptions = this.player.options_

      const volume = getStoredVolume()
      if (volume !== undefined) this.player.volume(volume)

      const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
      if (muted !== undefined) this.player.muted(muted)

      this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()

      this.player.on('volumechange', () => {
        saveVolumeInStore(this.player.volume())
        saveMuteInStore(this.player.muted())
      })

      if (options.stopTime) {
        const stopTime = timeToInt(options.stopTime)
        const self = this

        this.player.on('timeupdate', function onTimeUpdate () {
          if (self.player.currentTime() > stopTime) {
            self.player.pause()
            self.player.trigger('stopped')

            self.player.off('timeupdate', onTimeUpdate)
          }
        })
      }

      this.player.textTracks().addEventListener('change', () => {
        const showing = this.player.textTracks().tracks_.find(t => {
          return t.kind === 'captions' && t.mode === 'showing'
        })

        if (!showing) {
          saveLastSubtitle('off')
          return
        }

        saveLastSubtitle(showing.language)
      })

      this.player.on('sourcechange', () => this.initCaptions())

      this.player.duration(options.videoDuration)

      this.initializePlayer()
      this.runUserViewing()
    })
  }

  dispose () {
    if (this.videoViewInterval) clearInterval(this.videoViewInterval)
  }

  onMenuOpened () {
    this.menuOpened = true
    this.alterInactivity()
  }

  onMenuClosed () {
    this.menuOpened = false
    this.alterInactivity()
  }

  displayFatalError () {
    this.player.addClass('vjs-error-display-enabled')
  }

  hideFatalError () {
    this.player.removeClass('vjs-error-display-enabled')
  }

  private initializePlayer () {
    if (isMobile()) this.player.addClass('vjs-is-mobile')

    this.initSmoothProgressBar()

    this.initCaptions()

    this.listenControlBarMouse()

    this.listenFullScreenChange()
  }

  private runUserViewing () {
    let lastCurrentTime = this.startTime
    let lastViewEvent: VideoViewEvent

    this.player.one('play', () => {
      this.notifyUserIsWatching(this.startTime, lastViewEvent)
    })

    this.player.on('seeked', () => {
      // Don't take into account small seek events
      if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return

      lastViewEvent = 'seek'
    })

    this.player.one('ended', () => {
      const currentTime = Math.floor(this.player.duration())
      lastCurrentTime = currentTime

      this.notifyUserIsWatching(currentTime, lastViewEvent)

      lastViewEvent = undefined
    })

    this.videoViewInterval = setInterval(() => {
      const currentTime = Math.floor(this.player.currentTime())

      // No need to update
      if (currentTime === lastCurrentTime) return

      lastCurrentTime = currentTime

      this.notifyUserIsWatching(currentTime, lastViewEvent)
        .catch(err => console.error('Cannot notify user is watching.', err))

      lastViewEvent = undefined

      // Server won't save history, so save the video position in local storage
      if (!this.authorizationHeader) {
        saveVideoWatchHistory(this.videoUUID, currentTime)
      }
    }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
  }

  private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
    if (!this.videoViewUrl) return Promise.resolve(undefined)

    const body: VideoView = {
      currentTime,
      viewEvent
    }

    const headers = new Headers({
      'Content-type': 'application/json; charset=UTF-8'
    })

    if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)

    return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
  }

  private listenFullScreenChange () {
    this.player.on('fullscreenchange', () => {
      if (this.player.isFullscreen()) this.player.focus()
    })
  }

  private listenControlBarMouse () {
    const controlBar = this.player.controlBar
    const settingsButton: SettingsButton = (controlBar as any).settingsButton

    controlBar.on('mouseenter', () => {
      this.mouseInControlBar = true
      this.alterInactivity()
    })

    controlBar.on('mouseleave', () => {
      this.mouseInControlBar = false
      this.alterInactivity()
    })

    settingsButton.dialog.on('mouseenter', () => {
      this.mouseInSettings = true
      this.alterInactivity()
    })

    settingsButton.dialog.on('mouseleave', () => {
      this.mouseInSettings = false
      this.alterInactivity()
    })
  }

  private alterInactivity () {
    if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
      this.setInactivityTimeout(0)
      return
    }

    this.setInactivityTimeout(this.initialInactivityTimeout)
    this.player.reportUserActivity(true)
  }

  private setInactivityTimeout (timeout: number) {
    (this.player as any).cache_.inactivityTimeout = timeout
    this.player.options_.inactivityTimeout = timeout

    logger('Set player inactivity to ' + timeout)
  }

  private initCaptions () {
    for (const caption of this.videoCaptions) {
      this.player.addRemoteTextTrack({
        kind: 'captions',
        label: caption.label,
        language: caption.language,
        id: caption.language,
        src: caption.src,
        default: this.defaultSubtitle === caption.language
      }, false)
    }

    this.player.trigger('captionsChanged')
  }

  // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
  private initSmoothProgressBar () {
    const SeekBar = videojs.getComponent('SeekBar') as any
    SeekBar.prototype.getPercent = function getPercent () {
      // Allows for smooth scrubbing, when player can't keep up.
      // const time = (this.player_.scrubbing()) ?
      //   this.player_.getCache().currentTime :
      //   this.player_.currentTime()
      const time = this.player_.currentTime()
      const percent = time / this.player_.duration()
      return percent >= 1 ? 1 : percent
    }
    SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
      let newTime = this.calculateDistance(event) * this.player_.duration()
      if (newTime === this.player_.duration()) {
        newTime = newTime - 0.1
      }
      this.player_.currentTime(newTime)
      this.update()
    }
  }
}

videojs.registerPlugin('peertube', PeerTubePlugin)
export { PeerTubePlugin }
<commit_msg>Use round for views stats<commit_after>import debug from 'debug'
import videojs from 'video.js'
import { isMobile } from '@root-helpers/web-browser'
import { timeToInt } from '@shared/core-utils'
import { VideoView, VideoViewEvent } from '@shared/models/videos'
import {
  getStoredLastSubtitle,
  getStoredMute,
  getStoredVolume,
  saveLastSubtitle,
  saveMuteInStore,
  saveVideoWatchHistory,
  saveVolumeInStore
} from '../../peertube-player-local-storage'
import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
import { SettingsButton } from '../settings/settings-menu-button'

const logger = debug('peertube:player:peertube')

const Plugin = videojs.getPlugin('plugin')

class PeerTubePlugin extends Plugin {
  private readonly videoViewUrl: string
  private readonly authorizationHeader: string

  private readonly videoUUID: string
  private readonly startTime: number

  private readonly CONSTANTS = {
    USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
  }

  private videoCaptions: VideoJSCaption[]
  private defaultSubtitle: string

  private videoViewInterval: any

  private menuOpened = false
  private mouseInControlBar = false
  private mouseInSettings = false
  private readonly initialInactivityTimeout: number

  constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
    super(player)

    this.videoViewUrl = options.videoViewUrl
    this.authorizationHeader = options.authorizationHeader
    this.videoUUID = options.videoUUID
    this.startTime = timeToInt(options.startTime)

    this.videoCaptions = options.videoCaptions
    this.initialInactivityTimeout = this.player.options_.inactivityTimeout

    if (options.autoplay) this.player.addClass('vjs-has-autoplay')

    this.player.on('autoplay-failure', () => {
      this.player.removeClass('vjs-has-autoplay')
    })

    this.player.ready(() => {
      const playerOptions = this.player.options_

      const volume = getStoredVolume()
      if (volume !== undefined) this.player.volume(volume)

      const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
      if (muted !== undefined) this.player.muted(muted)

      this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()

      this.player.on('volumechange', () => {
        saveVolumeInStore(this.player.volume())
        saveMuteInStore(this.player.muted())
      })

      if (options.stopTime) {
        const stopTime = timeToInt(options.stopTime)
        const self = this

        this.player.on('timeupdate', function onTimeUpdate () {
          if (self.player.currentTime() > stopTime) {
            self.player.pause()
            self.player.trigger('stopped')

            self.player.off('timeupdate', onTimeUpdate)
          }
        })
      }

      this.player.textTracks().addEventListener('change', () => {
        const showing = this.player.textTracks().tracks_.find(t => {
          return t.kind === 'captions' && t.mode === 'showing'
        })

        if (!showing) {
          saveLastSubtitle('off')
          return
        }

        saveLastSubtitle(showing.language)
      })

      this.player.on('sourcechange', () => this.initCaptions())

      this.player.duration(options.videoDuration)

      this.initializePlayer()
      this.runUserViewing()
    })
  }

  dispose () {
    if (this.videoViewInterval) clearInterval(this.videoViewInterval)
  }

  onMenuOpened () {
    this.menuOpened = true
    this.alterInactivity()
  }

  onMenuClosed () {
    this.menuOpened = false
    this.alterInactivity()
  }

  displayFatalError () {
    this.player.addClass('vjs-error-display-enabled')
  }

  hideFatalError () {
    this.player.removeClass('vjs-error-display-enabled')
  }

  private initializePlayer () {
    if (isMobile()) this.player.addClass('vjs-is-mobile')

    this.initSmoothProgressBar()

    this.initCaptions()

    this.listenControlBarMouse()

    this.listenFullScreenChange()
  }

  private runUserViewing () {
    let lastCurrentTime = this.startTime
    let lastViewEvent: VideoViewEvent

    this.player.one('play', () => {
      this.notifyUserIsWatching(this.startTime, lastViewEvent)
    })

    this.player.on('seeked', () => {
      // Don't take into account small seek events
      if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return

      lastViewEvent = 'seek'
    })

    this.player.one('ended', () => {
      const currentTime = Math.round(this.player.duration())
      lastCurrentTime = currentTime

      this.notifyUserIsWatching(currentTime, lastViewEvent)

      lastViewEvent = undefined
    })

    this.videoViewInterval = setInterval(() => {
      const currentTime = Math.round(this.player.currentTime())

      // No need to update
      if (currentTime === lastCurrentTime) return

      lastCurrentTime = currentTime

      this.notifyUserIsWatching(currentTime, lastViewEvent)
        .catch(err => console.error('Cannot notify user is watching.', err))

      lastViewEvent = undefined

      // Server won't save history, so save the video position in local storage
      if (!this.authorizationHeader) {
        saveVideoWatchHistory(this.videoUUID, currentTime)
      }
    }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
  }

  private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
    if (!this.videoViewUrl) return Promise.resolve(undefined)

    const body: VideoView = {
      currentTime,
      viewEvent
    }

    const headers = new Headers({
      'Content-type': 'application/json; charset=UTF-8'
    })

    if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)

    return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
  }

  private listenFullScreenChange () {
    this.player.on('fullscreenchange', () => {
      if (this.player.isFullscreen()) this.player.focus()
    })
  }

  private listenControlBarMouse () {
    const controlBar = this.player.controlBar
    const settingsButton: SettingsButton = (controlBar as any).settingsButton

    controlBar.on('mouseenter', () => {
      this.mouseInControlBar = true
      this.alterInactivity()
    })

    controlBar.on('mouseleave', () => {
      this.mouseInControlBar = false
      this.alterInactivity()
    })

    settingsButton.dialog.on('mouseenter', () => {
      this.mouseInSettings = true
      this.alterInactivity()
    })

    settingsButton.dialog.on('mouseleave', () => {
      this.mouseInSettings = false
      this.alterInactivity()
    })
  }

  private alterInactivity () {
    if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
      this.setInactivityTimeout(0)
      return
    }

    this.setInactivityTimeout(this.initialInactivityTimeout)
    this.player.reportUserActivity(true)
  }

  private setInactivityTimeout (timeout: number) {
    (this.player as any).cache_.inactivityTimeout = timeout
    this.player.options_.inactivityTimeout = timeout

    logger('Set player inactivity to ' + timeout)
  }

  private initCaptions () {
    for (const caption of this.videoCaptions) {
      this.player.addRemoteTextTrack({
        kind: 'captions',
        label: caption.label,
        language: caption.language,
        id: caption.language,
        src: caption.src,
        default: this.defaultSubtitle === caption.language
      }, false)
    }

    this.player.trigger('captionsChanged')
  }

  // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
  private initSmoothProgressBar () {
    const SeekBar = videojs.getComponent('SeekBar') as any
    SeekBar.prototype.getPercent = function getPercent () {
      // Allows for smooth scrubbing, when player can't keep up.
      // const time = (this.player_.scrubbing()) ?
      //   this.player_.getCache().currentTime :
      //   this.player_.currentTime()
      const time = this.player_.currentTime()
      const percent = time / this.player_.duration()
      return percent >= 1 ? 1 : percent
    }
    SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
      let newTime = this.calculateDistance(event) * this.player_.duration()
      if (newTime === this.player_.duration()) {
        newTime = newTime - 0.1
      }
      this.player_.currentTime(newTime)
      this.update()
    }
  }
}

videojs.registerPlugin('peertube', PeerTubePlugin)
export { PeerTubePlugin }

{'fraction_non_alphanumeric': 0.11591798032475999, 'fraction_numerical': 0.0029631385563588953, 'mean_word_length': 3.1390728476821192, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 39, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2650503', 'n_tokens_mistral': 5033, 'n_tokens_neox': 4840, 'n_words': 938}