Refactor embed
This commit is contained in:
parent
e5a781ec25
commit
f1a0f3b701
|
@ -5,5 +5,6 @@ export * from './local-storage-utils'
|
||||||
export * from './peertube-web-storage'
|
export * from './peertube-web-storage'
|
||||||
export * from './plugins-manager'
|
export * from './plugins-manager'
|
||||||
export * from './string'
|
export * from './string'
|
||||||
|
export * from './url'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './video'
|
export * from './video'
|
||||||
|
|
26
client/src/root-helpers/url.ts
Normal file
26
client/src/root-helpers/url.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
function getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
|
||||||
|
return params.has(name)
|
||||||
|
? (params.get(name) === '1' || params.get(name) === 'true')
|
||||||
|
: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParamString (params: URLSearchParams, name: string, defaultValue?: string) {
|
||||||
|
return params.has(name)
|
||||||
|
? params.get(name)
|
||||||
|
: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectToUrlEncoded (obj: any) {
|
||||||
|
const str: string[] = []
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
str.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getParamToggle,
|
||||||
|
getParamString,
|
||||||
|
objectToUrlEncoded
|
||||||
|
}
|
|
@ -1,12 +1,3 @@
|
||||||
function objectToUrlEncoded (obj: any) {
|
|
||||||
const str: string[] = []
|
|
||||||
for (const key of Object.keys(obj)) {
|
|
||||||
str.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.join('&')
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard (text: string) {
|
function copyToClipboard (text: string) {
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
el.value = text
|
el.value = text
|
||||||
|
@ -27,6 +18,5 @@ function wait (ms: number) {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
objectToUrlEncoded,
|
|
||||||
wait
|
wait
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,11 @@ export class PeerTubeEmbedApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get element () {
|
private get element () {
|
||||||
return this.embed.playerElement
|
return this.embed.getPlayerElement()
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructChannel () {
|
private constructChannel () {
|
||||||
const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope })
|
const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.getScope() })
|
||||||
|
|
||||||
channel.bind('play', (txn, params) => this.embed.player.play())
|
channel.bind('play', (txn, params) => this.embed.player.play())
|
||||||
channel.bind('pause', (txn, params) => this.embed.player.pause())
|
channel.bind('pause', (txn, params) => this.embed.player.pause())
|
||||||
|
@ -52,9 +52,9 @@ export class PeerTubeEmbedApi {
|
||||||
channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate())
|
channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate())
|
||||||
channel.bind('getPlaybackRates', (txn, params) => this.embed.player.options_.playbackRates)
|
channel.bind('getPlaybackRates', (txn, params) => this.embed.player.options_.playbackRates)
|
||||||
|
|
||||||
channel.bind('playNextVideo', (txn, params) => this.embed.playNextVideo())
|
channel.bind('playNextVideo', (txn, params) => this.embed.playNextPlaylistVideo())
|
||||||
channel.bind('playPreviousVideo', (txn, params) => this.embed.playPreviousVideo())
|
channel.bind('playPreviousVideo', (txn, params) => this.embed.playPreviousPlaylistVideo())
|
||||||
channel.bind('getCurrentPosition', (txn, params) => this.embed.getCurrentPosition())
|
channel.bind('getCurrentPosition', (txn, params) => this.embed.getCurrentPlaylistPosition())
|
||||||
this.channel = channel
|
this.channel = channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
105
client/src/standalone/videos/shared/auth-http.ts
Normal file
105
client/src/standalone/videos/shared/auth-http.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models'
|
||||||
|
import { objectToUrlEncoded, UserTokens } from '../../../root-helpers'
|
||||||
|
import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage'
|
||||||
|
|
||||||
|
export class AuthHTTP {
|
||||||
|
private readonly LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
|
||||||
|
CLIENT_ID: 'client_id',
|
||||||
|
CLIENT_SECRET: 'client_secret'
|
||||||
|
}
|
||||||
|
|
||||||
|
private userTokens: UserTokens
|
||||||
|
|
||||||
|
private headers = new Headers()
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage)
|
||||||
|
|
||||||
|
if (this.userTokens) this.setHeadersFromTokens()
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch (url: string, { optionalAuth }: { optionalAuth: boolean }) {
|
||||||
|
const refreshFetchOptions = optionalAuth
|
||||||
|
? { headers: this.headers }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
return this.refreshFetch(url.toString(), refreshFetchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaderTokenValue () {
|
||||||
|
return `${this.userTokens.tokenType} ${this.userTokens.accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.userTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshFetch (url: string, options?: RequestInit) {
|
||||||
|
return fetch(url, options)
|
||||||
|
.then((res: Response) => {
|
||||||
|
if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
|
||||||
|
|
||||||
|
const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
|
||||||
|
const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
refresh_token: this.userTokens.refreshToken,
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
response_type: 'code',
|
||||||
|
grant_type: 'refresh_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/v1/users/token', {
|
||||||
|
headers,
|
||||||
|
method: 'POST',
|
||||||
|
body: objectToUrlEncoded(data)
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
}).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
|
||||||
|
if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
|
||||||
|
UserTokens.flushLocalStorage(peertubeLocalStorage)
|
||||||
|
this.removeTokensFromHeaders()
|
||||||
|
|
||||||
|
return resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userTokens.accessToken = obj.access_token
|
||||||
|
this.userTokens.refreshToken = obj.refresh_token
|
||||||
|
UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens)
|
||||||
|
|
||||||
|
this.setHeadersFromTokens()
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
}).catch((refreshTokenError: any) => {
|
||||||
|
reject(refreshTokenError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return refreshingTokenPromise
|
||||||
|
.catch(() => {
|
||||||
|
UserTokens.flushLocalStorage(peertubeLocalStorage)
|
||||||
|
|
||||||
|
this.removeTokensFromHeaders()
|
||||||
|
}).then(() => fetch(url, {
|
||||||
|
...options,
|
||||||
|
|
||||||
|
headers: this.headers
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setHeadersFromTokens () {
|
||||||
|
this.headers.set('Authorization', this.getHeaderTokenValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeTokensFromHeaders () {
|
||||||
|
this.headers.delete('Authorization')
|
||||||
|
}
|
||||||
|
}
|
8
client/src/standalone/videos/shared/index.ts
Normal file
8
client/src/standalone/videos/shared/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export * from './auth-http'
|
||||||
|
export * from './peertube-plugin'
|
||||||
|
export * from './player-html'
|
||||||
|
export * from './player-manager-options'
|
||||||
|
export * from './playlist-fetcher'
|
||||||
|
export * from './playlist-tracker'
|
||||||
|
export * from './translations'
|
||||||
|
export * from './video-fetcher'
|
85
client/src/standalone/videos/shared/peertube-plugin.ts
Normal file
85
client/src/standalone/videos/shared/peertube-plugin.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
|
||||||
|
import { HTMLServerConfig, PublicServerSetting } from '../../../../../shared/models'
|
||||||
|
import { PluginInfo, PluginsManager } from '../../../root-helpers'
|
||||||
|
import { RegisterClientHelpers } from '../../../types'
|
||||||
|
import { AuthHTTP } from './auth-http'
|
||||||
|
import { Translations } from './translations'
|
||||||
|
|
||||||
|
export class PeerTubePlugin {
|
||||||
|
|
||||||
|
private pluginsManager: PluginsManager
|
||||||
|
|
||||||
|
constructor (private readonly http: AuthHTTP) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlugins (config: HTMLServerConfig, translations?: Translations) {
|
||||||
|
this.pluginsManager = new PluginsManager({
|
||||||
|
peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers({
|
||||||
|
pluginInfo,
|
||||||
|
translations
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pluginsManager.loadPluginsList(config)
|
||||||
|
|
||||||
|
return this.pluginsManager.ensurePluginsAreLoaded('embed')
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginsManager () {
|
||||||
|
return this.pluginsManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPeerTubeHelpers (options: {
|
||||||
|
pluginInfo: PluginInfo
|
||||||
|
translations?: Translations
|
||||||
|
}): RegisterClientHelpers {
|
||||||
|
const { pluginInfo, translations } = options
|
||||||
|
|
||||||
|
const unimplemented = () => {
|
||||||
|
throw new Error('This helper is not implemented in embed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getBaseStaticRoute: unimplemented,
|
||||||
|
getBaseRouterRoute: unimplemented,
|
||||||
|
getBasePluginClientPath: unimplemented,
|
||||||
|
|
||||||
|
getSettings: () => {
|
||||||
|
const url = this.getPluginUrl() + '/' + pluginInfo.plugin.npmName + '/public-settings'
|
||||||
|
|
||||||
|
return this.http.fetch(url, { optionalAuth: true })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((obj: PublicServerSetting) => obj.publicSettings)
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoggedIn: () => this.http.isLoggedIn(),
|
||||||
|
getAuthHeader: () => {
|
||||||
|
if (!this.http.isLoggedIn()) return undefined
|
||||||
|
|
||||||
|
return { Authorization: this.http.getHeaderTokenValue() }
|
||||||
|
},
|
||||||
|
|
||||||
|
notifier: {
|
||||||
|
info: unimplemented,
|
||||||
|
error: unimplemented,
|
||||||
|
success: unimplemented
|
||||||
|
},
|
||||||
|
|
||||||
|
showModal: unimplemented,
|
||||||
|
|
||||||
|
getServerConfig: unimplemented,
|
||||||
|
|
||||||
|
markdownRenderer: {
|
||||||
|
textMarkdownToHTML: unimplemented,
|
||||||
|
enhancedMarkdownToHTML: unimplemented
|
||||||
|
},
|
||||||
|
|
||||||
|
translate: (value: string) => Promise.resolve(peertubeTranslate(value, translations))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPluginUrl () {
|
||||||
|
return window.location.origin + '/api/v1/plugins'
|
||||||
|
}
|
||||||
|
}
|
76
client/src/standalone/videos/shared/player-html.ts
Normal file
76
client/src/standalone/videos/shared/player-html.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
|
||||||
|
import { VideoDetails } from '../../../../../shared/models'
|
||||||
|
import { Translations } from './translations'
|
||||||
|
|
||||||
|
export class PlayerHTML {
|
||||||
|
private readonly wrapperElement: HTMLElement
|
||||||
|
|
||||||
|
private playerElement: HTMLVideoElement
|
||||||
|
|
||||||
|
constructor (private readonly videoWrapperId: string) {
|
||||||
|
this.wrapperElement = document.getElementById(this.videoWrapperId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerElement () {
|
||||||
|
return this.playerElement
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlayerElement (playerElement: HTMLVideoElement) {
|
||||||
|
this.playerElement = playerElement
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlayerElement () {
|
||||||
|
this.playerElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlayerElementToDOM () {
|
||||||
|
this.wrapperElement.appendChild(this.playerElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
displayError (text: string, translations: Translations) {
|
||||||
|
console.error(text)
|
||||||
|
|
||||||
|
// Remove video element
|
||||||
|
if (this.playerElement) {
|
||||||
|
this.removeElement(this.playerElement)
|
||||||
|
this.playerElement = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatedText = peertubeTranslate(text, translations)
|
||||||
|
const translatedSorry = peertubeTranslate('Sorry', translations)
|
||||||
|
|
||||||
|
document.title = translatedSorry + ' - ' + translatedText
|
||||||
|
|
||||||
|
const errorBlock = document.getElementById('error-block')
|
||||||
|
errorBlock.style.display = 'flex'
|
||||||
|
|
||||||
|
const errorTitle = document.getElementById('error-title')
|
||||||
|
errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
|
||||||
|
|
||||||
|
const errorText = document.getElementById('error-content')
|
||||||
|
errorText.innerHTML = translatedText
|
||||||
|
|
||||||
|
this.wrapperElement.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPlaceholder (video: VideoDetails) {
|
||||||
|
const placeholder = this.getPlaceholderElement()
|
||||||
|
|
||||||
|
const url = window.location.origin + video.previewPath
|
||||||
|
placeholder.style.backgroundImage = `url("${url}")`
|
||||||
|
placeholder.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlaceholder () {
|
||||||
|
const placeholder = this.getPlaceholderElement()
|
||||||
|
placeholder.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlaceholderElement () {
|
||||||
|
return document.getElementById('placeholder-preview')
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeElement (element: HTMLElement) {
|
||||||
|
element.parentElement.removeChild(element)
|
||||||
|
}
|
||||||
|
}
|
323
client/src/standalone/videos/shared/player-manager-options.ts
Normal file
323
client/src/standalone/videos/shared/player-manager-options.ts
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
|
||||||
|
import {
|
||||||
|
HTMLServerConfig,
|
||||||
|
LiveVideo,
|
||||||
|
Video,
|
||||||
|
VideoCaption,
|
||||||
|
VideoDetails,
|
||||||
|
VideoPlaylistElement,
|
||||||
|
VideoStreamingPlaylistType
|
||||||
|
} from '../../../../../shared/models'
|
||||||
|
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player'
|
||||||
|
import {
|
||||||
|
getBoolOrDefault,
|
||||||
|
getParamString,
|
||||||
|
getParamToggle,
|
||||||
|
isP2PEnabled,
|
||||||
|
peertubeLocalStorage,
|
||||||
|
UserLocalStorageKeys
|
||||||
|
} 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 PlayerManagerOptions {
|
||||||
|
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 title: boolean
|
||||||
|
private warningTitle: boolean
|
||||||
|
private peertubeLink: boolean
|
||||||
|
private p2pEnabled: boolean
|
||||||
|
private bigPlayBackgroundColor: string
|
||||||
|
private foregroundColor: string
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
loadParams (config: HTMLServerConfig, video: VideoDetails) {
|
||||||
|
try {
|
||||||
|
const params = new URL(window.location.toString()).searchParams
|
||||||
|
|
||||||
|
this.autoplay = getParamToggle(params, 'autoplay', false)
|
||||||
|
|
||||||
|
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.warningTitle = getParamToggle(params, 'warningTitle', true)
|
||||||
|
this.peertubeLink = getParamToggle(params, 'peertubeLink', true)
|
||||||
|
this.p2pEnabled = getParamToggle(params, 'p2p', this.isP2PEnabled(config, video))
|
||||||
|
|
||||||
|
this.scope = getParamString(params, 'scope', this.scope)
|
||||||
|
this.subtitle = getParamString(params, 'subtitle')
|
||||||
|
this.startTime = getParamString(params, 'start')
|
||||||
|
this.stopTime = getParamString(params, 'stop')
|
||||||
|
|
||||||
|
this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor')
|
||||||
|
this.foregroundColor = getParamString(params, 'foregroundColor')
|
||||||
|
|
||||||
|
const modeParam = getParamString(params, 'mode')
|
||||||
|
|
||||||
|
if (modeParam) {
|
||||||
|
if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
|
||||||
|
else this.mode = 'webtorrent'
|
||||||
|
} else {
|
||||||
|
if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
|
||||||
|
else this.mode = 'webtorrent'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Cannot get params from URL.', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async getPlayerOptions (options: {
|
||||||
|
video: VideoDetails
|
||||||
|
captionsResponse: Response
|
||||||
|
live?: LiveVideo
|
||||||
|
|
||||||
|
alreadyHadPlayer: boolean
|
||||||
|
|
||||||
|
translations: Translations
|
||||||
|
|
||||||
|
playlistTracker?: PlaylistTracker
|
||||||
|
playNextPlaylistVideo?: () => any
|
||||||
|
playPreviousPlaylistVideo?: () => any
|
||||||
|
onVideoUpdate?: (uuid: string) => any
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
video,
|
||||||
|
captionsResponse,
|
||||||
|
alreadyHadPlayer,
|
||||||
|
translations,
|
||||||
|
playlistTracker,
|
||||||
|
live
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const videoCaptions = await this.buildCaptions(captionsResponse, translations)
|
||||||
|
|
||||||
|
const playerOptions: PeertubePlayerManagerOptions = {
|
||||||
|
common: {
|
||||||
|
// Autoplay in playlist mode
|
||||||
|
autoplay: alreadyHadPlayer ? true : this.autoplay,
|
||||||
|
|
||||||
|
controls: this.controls,
|
||||||
|
controlBar: this.controlBar,
|
||||||
|
|
||||||
|
muted: this.muted,
|
||||||
|
loop: this.loop,
|
||||||
|
|
||||||
|
p2pEnabled: this.p2pEnabled,
|
||||||
|
|
||||||
|
captions: videoCaptions.length !== 0,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
|
||||||
|
startTime: playlistTracker
|
||||||
|
? playlistTracker.getCurrentElement().startTimestamp
|
||||||
|
: this.startTime,
|
||||||
|
stopTime: playlistTracker
|
||||||
|
? playlistTracker.getCurrentElement().stopTimestamp
|
||||||
|
: this.stopTime,
|
||||||
|
|
||||||
|
videoCaptions,
|
||||||
|
inactivityTimeout: 2500,
|
||||||
|
videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
|
||||||
|
|
||||||
|
videoShortUUID: video.shortUUID,
|
||||||
|
videoUUID: video.uuid,
|
||||||
|
|
||||||
|
playerElement: this.playerHTML.getPlayerElement(),
|
||||||
|
onPlayerElementChange: (element: HTMLVideoElement) => {
|
||||||
|
this.playerHTML.setPlayerElement(element)
|
||||||
|
},
|
||||||
|
|
||||||
|
videoDuration: video.duration,
|
||||||
|
enableHotkeys: true,
|
||||||
|
peertubeLink: this.peertubeLink,
|
||||||
|
poster: window.location.origin + video.previewPath,
|
||||||
|
theaterButton: false,
|
||||||
|
|
||||||
|
serverUrl: window.location.origin,
|
||||||
|
language: navigator.language,
|
||||||
|
embedUrl: window.location.origin + video.embedPath,
|
||||||
|
embedTitle: video.name,
|
||||||
|
|
||||||
|
errorNotifier: () => {
|
||||||
|
// Empty, we don't have a notifier in the embed
|
||||||
|
},
|
||||||
|
|
||||||
|
...this.buildLiveOptions(video, live),
|
||||||
|
|
||||||
|
...this.buildPlaylistOptions(options)
|
||||||
|
},
|
||||||
|
|
||||||
|
webtorrent: {
|
||||||
|
videoFiles: video.files
|
||||||
|
},
|
||||||
|
|
||||||
|
...this.buildP2PMediaLoaderOptions(video),
|
||||||
|
|
||||||
|
pluginsManager: this.peertubePlugin.getPluginsManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildLiveOptions (video: VideoDetails, live: LiveVideo) {
|
||||||
|
if (!video.isLive) return { isLive: false }
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLive: true,
|
||||||
|
liveOptions: {
|
||||||
|
latencyMode: live.latencyMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPlaylistOptions (options: {
|
||||||
|
playlistTracker?: PlaylistTracker
|
||||||
|
playNextPlaylistVideo?: () => any
|
||||||
|
playPreviousPlaylistVideo?: () => any
|
||||||
|
onVideoUpdate?: (uuid: string) => any
|
||||||
|
}) {
|
||||||
|
const { playlistTracker, playNextPlaylistVideo, playPreviousPlaylistVideo, onVideoUpdate } = options
|
||||||
|
|
||||||
|
if (!playlistTracker) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
playlist: {
|
||||||
|
elements: playlistTracker.getPlaylistElements(),
|
||||||
|
playlist: playlistTracker.getPlaylist(),
|
||||||
|
|
||||||
|
getCurrentPosition: () => playlistTracker.getCurrentPosition(),
|
||||||
|
|
||||||
|
onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
|
||||||
|
playlistTracker.setCurrentElement(videoPlaylistElement)
|
||||||
|
|
||||||
|
onVideoUpdate(videoPlaylistElement.video.uuid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nextVideo: () => playNextPlaylistVideo(),
|
||||||
|
hasNextVideo: () => playlistTracker.hasNextPlaylistElement(),
|
||||||
|
|
||||||
|
previousVideo: () => playPreviousPlaylistVideo(),
|
||||||
|
hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildP2PMediaLoaderOptions (video: VideoDetails) {
|
||||||
|
if (this.mode !== 'p2p-media-loader') return {}
|
||||||
|
|
||||||
|
const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||||
|
|
||||||
|
return {
|
||||||
|
p2pMediaLoader: {
|
||||||
|
playlistUrl: hlsPlaylist.playlistUrl,
|
||||||
|
segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
|
||||||
|
redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
|
||||||
|
trackerAnnounce: video.trackerUrls,
|
||||||
|
videoFiles: hlsPlaylist.files
|
||||||
|
} as P2PMediaLoaderOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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 isP2PEnabled (config: HTMLServerConfig, video: Video) {
|
||||||
|
const userP2PEnabled = getBoolOrDefault(
|
||||||
|
peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
|
||||||
|
config.defaults.p2p.embed.enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
return isP2PEnabled(video, config, userP2PEnabled)
|
||||||
|
}
|
||||||
|
}
|
72
client/src/standalone/videos/shared/playlist-fetcher.ts
Normal file
72
client/src/standalone/videos/shared/playlist-fetcher.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { HttpStatusCode, ResultList, VideoPlaylistElement } from '../../../../../shared/models'
|
||||||
|
import { AuthHTTP } from './auth-http'
|
||||||
|
|
||||||
|
export class PlaylistFetcher {
|
||||||
|
|
||||||
|
constructor (private readonly http: AuthHTTP) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPlaylist (playlistId: string) {
|
||||||
|
const playlistPromise = this.loadPlaylistInfo(playlistId)
|
||||||
|
const playlistElementsPromise = this.loadPlaylistElements(playlistId)
|
||||||
|
|
||||||
|
let playlistResponse: Response
|
||||||
|
let isResponseOk: boolean
|
||||||
|
|
||||||
|
try {
|
||||||
|
playlistResponse = await playlistPromise
|
||||||
|
isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
isResponseOk = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isResponseOk) {
|
||||||
|
if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
|
||||||
|
throw new Error('This playlist does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('We cannot fetch the playlist. Please try again later.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { playlistResponse, videosResponse: await playlistElementsPromise }
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList<VideoPlaylistElement>) {
|
||||||
|
let elements = baseResult.data
|
||||||
|
let total = baseResult.total
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (total > elements.length && i < 10) {
|
||||||
|
const result = await this.loadPlaylistElements(playlistId, elements.length)
|
||||||
|
|
||||||
|
const json = await result.json()
|
||||||
|
total = json.total
|
||||||
|
|
||||||
|
elements = elements.concat(json.data)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === 10) {
|
||||||
|
console.error('Cannot fetch all playlists elements, there are too many!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPlaylistInfo (playlistId: string): Promise<Response> {
|
||||||
|
return this.http.fetch(this.getPlaylistUrl(playlistId), { optionalAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPlaylistElements (playlistId: string, start = 0): Promise<Response> {
|
||||||
|
const url = new URL(this.getPlaylistUrl(playlistId) + '/videos')
|
||||||
|
url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString()
|
||||||
|
|
||||||
|
return this.http.fetch(url.toString(), { optionalAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlaylistUrl (id: string) {
|
||||||
|
return window.location.origin + '/api/v1/video-playlists/' + id
|
||||||
|
}
|
||||||
|
}
|
93
client/src/standalone/videos/shared/playlist-tracker.ts
Normal file
93
client/src/standalone/videos/shared/playlist-tracker.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { VideoPlaylist, VideoPlaylistElement } from '../../../../../shared/models'
|
||||||
|
|
||||||
|
export class PlaylistTracker {
|
||||||
|
private currentPlaylistElement: VideoPlaylistElement
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly playlist: VideoPlaylist,
|
||||||
|
private readonly playlistElements: VideoPlaylistElement[]
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaylist () {
|
||||||
|
return this.playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaylistElements () {
|
||||||
|
return this.playlistElements
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPlaylistElement (position?: number) {
|
||||||
|
return !!this.getNextPlaylistElement(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPlaylistElement (position?: number): VideoPlaylistElement {
|
||||||
|
if (!position) position = this.currentPlaylistElement.position + 1
|
||||||
|
|
||||||
|
if (position > this.playlist.videosLength) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = this.playlistElements.find(e => e.position === position)
|
||||||
|
|
||||||
|
if (!next || !next.video) {
|
||||||
|
return this.getNextPlaylistElement(position + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPreviousPlaylistElement (position?: number) {
|
||||||
|
return !!this.getPreviousPlaylistElement(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
|
||||||
|
if (!position) position = this.currentPlaylistElement.position - 1
|
||||||
|
|
||||||
|
if (position < 1) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = this.playlistElements.find(e => e.position === position)
|
||||||
|
|
||||||
|
if (!prev || !prev.video) {
|
||||||
|
return this.getNextPlaylistElement(position - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVideoTitle () {
|
||||||
|
const next = this.getNextPlaylistElement()
|
||||||
|
if (!next) return ''
|
||||||
|
|
||||||
|
return next.video.name
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition (position: number) {
|
||||||
|
this.currentPlaylistElement = this.playlistElements.find(e => e.position === position)
|
||||||
|
if (!this.currentPlaylistElement || !this.currentPlaylistElement.video) {
|
||||||
|
console.error('Current playlist element is not valid.', this.currentPlaylistElement)
|
||||||
|
this.currentPlaylistElement = this.getNextPlaylistElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentPlaylistElement) {
|
||||||
|
throw new Error('This playlist does not have any valid element')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentElement (playlistElement: VideoPlaylistElement) {
|
||||||
|
this.currentPlaylistElement = playlistElement
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentElement () {
|
||||||
|
return this.currentPlaylistElement
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPosition () {
|
||||||
|
if (!this.currentPlaylistElement) return -1
|
||||||
|
|
||||||
|
return this.currentPlaylistElement.position
|
||||||
|
}
|
||||||
|
}
|
5
client/src/standalone/videos/shared/translations.ts
Normal file
5
client/src/standalone/videos/shared/translations.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
type Translations = { [ id: string ]: string }
|
||||||
|
|
||||||
|
export {
|
||||||
|
Translations
|
||||||
|
}
|
63
client/src/standalone/videos/shared/video-fetcher.ts
Normal file
63
client/src/standalone/videos/shared/video-fetcher.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { HttpStatusCode, LiveVideo, VideoDetails } from '../../../../../shared/models'
|
||||||
|
import { AuthHTTP } from './auth-http'
|
||||||
|
|
||||||
|
export class VideoFetcher {
|
||||||
|
|
||||||
|
constructor (private readonly http: AuthHTTP) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadVideo (videoId: string) {
|
||||||
|
const videoPromise = this.loadVideoInfo(videoId)
|
||||||
|
|
||||||
|
let videoResponse: Response
|
||||||
|
let isResponseOk: boolean
|
||||||
|
|
||||||
|
try {
|
||||||
|
videoResponse = await videoPromise
|
||||||
|
isResponseOk = videoResponse.status === HttpStatusCode.OK_200
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
isResponseOk = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isResponseOk) {
|
||||||
|
if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
|
||||||
|
throw new Error('This video does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('We cannot fetch the video. Please try again later.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const captionsPromise = this.loadVideoCaptions(videoId)
|
||||||
|
|
||||||
|
return { captionsPromise, videoResponse }
|
||||||
|
}
|
||||||
|
|
||||||
|
loadVideoWithLive (video: VideoDetails) {
|
||||||
|
return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((live: LiveVideo) => ({ video, live }))
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoViewsUrl (videoUUID: string) {
|
||||||
|
return this.getVideoUrl(videoUUID) + '/views'
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadVideoInfo (videoId: string): Promise<Response> {
|
||||||
|
return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadVideoCaptions (videoId: string): Promise<Response> {
|
||||||
|
return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVideoUrl (id: string) {
|
||||||
|
return window.location.origin + '/api/v1/videos/' + id
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLiveUrl (videoId: string) {
|
||||||
|
return window.location.origin + '/api/v1/videos/live/' + videoId
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user