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 { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import {
- ClientHookName,
- HTMLServerConfig,
- OAuth2ErrorCode,
- PluginType,
- 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 { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
-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<any>
- 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 peertubeHooks: Hooks = {}
- private loadedScripts = new Set<string>()
+ 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'])
} 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<void>((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<Response> {
- return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers })
- }
+ // ---------------------------------------------------------------------------
- loadVideoCaptions (videoId: string): Promise<Response> {
- 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<Response> {
- 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<Response> {
- 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
- }
-
- const translatedText = peertubeTranslate(text, translations)
- const translatedSorry = peertubeTranslate('Sorry', translations)
+ private async initPlaylist () {
+ const playlistId = this.getResourceId()
- document.title = translatedSorry + ' - ' + translatedText
+ try {
+ const res = await this.playlistFetcher.loadPlaylist(playlistId)
- const errorBlock = document.getElementById('error-block')
- errorBlock.style.display = 'flex'
+ const [ playlist, playlistElementResult ] = await Promise.all([
+ res.playlistResponse.json() as Promise<VideoPlaylist>,
+ res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
+ ])
- const errorTitle = document.getElementById('error-title')
- errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
+ const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
- const errorText = document.getElementById('error-content')
- errorText.innerHTML = translatedText
+ this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
- this.wrapperElement.style.display = 'none'
- }
+ 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
+ }
- 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
-
- await this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
- }
-
- getCurrentPosition () {
- if (!this.currentPlaylistElement) return -1
-
- return this.currentPlaylistElement.position
- }
-
- 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 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')
+ this.playlistTracker.setCurrentElement(previous)
- 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'
- }
- } catch (err) {
- console.error('Cannot get params from URL.', err)
- }
+ await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
}
- private async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList<VideoPlaylistElement>) {
- 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<VideoPlaylistElement>
- 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
+ getCurrentPlaylistPosition () {
+ return this.playlistTracker.getCurrentPosition()
}
- private async loadPlaylist (playlistId: string) {
- const playlistPromise = this.loadPlaylistInfo(playlistId)
- const playlistElementsPromise = this.loadPlaylistElements(playlistId)
+ // ---------------------------------------------------------------------------
- let playlistResponse: Response
- let isResponseOk: boolean
+ private async loadVideoAndBuildPlayer (options: {
+ uuid: string
+ autoplayFromPreviousVideo: boolean
+ forceAutoplay: boolean
+ }) {
+ const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options
try {
- playlistResponse = await playlistPromise
- isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
- } catch (err) {
- console.error(err)
- isResponseOk = false
- }
+ const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
- 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
+ return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
} 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
- }
-
- const next = this.playlistElements.find(e => e.position === position)
-
- if (!next || !next.video) {
- return this.getNextPlaylistElement(position + 1)
+ this.playerHTML.displayError(err.message, await this.translationsPromise)
}
-
- 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)
-
- if (!prev || !prev.video) {
- return this.getNextPlaylistElement(position - 1)
- }
+ private async buildVideoPlayer (options: {
+ videoResponse: Response
+ captionsPromise: Promise<Response>
+ autoplayFromPreviousVideo: boolean
+ forceAutoplay: boolean
+ }) {
+ const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options
- return prev
- }
+ this.resetPlayerElement()
- private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
- let alreadyHadPlayer = false
-
- if (this.player) {
- this.player.dispose()
- alreadyHadPlayer = true
- }
-
- 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.ensurePluginsAreLoaded(serverTranslations)
-
- const videoInfo: VideoDetails = videoInfoTmp
-
- const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
- const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
-
- 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 options: PeertubePlayerManagerOptions = {
- common: {
- // Autoplay in playlist mode
- autoplay: alreadyHadPlayer ? true : this.autoplay,
- controls: this.controls,
- muted: this.muted,
- loop: this.loop,
+ await this.peertubePlugin.loadPlugins(this.config, translations)
- captions: videoCaptions.length !== 0,
- subtitle: this.subtitle,
+ const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
- startTime: this.playlist ? this.currentPlaylistElement.startTimestamp : this.startTime,
- stopTime: this.playlist ? this.currentPlaylistElement.stopTimestamp : this.stopTime,
+ const playerOptions = await this.playerManagerOptions.getPlayerOptions({
+ video,
+ captionsResponse,
+ autoplayFromPreviousVideo,
+ translations,
+ serverConfig: this.config,
- nextVideo: this.playlist ? () => this.playNextVideo() : undefined,
- hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined,
+ authorizationHeader: () => this.http.getHeaderTokenValue(),
+ videoFileToken: () => videoFileToken,
- previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined,
- hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined,
+ onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),
- playlist: playlistPlugin,
+ playlistTracker: this.playlistTracker,
+ playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
+ playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
- 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,
+ live,
+ forceAutoplay
+ })
- serverUrl: window.location.origin,
- language: navigator.language,
- embedUrl: window.location.origin + videoInfo.embedPath,
- embedTitle: videoInfo.name
- },
+ this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => {
+ this.player = player
+ })
- webtorrent: {
- videoFiles: videoInfo.files
- }
- }
+ this.player.on('customError', (event: any, data: any) => {
+ const message = data?.err?.message || ''
+ if (!message.includes('from xs param')) return
- 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 = 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['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.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 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) {
- 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 description = this.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
- 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.playerManagerOptions.hasBigPlayBackgroundColor()) {
+ body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
}
- if (this.foregroundColor) {
- body.style.setProperty('--embedForegroundColor', this.foregroundColor)
- }
- }
-
- 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}")`
- 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 async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) {
- if (this.config.plugin.registered.length === 0) return
-
- for (const plugin of this.config.plugin.registered) {
- for (const key of Object.keys(plugin.clientScripts)) {
- const clientScript = plugin.clientScripts[key]
-
- if (clientScript.scopes.includes('embed') === false) continue
-
- const script = `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
+ // ---------------------------------------------------------------------------
- if (this.loadedScripts.has(script)) continue
+ private correctlyHandleLiveEnding (translations: Translations) {
+ this.player.one('ended', () => {
+ // Display the live ended information
+ this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
- const pluginInfo = {
- plugin,
- clientScript: {
- script,
- scopes: clientScript.scopes
- },
- pluginType: PluginType.PLUGIN,
- isTheme: false
- }
-
- await loadPlugin({
- hooks: this.peertubeHooks,
- pluginInfo,
- onSettingsScripts: () => undefined,
- peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
- })
- }
- }
+ 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,
-
- 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'
}
- private runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
- return runHook(this.peertubeHooks, hookName, result, params)
- }
}
PeerTubeEmbed.main()
- .catch(err => console.error('Cannot init embed.', err))
+ .catch(err => {
+ (window as any).displayIncompatibleBrowser()
+
+ logger.error('Cannot init embed.', err)
+ })