2 import '../../assets/player/shared/dock/peertube-dock-component'
3 import '../../assets/player/shared/dock/peertube-dock-plugin'
4 import videojs from 'video.js'
5 import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6 import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models'
7 import { PeertubePlayerManager } from '../../assets/player'
8 import { TranslationsManager } from '../../assets/player/translations-manager'
9 import { getParamString, logger } from '../../root-helpers'
10 import { PeerTubeEmbedApi } from './embed-api'
11 import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared'
12 import { PlayerHTML } from './shared/player-html'
14 export class PeerTubeEmbed {
15 player: videojs.Player
16 api: PeerTubeEmbedApi = null
18 config: HTMLServerConfig
20 private translationsPromise: Promise<{ [id: string]: string }>
21 private PeertubePlayerManagerModulePromise: Promise<any>
23 private readonly http: AuthHTTP
24 private readonly videoFetcher: VideoFetcher
25 private readonly playlistFetcher: PlaylistFetcher
26 private readonly peertubePlugin: PeerTubePlugin
27 private readonly playerHTML: PlayerHTML
28 private readonly playerManagerOptions: PlayerManagerOptions
29 private readonly liveManager: LiveManager
31 private playlistTracker: PlaylistTracker
33 constructor (videoWrapperId: string) {
34 logger.registerServerSending(window.location.origin)
36 this.http = new AuthHTTP()
38 this.videoFetcher = new VideoFetcher(this.http)
39 this.playlistFetcher = new PlaylistFetcher(this.http)
40 this.peertubePlugin = new PeerTubePlugin(this.http)
41 this.playerHTML = new PlayerHTML(videoWrapperId)
42 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
43 this.liveManager = new LiveManager(this.playerHTML)
46 this.config = JSON.parse(window['PeerTubeServerConfig'])
48 logger.error('Cannot parse HTML config.', err)
52 static async main () {
53 const videoContainerId = 'video-wrapper'
54 const embed = new PeerTubeEmbed(videoContainerId)
59 return this.playerHTML.getPlayerElement()
63 return this.playerManagerOptions.getScope()
66 // ---------------------------------------------------------------------------
69 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
70 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
72 // Issue when we parsed config from HTML, fallback to API
74 this.config = await this.http.fetch('/api/v1/config', { optionalAuth: false })
75 .then(res => res.json())
78 const videoId = this.isPlaylistEmbed()
79 ? await this.initPlaylist()
80 : this.getResourceId()
84 return this.loadVideoAndBuildPlayer(videoId)
87 private async initPlaylist () {
88 const playlistId = this.getResourceId()
91 const res = await this.playlistFetcher.loadPlaylist(playlistId)
93 const [ playlist, playlistElementResult ] = await Promise.all([
94 res.playlistResponse.json() as Promise<VideoPlaylist>,
95 res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
98 const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
100 this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
102 const params = new URL(window.location.toString()).searchParams
103 const playlistPositionParam = getParamString(params, 'playlistPosition')
105 const position = playlistPositionParam
106 ? parseInt(playlistPositionParam + '', 10)
109 this.playlistTracker.setPosition(position)
111 this.playerHTML.displayError(err.message, await this.translationsPromise)
115 return this.playlistTracker.getCurrentElement().video.uuid
118 private initializeApi () {
119 if (this.playerManagerOptions.hasAPIEnabled()) {
120 this.api = new PeerTubeEmbedApi(this)
121 this.api.initialize()
125 // ---------------------------------------------------------------------------
127 async playNextPlaylistVideo () {
128 const next = this.playlistTracker.getNextPlaylistElement()
130 logger.info('Next element not found in playlist.')
134 this.playlistTracker.setCurrentElement(next)
136 return this.loadVideoAndBuildPlayer(next.video.uuid)
139 async playPreviousPlaylistVideo () {
140 const previous = this.playlistTracker.getPreviousPlaylistElement()
142 logger.info('Previous element not found in playlist.')
146 this.playlistTracker.setCurrentElement(previous)
148 await this.loadVideoAndBuildPlayer(previous.video.uuid)
151 getCurrentPlaylistPosition () {
152 return this.playlistTracker.getCurrentPosition()
155 // ---------------------------------------------------------------------------
157 private async loadVideoAndBuildPlayer (uuid: string) {
159 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
161 return this.buildVideoPlayer(videoResponse, captionsPromise)
163 this.playerHTML.displayError(err.message, await this.translationsPromise)
167 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
168 const alreadyHadPlayer = this.resetPlayerElement()
170 const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
171 .then((videoInfo: VideoDetails) => {
172 this.playerManagerOptions.loadParams(this.config, videoInfo)
174 if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) {
175 this.playerHTML.buildPlaceholder(videoInfo)
178 if (!videoInfo.isLive) {
179 return { video: videoInfo }
182 return this.videoFetcher.loadVideoWithLive(videoInfo)
185 const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
187 this.translationsPromise,
189 this.PeertubePlayerManagerModulePromise
192 await this.peertubePlugin.loadPlugins(this.config, translations)
194 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
196 const options = await this.playerManagerOptions.getPlayerOptions({
201 serverConfig: this.config,
203 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid),
205 playlistTracker: this.playlistTracker,
206 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
207 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
212 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), options, (player: videojs.Player) => {
216 this.player.on('customError', (event: any, data: any) => {
217 const message = data?.err?.message || ''
218 if (!message.includes('from xs param')) return
220 this.player.dispose()
221 this.playerHTML.removePlayerElement()
222 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations)
225 window['videojsPlayer'] = this.player
228 this.buildPlayerDock(video)
231 this.playerHTML.removePlaceholder()
233 if (this.isPlaylistEmbed()) {
234 await this.buildPlayerPlaylistUpnext()
236 this.player.playlist().updateSelected()
238 this.player.on('stopped', () => {
239 this.playNextPlaylistVideo()
243 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
246 this.liveManager.displayInfoAndListenForChanges({
249 onPublishedVideo: () => {
250 this.liveManager.stopListeningForChanges(video)
251 this.loadVideoAndBuildPlayer(video.uuid)
257 private resetPlayerElement () {
258 let alreadyHadPlayer = false
261 this.player.dispose()
262 alreadyHadPlayer = true
265 const playerElement = document.createElement('video')
266 playerElement.className = 'video-js vjs-peertube-skin'
267 playerElement.setAttribute('playsinline', 'true')
269 this.playerHTML.setPlayerElement(playerElement)
270 this.playerHTML.addPlayerElementToDOM()
272 return alreadyHadPlayer
275 private async buildPlayerPlaylistUpnext () {
276 const translations = await this.translationsPromise
279 timeout: 10000, // 10s
280 headText: peertubeTranslate('Up Next', translations),
281 cancelText: peertubeTranslate('Cancel', translations),
282 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
283 getTitle: () => this.playlistTracker.nextVideoTitle(),
284 next: () => this.playNextPlaylistVideo(),
285 condition: () => !!this.playlistTracker.getNextPlaylistElement(),
286 suspended: () => false
290 private buildPlayerDock (videoInfo: VideoDetails) {
291 if (!this.playerManagerOptions.hasControls()) return
293 // On webtorrent fallback, player may have been disposed
294 if (!this.player.player_) return
296 const title = this.playerManagerOptions.hasTitle()
300 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
301 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
304 if (!title && !description) return
306 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
307 const avatar = availableAvatars.length !== 0
308 ? availableAvatars[0]
311 this.player.peertubeDock({
314 avatarUrl: title && avatar
320 private buildCSS () {
321 const body = document.getElementById('custom-css')
323 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
324 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
327 if (this.playerManagerOptions.hasForegroundColor()) {
328 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
332 // ---------------------------------------------------------------------------
334 private getResourceId () {
335 const urlParts = window.location.pathname.split('/')
336 return urlParts[urlParts.length - 1]
339 private isPlaylistEmbed () {
340 return window.location.pathname.split('/')[1] === 'video-playlists'
346 (window as any).displayIncompatibleBrowser()
348 logger.error('Cannot init embed.', err)