]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/standalone/videos/embed.ts
Don't not autoplay live without autoplay setting
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
index fc61d37303bc4b6cc94e27fce1d52f4c7fe1068d..d268f4762ad8d82076e5236dbc820537e5bf2476 100644 (file)
 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 { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import {
-  ClientHookName,
-  HTMLServerConfig,
-  PluginType,
-  ResultList,
-  UserRefreshToken,
-  VideoCaption,
-  VideoDetails,
-  VideoPlaylist,
-  VideoPlaylistElement,
-  VideoStreamingPlaylistType
-} from '../../../../shared/models'
-import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
-import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
+import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models'
+import { PeertubePlayerManager } from '../../assets/player'
 import { TranslationsManager } from '../../assets/player/translations-manager'
-import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
-import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
-import { Tokens } from '../../root-helpers/users'
-import { objectToUrlEncoded } from '../../root-helpers/utils'
-import { RegisterClientHelpers } from '../../types/register-client-option.model'
+import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
 import { PeerTubeEmbedApi } from './embed-api'
-
-type Translations = { [ id: string ]: string }
+import {
+  AuthHTTP,
+  LiveManager,
+  PeerTubePlugin,
+  PlayerManagerOptions,
+  PlaylistFetcher,
+  PlaylistTracker,
+  Translations,
+  VideoFetcher
+} from './shared'
+import { PlayerHTML } from './shared/player-html'
 
 export class PeerTubeEmbed {
-  playerElement: HTMLVideoElement
   player: videojs.Player
   api: PeerTubeEmbedApi = null
 
-  autoplay: boolean
-  controls: boolean
-  muted: boolean
-  loop: boolean
-  subtitle: string
-  enableApi = false
-  startTime: number | string = 0
-  stopTime: number | string
-
-  title: boolean
-  warningTitle: boolean
-  peertubeLink: boolean
-  bigPlayBackgroundColor: string
-  foregroundColor: string
-
-  mode: PlayerMode
-  scope = 'peertube'
-
-  userTokens: Tokens
-  headers = new Headers()
-  LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
-    CLIENT_ID: 'client_id',
-    CLIENT_SECRET: 'client_secret'
-  }
-
   config: HTMLServerConfig
 
   private translationsPromise: Promise<{ [id: string]: string }>
   private PeertubePlayerManagerModulePromise: Promise<any>
 
-  private playlist: VideoPlaylist
-  private playlistElements: VideoPlaylistElement[]
-  private currentPlaylistElement: VideoPlaylistElement
+  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 wrapperElement: HTMLElement
+  private playlistTracker: PlaylistTracker
 
-  private peertubeHooks: Hooks = {}
-  private loadedScripts = new Set<string>()
+  constructor (videoWrapperId: string) {
+    logger.registerServerSending(window.location.origin)
 
-  static async main () {
-    const videoContainerId = 'video-wrapper'
-    const embed = new PeerTubeEmbed(videoContainerId)
-    await embed.init()
-  }
+    this.http = new AuthHTTP()
 
-  constructor (private videoWrapperId: string) {
-    this.wrapperElement = document.getElementById(this.videoWrapperId)
+    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) {
-      console.error('Cannot parse HTML config.', err)
+      logger.error('Cannot parse HTML config.', err)
     }
   }
 
-  getVideoUrl (id: string) {
-    return window.location.origin + '/api/v1/videos/' + id
+  static async main () {
+    const videoContainerId = 'video-wrapper'
+    const embed = new PeerTubeEmbed(videoContainerId)
+    await embed.init()
   }
 
-  refreshFetch (url: string, options?: RequestInit) {
-    return fetch(url, options)
-      .then((res: Response) => {
-        if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
-
-        const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
-          const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
-          const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
-
-          const headers = new Headers()
-          headers.set('Content-Type', 'application/x-www-form-urlencoded')
-
-          const data = {
-            refresh_token: this.userTokens.refreshToken,
-            client_id: clientId,
-            client_secret: clientSecret,
-            response_type: 'code',
-            grant_type: 'refresh_token'
-          }
-
-          fetch('/api/v1/users/token', {
-            headers,
-            method: 'POST',
-            body: objectToUrlEncoded(data)
-          }).then(res => {
-            if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
-
-            return res.json()
-          }).then((obj: UserRefreshToken & { code: 'invalid_grant'}) => {
-            if (!obj || obj.code === 'invalid_grant') {
-              Tokens.flush()
-              this.removeTokensFromHeaders()
-
-              return resolve()
-            }
-
-            this.userTokens.accessToken = obj.access_token
-            this.userTokens.refreshToken = obj.refresh_token
-            this.userTokens.save()
-
-            this.setHeadersFromTokens()
-
-            resolve()
-          }).catch((refreshTokenError: any) => {
-            reject(refreshTokenError)
-          })
-        })
-
-        return refreshingTokenPromise
-          .catch(() => {
-            Tokens.flush()
-
-            this.removeTokensFromHeaders()
-          }).then(() => fetch(url, {
-            ...options,
-            headers: this.headers
-          }))
-      })
+  getPlayerElement () {
+    return this.playerHTML.getPlayerElement()
   }
 
-  getPlaylistUrl (id: string) {
-    return window.location.origin + '/api/v1/video-playlists/' + id
+  getScope () {
+    return this.playerManagerOptions.getScope()
   }
 
-  loadVideoInfo (videoId: string): Promise<Response> {
-    return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers })
-  }
+  // ---------------------------------------------------------------------------
 
