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 './plugins-manager'
|
||||
export * from './string'
|
||||
export * from './url'
|
||||
export * from './utils'
|
||||
export * from './video'
|
||||
|
|
|
@ -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) {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
|
@ -27,6 +18,5 @@ function wait (ms: number) {
|
|||
|
||||
export {
|
||||
copyToClipboard,
|
||||
objectToUrlEncoded,
|
||||
wait
|
||||
}
|
||||
|
|
|
@ -27,11 +27,11 @@ export class PeerTubeEmbedApi {
|
|||
}
|
||||
|
||||
private get element () {
|
||||
return this.embed.playerElement
|
||||
return this.embed.getPlayerElement()
|
||||
}
|
||||
|
||||
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('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('getPlaybackRates', (txn, params) => this.embed.player.options_.playbackRates)
|
||||
|
||||
channel.bind('playNextVideo', (txn, params) => this.embed.playNextVideo())
|
||||
channel.bind('playPreviousVideo', (txn, params) => this.embed.playPreviousVideo())
|
||||
channel.bind('getCurrentPosition', (txn, params) => this.embed.getCurrentPosition())
|
||||
channel.bind('playNextVideo', (txn, params) => this.embed.playNextPlaylistVideo())
|
||||
channel.bind('playPreviousVideo', (txn, params) => this.embed.playPreviousPlaylistVideo())
|
||||
channel.bind('getCurrentPosition', (txn, params) => this.embed.getCurrentPlaylistPosition())
|
||||
this.channel = channel
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -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'
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
type Translations = { [ id: string ]: string }
|
||||
|
||||
export {
|
||||
Translations
|
||||
}
|
|
@ -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