p2p-media-loader-plugin.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import Hlsjs from 'hls.js'
  2. import videojs from 'video.js'
  3. import { Events, Segment } from '@peertube/p2p-media-loader-core'
  4. import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
  5. import { logger } from '@root-helpers/logger'
  6. import { addQueryParams, timeToInt } from '@shared/core-utils'
  7. import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
  8. import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
  9. registerConfigPlugin(videojs)
  10. registerSourceHandler(videojs)
  11. const Plugin = videojs.getPlugin('plugin')
  12. class P2pMediaLoaderPlugin extends Plugin {
  13. private readonly CONSTANTS = {
  14. INFO_SCHEDULER: 1000 // Don't change this
  15. }
  16. private readonly options: P2PMediaLoaderPluginOptions
  17. private hlsjs: Hlsjs
  18. private p2pEngine: Engine
  19. private statsP2PBytes = {
  20. pendingDownload: [] as number[],
  21. pendingUpload: [] as number[],
  22. numPeers: 0,
  23. totalDownload: 0,
  24. totalUpload: 0
  25. }
  26. private statsHTTPBytes = {
  27. pendingDownload: [] as number[],
  28. totalDownload: 0
  29. }
  30. private startTime: number
  31. private networkInfoInterval: any
  32. constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
  33. super(player)
  34. this.options = options
  35. this.startTime = timeToInt(options.startTime)
  36. // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
  37. if (!(videojs as any).Html5Hlsjs) {
  38. if (player.canPlayType('application/vnd.apple.mpegurl')) {
  39. this.fallbackToBuiltInIOS()
  40. return
  41. }
  42. const message = 'HLS.js does not seem to be supported. Cannot fallback to built-in HLS'
  43. logger.warn(message)
  44. const error: MediaError = {
  45. code: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
  46. message,
  47. MEDIA_ERR_ABORTED: MediaError.MEDIA_ERR_ABORTED,
  48. MEDIA_ERR_DECODE: MediaError.MEDIA_ERR_DECODE,
  49. MEDIA_ERR_NETWORK: MediaError.MEDIA_ERR_NETWORK,
  50. MEDIA_ERR_SRC_NOT_SUPPORTED: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
  51. }
  52. player.ready(() => player.error(error))
  53. return
  54. }
  55. // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
  56. (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (_videojsPlayer: any, hlsjs: any) => {
  57. this.hlsjs = hlsjs
  58. })
  59. initVideoJsContribHlsJsPlayer(player)
  60. player.src({
  61. type: options.type,
  62. src: options.src
  63. })
  64. player.ready(() => {
  65. this.initializeCore()
  66. this.initializePlugin()
  67. })
  68. }
  69. dispose () {
  70. if (this.hlsjs) this.hlsjs.destroy()
  71. if (this.p2pEngine) this.p2pEngine.destroy()
  72. clearInterval(this.networkInfoInterval)
  73. }
  74. getCurrentLevel () {
  75. if (!this.hlsjs) return undefined
  76. return this.hlsjs.levels[this.hlsjs.currentLevel]
  77. }
  78. getLiveLatency () {
  79. return Math.round(this.hlsjs.latency)
  80. }
  81. getHLSJS () {
  82. return this.hlsjs
  83. }
  84. private initializeCore () {
  85. this.player.one('play', () => {
  86. this.player.addClass('vjs-has-big-play-button-clicked')
  87. })
  88. this.player.one('canplay', () => {
  89. if (this.startTime) {
  90. this.player.currentTime(this.startTime)
  91. }
  92. })
  93. }
  94. private initializePlugin () {
  95. initHlsJsPlayer(this.hlsjs)
  96. this.p2pEngine = this.options.loader.getEngine()
  97. this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
  98. if (navigator.onLine === false) return
  99. logger.error(`Segment ${segment.id} error.`, err)
  100. this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
  101. })
  102. this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
  103. this.runStats()
  104. this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange'))
  105. }
  106. private runStats () {
  107. this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, _segment, bytes: number) => {
  108. const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
  109. elem.pendingDownload.push(bytes)
  110. elem.totalDownload += bytes
  111. })
  112. this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => {
  113. if (method !== 'p2p') {
  114. logger.error(`Received upload from unknown method ${method}`)
  115. return
  116. }
  117. this.statsP2PBytes.pendingUpload.push(bytes)
  118. this.statsP2PBytes.totalUpload += bytes
  119. })
  120. this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
  121. this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
  122. this.networkInfoInterval = setInterval(() => {
  123. const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
  124. const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
  125. const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
  126. this.statsP2PBytes.pendingDownload = []
  127. this.statsP2PBytes.pendingUpload = []
  128. this.statsHTTPBytes.pendingDownload = []
  129. return this.player.trigger('p2pInfo', {
  130. source: 'p2p-media-loader',
  131. http: {
  132. downloadSpeed: httpDownloadSpeed,
  133. downloaded: this.statsHTTPBytes.totalDownload
  134. },
  135. p2p: {
  136. downloadSpeed: p2pDownloadSpeed,
  137. uploadSpeed: p2pUploadSpeed,
  138. numPeers: this.statsP2PBytes.numPeers,
  139. downloaded: this.statsP2PBytes.totalDownload,
  140. uploaded: this.statsP2PBytes.totalUpload
  141. },
  142. bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8
  143. } as PlayerNetworkInfo)
  144. }, this.CONSTANTS.INFO_SCHEDULER)
  145. }
  146. private arraySum (data: number[]) {
  147. return data.reduce((a: number, b: number) => a + b, 0)
  148. }
  149. private fallbackToBuiltInIOS () {
  150. logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.');
  151. // Workaround to force video.js to not re create a video element
  152. (this.player as any).playerElIngest_ = this.player.el().parentNode
  153. this.player.src({
  154. type: this.options.type,
  155. src: addQueryParams(this.options.src, {
  156. videoFileToken: this.options.videoFileToken(),
  157. reinjectVideoFileToken: 'true'
  158. })
  159. })
  160. this.player.ready(() => {
  161. this.initializeCore()
  162. })
  163. }
  164. }
  165. videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
  166. export { P2pMediaLoaderPlugin }