-  loadVideoCaptions (videoId: string): Promise<Response> {
-    return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
-  }
+  async init () {
+    this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
+    this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
 
-  loadPlaylistInfo (playlistId: string): Promise<Response> {
-    return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
-  }
+    // 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())
+    }
 
-  loadPlaylistElements (playlistId: string, start = 0): Promise<Response> {
-    const url = new URL(this.getPlaylistUrl(playlistId) + '/videos')
-    url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString()
+    const videoId = this.isPlaylistEmbed()
+      ? await this.initPlaylist()
+      : this.getResourceId()
 
-    return this.refreshFetch(url.toString(), { headers: this.headers })
-  }
+    if (!videoId) return
 
-  removeElement (element: HTMLElement) {
-    element.parentElement.removeChild(element)
+    return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false })
   }
 
-  displayError (text: string, translations?: Translations) {
-    // Remove video element
-    if (this.playerElement) {
-      this.removeElement(this.playerElement)
-      this.playerElement = undefined
-    }
-
-    const translatedText = peertubeTranslate(text, translations)
-    const translatedSorry = peertubeTranslate('Sorry', translations)
+  private async initPlaylist () {
+    const playlistId = this.getResourceId()
 
-    document.title = translatedSorry + ' - ' + translatedText
+    try {
+      const res = await this.playlistFetcher.loadPlaylist(playlistId)
 
-    const errorBlock = document.getElementById('error-block')
-    errorBlock.style.display = 'flex'
+      const [ playlist, playlistElementResult ] = await Promise.all([
+        res.playlistResponse.json() as Promise<VideoPlaylist>,
+        res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
+      ])
 
-    const errorTitle = document.getElementById('error-title')
-    errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
+      const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
 
-    const errorText = document.getElementById('error-content')
-    errorText.innerHTML = translatedText
+      this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
 
-    this.wrapperElement.style.display = 'none'
-  }
+      const params = new URL(window.location.toString()).searchParams
+      const playlistPositionParam = getParamString(params, 'playlistPosition')
 
-  videoNotFound (translations?: Translations) {
-    const text = 'This video does not exist.'
-    this.displayError(text, translations)
-  }
+      const position = playlistPositionParam
+        ? parseInt(playlistPositionParam + '', 10)
+        : 1
 
-  videoFetchError (translations?: Translations) {
-    const text = 'We cannot fetch the video. Please try again later.'
-    this.displayError(text, translations)
-  }
+      this.playlistTracker.setPosition(position)
+    } catch (err) {
+      this.playerHTML.displayError(err.message, await this.translationsPromise)
+      return undefined
+    }
 
-  playlistNotFound (translations?: Translations) {
-    const text = 'This playlist does not exist.'
-    this.displayError(text, translations)
+    return this.playlistTracker.getCurrentElement().video.uuid
   }
 
-  playlistFetchError (translations?: Translations) {
-    const text = 'We cannot fetch the playlist. Please try again later.'
-    this.displayError(text, translations)
-  }
+  private initializeApi () {
+    if (this.playerManagerOptions.hasAPIEnabled()) {
+      if (this.api) {
+        this.api.reInit()
+        return
+      }
 
-  getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
-    return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
+      this.api = new PeerTubeEmbedApi(this)
+      this.api.initialize()
+    }
   }
 
-  getParamString (params: URLSearchParams, name: string, defaultValue?: string) {
-    return params.has(name) ? params.get(name) : defaultValue
-  }
+  // ---------------------------------------------------------------------------
 
-  async playNextVideo () {
-    const next = this.getNextPlaylistElement()
+  async playNextPlaylistVideo () {
+    const next = this.playlistTracker.getNextPlaylistElement()
     if (!next) {
-      console.log('Next element not found in playlist.')
+      logger.info('Next element not found in playlist.')
       return
     }
 
-    this.currentPlaylistElement = next
+    this.playlistTracker.setCurrentElement(next)
 
-    return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
+    return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
   }
 
-  async playPreviousVideo () {
-    const previous = this.getPreviousPlaylistElement()
+  async playPreviousPlaylistVideo () {
+    const previous = this.playlistTracker.getPreviousPlaylistElement()
     if (!previous) {
-      console.log('Previous element not found in playlist.')
+      logger.info('Previous element not found in playlist.')
       return
     }
 
-    this.currentPlaylistElement = previous
-
-    await this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
-  }
-
-  getCurrentPosition () {
-    if (!this.currentPlaylistElement) return -1
-
-    return this.currentPlaylistElement.position
-  }
-
-  async init () {
-    try {
-      this.userTokens = Tokens.load()
-      await this.initCore()
-    } catch (e) {
-      console.error(e)
-    }
-  }
-
-  private initializeApi () {
-    if (!this.enableApi) return
-
-    this.api = new PeerTubeEmbedApi(this)
-    this.api.initialize()
-  }
-
-  private loadParams (video: VideoDetails) {
-    try {
-      const params = new URL(window.location.toString()).searchParams
-
-      this.autoplay = this.getParamToggle(params, 'autoplay', false)
-      this.controls = this.getParamToggle(params, 'controls', true)
-      this.muted = this.getParamToggle(params, 'muted', undefined)
-      this.loop = this.getParamToggle(params, 'loop', false)
-      this.title = this.getParamToggle(params, 'title', true)
-      this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
-      this.warningTitle = this.getParamToggle(params, 'warningTitle', true)
-      this.peertubeLink = this.getParamToggle(params, 'peertubeLink', true)
-
-      this.scope = this.getParamString(params, 'scope', this.scope)
-      this.subtitle = this.getParamString(params, 'subtitle')
-      this.startTime = this.getParamString(params, 'start')
-      this.stopTime = this.getParamString(params, 'stop')
+    this.playlistTracker.setCurrentElement(previous)
 
-      this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor')
-      this.foregroundColor = this.getParamString(params, 'foregroundColor')
-
-      const modeParam = this.getParamString(params, 'mode')
-
-      if (modeParam) {
-        if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
-        else this.mode = 'webtorrent'
-      } else {
-        if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
-        else this.mode = 'webtorrent'
-      }
-    } catch (err) {
-      console.error('Cannot get params from URL.', err)
-    }
+    await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
   }
 
-  private async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList<VideoPlaylistElement>) {
-    let elements = baseResult.data
-    let total = baseResult.total
-    let i = 0
-
-    while (total > elements.length && i < 10) {
-      const result = await this.loadPlaylistElements(playlistId, elements.length)
-
-      const json = await result.json() as ResultList<VideoPlaylistElement>
-      total = json.total
-
-      elements = elements.concat(json.data)
-      i++
-    }
-
-    if (i === 10) {
-      console.error('Cannot fetch all playlists elements, there are too many!')
-    }
-
-    return elements
+  getCurrentPlaylistPosition () {
+    return this.playlistTracker.getCurrentPosition()
   }
 
-  private async loadPlaylist (playlistId: string) {
-    const playlistPromise = this.loadPlaylistInfo(playlistId)
-    const playlistElementsPromise = this.loadPlaylistElements(playlistId)
+  // ---------------------------------------------------------------------------
 
-    let playlistResponse: Response
-    let isResponseOk: boolean
+  private async loadVideoAndBuildPlayer (options: {
+    uuid: string
+    autoplayFromPreviousVideo: boolean
+    forceAutoplay: boolean
+  }) {
+    const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options
 
     try {
-      playlistResponse = await playlistPromise
-      isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
-    } catch (err) {
-      console.error(err)
-      isResponseOk = false
-    }
+      const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
 
-    if (!isResponseOk) {
-      const serverTranslations = await this.translationsPromise
-
-      if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
-        this.playlistNotFound(serverTranslations)
-        return undefined
-      }
-
-      this.playlistFetchError(serverTranslations)
-      return undefined
-    }
-
-    return { playlistResponse, videosResponse: await playlistElementsPromise }
-  }
-
-  private async loadVideo (videoId: string) {
-    const videoPromise = this.loadVideoInfo(videoId)
-
-    let videoResponse: Response
-    let isResponseOk: boolean
-
-    try {
-      videoResponse = await videoPromise
-      isResponseOk = videoResponse.status === HttpStatusCode.OK_200
+      return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
     } catch (err) {
-      console.error(err)
-
-      isResponseOk = false
-    }
-
-    if (!isResponseOk) {
-      const serverTranslations = await this.translationsPromise
-
-      if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
-        this.videoNotFound(serverTranslations)
-        return undefined
-      }
-
-      this.videoFetchError(serverTranslations)
-      return undefined
-    }
-
-    const captionsPromise = this.loadVideoCaptions(videoId)
-
-    return { captionsPromise, videoResponse }
-  }
-
-  private async buildPlaylistManager () {
-    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.nextVideoTitle(),
-      next: () => this.playNextVideo(),
-      condition: () => !!this.getNextPlaylistElement(),
-      suspended: () => false
-    })
-  }
-
-  private async loadVideoAndBuildPlayer (uuid: string) {
-    const res = await this.loadVideo(uuid)
-    if (res === undefined) return
-
-    return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
-  }
-
-  private nextVideoTitle () {
-    const next = this.getNextPlaylistElement()
-    if (!next) return ''
-
-    return next.video.name
-  }
-
-  private getNextPlaylistElement (position?: number): VideoPlaylistElement {
-    if (!position) position = this.currentPlaylistElement.position + 1
-
-    if (position > this.playlist.videosLength) {
-      return undefined
-    }
-
-    const next = this.playlistElements.find(e => e.position === position)
-
-    if (!next || !next.video) {
-      return this.getNextPlaylistElement(position + 1)
+      this.playerHTML.displayError(err.message, await this.translationsPromise)
     }
-
-    return next
   }
 
