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 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, res.videosResponse.json() as Promise> ]) 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 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() ? '' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '' : 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) })