client/src/standalone/videos/shared/player-options-builder.ts
import { peertubeTranslate } from '@peertube/peertube-core-utils'
import {
HTMLServerConfig,
LiveVideo,
Storyboard,
Video,
VideoCaption,
VideoChapter,
VideoDetails,
VideoPlaylistElement,
VideoState,
VideoStreamingPlaylistType
} from '@peertube/peertube-models'
import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player'
import {
getBoolOrDefault,
getParamString,
getParamToggle,
isP2PEnabled,
logger,
peertubeLocalStorage,
UserLocalStorageKeys,
videoRequiresUserAuth
} from '../../../root-helpers'
import { PeerTubePlugin } from './peertube-plugin'
import { PlayerHTML } from './player-html'
import { PlaylistTracker } from './playlist-tracker'
import { Translations } from './translations'
import { VideoFetcher } from './video-fetcher'
import { getBackendUrl } from './url'
export class PlayerOptionsBuilder {
private autoplay: boolean
private controls: boolean
private controlBar: boolean
private muted: boolean
private loop: boolean
private subtitle: string
private enableApi = false
private startTime: number | string = 0
private stopTime: number | string
private playbackRate: number | string
private title: boolean
private warningTitle: boolean
private peertubeLink: boolean
private p2pEnabled: boolean
private bigPlayBackgroundColor: string
private foregroundColor: string
private waitPasswordFromEmbedAPI = false
private mode: PlayerMode
private scope = 'peertube'
constructor (
private readonly playerHTML: PlayerHTML,
private readonly videoFetcher: VideoFetcher,
private readonly peertubePlugin: PeerTubePlugin
) {}
hasAPIEnabled () {
return this.enableApi
}
hasAutoplay () {
return this.autoplay
}
hasControls () {
return this.controls
}
hasTitle () {
return this.title
}
hasWarningTitle () {
return this.warningTitle
}
hasP2PEnabled () {
return !!this.p2pEnabled
}
hasBigPlayBackgroundColor () {
return !!this.bigPlayBackgroundColor
}
getBigPlayBackgroundColor () {
return this.bigPlayBackgroundColor
}
hasForegroundColor () {
return !!this.foregroundColor
}
getForegroundColor () {
return this.foregroundColor
}
getMode () {
return this.mode
}
getScope () {
return this.scope
}
mustWaitPasswordFromEmbedAPI () {
return this.waitPasswordFromEmbedAPI
}
// ---------------------------------------------------------------------------
loadCommonParams () {
try {
const params = new URL(window.location.toString()).searchParams
this.controls = getParamToggle(params, 'controls', true)
this.controlBar = getParamToggle(params, 'controlBar', true)
this.muted = getParamToggle(params, 'muted', undefined)
this.loop = getParamToggle(params, 'loop', false)
this.title = getParamToggle(params, 'title', true)
this.enableApi = getParamToggle(params, 'api', this.enableApi)
this.waitPasswordFromEmbedAPI = getParamToggle(params, 'waitPasswordFromEmbedAPI', this.waitPasswordFromEmbedAPI)
this.warningTitle = getParamToggle(params, 'warningTitle', true)
this.peertubeLink = getParamToggle(params, 'peertubeLink', true)
this.scope = getParamString(params, 'scope', this.scope)
this.subtitle = getParamString(params, 'subtitle')
this.startTime = getParamString(params, 'start')
this.stopTime = getParamString(params, 'stop')
this.playbackRate = getParamString(params, 'playbackRate')
this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor')
this.foregroundColor = getParamString(params, 'foregroundColor')
} catch (err) {
logger.error('Cannot get params from URL.', err)
}
}
loadVideoParams (config: HTMLServerConfig, video: VideoDetails) {
try {
const params = new URL(window.location.toString()).searchParams
this.autoplay = getParamToggle(params, 'autoplay', false)
// Disable auto play on live videos that are not streamed
if (video.state.id === VideoState.LIVE_ENDED || video.state.id === VideoState.WAITING_FOR_LIVE) {
this.autoplay = false
}
this.p2pEnabled = getParamToggle(params, 'p2p', this.isP2PEnabled(config, video))
const modeParam = getParamString(params, 'mode')
if (modeParam) {
if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
else this.mode = 'web-video'
} else {
if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
else this.mode = 'web-video'
}
} catch (err) {
logger.error('Cannot get params from URL.', err)
}
}
// ---------------------------------------------------------------------------
getPlayerConstructorOptions (options: {
serverConfig: HTMLServerConfig
authorizationHeader: () => string
}): PeerTubePlayerContructorOptions {
const { serverConfig, authorizationHeader } = options
return {
controls: this.controls,
controlBar: this.controlBar,
muted: this.muted,
loop: this.loop,
playbackRate: this.playbackRate,
inactivityTimeout: 2500,
videoViewIntervalMs: serverConfig.views.videos.watchingInterval.anonymous,
metricsUrl: serverConfig.openTelemetry.metrics.enabled
? getBackendUrl() + '/api/v1/metrics/playback'
: null,
metricsInterval: serverConfig.openTelemetry.metrics.playbackStatsInterval,
authorizationHeader,
playerElement: () => this.playerHTML.getInitVideoEl(),
enableHotkeys: true,
peertubeLink: () => this.peertubeLink,
instanceName: serverConfig.instance.name,
theaterButton: false,
serverUrl: getBackendUrl(),
stunServers: serverConfig.webrtc.stunServers,
language: navigator.language,
pluginsManager: this.peertubePlugin.getPluginsManager(),
errorNotifier: () => {
// Empty, we don't have a notifier in the embed
}
}
}
async getPlayerLoadOptions (options: {
video: VideoDetails
captionsResponse: Response
storyboardsResponse: Response
chaptersResponse: Response
live?: LiveVideo
alreadyPlayed: boolean
forceAutoplay: boolean
videoFileToken: () => string
videoPassword: () => string
requiresPassword: boolean
translations: Translations
playlist?: {
playlistTracker: PlaylistTracker
playNext: () => any
playPrevious: () => any
onVideoUpdate: (uuid: string) => any
}
}): Promise<PeerTubePlayerLoadOptions> {
const {
video,
captionsResponse,
videoFileToken,
videoPassword,
requiresPassword,
translations,
alreadyPlayed,
forceAutoplay,
playlist,
live,
storyboardsResponse,
chaptersResponse
} = options
const [ videoCaptions, storyboard, chapters ] = await Promise.all([
this.buildCaptions(captionsResponse, translations),
this.buildStoryboard(storyboardsResponse),
this.buildChapters(chaptersResponse)
])
return {
mode: this.mode,
autoplay: forceAutoplay || alreadyPlayed || this.autoplay,
forceAutoplay,
p2pEnabled: this.p2pEnabled,
subtitle: this.subtitle,
storyboard,
videoChapters: chapters,
startTime: playlist
? playlist.playlistTracker.getCurrentElement().startTimestamp
: this.startTime,
stopTime: playlist
? playlist.playlistTracker.getCurrentElement().stopTimestamp
: this.stopTime,
videoCaptions,
videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
videoShortUUID: video.shortUUID,
videoUUID: video.uuid,
duration: video.duration,
videoRatio: video.aspectRatio,
poster: getBackendUrl() + video.previewPath,
embedUrl: getBackendUrl() + video.embedPath,
embedTitle: video.name,
requiresUserAuth: videoRequiresUserAuth(video),
videoFileToken,
requiresPassword,
videoPassword,
...this.buildLiveOptions(video, live),
...this.buildPlaylistOptions(playlist),
dock: this.buildDockOptions(video),
webVideo: {
videoFiles: video.files
},
hls: this.buildHLSOptions(video)
}
}
private buildLiveOptions (video: VideoDetails, live: LiveVideo) {
if (!video.isLive) return { isLive: false }
return {
isLive: true,
liveOptions: {
latencyMode: live.latencyMode
}
}
}
private async buildStoryboard (storyboardsResponse: Response) {
const { storyboards } = await storyboardsResponse.json() as { storyboards: Storyboard[] }
if (!storyboards || storyboards.length === 0) return undefined
return {
url: getBackendUrl() + storyboards[0].storyboardPath,
height: storyboards[0].spriteHeight,
width: storyboards[0].spriteWidth,
interval: storyboards[0].spriteDuration
}
}
private async buildChapters (chaptersResponse: Response) {
const { chapters } = await chaptersResponse.json() as { chapters: VideoChapter[] }
return chapters
}
private buildPlaylistOptions (options?: {
playlistTracker: PlaylistTracker
playNext: () => any
playPrevious: () => any
onVideoUpdate: (uuid: string) => any
}) {
if (!options) {
return {
nextVideo: {
enabled: false,
displayControlBarButton: false,
getVideoTitle: () => ''
},
previousVideo: {
enabled: false,
displayControlBarButton: false
}
}
}
const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options
return {
playlist: {
elements: playlistTracker.getPlaylistElements(),
playlist: playlistTracker.getPlaylist(),
getCurrentPosition: () => playlistTracker.getCurrentPosition(),
onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
playlistTracker.setCurrentElement(videoPlaylistElement)
onVideoUpdate(videoPlaylistElement.video.uuid)
}
},
previousVideo: {
enabled: playlistTracker.hasPreviousPlaylistElement(),
handler: () => playPrevious(),
displayControlBarButton: true
},
nextVideo: {
enabled: playlistTracker.hasNextPlaylistElement(),
handler: () => playNext(),
getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name,
displayControlBarButton: true
},
upnext: {
isEnabled: () => true,
isSuspended: () => false,
timeout: 0
}
}
}
private buildHLSOptions (video: VideoDetails): HLSOptions {
const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
if (!hlsPlaylist) return undefined
return {
playlistUrl: hlsPlaylist.playlistUrl,
segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
trackerAnnounce: video.trackerUrls,
videoFiles: hlsPlaylist.files
}
}
// ---------------------------------------------------------------------------
private async buildCaptions (captionsResponse: Response, translations: Translations): Promise<VideoJSCaption[]> {
if (captionsResponse.ok) {
const { data } = await captionsResponse.json()
return data.map((c: VideoCaption) => ({
label: peertubeTranslate(c.language.label, translations),
language: c.language.id,
automaticallyGenerated: c.automaticallyGenerated,
src: getBackendUrl() + c.captionPath
}))
}
return []
}
// ---------------------------------------------------------------------------
private buildDockOptions (videoInfo: VideoDetails) {
if (!this.hasControls()) return undefined
const title = this.hasTitle()
? videoInfo.name
: undefined
const description = this.hasWarningTitle() && this.hasP2PEnabled()
? peertubeTranslate('Watching this video may reveal your IP address to others.')
: undefined
if (!title && !description) return
const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
const avatar = availableAvatars.length !== 0
? availableAvatars[0]
: undefined
return {
title,
description,
avatarUrl: title && avatar
? avatar.path
: undefined
}
}
// ---------------------------------------------------------------------------
private isP2PEnabled (config: HTMLServerConfig, video: Video) {
const userP2PEnabled = getBoolOrDefault(
peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
config.defaults.p2p.embed.enabled
)
return isP2PEnabled(video, config, userP2PEnabled)
}
}