]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/standalone/videos/embed.ts
Translated using Weblate (Toki Pona (tok))
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
index 879850daf68adf461fbff343216f651b5d569f80..0a2b0ccbde7ae82a789f914757a575e509722405 100644 (file)
 import './embed.scss'
-
-import {
-  peertubeTranslate,
-  ResultList,
-  ServerConfig,
-  VideoDetails
-} from '../../../../shared'
-import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
-import {
-  P2PMediaLoaderOptions,
-  PeertubePlayerManagerOptions,
-  PlayerMode
-} from '../../assets/player/peertube-player-manager'
-import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
-import { PeerTubeEmbedApi } from './embed-api'
+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, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models'
+import { PeertubePlayerManager } from '../../assets/player'
 import { TranslationsManager } from '../../assets/player/translations-manager'
-import { VideoJsPlayer } from 'video.js'
-import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
-
-type Translations = { [ id: string ]: string }
+import { getParamString } from '../../root-helpers'
+import { PeerTubeEmbedApi } from './embed-api'
+import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared'
+import { PlayerHTML } from './shared/player-html'
 
 export class PeerTubeEmbed {
-  videoElement: HTMLVideoElement
-  player: VideoJsPlayer
+  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
-  bigPlayBackgroundColor: string
-  foregroundColor: string
-
-  mode: PlayerMode
-  scope = 'peertube'
+
+  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) {
+    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) {
+      console.error('Cannot parse HTML config.', err)
+    }
+  }
 
   static async main () {
-    const videoContainerId = 'video-container'
+    const videoContainerId = 'video-wrapper'
     const embed = new PeerTubeEmbed(videoContainerId)
     await embed.init()
   }
 
