467 lines
14 KiB
TypeScript
467 lines
14 KiB
TypeScript
import './embed.scss'
|
|
import '../../assets/player/shared/dock/peertube-dock-component'
|
|
import '../../assets/player/shared/dock/peertube-dock-plugin'
|
|
import { PeerTubeServerError } from 'src/types'
|
|
import videojs from 'video.js'
|
|
import {
|
|
HTMLServerConfig,
|
|
ResultList,
|
|
ServerErrorCode,
|
|
VideoDetails,
|
|
VideoPlaylist,
|
|
VideoPlaylistElement,
|
|
VideoState
|
|
} from '@peertube/peertube-models'
|
|
import { PeerTubePlayer } from '../../assets/player/peertube-player'
|
|
import { TranslationsManager } from '../../assets/player/translations-manager'
|
|
import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers'
|
|
import { PeerTubeEmbedApi } from './embed-api'
|
|
import {
|
|
AuthHTTP,
|
|
LiveManager,
|
|
PeerTubePlugin,
|
|
PlayerOptionsBuilder,
|
|
PlaylistFetcher,
|
|
PlaylistTracker,
|
|
Translations,
|
|
VideoFetcher,
|
|
getBackendUrl
|
|
} 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<any>
|
|
|
|
private readonly http: AuthHTTP
|
|
private readonly videoFetcher: VideoFetcher
|
|
private readonly playlistFetcher: PlaylistFetcher
|
|
private readonly peertubePlugin: PeerTubePlugin
|
|
private readonly playerHTML: PlayerHTML
|
|
private readonly playerOptionsBuilder: PlayerOptionsBuilder
|
|
private readonly liveManager: LiveManager
|
|
|
|
private peertubePlayer: PeerTubePlayer
|
|
|
|
private playlistTracker: PlaylistTracker
|
|
|
|
private alreadyInitialized = false
|
|
private alreadyPlayed = false
|
|
|
|
private videoPassword: string
|
|
private videoPasswordFromAPI: string
|
|
private onVideoPasswordFromAPIResolver: (value: string) => void
|
|
private requiresPassword: boolean
|
|
|
|
constructor (videoWrapperId: string) {
|
|
logger.registerServerSending(getBackendUrl())
|
|
|
|
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.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
|
|
this.liveManager = new LiveManager(this.playerHTML)
|
|
this.requiresPassword = false
|
|
|
|
try {
|
|
this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
|
|
} catch (err) {
|
|
if (!(import.meta as any).env.DEV) {
|
|
logger.error('Cannot parse HTML config.', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
static async main () {
|
|
const videoContainerId = 'video-wrapper'
|
|
const embed = new PeerTubeEmbed(videoContainerId)
|
|
await embed.init()
|
|
}
|
|
|
|
getScope () {
|
|
return this.playerOptionsBuilder.getScope()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async init () {
|
|
this.translationsPromise = TranslationsManager.getServerTranslations(getBackendUrl(), navigator.language)
|
|
this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player')
|
|
|
|
// Issue when we parsed config from HTML, fallback to API
|
|
if (!this.config) {
|
|
this.config = await this.http.fetch(getBackendUrl() + '/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<VideoPlaylist>,
|
|
res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
|
|
])
|
|
|
|
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.playerOptionsBuilder.hasAPIEnabled()) return
|
|
if (this.api) return
|
|
|
|
this.api = new PeerTubeEmbedApi(this)
|
|
this.api.initialize()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
setVideoPasswordByAPI (password: string) {
|
|
logger.info('Setting password from API')
|
|
|
|
this.videoPasswordFromAPI = password
|
|
|
|
if (this.onVideoPasswordFromAPIResolver) {
|
|
this.onVideoPasswordFromAPIResolver(password)
|
|
}
|
|
}
|
|
|
|
private getPasswordByAPI () {
|
|
if (this.videoPasswordFromAPI) return Promise.resolve(this.videoPasswordFromAPI)
|
|
|
|
return new Promise<string>(res => {
|
|
this.onVideoPasswordFromAPIResolver = res
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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
|
|
|
|
this.playerOptionsBuilder.loadCommonParams()
|
|
this.initializeApi()
|
|
|
|
try {
|
|
const {
|
|
videoResponse,
|
|
captionsPromise,
|
|
chaptersPromise,
|
|
storyboardsPromise
|
|
} = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
|
|
|
|
return this.buildVideoPlayer({ videoResponse, captionsPromise, chaptersPromise, storyboardsPromise, forceAutoplay })
|
|
} catch (err) {
|
|
|
|
if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
|
|
else this.playerHTML.displayError(err.message, await this.translationsPromise)
|
|
}
|
|
}
|
|
|
|
private async buildVideoPlayer (options: {
|
|
videoResponse: Response
|
|
storyboardsPromise: Promise<Response>
|
|
captionsPromise: Promise<Response>
|
|
chaptersPromise: Promise<Response>
|
|
forceAutoplay: boolean
|
|
}) {
|
|
const { videoResponse, captionsPromise, chaptersPromise, storyboardsPromise, forceAutoplay } = options
|
|
|
|
const videoInfoPromise = videoResponse.json()
|
|
.then(async (videoInfo: VideoDetails) => {
|
|
this.playerOptionsBuilder.loadVideoParams(this.config, videoInfo)
|
|
|
|
const live = videoInfo.isLive
|
|
? await this.videoFetcher.loadLive(videoInfo)
|
|
: undefined
|
|
|
|
const videoFileToken = videoRequiresFileToken(videoInfo)
|
|
? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
|
|
: undefined
|
|
|
|
return { live, video: videoInfo, videoFileToken }
|
|
})
|
|
|
|
const [
|
|
{ video, live, videoFileToken },
|
|
translations,
|
|
captionsResponse,
|
|
chaptersResponse,
|
|
storyboardsResponse
|
|
] = await Promise.all([
|
|
videoInfoPromise,
|
|
this.translationsPromise,
|
|
captionsPromise,
|
|
chaptersPromise,
|
|
storyboardsPromise,
|
|
this.buildPlayerIfNeeded()
|
|
])
|
|
|
|
const playlist = this.playlistTracker
|
|
? {
|
|
onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
|
|
|
|
playlistTracker: this.playlistTracker,
|
|
playNext: () => this.playNextPlaylistVideo(),
|
|
playPrevious: () => this.playPreviousPlaylistVideo()
|
|
}
|
|
: undefined
|
|
|
|
const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({
|
|
video,
|
|
captionsResponse,
|
|
chaptersResponse,
|
|
translations,
|
|
|
|
storyboardsResponse,
|
|
|
|
videoFileToken: () => videoFileToken,
|
|
videoPassword: () => this.videoPassword,
|
|
requiresPassword: this.requiresPassword,
|
|
|
|
playlist,
|
|
|
|
live,
|
|
forceAutoplay,
|
|
alreadyPlayed: this.alreadyPlayed
|
|
})
|
|
await this.peertubePlayer.load(loadOptions)
|
|
|
|
if (!this.alreadyInitialized) {
|
|
this.player = this.peertubePlayer.getPlayer();
|
|
|
|
(window as any)['videojsPlayer'] = this.player
|
|
|
|
this.buildCSS()
|
|
|
|
if (this.api) this.api.initWithVideo()
|
|
}
|
|
|
|
this.alreadyInitialized = true
|
|
|
|
this.player.one('play', () => {
|
|
this.alreadyPlayed = true
|
|
})
|
|
|
|
if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
|
|
|
|
if (video.isLive) {
|
|
this.liveManager.listenForChanges({
|
|
video,
|
|
|
|
onPublishedVideo: () => {
|
|
this.liveManager.stopListeningForChanges(video)
|
|
this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true })
|
|
},
|
|
|
|
onForceEnd: () => this.endLive(video, translations)
|
|
})
|
|
|
|
if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
|
|
this.liveManager.displayInfo({ state: video.state.id, translations })
|
|
this.peertubePlayer.disable()
|
|
} else {
|
|
this.player.one('ended', () => this.endLive(video, translations))
|
|
}
|
|
}
|
|
|
|
this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
|
|
}
|
|
|
|
private buildCSS () {
|
|
const body = document.getElementById('custom-css')
|
|
|
|
if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) {
|
|
body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor())
|
|
}
|
|
|
|
if (this.playerOptionsBuilder.hasForegroundColor()) {
|
|
body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private getResourceId () {
|
|
const search = window.location.search
|
|
|
|
if (search.startsWith('?videoId=')) {
|
|
return search.replace(/^\?videoId=/, '')
|
|
}
|
|
|
|
if (search.startsWith('?videoPlaylistId=')) {
|
|
return search.replace(/^\?videoPlaylistId=/, '')
|
|
}
|
|
|
|
const urlParts = window.location.pathname.split('/')
|
|
return urlParts[urlParts.length - 1]
|
|
}
|
|
|
|
private isPlaylistEmbed () {
|
|
return window.location.pathname.split('/')[1] === 'video-playlists'
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private endLive (video: VideoDetails, translations: Translations) {
|
|
// Display the live ended information
|
|
this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
|
|
|
|
this.peertubePlayer.unload()
|
|
this.peertubePlayer.disable()
|
|
this.peertubePlayer.setPoster(video.previewPath)
|
|
}
|
|
|
|
private async handlePasswordError (err: PeerTubeServerError) {
|
|
let incorrectPassword: boolean = null
|
|
if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
|
|
else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true
|
|
|
|
if (incorrectPassword === null) return false
|
|
|
|
this.requiresPassword = true
|
|
|
|
if (this.playerOptionsBuilder.mustWaitPasswordFromEmbedAPI()) {
|
|
logger.info('Waiting for password from Embed API')
|
|
|
|
const videoPasswordFromAPI = await this.getPasswordByAPI()
|
|
|
|
if (videoPasswordFromAPI && this.videoPassword !== videoPasswordFromAPI) {
|
|
logger.info('Using video password from API')
|
|
|
|
this.videoPassword = videoPasswordFromAPI
|
|
|
|
return true
|
|
}
|
|
|
|
logger.error('Password from embed API is not valid')
|
|
|
|
return false
|
|
}
|
|
|
|
this.videoPassword = await this.playerHTML.askVideoPassword({
|
|
incorrectPassword,
|
|
translations: await this.translationsPromise
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
private async buildPlayerIfNeeded () {
|
|
if (this.peertubePlayer) {
|
|
this.peertubePlayer.enable()
|
|
|
|
return
|
|
}
|
|
|
|
const playerElement = document.createElement('video')
|
|
playerElement.className = 'video-js vjs-peertube-skin'
|
|
playerElement.setAttribute('playsinline', 'true')
|
|
|
|
this.playerHTML.setInitVideoEl(playerElement)
|
|
this.playerHTML.addInitVideoElToDOM()
|
|
|
|
const [ { PeerTubePlayer } ] = await Promise.all([
|
|
this.PeerTubePlayerManagerModulePromise,
|
|
this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise)
|
|
])
|
|
|
|
const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({
|
|
serverConfig: this.config,
|
|
authorizationHeader: () => this.http.getHeaderTokenValue()
|
|
})
|
|
this.peertubePlayer = new PeerTubePlayer(constructorOptions)
|
|
|
|
this.player = this.peertubePlayer.getPlayer()
|
|
}
|
|
|
|
getImageDataUrl (): string {
|
|
const canvas = document.createElement('canvas')
|
|
|
|
canvas.width = this.player.videoWidth()
|
|
canvas.height = this.player.videoHeight()
|
|
|
|
const videoEl = this.player.tech(true).el() as HTMLVideoElement
|
|
|
|
canvas.getContext('2d').drawImage(videoEl, 0, 0, canvas.width, canvas.height)
|
|
|
|
return canvas.toDataURL('image/jpeg')
|
|
}
|
|
}
|
|
|
|
PeerTubeEmbed.main()
|
|
.catch(err => {
|
|
(window as any).displayIncompatibleBrowser()
|
|
|
|
logger.error('Cannot init embed.', err)
|
|
})
|