PeerTube/client/src/standalone/videos/embed.ts

412 lines
13 KiB
TypeScript
Raw Normal View History

2017-07-23 14:49:52 +02:00
import './embed.scss'
2022-03-14 14:28:20 +01:00
import '../../assets/player/shared/dock/peertube-dock-component'
import '../../assets/player/shared/dock/peertube-dock-plugin'
2020-06-26 08:37:26 +02:00
import videojs from 'video.js'
2020-08-06 14:58:01 +02:00
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
2022-11-15 08:55:27 +01:00
import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models'
2022-05-31 14:18:41 +02:00
import { PeertubePlayerManager } from '../../assets/player'
2020-06-26 08:37:26 +02:00
import { TranslationsManager } from '../../assets/player/translations-manager'
import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
2021-05-14 12:04:44 +02:00
import { PeerTubeEmbedApi } from './embed-api'
import {
AuthHTTP,
LiveManager,
PeerTubePlugin,
PlayerManagerOptions,
PlaylistFetcher,
PlaylistTracker,
Translations,
VideoFetcher
} from './shared'
2022-05-31 08:59:30 +02:00
import { PlayerHTML } from './shared/player-html'
2017-07-23 14:49:52 +02:00
2019-06-11 15:59:10 +02:00
export class PeerTubeEmbed {
2020-04-17 11:20:12 +02:00
player: videojs.Player
2018-07-10 18:02:30 +02:00
api: PeerTubeEmbedApi = null
2020-08-04 11:42:06 +02:00
2021-05-14 12:04:44 +02:00
config: HTMLServerConfig
2020-08-04 11:42:06 +02:00
private translationsPromise: Promise<{ [id: string]: string }>
private PeertubePlayerManagerModulePromise: Promise<any>
2022-05-31 08:59:30 +02:00
private readonly http: AuthHTTP
private readonly videoFetcher: VideoFetcher
private readonly playlistFetcher: PlaylistFetcher
private readonly peertubePlugin: PeerTubePlugin
private readonly playerHTML: PlayerHTML
private readonly playerManagerOptions: PlayerManagerOptions
2022-05-31 14:18:41 +02:00
private readonly liveManager: LiveManager
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
private playlistTracker: PlaylistTracker
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
constructor (videoWrapperId: string) {
logger.registerServerSending(window.location.origin)
2022-05-31 08:59:30 +02:00
this.http = new AuthHTTP()
2020-08-20 11:46:25 +02:00
2022-05-31 08:59:30 +02:00
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)
2022-05-31 14:18:41 +02:00
this.liveManager = new LiveManager(this.playerHTML)
2021-05-14 12:04:44 +02:00
try {
this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
2021-05-14 12:04:44 +02:00
} catch (err) {
logger.error('Cannot parse HTML config.', err)
2021-05-14 12:04:44 +02:00
}
2018-07-10 18:02:30 +02:00
}
2021-08-17 14:42:53 +02:00
static async main () {
const videoContainerId = 'video-wrapper'
const embed = new PeerTubeEmbed(videoContainerId)
await embed.init()
}
2022-05-31 08:59:30 +02:00
getPlayerElement () {
return this.playerHTML.getPlayerElement()
2020-08-04 11:42:06 +02:00
}
2022-05-31 08:59:30 +02:00
getScope () {
return this.playerManagerOptions.getScope()
}
2018-04-19 18:06:59 +02:00
2022-05-31 08:59:30 +02:00
// ---------------------------------------------------------------------------
2018-07-13 18:21:19 +02:00
2022-05-31 08:59:30 +02:00
async init () {
this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
2022-03-04 13:40:02 +01:00
2022-05-31 08:59:30 +02:00
// 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())
}
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
const videoId = this.isPlaylistEmbed()
? await this.initPlaylist()
: this.getResourceId()
2020-08-05 13:46:01 +02:00
2022-05-31 08:59:30 +02:00
if (!videoId) return
2020-08-04 11:42:06 +02:00
return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false })
}
2018-04-19 18:06:59 +02:00
2022-05-31 08:59:30 +02:00
private async initPlaylist () {
const playlistId = this.getResourceId()
2022-05-31 08:59:30 +02:00
try {
const res = await this.playlistFetcher.loadPlaylist(playlistId)
2022-05-31 08:59:30 +02:00
const [ playlist, playlistElementResult ] = await Promise.all([
res.playlistResponse.json() as Promise<VideoPlaylist>,
res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
])
2022-05-31 08:59:30 +02:00
const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
2022-05-31 08:59:30 +02:00
this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
2020-08-05 14:00:36 +02:00
2022-05-31 08:59:30 +02:00
const params = new URL(window.location.toString()).searchParams
const playlistPositionParam = getParamString(params, 'playlistPosition')
2022-05-31 08:59:30 +02:00
const position = playlistPositionParam
? parseInt(playlistPositionParam + '', 10)
: 1
2022-05-31 08:59:30 +02:00
this.playlistTracker.setPosition(position)
} catch (err) {
this.playerHTML.displayError(err.message, await this.translationsPromise)
return undefined
}
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
return this.playlistTracker.getCurrentElement().video.uuid
2020-08-04 11:42:06 +02:00
}
2022-05-31 08:59:30 +02:00
private initializeApi () {
if (this.playerManagerOptions.hasAPIEnabled()) {
2022-11-14 10:26:41 +01:00
if (this.api) {
this.api.reInit()
return
}
2022-05-31 08:59:30 +02:00
this.api = new PeerTubeEmbedApi(this)
this.api.initialize()
}
}
2018-04-19 18:06:59 +02:00
2022-05-31 08:59:30 +02:00
// ---------------------------------------------------------------------------
2018-03-27 10:34:40 +02:00
2022-05-31 08:59:30 +02:00
async playNextPlaylistVideo () {
const next = this.playlistTracker.getNextPlaylistElement()
2020-08-05 14:16:39 +02:00
if (!next) {
logger.info('Next element not found in playlist.')
2020-08-05 14:16:39 +02:00
return
}
2022-05-31 08:59:30 +02:00
this.playlistTracker.setCurrentElement(next)
2020-08-05 14:16:39 +02:00
return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
2020-08-05 14:16:39 +02:00
}
2022-05-31 08:59:30 +02:00
async playPreviousPlaylistVideo () {
const previous = this.playlistTracker.getPreviousPlaylistElement()
2020-08-05 14:16:39 +02:00
if (!previous) {
logger.info('Previous element not found in playlist.')
2020-08-05 14:16:39 +02:00
return
}
2022-05-31 08:59:30 +02:00
this.playlistTracker.setCurrentElement(previous)
2020-08-05 14:16:39 +02:00
await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false })
2020-08-05 14:16:39 +02:00
}
2022-05-31 08:59:30 +02:00
getCurrentPlaylistPosition () {
return this.playlistTracker.getCurrentPosition()
2018-07-10 18:02:30 +02:00
}
2022-05-31 08:59:30 +02:00
// ---------------------------------------------------------------------------
2020-08-04 11:42:06 +02:00
private async loadVideoAndBuildPlayer (options: {
uuid: string
autoplayFromPreviousVideo: boolean
forceAutoplay: boolean
}) {
const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options
try {
2022-05-31 08:59:30 +02:00
const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
2020-08-04 11:42:06 +02:00
return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
} catch (err) {
2022-05-31 08:59:30 +02:00
this.playerHTML.displayError(err.message, await this.translationsPromise)
}
2020-08-05 11:02:14 +02:00
}
private async buildVideoPlayer (options: {
videoResponse: Response
captionsPromise: Promise<Response>
autoplayFromPreviousVideo: boolean
forceAutoplay: boolean
}) {
const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options
this.resetPlayerElement()
2021-05-14 12:04:44 +02:00
const videoInfoPromise = videoResponse.json()
.then(async (videoInfo: VideoDetails) => {
2022-05-31 08:59:30 +02:00
this.playerManagerOptions.loadParams(this.config, videoInfo)
2021-08-02 11:46:11 +02:00
if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
2022-05-31 08:59:30 +02:00
this.playerHTML.buildPlaceholder(videoInfo)
}
const live = videoInfo.isLive
? await this.videoFetcher.loadLive(videoInfo)
: undefined
2020-08-04 11:42:06 +02:00
const videoFileToken = videoRequiresAuth(videoInfo)
? await this.videoFetcher.loadVideoToken(videoInfo)
: undefined
2022-03-04 13:40:02 +01:00
return { live, video: videoInfo, videoFileToken }
2020-08-04 11:42:06 +02:00
})
const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
2020-08-04 11:42:06 +02:00
videoInfoPromise,
this.translationsPromise,
captionsPromise,
this.PeertubePlayerManagerModulePromise
])
2019-12-17 11:20:24 +01:00
2022-05-31 08:59:30 +02:00
await this.peertubePlugin.loadPlugins(this.config, translations)
2022-05-31 08:59:30 +02:00
const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
const playerOptions = await this.playerManagerOptions.getPlayerOptions({
2022-05-31 08:59:30 +02:00
video,
captionsResponse,
autoplayFromPreviousVideo,
2022-05-31 08:59:30 +02:00
translations,
2022-06-28 14:04:03 +02:00
serverConfig: this.config,
authorizationHeader: () => this.http.getHeaderTokenValue(),
videoFileToken: () => videoFileToken,
onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),
2022-05-31 08:59:30 +02:00
playlistTracker: this.playlistTracker,
playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
live,
forceAutoplay
2022-05-31 08:59:30 +02:00
})
2017-07-23 14:49:52 +02:00
this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => {
2021-08-17 14:42:53 +02:00
this.player = player
})
2022-05-31 08:59:30 +02:00
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 as any)['videojsPlayer'] = this.player
2018-07-10 18:02:30 +02:00
2019-06-11 15:59:10 +02:00
this.buildCSS()
2022-05-31 08:59:30 +02:00
this.buildPlayerDock(video)
2019-06-11 15:59:10 +02:00
this.initializeApi()
2019-12-17 11:20:24 +01:00
2022-05-31 08:59:30 +02:00
this.playerHTML.removePlaceholder()
2020-08-04 11:42:06 +02:00
if (this.isPlaylistEmbed()) {
2022-05-31 08:59:30 +02:00
await this.buildPlayerPlaylistUpnext()
2020-08-05 09:44:58 +02:00
this.player.playlist().updateSelected()
this.player.on('stopped', () => {
2022-05-31 08:59:30 +02:00
this.playNextPlaylistVideo()
})
2020-08-04 11:42:06 +02:00
}
2020-08-20 11:46:25 +02:00
2022-05-31 14:18:41 +02:00
if (video.isLive) {
2022-11-15 08:55:27 +01:00
this.liveManager.listenForChanges({
2022-05-31 14:18:41 +02:00
video,
onPublishedVideo: () => {
this.liveManager.stopListeningForChanges(video)
this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true })
2022-05-31 14:18:41 +02:00
}
})
2022-11-15 08:55:27 +01:00
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)
}
2022-05-31 14:18:41 +02:00
}
2022-11-15 08:55:27 +01:00
this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
2020-08-04 11:42:06 +02:00
}
2022-05-31 08:59:30 +02:00
private resetPlayerElement () {
if (this.player) {
this.player.dispose()
2022-10-24 10:32:35 +02:00
this.player = undefined
2022-05-31 08:59:30 +02:00
}
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
const playerElement = document.createElement('video')
playerElement.className = 'video-js vjs-peertube-skin'
playerElement.setAttribute('playsinline', 'true')
2020-08-04 11:42:06 +02:00
2022-05-31 08:59:30 +02:00
this.playerHTML.setPlayerElement(playerElement)
this.playerHTML.addPlayerElementToDOM()
2019-06-11 15:59:10 +02:00
}
2022-05-31 08:59:30 +02:00
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
})
2019-06-11 15:59:10 +02:00
}
2022-05-31 08:59:30 +02:00
private buildPlayerDock (videoInfo: VideoDetails) {
if (!this.playerManagerOptions.hasControls()) return
2019-06-11 15:59:10 +02:00
// On webtorrent fallback, player may have been disposed
if (!this.player.player_) return
2019-06-11 15:59:10 +02:00
2022-05-31 08:59:30 +02:00
const title = this.playerManagerOptions.hasTitle()
? videoInfo.name
: undefined
const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
2020-02-03 13:33:42 +01:00
? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
: undefined
2022-05-31 08:59:30 +02:00
if (!title && !description) return
2022-03-08 16:26:30 +01:00
const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
const avatar = availableAvatars.length !== 0
? availableAvatars[0]
: undefined
2022-05-31 08:59:30 +02:00
this.player.peertubeDock({
title,
description,
avatarUrl: title && avatar
? avatar.path
: undefined
})
2019-06-11 15:59:10 +02:00
}
2018-07-13 18:21:19 +02:00
2019-06-11 15:59:10 +02:00
private buildCSS () {
const body = document.getElementById('custom-css')
2022-05-31 08:59:30 +02:00
if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
2019-06-11 15:59:10 +02:00
}
2022-05-31 08:59:30 +02:00
if (this.playerManagerOptions.hasForegroundColor()) {
body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
2019-06-11 15:59:10 +02:00
}
}
2022-05-31 08:59:30 +02:00
// ---------------------------------------------------------------------------
2020-08-04 11:42:06 +02:00
private getResourceId () {
const urlParts = window.location.pathname.split('/')
2021-08-17 14:42:53 +02:00
return urlParts[urlParts.length - 1]
2020-08-04 11:42:06 +02:00
}
private isPlaylistEmbed () {
return window.location.pathname.split('/')[1] === 'video-playlists'
}
2022-11-15 08:55:27 +01:00
// ---------------------------------------------------------------------------
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)
})