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 { TranslationsManager } from '../../assets/player/translations-manager'
+import '../../assets/player/shared/dock/peertube-dock-component'
+import '../../assets/player/shared/dock/peertube-dock-plugin'
import videojs from 'video.js'
-import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
-
-type Translations = { [ id: string ]: string }
+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 { 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: 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()
+
+ if (!videoId) return
- removeElement (element: HTMLElement) {
- element.parentElement.removeChild(element)
+ 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.api = new PeerTubeEmbedApi(this)
- this.api.initialize()
+ this.playlistTracker.setCurrentElement(previous)
+
+ await this.loadVideoAndBuildPlayer(previous.video.uuid)
+ }
+
+ getCurrentPlaylistPosition () {
+ return this.playlistTracker.getCurrentPosition()
}
- private loadParams (video: VideoDetails) {
+ // ---------------------------------------------------------------------------
+
+ 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()
- 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.playlist().updateSelected()
+
+ this.player.on('stopped', () => {
+ this.playNextPlaylistVideo()
})
}
- 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.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.removePlaceholder()
+ this.playerHTML.setPlayerElement(playerElement)
+ this.playerHTML.addPlayerElementToDOM()
+
+ 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)
+ })