aboutsummaryrefslogblamecommitdiffhomepage
path: root/client/src/standalone/videos/embed.ts
blob: d268f4762ad8d82076e5236dbc820537e5bf2476 (plain) (tree)
1
2
3
4
5
6
7
8
9
                     

                                                                
                              
                                                                      
                                                                                                                                       
                                                           
                                                                              
                                                                              
                                              









                       
                                                 
 
                            
                        
                              
 

                          
                                                                

                                                          





                                                             
                                           
 
                                          
 
                                        

                                                        
                              
 




                                                                                                                 
                                                       



                                                              
                                                    
     

   





                                                     

                                             

   

                                               
   
 
                                                                                
 


                                                                                                                    
 




                                                                                    
 


                                          
 
                        
 
                                                                                                                  
   
 

                                           
 

                                                                     
 



                                                                              
 
                                                                                                                     
 
                                                                               
 

                                                                              
 


                                                  
 




                                                                               
 
                                                              

   

                                                    




                         


                                           
   
 
                                                                                
 

                                                              
                
                                                        


            
                                                
 
                                                                                                                         

   

                                                                      
                    
                                                            


            
                                                    
 
                                                                                                                            

   

                                                    

   
                                                                                
 

                                                   
                                      

                          
                                                                      
 
         
                                                                                        
 
                                                                                                                
                   
                                                                               
     

   


                                            
                                      

                          
                                                                                                
 
                             
 

                                                 
                                                                    
 
                                                                                     

                                                     


                                                       
 


                                                             
 
                                                         

        
                                                                                                                                


                               

                                             
 
                                                                    
 
                                                                                                         
 
                                                                            

                       
                                
                   

                                


                                                                 
                                                                                                                                     
 


                                                                        
 

                   
      
 
                                                                                                                                  


                          







                                                                                                                              
 
                                         
 
                   
                               
                        
 
                                       

                                 
                                            
 
                                             

                                       
                                    
        
     
 
                       
                                         
              

                                                         
                                                                                                                   

         







                                                                                                       
     

                                                                                                                                     

   
                                 

                           
                             
     
 


                                                          
 

                                                   

   












                                                                              

   

                                                        
 

                                                            
 




                                                                                                                


                                                                                                                          

                                      




                                                                                






                                
   
 


                                                      

                                                                                                                    

     

                                                                                                      
     
   
 
                                                                                
 

                                                        
                                        




                                                                       
























                                                                                  


                    


                                                
                                           
    
import './embed.scss'
import '../../assets/player/shared/dock/peertube-dock-component'
import '../../assets/player/shared/dock/peertube-dock-plugin'
import videojs from 'video.js'
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models'
import { PeertubePlayerManager } from '../../assets/player'
import { TranslationsManager } from '../../assets/player/translations-manager'
import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
import { PeerTubeEmbedApi } from './embed-api'
import {
  AuthHTTP,
  LiveManager,
  PeerTubePlugin,
  PlayerManagerOptions,
  PlaylistFetcher,
  PlaylistTracker,
  Translations,
  VideoFetcher
} from './shared'
import { PlayerHTML } from './shared/player-html'

export class PeerTubeEmbed {
  player: videojs.Player
  api: PeerTubeEmbedApi = null

  config: HTMLServerConfig

  private translationsPromise: Promise<{ [id: string]: string }>
  private PeertubePlayerManagerModulePromise: Promise<any>

  private readonly http: AuthHTTP
  private readonly videoFetcher: VideoFetcher
  private readonly playlistFetcher: PlaylistFetcher
  private readonly peertubePlugin: PeerTubePlugin
  private readonly playerHTML: PlayerHTML
  private readonly playerManagerOptions: PlayerManagerOptions
  private readonly liveManager: LiveManager

  private playlistTracker: PlaylistTracker

  constructor (videoWrapperId: string) {
    logger.registerServerSending(window.location.origin)

    this.http = new AuthHTTP()

    this.videoFetcher = new VideoFetcher(this.http)
    this.playlistFetcher = new PlaylistFetcher(this.http)
    this.peertubePlugin = new PeerTubePlugin(this.http)
    this.playerHTML = new PlayerHTML(videoWrapperId)
    this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
    this.liveManager = new LiveManager(this.playerHTML)

    try {
      this.config = JSON.parse(window['PeerTubeServerConfig'])
    } catch (err) {
      logger.error('Cannot parse HTML config.', err)
    }
  }

