peertube-plugin.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import videojs, { VideoJsPlayer } from 'video.js'
  2. import './videojs-components/settings-menu-button'
  3. import {
  4. PeerTubePluginOptions,
  5. ResolutionUpdateData,
  6. UserWatching,
  7. VideoJSCaption
  8. } from './peertube-videojs-typings'
  9. import { isMobile, timeToInt } from './utils'
  10. import {
  11. getStoredLastSubtitle,
  12. getStoredMute,
  13. getStoredVolume,
  14. saveLastSubtitle,
  15. saveMuteInStore,
  16. saveVolumeInStore
  17. } from './peertube-player-local-storage'
  18. const Plugin = videojs.getPlugin('plugin')
  19. class PeerTubePlugin extends Plugin {
  20. private readonly videoViewUrl: string
  21. private readonly videoDuration: number
  22. private readonly CONSTANTS = {
  23. USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
  24. }
  25. private videoCaptions: VideoJSCaption[]
  26. private defaultSubtitle: string
  27. private videoViewInterval: any
  28. private userWatchingVideoInterval: any
  29. private lastResolutionChange: ResolutionUpdateData
  30. private menuOpened = false
  31. private mouseInControlBar = false
  32. private readonly savedInactivityTimeout: number
  33. constructor (player: VideoJsPlayer, options?: PeerTubePluginOptions) {
  34. super(player)
  35. this.videoViewUrl = options.videoViewUrl
  36. this.videoDuration = options.videoDuration
  37. this.videoCaptions = options.videoCaptions
  38. this.savedInactivityTimeout = player.options_.inactivityTimeout
  39. if (options.autoplay === true) this.player.addClass('vjs-has-autoplay')
  40. this.player.on('autoplay-failure', () => {
  41. this.player.removeClass('vjs-has-autoplay')
  42. })
  43. this.player.ready(() => {
  44. const playerOptions = this.player.options_
  45. if (options.mode === 'webtorrent') {
  46. this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
  47. this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
  48. }
  49. if (options.mode === 'p2p-media-loader') {
  50. this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
  51. }
  52. this.player.tech(true).on('loadedqualitydata', () => {
  53. setTimeout(() => {
  54. // Replay a resolution change, now we loaded all quality data
  55. if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
  56. }, 0)
  57. })
  58. const volume = getStoredVolume()
  59. if (volume !== undefined) this.player.volume(volume)
  60. const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
  61. if (muted !== undefined) this.player.muted(muted)
  62. this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
  63. this.player.on('volumechange', () => {
  64. saveVolumeInStore(this.player.volume())
  65. saveMuteInStore(this.player.muted())
  66. })
  67. if (options.stopTime) {
  68. const stopTime = timeToInt(options.stopTime)
  69. const self = this
  70. this.player.on('timeupdate', function onTimeUpdate () {
  71. if (self.player.currentTime() > stopTime) {
  72. self.player.pause()
  73. self.player.trigger('stopped')
  74. self.player.off('timeupdate', onTimeUpdate)
  75. }
  76. })
  77. }
  78. this.player.textTracks().on('change', () => {
  79. const showing = this.player.textTracks().tracks_.find(t => {
  80. return t.kind === 'captions' && t.mode === 'showing'
  81. })
  82. if (!showing) {
  83. saveLastSubtitle('off')
  84. return
  85. }
  86. saveLastSubtitle(showing.language)
  87. })
  88. this.player.on('sourcechange', () => this.initCaptions())
  89. this.player.duration(options.videoDuration)
  90. this.initializePlayer()
  91. this.runViewAdd()
  92. if (options.userWatching) this.runUserWatchVideo(options.userWatching)
  93. })
  94. }
  95. dispose () {
  96. if (this.videoViewInterval) clearInterval(this.videoViewInterval)
  97. if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
  98. }
  99. onMenuOpen () {
  100. this.menuOpened = false
  101. this.alterInactivity()
  102. }
  103. onMenuClosed () {
  104. this.menuOpened = true
  105. this.alterInactivity()
  106. }
  107. private initializePlayer () {
  108. if (isMobile()) this.player.addClass('vjs-is-mobile')
  109. this.initSmoothProgressBar()
  110. this.initCaptions()
  111. this.listenControlBarMouse()
  112. }
  113. private runViewAdd () {
  114. this.clearVideoViewInterval()
  115. // After 30 seconds (or 3/4 of the video), add a view to the video
  116. let minSecondsToView = 30
  117. if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
  118. let secondsViewed = 0
  119. this.videoViewInterval = setInterval(() => {
  120. if (this.player && !this.player.paused()) {
  121. secondsViewed += 1
  122. if (secondsViewed > minSecondsToView) {
  123. this.clearVideoViewInterval()
  124. this.addViewToVideo().catch(err => console.error(err))
  125. }
  126. }
  127. }, 1000)
  128. }
  129. private runUserWatchVideo (options: UserWatching) {
  130. let lastCurrentTime = 0
  131. this.userWatchingVideoInterval = setInterval(() => {
  132. const currentTime = Math.floor(this.player.currentTime())
  133. if (currentTime - lastCurrentTime >= 1) {
  134. lastCurrentTime = currentTime
  135. this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
  136. .catch(err => console.error('Cannot notify user is watching.', err))
  137. }
  138. }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
  139. }
  140. private clearVideoViewInterval () {
  141. if (this.videoViewInterval !== undefined) {
  142. clearInterval(this.videoViewInterval)
  143. this.videoViewInterval = undefined
  144. }
  145. }
  146. private addViewToVideo () {
  147. if (!this.videoViewUrl) return Promise.resolve(undefined)
  148. return fetch(this.videoViewUrl, { method: 'POST' })
  149. }
  150. private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
  151. const body = new URLSearchParams()
  152. body.append('currentTime', currentTime.toString())
  153. const headers = new Headers({ 'Authorization': authorizationHeader })
  154. return fetch(url, { method: 'PUT', body, headers })
  155. }
  156. private handleResolutionChange (data: ResolutionUpdateData) {
  157. this.lastResolutionChange = data
  158. const qualityLevels = this.player.qualityLevels()
  159. for (let i = 0; i < qualityLevels.length; i++) {
  160. if (qualityLevels[i].height === data.resolutionId) {
  161. data.id = qualityLevels[i].id
  162. break
  163. }
  164. }
  165. this.trigger('resolutionChange', data)
  166. }
  167. private listenControlBarMouse () {
  168. this.player.controlBar.on('mouseenter', () => {
  169. this.mouseInControlBar = true
  170. this.alterInactivity()
  171. })
  172. this.player.controlBar.on('mouseleave', () => {
  173. this.mouseInControlBar = false
  174. this.alterInactivity()
  175. })
  176. }
  177. private alterInactivity () {
  178. if (this.menuOpened || this.mouseInControlBar) {
  179. this.player.options_.inactivityTimeout = this.savedInactivityTimeout
  180. return
  181. }
  182. this.player.options_.inactivityTimeout = 1
  183. }
  184. private initCaptions () {
  185. for (const caption of this.videoCaptions) {
  186. this.player.addRemoteTextTrack({
  187. kind: 'captions',
  188. label: caption.label,
  189. language: caption.language,
  190. id: caption.language,
  191. src: caption.src,
  192. default: this.defaultSubtitle === caption.language
  193. }, false)
  194. }
  195. this.player.trigger('captionsChanged')
  196. }
  197. // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
  198. private initSmoothProgressBar () {
  199. const SeekBar = videojs.getComponent('SeekBar') as any
  200. SeekBar.prototype.getPercent = function getPercent () {
  201. // Allows for smooth scrubbing, when player can't keep up.
  202. // const time = (this.player_.scrubbing()) ?
  203. // this.player_.getCache().currentTime :
  204. // this.player_.currentTime()
  205. const time = this.player_.currentTime()
  206. const percent = time / this.player_.duration()
  207. return percent >= 1 ? 1 : percent
  208. }
  209. SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
  210. let newTime = this.calculateDistance(event) * this.player_.duration()
  211. if (newTime === this.player_.duration()) {
  212. newTime = newTime - 0.1
  213. }
  214. this.player_.currentTime(newTime)
  215. this.update()
  216. }
  217. }
  218. }
  219. videojs.registerPlugin('peertube', PeerTubePlugin)
  220. export { PeerTubePlugin }