-  private getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
-    if (!position) position = this.currentPlaylistElement.position - 1
-
-    if (position < 1) {
-      return undefined
-    }
-
-    const prev = this.playlistElements.find(e => e.position === position)
-
-    if (!prev || !prev.video) {
-      return this.getNextPlaylistElement(position - 1)
-    }
+  private async buildVideoPlayer (options: {
+    videoResponse: Response
+    captionsPromise: Promise<Response>
+    autoplayFromPreviousVideo: boolean
+    forceAutoplay: boolean
+  }) {
+    const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options
 
-    return prev
-  }
+    this.resetPlayerElement()
 
-  private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
-    let alreadyHadPlayer = false
-
-    if (this.player) {
-      this.player.dispose()
-      alreadyHadPlayer = true
-    }
-
-    this.playerElement = document.createElement('video')
-    this.playerElement.className = 'video-js vjs-peertube-skin'
-    this.playerElement.setAttribute('playsinline', 'true')
-    this.wrapperElement.appendChild(this.playerElement)
+    const videoInfoPromise = videoResponse.json()
+      .then(async (videoInfo: VideoDetails) => {
+        this.playerManagerOptions.loadParams(this.config, videoInfo)
 
-    // Issue when we parsed config from HTML, fallback to API
-    if (!this.config) {
-      this.config = await this.refreshFetch('/api/v1/config')
-                              .then(res => res.json())
-    }
+        if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
+          this.playerHTML.buildPlaceholder(videoInfo)
+        }
+        const live = videoInfo.isLive
+          ? await this.videoFetcher.loadLive(videoInfo)
+          : undefined
 
-    const videoInfoPromise = videoResponse.json()
-      .then((videoInfo: VideoDetails) => {
-        if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo)
+        const videoFileToken = videoRequiresAuth(videoInfo)
+          ? await this.videoFetcher.loadVideoToken(videoInfo)
+          : undefined
 
-        return videoInfo
+        return { live, video: videoInfo, videoFileToken }
       })
 