  static async main () {
    const videoContainerId = 'video-wrapper'
    const embed = new PeerTubeEmbed(videoContainerId)
    await embed.init()
  }

  getPlayerElement () {
    return this.playerHTML.getPlayerElement()
  }

  getScope () {
    return this.playerManagerOptions.getScope()
  }

  // ---------------------------------------------------------------------------

  async init () {
    this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
    this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')

    // Issue when we parsed config from HTML, fallback to API
    if (!this.config) {
      this.config = await this.http.fetch('/api/v1/config', { optionalAuth: false })
        .then(res => res.json())
    }

    const videoId = this.isPlaylistEmbed()
      ? await this.initPlaylist()
      : this.getResourceId()

    if (!videoId) return

    return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false })
  }

  private async initPlaylist () {
    const playlistId = this.getResourceId()

    try {
      const res = await this.playlistFetcher.loadPlaylist(playlistId)

      const [ playlist, playlistElementResult ] = await Promise.all([
        res.playlistResponse.json() as Promise<VideoPlaylist>,
        res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
      ])

      const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)

      this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)

      const params = new URL(window.location.toString()).searchParams
      const playlistPositionParam = getParamString(params, 'playlistPosition')

      const position = playlistPositionParam
        ? parseInt(playlistPositionParam + '', 10)
        : 1

      this.playlistTracker.setPosition(position)
    } catch (err) {
      this.playerHTML.displayError(err.message, await this.translationsPromise)
      return undefined
    }

    return this.playlistTracker.getCurrentElement().video.uuid
  }

  private initializeApi () {
    if (this.playerManagerOptions.hasAPIEnabled()) {
      if (this.api) {
        this.api.reInit()
        return
      }

      this.api = new PeerTubeEmbedApi(this)
      this.api.initialize()
    }
  }

  // ---------------------------------------------------------------------------

  async playNextPlaylistVideo () {
    const next = this.playlistTracker.getNextPlaylistElement()
    if (!next) {
      logger.info('Next element not found in playlist.')
      return
    }

    this.playlistTracker.setCurrentElement(next)

    return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
  }

  async playPreviousPlaylistVideo () {
    const previous = this.playlistTracker.getPreviousPlaylistElement()
    if (!previous) {
      logger.info('Previous element not found in playlist.')
      return
    }

    this.playlistTracker.setCurrentElement(previous)

    await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
  }

  getCurrentPlaylistPosition () {
    return this.playlistTracker.getCurrentPosition()
  }

  // ---------------------------------------------------------------------------

  private async loadVideoAndBuildPlayer (options: {
    uuid: string
    autoplayFromPreviousVideo: boolean
    forceAutoplay: boolean
  }) {
    const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options

    try {
      const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)

      return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
    } catch (err) {
      this.playerHTML.displayError(err.message, await this.translationsPromise)
    }
  }

  private async buildVideoPlayer (options: {
    videoResponse: Response
    captionsPromise: Promise<Response>
    autoplayFromPreviousVideo: boolean
    forceAutoplay: boolean
  }) {
    const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options

    this.resetPlayerElement()

    const videoInfoPromise = videoResponse.json()
      .then(async (videoInfo: VideoDetails) => {
        this.playerManagerOptions.loadParams(this.config, videoInfo)

        if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
          this.playerHTML.buildPlaceholder(videoInfo)
        }
        const live = videoInfo.isLive
          ? await this.videoFetcher.loadLive(videoInfo)
          : undefined

        const videoFileToken = videoRequiresAuth(videoInfo)
          ? await this.videoFetcher.loadVideoToken(videoInfo)
          : undefined

        return { live, video: videoInfo, videoFileToken }
      })

    const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
      videoInfoPromise,
      this.translationsPromise,
      captionsPromise,
      this.PeertubePlayerManagerModulePromise
    ])

    await this.peertubePlugin.loadPlugins(this.config, translations)

    const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager

    const playerOptions = await this.playerManagerOptions.getPlayerOptions({
      video,
      captionsResponse,
      autoplayFromPreviousVideo,
      translations,
      serverConfig: this.config,

      authorizationHeader: () => this.http.getHeaderTokenValue(),
      videoFileToken: () => videoFileToken,

      onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),

      playlistTracker: this.playlistTracker,
      playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
      playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),

      live,
      forceAutoplay
    })

    this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => {
      this.player = player
    })

    this.player.on('customError', (event: any, data: any) => {
      const message = data?.err?.message || ''
      if (!message.includes('from xs param')) return

      this.player.dispose()
      this.playerHTML.removePlayerElement()
      this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations)
    })

    window['videojsPlayer'] = this.player

    this.buildCSS()
    this.buildPlayerDock(video)
    this.initializeApi()

    this.playerHTML.removePlaceholder()

    if (this.isPlaylistEmbed()) {
      await this.buildPlayerPlaylistUpnext()

      this.player.playlist().updateSelected()

      this.player.on('stopped', () => {
        this.playNextPlaylistVideo()
      })
    }

    if (video.isLive) {
      this.liveManager.listenForChanges({
        video,
        onPublishedVideo: () => {
          this.liveManager.stopListeningForChanges(video)
          this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true })
        }
      })

      if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
        this.liveManager.displayInfo({ state: video.state.id, translations })

        this.disablePlayer()
      } else {
        this.correctlyHandleLiveEnding(translations)
      }
    }

    this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
  }

  private resetPlayerElement () {
    if (this.player) {
      this.player.dispose()
      this.player = undefined
    }

    const playerElement = document.createElement('video')
    playerElement.className = 'video-js vjs-peertube-skin'
    playerElement.setAttribute('playsinline', 'true')

    this.playerHTML.setPlayerElement(playerElement)
    this.playerHTML.addPlayerElementToDOM()
  }

  private async buildPlayerPlaylistUpnext () {
    const translations = await this.translationsPromise

    this.player.upnext({
      timeout: 10000, // 10s
      headText: peertubeTranslate('Up Next', translations),
      cancelText: peertubeTranslate('Cancel', translations),
      suspendedText: peertubeTranslate('Autoplay is suspended', translations),
      getTitle: () => this.playlistTracker.nextVideoTitle(),
      next: () => this.playNextPlaylistVideo(),
      condition: () => !!this.playlistTracker.getNextPlaylistElement(),
      suspended: () => false
    })
  }

  private buildPlayerDock (videoInfo: VideoDetails) {
    if (!this.playerManagerOptions.hasControls()) return

    // On webtorrent fallback, player may have been disposed
    if (!this.player.player_) return

    const title = this.playerManagerOptions.hasTitle()
      ? videoInfo.name
      : undefined

    const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
      ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
      : undefined

    if (!title && !description) return

    const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
    const avatar = availableAvatars.length !== 0
      ? availableAvatars[0]
      : undefined

    this.player.peertubeDock({
      title,
      description,
      avatarUrl: title && avatar
        ? avatar.path
        : undefined
    })
  }

  private buildCSS () {
    const body = document.getElementById('custom-css')

    if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
      body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
    }

    if (this.playerManagerOptions.hasForegroundColor()) {
      body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
    }
  }

  // ---------------------------------------------------------------------------

  private getResourceId () {
    const urlParts = window.location.pathname.split('/')
    return urlParts[urlParts.length - 1]
  }

  private isPlaylistEmbed () {
    return window.location.pathname.split('/')[1] === 'video-playlists'
  }

  // ---------------------------------------------------------------------------

  private correctlyHandleLiveEnding (translations: Translations) {
    this.player.one('ended', () => {
      // Display the live ended information
      this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })

      this.disablePlayer()
    })
  }

  private disablePlayer () {
    if (this.player.isFullscreen()) {
      this.player.exitFullscreen()
    }

    // Disable player
    this.player.hasStarted(false)
    this.player.removeClass('vjs-has-autoplay')
    this.player.bigPlayButton.hide();

    (this.player.el() as HTMLElement).style.pointerEvents = 'none'
  }

}

PeerTubeEmbed.main()
  .catch(err => {
    (window as any).displayIncompatibleBrowser()

    logger.error('Cannot init embed.', err)
  })