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, 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, 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, forceAutoplay: false })
}
getCurrentPlaylistPosition () {
return this.playlistTracker.getCurrentPosition()
}
// ---------------------------------------------------------------------------
private async loadVideoAndBuildPlayer (options: {
uuid: string
forceAutoplay: boolean
}) {
const { uuid, forceAutoplay } = options
try {
const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
return this.buildVideoPlayer({ videoResponse, captionsPromise, forceAutoplay })
} catch (err) {
this.playerHTML.displayError(err.message, await this.translationsPromise)
}
}
private async buildVideoPlayer (options: {
videoResponse: Response
captionsPromise: Promise<Response>
forceAutoplay: boolean
}) {
const { videoResponse, captionsPromise, forceAutoplay } = options
const alreadyHadPlayer = this.resetPlayerElement()
const videoInfoPromise = videoResponse.json()
.then(async (videoInfo: VideoDetails) => {
this.playerManagerOptions.loadParams(this.config, videoInfo)
if (!alreadyHadPlayer && !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,
alreadyHadPlayer,
translations,
serverConfig: this.config,
authorizationHeader: () => this.http.getHeaderTokenValue(),
videoFileToken: () => videoFileToken,
onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, 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, 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 () {
let alreadyHadPlayer = false
if (this.player) {
this.player.dispose()
this.player = undefined
alreadyHadPlayer = true
}
const playerElement = document.createElement('video')
playerElement.className = 'video-js vjs-peertube-skin'
playerElement.setAttribute('playsinline', 'true')
this.playerHTML.setPlayerElement(playerElement)
this.playerHTML.addPlayerElementToDOM()
return alreadyHadPlayer
}
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)
})