-  constructor (private videoContainerId: string) {
-    this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
+  getPlayerElement () {
+    return this.playerHTML.getPlayerElement()
   }
 
-  getVideoUrl (id: string) {
-    return window.location.origin + '/api/v1/videos/' + id
+  getScope () {
+    return this.playerManagerOptions.getScope()
   }
 
-  loadVideoInfo (videoId: string): Promise<Response> {
-    return fetch(this.getVideoUrl(videoId))
-  }
+  // ---------------------------------------------------------------------------
 
-  loadVideoCaptions (videoId: string): Promise<Response> {
-    return fetch(this.getVideoUrl(videoId) + '/captions')
-  }
+  async init () {
+    this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
+    this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
 
-  loadConfig (): Promise<Response> {
-    return fetch('/api/v1/config')
-  }
+    // 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()
 
-  removeElement (element: HTMLElement) {
-    element.parentElement.removeChild(element)
+    if (!videoId) return
+
+    return this.loadVideoAndBuildPlayer(videoId)
   }
 
-  displayError (text: string, translations?: Translations) {
-    // Remove video element
-    if (this.videoElement) this.removeElement(this.videoElement)
+  private async initPlaylist () {
+    const playlistId = this.getResourceId()
 
-    const translatedText = peertubeTranslate(text, translations)
-    const translatedSorry = peertubeTranslate('Sorry', translations)
+    try {
+      const res = await this.playlistFetcher.loadPlaylist(playlistId)
 
-    document.title = translatedSorry + ' - ' + translatedText
+      const [ playlist, playlistElementResult ] = await Promise.all([
+        res.playlistResponse.json() as Promise<VideoPlaylist>,
+        res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
+      ])
 
-    const errorBlock = document.getElementById('error-block')
-    errorBlock.style.display = 'flex'
+      const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
 
-    const errorTitle = document.getElementById('error-title')
-    errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
+      this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
 
-    const errorText = document.getElementById('error-content')
-    errorText.innerHTML = translatedText
-  }
+      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
+    }
 
-  getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
-    return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
+    return this.playlistTracker.getCurrentElement().video.uuid
   }
 
-  getParamString (params: URLSearchParams, name: string, defaultValue?: string) {
-    return params.has(name) ? params.get(name) : defaultValue
+  private initializeApi () {
+    if (this.playerManagerOptions.hasAPIEnabled()) {
+      this.api = new PeerTubeEmbedApi(this)
+      this.api.initialize()
+    }
   }
 
-  async init () {
-    try {
-      await this.initCore()
-    } catch (e) {
-      console.error(e)
+  // ---------------------------------------------------------------------------
+
+  async playNextPlaylistVideo () {
+    const next = this.playlistTracker.getNextPlaylistElement()
+    if (!next) {
+      console.log('Next element not found in playlist.')
+      return
     }
+
+    this.playlistTracker.setCurrentElement(next)
+
+    return this.loadVideoAndBuildPlayer(next.video.uuid)
   }
 
-  private initializeApi () {
-    if (!this.enableApi) return
+  async playPreviousPlaylistVideo () {
+    const previous = this.playlistTracker.getPreviousPlaylistElement()
+    if (!previous) {
+      console.log('Previous element not found in playlist.')
+      return
+    }
+
+    this.playlistTracker.setCurrentElement(previous)
 
-    this.api = new PeerTubeEmbedApi(this)
-    this.api.initialize()
+    await this.loadVideoAndBuildPlayer(previous.video.uuid)
   }
 
-  private loadParams (video: VideoDetails) {
+  getCurrentPlaylistPosition () {
+    return this.playlistTracker.getCurrentPosition()
+  }
+
+  // ---------------------------------------------------------------------------
+
+  private async loadVideoAndBuildPlayer (uuid: string) {
     try {
-      const params = new URL(window.location.toString()).searchParams
+      const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
 
-      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.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.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'
-      }
+      return this.buildVideoPlayer(videoResponse, captionsPromise)
     } catch (err) {
-      console.error('Cannot get params from URL.', err)
+      this.playerHTML.displayError(err.message, await this.translationsPromise)
     }
   }
 
-  private async initCore () {
-    const urlParts = window.location.pathname.split('/')
-    const videoId = urlParts[ urlParts.length - 1 ]
+  private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
+    const alreadyHadPlayer = this.resetPlayerElement()
 
-    const videoPromise = this.loadVideoInfo(videoId)
-    const captionsPromise = this.loadVideoCaptions(videoId)
-    const configPromise = this.loadConfig()
+    const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
+      .then((videoInfo: VideoDetails) => {
+        this.playerManagerOptions.loadParams(this.config, videoInfo)
 
-    const translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
-    const videoResponse = await videoPromise
+        if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) {
+          this.playerHTML.buildPlaceholder(videoInfo)
+        }
 
-    if (!videoResponse.ok) {
-      const serverTranslations = await translationsPromise
+        if (!videoInfo.isLive) {
+          return { video: videoInfo }
+        }
 
-      if (videoResponse.status === 404) return this.videoNotFound(serverTranslations)
+        return this.videoFetcher.loadVideoWithLive(videoInfo)
+      })
 
-      return this.videoFetchError(serverTranslations)
-    }
+    const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
+      videoInfoPromise,
+      this.translationsPromise,
+      captionsPromise,
+      this.PeertubePlayerManagerModulePromise
+    ])
 
-    const videoInfo: VideoDetails = await videoResponse.json()
-    this.loadPlaceholder(videoInfo)
+    await this.peertubePlugin.loadPlugins(this.config, translations)
 
-    const PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
+    const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
 
-    const promises = [ translationsPromise, captionsPromise, configPromise, PeertubePlayerManagerModulePromise ]
-    const [ serverTranslations, captionsResponse, configResponse, PeertubePlayerManagerModule ] = await Promise.all(promises)
+    const options = await this.playerManagerOptions.getPlayerOptions({
+      video,
+      captionsResponse,
+      alreadyHadPlayer,
+      translations,
+      onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid),
 
-    const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
-    const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
+      playlistTracker: this.playlistTracker,
+      playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
+      playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
 
-    this.loadParams(videoInfo)
+      live
+    })
 
-    const options: PeertubePlayerManagerOptions = {
-      common: {
-        autoplay: this.autoplay,
-        controls: this.controls,
-        muted: this.muted,
-        loop: this.loop,
-        captions: videoCaptions.length !== 0,
-        startTime: this.startTime,
-        stopTime: this.stopTime,
-        subtitle: this.subtitle,
+    this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), options, (player: videojs.Player) => {
+      this.player = player
+    })
 
-        videoCaptions,
-        inactivityTimeout: 1500,
-        videoViewUrl: this.getVideoUrl(videoId) + '/views',
+    this.player.on('customError', (event: any, data: any) => {
+      const message = data?.err?.message || ''
+      if (!message.includes('from xs param')) return
 
-        playerElement: this.videoElement,
-        onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element,
+      this.player.dispose()
+      this.playerHTML.removePlayerElement()
+      this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations)
+    })
 
-        videoDuration: videoInfo.duration,
-        enableHotkeys: true,
-        peertubeLink: true,
-        poster: window.location.origin + videoInfo.previewPath,
-        theaterButton: false,
+    window['videojsPlayer'] = this.player
 
-        serverUrl: window.location.origin,
-        language: navigator.language,
-        embedUrl: window.location.origin + videoInfo.embedPath
-      },
+    this.buildCSS()
+    this.buildPlayerDock(video)
+    this.initializeApi()
 
-      webtorrent: {
-        videoFiles: videoInfo.files
-      }
-    }
+    this.playerHTML.removePlaceholder()
+
+    if (this.isPlaylistEmbed()) {
+      await this.buildPlayerPlaylistUpnext()
+
+      this.player.playlist().updateSelected()
 
-    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.on('stopped', () => {
+        this.playNextPlaylistVideo()
       })
     }
 
-    this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: VideoJsPlayer) => this.player = player)
-    this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
+    this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
 
