1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 01:39:37 +02:00
Peertube/client/src/standalone/videos/embed.ts
Chocobozzz 3a4992633e
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:

 * Server can be faster at startup because imports() are async and we can
   easily lazy import big modules
 * Angular doesn't seem to support ES import (with .js extension), so we
   had to correctly organize peertube into a monorepo:
    * Use yarn workspace feature
    * Use typescript reference projects for dependencies
    * Shared projects have been moved into "packages", each one is now a
      node module (with a dedicated package.json/tsconfig.json)
    * server/tools have been moved into apps/ and is now a dedicated app
      bundled and published on NPM so users don't have to build peertube
      cli tools manually
    * server/tests have been moved into packages/ so we don't compile
      them every time we want to run the server
 * Use isolatedModule option:
   * Had to move from const enum to const
     (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
   * Had to explictely specify "type" imports when used in decorators
 * Prefer tsx (that uses esbuild under the hood) instead of ts-node to
   load typescript files (tests with mocha or scripts):
     * To reduce test complexity as esbuild doesn't support decorator
       metadata, we only test server files that do not import server
       models
     * We still build tests files into js files for a faster CI
 * Remove unmaintained peertube CLI import script
 * Removed some barrels to speed up execution (less imports)
2023-08-11 15:02:33 +02:00

399 lines
12 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
} 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 requiresPassword: boolean
constructor (videoWrapperId: string) {
logger.registerServerSending(window.location.origin)
this.http = new AuthHTTP()
this.videoFetcher = new VideoFetcher(this.http)
this.playlistFetcher = new PlaylistFetcher(this.http)
this.peertubePlugin = new PeerTubePlugin(this.http)
this.playerHTML = new PlayerHTML(videoWrapperId)
this.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) {
logger.error('Cannot parse HTML config.', err)
}
}
static async main () {
const videoContainerId = 'video-wrapper'
const embed = new PeerTubeEmbed(videoContainerId)
await embed.init()
}
getPlayerElement () {
return this.playerHTML.getPlayerElement()
}
getScope () {
return this.playerOptionsBuilder.getScope()
}
// ---------------------------------------------------------------------------
async init () {
this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, 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('/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()) {
if (this.api) {
this.api.reInit()
return
}
this.api = new PeerTubeEmbedApi(this)
this.api.initialize()
}
}
// ---------------------------------------------------------------------------
async playNextPlaylistVideo () {
const next = this.playlistTracker.getNextPlaylistElement()
if (!next) {
logger.info('Next element not found in playlist.')
return
}
this.playlistTracker.setCurrentElement(next)
return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false })
}
async playPreviousPlaylistVideo () {
const previous = this.playlistTracker.getPreviousPlaylistElement()
if (!previous) {
logger.info('Previous element not found in playlist.')
return
}
this.playlistTracker.setCurrentElement(previous)
await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false })
}
getCurrentPlaylistPosition () {
return this.playlistTracker.getCurrentPosition()
}
// ---------------------------------------------------------------------------
private async loadVideoAndBuildPlayer (options: {
uuid: string
forceAutoplay: boolean
}) {
const { uuid, forceAutoplay } = options
try {
const {
videoResponse,
captionsPromise,
storyboardsPromise
} = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
return this.buildVideoPlayer({ videoResponse, captionsPromise, 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>
forceAutoplay: boolean
}) {
const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options
const videoInfoPromise = videoResponse.json()
.then(async (videoInfo: VideoDetails) => {
this.playerOptionsBuilder.loadParams(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,
storyboardsResponse
] = await Promise.all([
videoInfoPromise,
this.translationsPromise,
captionsPromise,
storyboardsPromise,
this.buildPlayerIfNeeded()
])
// If already played, we are in a playlist so we don't want to display the poster between videos
if (!this.alreadyPlayed) {
this.peertubePlayer.setPoster(window.location.origin + video.previewPath)
}
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,
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()
this.initializeApi()
}
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 })
}
})
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.correctlyHandleLiveEnding(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 urlParts = window.location.pathname.split('/')
return urlParts[urlParts.length - 1]
}
private isPlaylistEmbed () {
return window.location.pathname.split('/')[1] === 'video-playlists'
}
// ---------------------------------------------------------------------------
private correctlyHandleLiveEnding (translations: Translations) {
this.player.one('ended', () => {
// Display the live ended information
this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
this.peertubePlayer.disable()
})
}
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
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.setPlayerElement(playerElement)
this.playerHTML.addPlayerElementToDOM()
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()
}
}
PeerTubeEmbed.main()
.catch(err => {
(window as any).displayIncompatibleBrowser()
logger.error('Cannot init embed.', err)
})