-    const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
+    const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
       videoInfoPromise,
       this.translationsPromise,
       captionsPromise,
       this.PeertubePlayerManagerModulePromise
     ])
 
-    await this.ensurePluginsAreLoaded(serverTranslations)
-
-    const videoInfo: VideoDetails = videoInfoTmp
-
-    const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
-    const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
-
-    this.loadParams(videoInfo)
-
-    const playlistPlugin = this.currentPlaylistElement
-      ? {
-        elements: this.playlistElements,
-        playlist: this.playlist,
-
-        getCurrentPosition: () => this.currentPlaylistElement.position,
-
-        onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
-          this.currentPlaylistElement = videoPlaylistElement
-
-          this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
-            .catch(err => console.error(err))
-        }
-      }
-      : undefined
-
-    const options: PeertubePlayerManagerOptions = {
-      common: {
-        // Autoplay in playlist mode
-        autoplay: alreadyHadPlayer ? true : this.autoplay,
-        controls: this.controls,
-        muted: this.muted,
-        loop: this.loop,
+    await this.peertubePlugin.loadPlugins(this.config, translations)
 
-        captions: videoCaptions.length !== 0,
-        subtitle: this.subtitle,
+    const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
 
-        startTime: this.playlist ? this.currentPlaylistElement.startTimestamp : this.startTime,
-        stopTime: this.playlist ? this.currentPlaylistElement.stopTimestamp : this.stopTime,
+    const playerOptions = await this.playerManagerOptions.getPlayerOptions({
+      video,
+      captionsResponse,
+      autoplayFromPreviousVideo,
+      translations,
+      serverConfig: this.config,
 
-        nextVideo: this.playlist ? () => this.playNextVideo() : undefined,
-        hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined,
+      authorizationHeader: () => this.http.getHeaderTokenValue(),
+      videoFileToken: () => videoFileToken,
 
-        previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined,
-        hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined,
+      onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),
 
-        playlist: playlistPlugin,
+      playlistTracker: this.playlistTracker,
+      playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
+      playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
 
-        videoCaptions,
-        inactivityTimeout: 2500,
-        videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
-        videoUUID: videoInfo.uuid,
-
-        isLive: videoInfo.isLive,
-
-        playerElement: this.playerElement,
-        onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
-
-        videoDuration: videoInfo.duration,
-        enableHotkeys: true,
-        peertubeLink: this.peertubeLink,
-        poster: window.location.origin + videoInfo.previewPath,
-        theaterButton: false,
+      live,
+      forceAutoplay
+    })
 
-        serverUrl: window.location.origin,
-        language: navigator.language,
-        embedUrl: window.location.origin + videoInfo.embedPath,
-        embedTitle: videoInfo.name
-      },
+    this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => {
+      this.player = player
+    })
 
