Refactor embed

pull/5020/head
Chocobozzz 2022-05-31 08:59:30 +02:00
parent e5a781ec25
commit f1a0f3b701
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
14 changed files with 1065 additions and 753 deletions

View File

@ -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'

View 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
}

View File

@ -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
}

View File

@ -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

View 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')
}
}

View 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'

View 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'
}
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View 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
}
}

View File

@ -0,0 +1,5 @@
type Translations = { [ id: string ]: string }
export {
Translations
}

View 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
}
}