-    window[ 'videojsPlayer' ] = this.player
+    if (video.isLive) {
+      this.liveManager.displayInfoAndListenForChanges({
+        video,
+        translations,
+        onPublishedVideo: () => {
+          this.liveManager.stopListeningForChanges(video)
+          this.loadVideoAndBuildPlayer(video.uuid)
+        }
+      })
+    }
+  }
 
-    this.buildCSS()
+  private resetPlayerElement () {
+    let alreadyHadPlayer = false
 
-    await this.buildDock(videoInfo, configResponse)
+    if (this.player) {
+      this.player.dispose()
+      alreadyHadPlayer = true
+    }
 
-    this.initializeApi()
+    const playerElement = document.createElement('video')
+    playerElement.className = 'video-js vjs-peertube-skin'
+    playerElement.setAttribute('playsinline', 'true')
+
+    this.playerHTML.setPlayerElement(playerElement)
+    this.playerHTML.addPlayerElementToDOM()
 
-    this.removePlaceholder()
+    return alreadyHadPlayer
   }
 
-  private handleError (err: Error, translations?: { [ id: string ]: string }) {
-    if (err.message.indexOf('from xs param') !== -1) {
-      this.player.dispose()
-      this.videoElement = null
-      this.displayError('This video is not available because the remote instance is not responding.', translations)
-      return
-    }
+  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, configResponse: Response) {
-    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 config: ServerConfig = await configResponse.json()
-    const description = 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
 
-    this.player.dock({
+    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
+      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.foregroundColor) {
-      body.style.setProperty('--embedForegroundColor', this.foregroundColor)
+    if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
+      body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
     }
-  }
-
-  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}")`
-  }
+  // ---------------------------------------------------------------------------
 
-  private removePlaceholder () {
-    const placeholder = this.getPlaceholderElement()
-    placeholder.parentElement.removeChild(placeholder)
+  private getResourceId () {
+    const urlParts = window.location.pathname.split('/')
+    return urlParts[urlParts.length - 1]
   }
 
-  private getPlaceholderElement () {
-    return document.getElementById('placeholder-preview')
+  private isPlaylistEmbed () {
+    return window.location.pathname.split('/')[1] === 'video-playlists'
   }
 }
 
 PeerTubeEmbed.main()
-  .catch(err => console.error('Cannot init embed.', err))
+  .catch(err => {
+    (window as any).displayIncompatibleBrowser()
+
+    console.error('Cannot init embed.', err)
+  })