-      webtorrent: {
-        videoFiles: videoInfo.files
-      }
-    }
+    this.player.on('customError', (event: any, data: any) => {
+      const message = data?.err?.message || ''
+      if (!message.includes('from xs param')) return
 
-    if (this.mode === 'p2p-media-loader') {
-      const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
-
-      Object.assign(options, {
-        p2pMediaLoader: {
-          playlistUrl: hlsPlaylist.playlistUrl,
-          segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
-          redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
-          trackerAnnounce: videoInfo.trackerUrls,
-          videoFiles: hlsPlaylist.files
-        } as P2PMediaLoaderOptions
-      })
-    }
-
-    this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: videojs.Player) => this.player = player)
-    this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
+      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
+    window['videojsPlayer'] = this.player
 
     this.buildCSS()
-
-    await this.buildDock(videoInfo)
-
+    this.buildPlayerDock(video)
     this.initializeApi()
 
-    this.removePlaceholder()
+    this.playerHTML.removePlaceholder()
 
     if (this.isPlaylistEmbed()) {
-      await this.buildPlaylistManager()
+      await this.buildPlayerPlaylistUpnext()
 
       this.player.playlist().updateSelected()
 
       this.player.on('stopped', () => {
-        this.playNextVideo()
+        this.playNextPlaylistVideo()
       })
     }
 
