mirror of https://github.com/Chocobozzz/PeerTube
475 lines
13 KiB
TypeScript
475 lines
13 KiB
TypeScript
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'
|
|
|
|
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
|
|
? window.location.origin + '/api/v1/metrics/playback'
|
|
: null,
|
|
metricsInterval: serverConfig.openTelemetry.metrics.playbackStatsInterval,
|
|
|
|
authorizationHeader,
|
|
|
|
playerElement: () => this.playerHTML.getPlayerElement(),
|
|
enableHotkeys: true,
|
|
|
|
peertubeLink: () => this.peertubeLink,
|
|
instanceName: serverConfig.instance.name,
|
|
|
|
theaterButton: false,
|
|
|
|
serverUrl: window.location.origin,
|
|
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: window.location.origin + video.previewPath,
|
|
|
|
embedUrl: window.location.origin + 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: window.location.origin + 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,
|
|
src: window.location.origin + 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)
|
|
}
|
|
}
|