X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fstandalone%2Fvideos%2Fembed.ts;h=cc4274b99810792a1cb950504067ce71d850787a;hb=54909304287f3c04dcfb39660be8ead57dc95440;hp=97437ce45ba24badaddb59a37582edfbb7aa52d5;hpb=a24bd1ed41b43790bab6ba789580bb4e85f07d85;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 97437ce45..cc4274b99 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -1,790 +1,411 @@ 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, - HttpStatusCode, - OAuth2ErrorCode, - ResultList, - UserRefreshToken, - VideoCaption, - VideoDetails, - VideoPlaylist, - VideoPlaylistElement, - VideoStreamingPlaylistType -} from '../../../../shared/models' -import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' -import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' +import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' +import { PeertubePlayerManager } from '../../assets/player' import { TranslationsManager } from '../../assets/player/translations-manager' -import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage' -import { PluginsManager } from '../../root-helpers/plugins-manager' -import { Tokens } from '../../root-helpers/users' -import { objectToUrlEncoded } from '../../root-helpers/utils' -import { RegisterClientHelpers } from '../../types/register-client-option.model' +import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' import { PeerTubeEmbedApi } from './embed-api' - -type Translations = { [ id: string ]: string } +import { + AuthHTTP, + LiveManager, + PeerTubePlugin, + PlayerManagerOptions, + PlaylistFetcher, + PlaylistTracker, + Translations, + VideoFetcher +} from './shared' +import { PlayerHTML } from './shared/player-html' export class PeerTubeEmbed { - playerElement: 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 - peertubeLink: boolean - bigPlayBackgroundColor: string - foregroundColor: string - - mode: PlayerMode - scope = 'peertube' - - userTokens: Tokens - headers = new Headers() - LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { - CLIENT_ID: 'client_id', - CLIENT_SECRET: 'client_secret' - } - config: HTMLServerConfig private translationsPromise: Promise<{ [id: string]: string }> private PeertubePlayerManagerModulePromise: Promise - private playlist: VideoPlaylist - private playlistElements: VideoPlaylistElement[] - private currentPlaylistElement: VideoPlaylistElement + 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 wrapperElement: HTMLElement + private playlistTracker: PlaylistTracker - private pluginsManager: PluginsManager + constructor (videoWrapperId: string) { + logger.registerServerSending(window.location.origin) - static async main () { - const videoContainerId = 'video-wrapper' - const embed = new PeerTubeEmbed(videoContainerId) - await embed.init() - } + this.http = new AuthHTTP() - constructor (private videoWrapperId: string) { - this.wrapperElement = document.getElementById(this.videoWrapperId) + 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']) + this.config = JSON.parse((window as any)['PeerTubeServerConfig']) } catch (err) { - console.error('Cannot parse HTML config.', err) + logger.error('Cannot parse HTML config.', err) } } - getVideoUrl (id: string) { - return window.location.origin + '/api/v1/videos/' + id + static async main () { + const videoContainerId = 'video-wrapper' + const embed = new PeerTubeEmbed(videoContainerId) + await embed.init() } - refreshFetch (url: string, options?: RequestInit) { - return fetch(url, options) - .then((res: Response) => { - if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res - - const refreshingTokenPromise = new Promise((resolve, reject) => { - const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) - const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) - - const headers = new Headers() - headers.set('Content-Type', 'application/x-www-form-urlencoded') - - const data = { - refresh_token: this.userTokens.refreshToken, - client_id: clientId, - client_secret: clientSecret, - response_type: 'code', - grant_type: 'refresh_token' - } - - fetch('/api/v1/users/token', { - headers, - method: 'POST', - body: objectToUrlEncoded(data) - }).then(res => { - if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined - - return res.json() - }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { - if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { - Tokens.flush() - this.removeTokensFromHeaders() - - return resolve() - } - - this.userTokens.accessToken = obj.access_token - this.userTokens.refreshToken = obj.refresh_token - this.userTokens.save() - - this.setHeadersFromTokens() - - resolve() - }).catch((refreshTokenError: any) => { - reject(refreshTokenError) - }) - }) - - return refreshingTokenPromise - .catch(() => { - Tokens.flush() - - this.removeTokensFromHeaders() - }).then(() => fetch(url, { - ...options, - headers: this.headers - })) - }) + getPlayerElement () { + return this.playerHTML.getPlayerElement() } - getPlaylistUrl (id: string) { - return window.location.origin + '/api/v1/video-playlists/' + id + getScope () { + return this.playerManagerOptions.getScope() } - loadVideoInfo (videoId: string): Promise { - return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers }) - } + // --------------------------------------------------------------------------- - loadVideoCaptions (videoId: string): Promise { - return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers }) - } + async init () { + this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) + this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') - loadPlaylistInfo (playlistId: string): Promise { - return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers }) - } + // 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()) + } - loadPlaylistElements (playlistId: string, start = 0): Promise { - const url = new URL(this.getPlaylistUrl(playlistId) + '/videos') - url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString() + const videoId = this.isPlaylistEmbed() + ? await this.initPlaylist() + : this.getResourceId() - return this.refreshFetch(url.toString(), { headers: this.headers }) - } + if (!videoId) return - removeElement (element: HTMLElement) { - element.parentElement.removeChild(element) + return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) } - displayError (text: string, translations?: Translations) { - // Remove video element - if (this.playerElement) { - this.removeElement(this.playerElement) - this.playerElement = undefined - } + 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, + res.videosResponse.json() as Promise> + ]) - 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') - this.wrapperElement.style.display = 'none' - } + const position = playlistPositionParam + ? parseInt(playlistPositionParam + '', 10) + : 1 - videoNotFound (translations?: Translations) { - const text = 'This video does not exist.' - this.displayError(text, translations) - } - - 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 + } - playlistNotFound (translations?: Translations) { - const text = 'This playlist does not exist.' - this.displayError(text, translations) + return this.playlistTracker.getCurrentElement().video.uuid } - playlistFetchError (translations?: Translations) { - const text = 'We cannot fetch the playlist. Please try again later.' - this.displayError(text, translations) - } + private initializeApi () { + if (this.playerManagerOptions.hasAPIEnabled()) { + if (this.api) { + this.api.reInit() + return + } - getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) { - return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue + this.api = new PeerTubeEmbedApi(this) + this.api.initialize() + } } - getParamString (params: URLSearchParams, name: string, defaultValue?: string) { - return params.has(name) ? params.get(name) : defaultValue - } + // --------------------------------------------------------------------------- - async playNextVideo () { - const next = this.getNextPlaylistElement() + async playNextPlaylistVideo () { + const next = this.playlistTracker.getNextPlaylistElement() if (!next) { - console.log('Next element not found in playlist.') + logger.info('Next element not found in playlist.') return } - this.currentPlaylistElement = next + this.playlistTracker.setCurrentElement(next) - return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) + return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) } - async playPreviousVideo () { - const previous = this.getPreviousPlaylistElement() + async playPreviousPlaylistVideo () { + const previous = this.playlistTracker.getPreviousPlaylistElement() if (!previous) { - console.log('Previous element not found in playlist.') + logger.info('Previous element not found in playlist.') return } - this.currentPlaylistElement = previous + this.playlistTracker.setCurrentElement(previous) - await this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) + await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) } - getCurrentPosition () { - if (!this.currentPlaylistElement) return -1 - - return this.currentPlaylistElement.position + getCurrentPlaylistPosition () { + return this.playlistTracker.getCurrentPosition() } - async init () { - try { - this.userTokens = Tokens.load() - await this.initCore() - } catch (e) { - console.error(e) - } - } - - private initializeApi () { - if (!this.enableApi) return + // --------------------------------------------------------------------------- - this.api = new PeerTubeEmbedApi(this) - this.api.initialize() - } + private async loadVideoAndBuildPlayer (options: { + uuid: string + autoplayFromPreviousVideo: boolean + forceAutoplay: boolean + }) { + const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options - private loadParams (video: VideoDetails) { try { - const params = new URL(window.location.toString()).searchParams - - 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.peertubeLink = this.getParamToggle(params, 'peertubeLink', 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') + const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid) - 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, autoplayFromPreviousVideo, forceAutoplay }) } catch (err) { - console.error('Cannot get params from URL.', err) - } - } - - private async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList) { - let elements = baseResult.data - let total = baseResult.total - let i = 0 - - while (total > elements.length && i < 10) { - const result = await this.loadPlaylistElements(playlistId, elements.length) - - const json = await result.json() as ResultList - total = json.total - - elements = elements.concat(json.data) - i++ - } - - if (i === 10) { - console.error('Cannot fetch all playlists elements, there are too many!') - } - - return elements - } - - private async loadPlaylist (playlistId: string) { - const playlistPromise = this.loadPlaylistInfo(playlistId) - const playlistElementsPromise = this.loadPlaylistElements(playlistId) - - let playlistResponse: Response - let isResponseOk: boolean - - try { - playlistResponse = await playlistPromise - isResponseOk = playlistResponse.status === HttpStatusCode.OK_200 - } catch (err) { - console.error(err) - isResponseOk = false - } - - if (!isResponseOk) { - const serverTranslations = await this.translationsPromise - - if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) { - this.playlistNotFound(serverTranslations) - return undefined - } - - this.playlistFetchError(serverTranslations) - return undefined - } - - return { playlistResponse, videosResponse: await playlistElementsPromise } - } - - private async loadVideo (videoId: string) { - const videoPromise = this.loadVideoInfo(videoId) - - let videoResponse: Response - let isResponseOk: boolean - - try { - videoResponse = await videoPromise - isResponseOk = videoResponse.status === HttpStatusCode.OK_200 - } catch (err) { - console.error(err) - - isResponseOk = false - } - - if (!isResponseOk) { - const serverTranslations = await this.translationsPromise - - if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) { - this.videoNotFound(serverTranslations) - return undefined - } - - this.videoFetchError(serverTranslations) - return undefined - } - - const captionsPromise = this.loadVideoCaptions(videoId) - - return { captionsPromise, videoResponse } - } - - private async buildPlaylistManager () { - 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.nextVideoTitle(), - next: () => this.playNextVideo(), - condition: () => !!this.getNextPlaylistElement(), - suspended: () => false - }) - } - - private async loadVideoAndBuildPlayer (uuid: string) { - const res = await this.loadVideo(uuid) - if (res === undefined) return - - return this.buildVideoPlayer(res.videoResponse, res.captionsPromise) - } - - private nextVideoTitle () { - const next = this.getNextPlaylistElement() - if (!next) return '' - - return next.video.name - } - - private getNextPlaylistElement (position?: number): VideoPlaylistElement { - if (!position) position = this.currentPlaylistElement.position + 1 - - if (position > this.playlist.videosLength) { - return undefined + this.playerHTML.displayError(err.message, await this.translationsPromise) } - - const next = this.playlistElements.find(e => e.position === position) - - if (!next || !next.video) { - return this.getNextPlaylistElement(position + 1) - } - - return next } - private getPreviousPlaylistElement (position?: number): VideoPlaylistElement { - if (!position) position = this.currentPlaylistElement.position - 1 - - if (position < 1) { - return undefined - } - - const prev = this.playlistElements.find(e => e.position === position) + private async buildVideoPlayer (options: { + videoResponse: Response + captionsPromise: Promise + autoplayFromPreviousVideo: boolean + forceAutoplay: boolean + }) { + const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options - if (!prev || !prev.video) { - return this.getNextPlaylistElement(position - 1) - } - - return prev - } - - private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise) { - let alreadyHadPlayer = false - - if (this.player) { - this.player.dispose() - alreadyHadPlayer = true - } + this.resetPlayerElement() - this.playerElement = document.createElement('video') - this.playerElement.className = 'video-js vjs-peertube-skin' - this.playerElement.setAttribute('playsinline', 'true') - this.wrapperElement.appendChild(this.playerElement) + const videoInfoPromise = videoResponse.json() + .then(async (videoInfo: VideoDetails) => { + this.playerManagerOptions.loadParams(this.config, videoInfo) - // Issue when we parsed config from HTML, fallback to API - if (!this.config) { - this.config = await this.refreshFetch('/api/v1/config') - .then(res => res.json()) - } + if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) { + this.playerHTML.buildPlaceholder(videoInfo) + } + const live = videoInfo.isLive + ? await this.videoFetcher.loadLive(videoInfo) + : undefined - const videoInfoPromise = videoResponse.json() - .then((videoInfo: VideoDetails) => { - if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo) + const videoFileToken = videoRequiresAuth(videoInfo) + ? await this.videoFetcher.loadVideoToken(videoInfo) + : undefined - return videoInfo + return { live, video: videoInfo, videoFileToken } }) - const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ + const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ videoInfoPromise, this.translationsPromise, captionsPromise, this.PeertubePlayerManagerModulePromise ]) - await this.loadPlugins(serverTranslations) - - const videoInfo: VideoDetails = videoInfoTmp + await this.peertubePlugin.loadPlugins(this.config, translations) - const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager - const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) + const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager - this.loadParams(videoInfo) - - const playlistPlugin = this.currentPlaylistElement - ? { - elements: this.playlistElements, - playlist: this.playlist, - - getCurrentPosition: () => this.currentPlaylistElement.position, - - onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => { - this.currentPlaylistElement = videoPlaylistElement - - this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) - .catch(err => console.error(err)) - } - } - : undefined + const playerOptions = await this.playerManagerOptions.getPlayerOptions({ + video, + captionsResponse, + autoplayFromPreviousVideo, + translations, + serverConfig: this.config, - const options: PeertubePlayerManagerOptions = { - common: { - // Autoplay in playlist mode - autoplay: alreadyHadPlayer ? true : this.autoplay, - controls: this.controls, - muted: this.muted, - loop: this.loop, + authorizationHeader: () => this.http.getHeaderTokenValue(), + videoFileToken: () => videoFileToken, - captions: videoCaptions.length !== 0, - subtitle: this.subtitle, + onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), - startTime: this.playlist ? this.currentPlaylistElement.startTimestamp : this.startTime, - stopTime: this.playlist ? this.currentPlaylistElement.stopTimestamp : this.stopTime, + playlistTracker: this.playlistTracker, + playNextPlaylistVideo: () => this.playNextPlaylistVideo(), + playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(), - nextVideo: this.playlist ? () => this.playNextVideo() : undefined, - hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined, - - previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined, - hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined, - - playlist: playlistPlugin, - - videoCaptions, - inactivityTimeout: 2500, - videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', - videoUUID: videoInfo.uuid, - - isLive: videoInfo.isLive, - - playerElement: this.playerElement, - onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, - - videoDuration: videoInfo.duration, - enableHotkeys: true, - peertubeLink: this.peertubeLink, - poster: window.location.origin + videoInfo.previewPath, - theaterButton: false, - - serverUrl: window.location.origin, - language: navigator.language, - embedUrl: window.location.origin + videoInfo.embedPath, - embedTitle: videoInfo.name - }, - - webtorrent: { - videoFiles: videoInfo.files - }, + live, + forceAutoplay + }) - pluginsManager: this.pluginsManager - } + this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { + this.player = player + }) - 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('customError', (event: any, data: any) => { + const message = data?.err?.message || '' + if (!message.includes('from xs param')) return - 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.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 + (window as any)['videojsPlayer'] = this.player this.buildCSS() - - await this.buildDock(videoInfo) - + this.buildPlayerDock(video) this.initializeApi() - this.removePlaceholder() + this.playerHTML.removePlaceholder() if (this.isPlaylistEmbed()) { - await this.buildPlaylistManager() + await this.buildPlayerPlaylistUpnext() this.player.playlist().updateSelected() this.player.on('stopped', () => { - this.playNextVideo() + this.playNextPlaylistVideo() }) } - this.pluginsManager.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo }) - } - - private async initCore () { - if (this.userTokens) this.setHeadersFromTokens() - - this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) - this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') - - let videoId: string - - if (this.isPlaylistEmbed()) { - const playlistId = this.getResourceId() - const res = await this.loadPlaylist(playlistId) - if (!res) return undefined - - this.playlist = await res.playlistResponse.json() - - const playlistElementResult = await res.videosResponse.json() - this.playlistElements = await this.loadAllPlaylistVideos(playlistId, playlistElementResult) - - const params = new URL(window.location.toString()).searchParams - const playlistPositionParam = this.getParamString(params, 'playlistPosition') - - let position = 1 - - if (playlistPositionParam) { - position = parseInt(playlistPositionParam + '', 10) - } + if (video.isLive) { + this.liveManager.listenForChanges({ + video, + onPublishedVideo: () => { + this.liveManager.stopListeningForChanges(video) + this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) + } + }) - this.currentPlaylistElement = this.playlistElements.find(e => e.position === position) - if (!this.currentPlaylistElement || !this.currentPlaylistElement.video) { - console.error('Current playlist element is not valid.', this.currentPlaylistElement) - this.currentPlaylistElement = this.getNextPlaylistElement() - } + if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { + this.liveManager.displayInfo({ state: video.state.id, translations }) - if (!this.currentPlaylistElement) { - console.error('This playlist does not have any valid element.') - const serverTranslations = await this.translationsPromise - this.playlistFetchError(serverTranslations) - return + this.disablePlayer() + } else { + this.correctlyHandleLiveEnding(translations) } - - videoId = this.currentPlaylistElement.video.uuid - } else { - videoId = this.getResourceId() } - return this.loadVideoAndBuildPlayer(videoId) + this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) } - private handleError (err: Error, translations?: { [ id: string ]: string }) { - if (err.message.indexOf('from xs param') !== -1) { + private resetPlayerElement () { + if (this.player) { this.player.dispose() - this.playerElement = null - this.displayError('This video is not available because the remote instance is not responding.', translations) - return + this.player = undefined } + + const playerElement = document.createElement('video') + playerElement.className = 'video-js vjs-peertube-skin' + playerElement.setAttribute('playsinline', 'true') + + this.playerHTML.setPlayerElement(playerElement) + this.playerHTML.addPlayerElementToDOM() } - private async buildDock (videoInfo: VideoDetails) { - if (!this.controls) 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 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 description = this.config.tracker.enabled && this.warningTitle + const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled() ? '' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '' : undefined - if (title || description) { - this.player.dock({ - title, - description - }) - } + 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.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 { - if (captionsResponse.ok) { - const { data } = (await captionsResponse.json()) as ResultList - - 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}")` - placeholder.style.display = 'block' - } - - private removePlaceholder () { - const placeholder = this.getPlaceholderElement() - placeholder.style.display = 'none' - } - - private getPlaceholderElement () { - return document.getElementById('placeholder-preview') - } - - private setHeadersFromTokens () { - this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`) - } - - private removeTokensFromHeaders () { - this.headers.delete('Authorization') - } + // --------------------------------------------------------------------------- private getResourceId () { const urlParts = window.location.pathname.split('/') - return urlParts[ urlParts.length - 1 ] + return urlParts[urlParts.length - 1] } private isPlaylistEmbed () { return window.location.pathname.split('/')[1] === 'video-playlists' } - private loadPlugins (translations?: { [ id: string ]: string }) { - this.pluginsManager = new PluginsManager({ - peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations) - }) + // --------------------------------------------------------------------------- - this.pluginsManager.loadPluginsList(this.config) + private correctlyHandleLiveEnding (translations: Translations) { + this.player.one('ended', () => { + // Display the live ended information + this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) - return this.pluginsManager.ensurePluginsAreLoaded('embed') + this.disablePlayer() + }) } - private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers { - function unimplemented (): any { - throw new Error('This helper is not implemented in embed.') + private disablePlayer () { + if (this.player.isFullscreen()) { + this.player.exitFullscreen() } - return { - getBaseStaticRoute: unimplemented, - - getBaseRouterRoute: unimplemented, - - getSettings: unimplemented, - - isLoggedIn: unimplemented, - getAuthHeader: unimplemented, - - notifier: { - info: unimplemented, - error: unimplemented, - success: unimplemented - }, + // Disable player + this.player.hasStarted(false) + this.player.removeClass('vjs-has-autoplay') + this.player.bigPlayButton.hide(); - showModal: unimplemented, - - getServerConfig: unimplemented, - - markdownRenderer: { - textMarkdownToHTML: unimplemented, - enhancedMarkdownToHTML: unimplemented - }, - - translate: (value: string) => { - return Promise.resolve(peertubeTranslate(value, translations)) - } - } + (this.player.el() as HTMLElement).style.pointerEvents = 'none' } + } PeerTubeEmbed.main() - .catch(err => console.error('Cannot init embed.', err)) + .catch(err => { + (window as any).displayIncompatibleBrowser() + + logger.error('Cannot init embed.', err) + })