-    this.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo })
-  }
-
-  private async initCore () {
-    if (this.userTokens) this.setHeadersFromTokens()
-
-    this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
-    this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
-
-    let videoId: string
-
-    if (this.isPlaylistEmbed()) {
-      const playlistId = this.getResourceId()
-      const res = await this.loadPlaylist(playlistId)
-      if (!res) return undefined
-
-      this.playlist = await res.playlistResponse.json()
-
-      const playlistElementResult = await res.videosResponse.json()
-      this.playlistElements = await this.loadAllPlaylistVideos(playlistId, playlistElementResult)
-
-      const params = new URL(window.location.toString()).searchParams
-      const playlistPositionParam = this.getParamString(params, 'playlistPosition')
-
-      let position = 1
-
-      if (playlistPositionParam) {
-        position = parseInt(playlistPositionParam + '', 10)
-      }
+    if (video.isLive) {
+      this.liveManager.listenForChanges({
+        video,
+        onPublishedVideo: () => {
+          this.liveManager.stopListeningForChanges(video)
+          this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true })
+        }
+      })
 
-      this.currentPlaylistElement = this.playlistElements.find(e => e.position === position)
-      if (!this.currentPlaylistElement || !this.currentPlaylistElement.video) {
-        console.error('Current playlist element is not valid.', this.currentPlaylistElement)
-        this.currentPlaylistElement = this.getNextPlaylistElement()
-      }
+      if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
+        this.liveManager.displayInfo({ state: video.state.id, translations })
 
-      if (!this.currentPlaylistElement) {
-        console.error('This playlist does not have any valid element.')
-        const serverTranslations = await this.translationsPromise
-        this.playlistFetchError(serverTranslations)
-        return
+        this.disablePlayer()
+      } else {
+        this.correctlyHandleLiveEnding(translations)
       }
-
-      videoId = this.currentPlaylistElement.video.uuid
-    } else {
-      videoId = this.getResourceId()
     }
 
-    return this.loadVideoAndBuildPlayer(videoId)
+    this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
   }
 
-  private handleError (err: Error, translations?: { [ id: string ]: string }) {
-    if (err.message.indexOf('from xs param') !== -1) {
+  private resetPlayerElement () {
+    if (this.player) {
       this.player.dispose()
-      this.playerElement = null
-      this.displayError('This video is not available because the remote instance is not responding.', translations)
-      return
+      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 async buildDock (videoInfo: VideoDetails) {
-    if (!this.controls) return
+  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.title ? videoInfo.name : undefined
+    const title = this.playerManagerOptions.hasTitle()
+      ? videoInfo.name
+      : undefined
 
-    const description = this.config.tracker.enabled && this.warningTitle
+    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) {
-      this.player.dock({
-        title,
-        description
-      })
-    }
+    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.bigPlayBackgroundColor) {
-      body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor)
+    if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
+      body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
     }
 
-    if (this.foregroundColor) {
-      body.style.setProperty('--embedForegroundColor', this.foregroundColor)
-    }
-  }
-
-  private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> {
-    if (captionsResponse.ok) {
-      const { data } = (await captionsResponse.json()) as ResultList<VideoCaption>
-
-      return data.map(c => ({
-        label: peertubeTranslate(c.language.label, serverTranslations),
-        language: c.language.id,
-        src: window.location.origin + c.captionPath
-      }))
+    if (this.playerManagerOptions.hasForegroundColor()) {
+      body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
     }
-
-    return []
-  }
-
-  private loadPlaceholder (video: VideoDetails) {
-    const placeholder = this.getPlaceholderElement()
-
-    const url = window.location.origin + video.previewPath
-    placeholder.style.backgroundImage = `url("${url}")`
-    placeholder.style.display = 'block'
-  }
-
-  private removePlaceholder () {
-    const placeholder = this.getPlaceholderElement()
-    placeholder.style.display = 'none'
   }
 
-  private getPlaceholderElement () {
-    return document.getElementById('placeholder-preview')
-  }
-
-  private setHeadersFromTokens () {
-    this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`)
-  }
-
-  private removeTokensFromHeaders () {
-    this.headers.delete('Authorization')
-  }
+  // ---------------------------------------------------------------------------
 
   private getResourceId () {
     const urlParts = window.location.pathname.split('/')
-    return urlParts[ urlParts.length - 1 ]
+    return urlParts[urlParts.length - 1]
   }
 
   private isPlaylistEmbed () {
     return window.location.pathname.split('/')[1] === 'video-playlists'
   }
 
-  private async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) {
-    if (this.config.plugin.registered.length === 0) return
-
-    for (const plugin of this.config.plugin.registered) {
-      for (const key of Object.keys(plugin.clientScripts)) {
-        const clientScript = plugin.clientScripts[key]
-
-        if (clientScript.scopes.includes('embed') === false) continue
-
-        const script = `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
+  // ---------------------------------------------------------------------------
 
-        if (this.loadedScripts.has(script)) continue
+  private correctlyHandleLiveEnding (translations: Translations) {
+    this.player.one('ended', () => {
+      // Display the live ended information
+      this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
 
-        const pluginInfo = {
-          plugin,
-          clientScript: {
-            script,
-            scopes: clientScript.scopes
-          },
-          pluginType: PluginType.PLUGIN,
-          isTheme: false
-        }
-
-        await loadPlugin({
-          hooks: this.peertubeHooks,
-          pluginInfo,
-          onSettingsScripts: () => undefined,
-          peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
-        })
-      }
-    }
+      this.disablePlayer()
+    })
   }
 
-  private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers {
-    function unimplemented (): any {
-      throw new Error('This helper is not implemented in embed.')
+  private disablePlayer () {
+    if (this.player.isFullscreen()) {
+      this.player.exitFullscreen()
     }
 
-    return {
-      getBaseStaticRoute: unimplemented,
-
-      getSettings: unimplemented,
-
-      isLoggedIn: unimplemented,
-      getAuthHeader: unimplemented,
-
-      notifier: {
-        info: unimplemented,
-        error: unimplemented,
-        success: unimplemented
-      },
+    // Disable player
+    this.player.hasStarted(false)
+    this.player.removeClass('vjs-has-autoplay')
+    this.player.bigPlayButton.hide();
 
-      showModal: unimplemented,
-
-      getServerConfig: unimplemented,
-
-      markdownRenderer: {
-        textMarkdownToHTML: unimplemented,
-        enhancedMarkdownToHTML: unimplemented
-      },
-
-      translate: (value: string) => {
-        return Promise.resolve(peertubeTranslate(value, translations))
-      }
-    }
+    (this.player.el() as HTMLElement).style.pointerEvents = 'none'
   }
 
-  private runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
-    return runHook(this.peertubeHooks, hookName, result, params)
-  }
 }
 
 PeerTubeEmbed.main()
-  .catch(err => console.error('Cannot init embed.', err))
+  .catch(err => {
+    (window as any).displayIncompatibleBrowser()
+
+    logger.error('Cannot init embed.', err)
+  })