aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/videos/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone/videos/shared')
-rw-r--r--client/src/standalone/videos/shared/auth-http.ts105
-rw-r--r--client/src/standalone/videos/shared/index.ts8
-rw-r--r--client/src/standalone/videos/shared/peertube-plugin.ts85
-rw-r--r--client/src/standalone/videos/shared/player-html.ts76
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts323
-rw-r--r--client/src/standalone/videos/shared/playlist-fetcher.ts72
-rw-r--r--client/src/standalone/videos/shared/playlist-tracker.ts93
-rw-r--r--client/src/standalone/videos/shared/translations.ts5
-rw-r--r--client/src/standalone/videos/shared/video-fetcher.ts63
9 files changed, 830 insertions, 0 deletions
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts
new file mode 100644
index 000000000..0356ab8a6
--- /dev/null
+++ b/client/src/standalone/videos/shared/auth-http.ts
@@ -0,0 +1,105 @@
1import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models'
2import { objectToUrlEncoded, UserTokens } from '../../../root-helpers'
3import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage'
4
5export class AuthHTTP {
6 private readonly LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
7 CLIENT_ID: 'client_id',
8 CLIENT_SECRET: 'client_secret'
9 }
10
11 private userTokens: UserTokens
12
13 private headers = new Headers()
14
15 constructor () {
16 this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage)
17
18 if (this.userTokens) this.setHeadersFromTokens()
19 }
20
21 fetch (url: string, { optionalAuth }: { optionalAuth: boolean }) {
22 const refreshFetchOptions = optionalAuth
23 ? { headers: this.headers }
24 : {}
25
26 return this.refreshFetch(url.toString(), refreshFetchOptions)
27 }
28
29 getHeaderTokenValue () {
30 return `${this.userTokens.tokenType} ${this.userTokens.accessToken}`
31 }
32
33 isLoggedIn () {
34 return !!this.userTokens
35 }
36
37 private refreshFetch (url: string, options?: RequestInit) {
38 return fetch(url, options)
39 .then((res: Response) => {
40 if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
41
42 const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
43 const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
44 const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
45
46 const headers = new Headers()
47 headers.set('Content-Type', 'application/x-www-form-urlencoded')
48
49 const data = {
50 refresh_token: this.userTokens.refreshToken,
51 client_id: clientId,
52 client_secret: clientSecret,
53 response_type: 'code',
54 grant_type: 'refresh_token'
55 }
56
57 fetch('/api/v1/users/token', {
58 headers,
59 method: 'POST',
60 body: objectToUrlEncoded(data)
61 }).then(res => {
62 if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
63
64 return res.json()
65 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
66 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
67 UserTokens.flushLocalStorage(peertubeLocalStorage)
68 this.removeTokensFromHeaders()
69
70 return resolve()
71 }
72
73 this.userTokens.accessToken = obj.access_token
74 this.userTokens.refreshToken = obj.refresh_token
75 UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens)
76
77 this.setHeadersFromTokens()
78
79 resolve()
80 }).catch((refreshTokenError: any) => {
81 reject(refreshTokenError)
82 })
83 })
84
85 return refreshingTokenPromise
86 .catch(() => {
87 UserTokens.flushLocalStorage(peertubeLocalStorage)
88
89 this.removeTokensFromHeaders()
90 }).then(() => fetch(url, {
91 ...options,
92
93 headers: this.headers
94 }))
95 })
96 }
97
98 private setHeadersFromTokens () {
99 this.headers.set('Authorization', this.getHeaderTokenValue())
100 }
101
102 private removeTokensFromHeaders () {
103 this.headers.delete('Authorization')
104 }
105}
diff --git a/client/src/standalone/videos/shared/index.ts b/client/src/standalone/videos/shared/index.ts
new file mode 100644
index 000000000..4b4e05b7c
--- /dev/null
+++ b/client/src/standalone/videos/shared/index.ts
@@ -0,0 +1,8 @@
1export * from './auth-http'
2export * from './peertube-plugin'
3export * from './player-html'
4export * from './player-manager-options'
5export * from './playlist-fetcher'
6export * from './playlist-tracker'
7export * from './translations'
8export * from './video-fetcher'
diff --git a/client/src/standalone/videos/shared/peertube-plugin.ts b/client/src/standalone/videos/shared/peertube-plugin.ts
new file mode 100644
index 000000000..968854ce8
--- /dev/null
+++ b/client/src/standalone/videos/shared/peertube-plugin.ts
@@ -0,0 +1,85 @@
1import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
2import { HTMLServerConfig, PublicServerSetting } from '../../../../../shared/models'
3import { PluginInfo, PluginsManager } from '../../../root-helpers'
4import { RegisterClientHelpers } from '../../../types'
5import { AuthHTTP } from './auth-http'
6import { Translations } from './translations'
7
8export class PeerTubePlugin {
9
10 private pluginsManager: PluginsManager
11
12 constructor (private readonly http: AuthHTTP) {
13
14 }
15
16 loadPlugins (config: HTMLServerConfig, translations?: Translations) {
17 this.pluginsManager = new PluginsManager({
18 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers({
19 pluginInfo,
20 translations
21 })
22 })
23
24 this.pluginsManager.loadPluginsList(config)
25
26 return this.pluginsManager.ensurePluginsAreLoaded('embed')
27 }
28
29 getPluginsManager () {
30 return this.pluginsManager
31 }
32
33 private buildPeerTubeHelpers (options: {
34 pluginInfo: PluginInfo
35 translations?: Translations
36 }): RegisterClientHelpers {
37 const { pluginInfo, translations } = options
38
39 const unimplemented = () => {
40 throw new Error('This helper is not implemented in embed.')
41 }
42
43 return {
44 getBaseStaticRoute: unimplemented,
45 getBaseRouterRoute: unimplemented,
46 getBasePluginClientPath: unimplemented,
47
48 getSettings: () => {
49 const url = this.getPluginUrl() + '/' + pluginInfo.plugin.npmName + '/public-settings'
50
51 return this.http.fetch(url, { optionalAuth: true })
52 .then(res => res.json())
53 .then((obj: PublicServerSetting) => obj.publicSettings)
54 },
55
56 isLoggedIn: () => this.http.isLoggedIn(),
57 getAuthHeader: () => {
58 if (!this.http.isLoggedIn()) return undefined
59
60 return { Authorization: this.http.getHeaderTokenValue() }
61 },
62
63 notifier: {
64 info: unimplemented,
65 error: unimplemented,
66 success: unimplemented
67 },
68
69 showModal: unimplemented,
70
71 getServerConfig: unimplemented,
72
73 markdownRenderer: {
74 textMarkdownToHTML: unimplemented,
75 enhancedMarkdownToHTML: unimplemented
76 },
77
78 translate: (value: string) => Promise.resolve(peertubeTranslate(value, translations))
79 }
80 }
81
82 private getPluginUrl () {
83 return window.location.origin + '/api/v1/plugins'
84 }
85}
diff --git a/client/src/standalone/videos/shared/player-html.ts b/client/src/standalone/videos/shared/player-html.ts
new file mode 100644
index 000000000..110124417
--- /dev/null
+++ b/client/src/standalone/videos/shared/player-html.ts
@@ -0,0 +1,76 @@
1import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
2import { VideoDetails } from '../../../../../shared/models'
3import { Translations } from './translations'
4
5export class PlayerHTML {
6 private readonly wrapperElement: HTMLElement
7
8 private playerElement: HTMLVideoElement
9
10 constructor (private readonly videoWrapperId: string) {
11 this.wrapperElement = document.getElementById(this.videoWrapperId)
12 }
13
14 getPlayerElement () {
15 return this.playerElement
16 }
17
18 setPlayerElement (playerElement: HTMLVideoElement) {
19 this.playerElement = playerElement
20 }
21
22 removePlayerElement () {
23 this.playerElement = null
24 }
25
26 addPlayerElementToDOM () {
27 this.wrapperElement.appendChild(this.playerElement)
28 }
29
30 displayError (text: string, translations: Translations) {
31 console.error(text)
32
33 // Remove video element
34 if (this.playerElement) {
35 this.removeElement(this.playerElement)
36 this.playerElement = undefined
37 }
38
39 const translatedText = peertubeTranslate(text, translations)
40 const translatedSorry = peertubeTranslate('Sorry', translations)
41
42 document.title = translatedSorry + ' - ' + translatedText
43
44 const errorBlock = document.getElementById('error-block')
45 errorBlock.style.display = 'flex'
46
47 const errorTitle = document.getElementById('error-title')
48 errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
49
50 const errorText = document.getElementById('error-content')
51 errorText.innerHTML = translatedText
52
53 this.wrapperElement.style.display = 'none'
54 }
55
56 buildPlaceholder (video: VideoDetails) {
57 const placeholder = this.getPlaceholderElement()
58
59 const url = window.location.origin + video.previewPath
60 placeholder.style.backgroundImage = `url("${url}")`
61 placeholder.style.display = 'block'
62 }
63
64 removePlaceholder () {
65 const placeholder = this.getPlaceholderElement()
66 placeholder.style.display = 'none'
67 }
68
69 private getPlaceholderElement () {
70 return document.getElementById('placeholder-preview')
71 }
72
73 private removeElement (element: HTMLElement) {
74 element.parentElement.removeChild(element)
75 }
76}
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
new file mode 100644
index 000000000..144d74319
--- /dev/null
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -0,0 +1,323 @@
1import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
2import {
3 HTMLServerConfig,
4 LiveVideo,
5 Video,
6 VideoCaption,
7 VideoDetails,
8 VideoPlaylistElement,
9 VideoStreamingPlaylistType
10} from '../../../../../shared/models'
11import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player'
12import {
13 getBoolOrDefault,
14 getParamString,
15 getParamToggle,
16 isP2PEnabled,
17 peertubeLocalStorage,
18 UserLocalStorageKeys
19} from '../../../root-helpers'
20import { PeerTubePlugin } from './peertube-plugin'
21import { PlayerHTML } from './player-html'
22import { PlaylistTracker } from './playlist-tracker'
23import { Translations } from './translations'
24import { VideoFetcher } from './video-fetcher'
25
26export class PlayerManagerOptions {
27 private autoplay: boolean
28
29 private controls: boolean
30 private controlBar: boolean
31
32 private muted: boolean
33 private loop: boolean
34 private subtitle: string
35 private enableApi = false
36 private startTime: number | string = 0
37 private stopTime: number | string
38
39 private title: boolean
40 private warningTitle: boolean
41 private peertubeLink: boolean
42 private p2pEnabled: boolean
43 private bigPlayBackgroundColor: string
44 private foregroundColor: string
45
46 private mode: PlayerMode
47 private scope = 'peertube'
48
49 constructor (
50 private readonly playerHTML: PlayerHTML,
51 private readonly videoFetcher: VideoFetcher,
52 private readonly peertubePlugin: PeerTubePlugin
53 ) {}
54
55 hasAPIEnabled () {
56 return this.enableApi
57 }
58
59 hasAutoplay () {
60 return this.autoplay
61 }
62
63 hasControls () {
64 return this.controls
65 }
66
67 hasTitle () {
68 return this.title
69 }
70
71 hasWarningTitle () {
72 return this.warningTitle
73 }
74
75 hasP2PEnabled () {
76 return !!this.p2pEnabled
77 }
78
79 hasBigPlayBackgroundColor () {
80 return !!this.bigPlayBackgroundColor
81 }
82
83 getBigPlayBackgroundColor () {
84 return this.bigPlayBackgroundColor
85 }
86
87 hasForegroundColor () {
88 return !!this.foregroundColor
89 }
90
91 getForegroundColor () {
92 return this.foregroundColor
93 }
94
95 getMode () {
96 return this.mode
97 }
98
99 getScope () {
100 return this.scope
101 }
102
103 // ---------------------------------------------------------------------------
104
105 loadParams (config: HTMLServerConfig, video: VideoDetails) {
106 try {
107 const params = new URL(window.location.toString()).searchParams
108
109 this.autoplay = getParamToggle(params, 'autoplay', false)
110
111 this.controls = getParamToggle(params, 'controls', true)
112 this.controlBar = getParamToggle(params, 'controlBar', true)
113
114 this.muted = getParamToggle(params, 'muted', undefined)
115 this.loop = getParamToggle(params, 'loop', false)
116 this.title = getParamToggle(params, 'title', true)
117 this.enableApi = getParamToggle(params, 'api', this.enableApi)
118 this.warningTitle = getParamToggle(params, 'warningTitle', true)
119 this.peertubeLink = getParamToggle(params, 'peertubeLink', true)
120 this.p2pEnabled = getParamToggle(params, 'p2p', this.isP2PEnabled(config, video))
121
122 this.scope = getParamString(params, 'scope', this.scope)
123 this.subtitle = getParamString(params, 'subtitle')
124 this.startTime = getParamString(params, 'start')
125 this.stopTime = getParamString(params, 'stop')
126
127 this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor')
128 this.foregroundColor = getParamString(params, 'foregroundColor')
129
130 const modeParam = getParamString(params, 'mode')
131
132 if (modeParam) {
133 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
134 else this.mode = 'webtorrent'
135 } else {
136 if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
137 else this.mode = 'webtorrent'
138 }
139 } catch (err) {
140 console.error('Cannot get params from URL.', err)
141 }
142 }
143
144 // ---------------------------------------------------------------------------
145
146 async getPlayerOptions (options: {
147 video: VideoDetails
148 captionsResponse: Response
149 live?: LiveVideo
150
151 alreadyHadPlayer: boolean
152
153 translations: Translations
154
155 playlistTracker?: PlaylistTracker
156 playNextPlaylistVideo?: () => any
157 playPreviousPlaylistVideo?: () => any
158 onVideoUpdate?: (uuid: string) => any
159 }) {
160 const {
161 video,
162 captionsResponse,
163 alreadyHadPlayer,
164 translations,
165 playlistTracker,
166 live
167 } = options
168
169 const videoCaptions = await this.buildCaptions(captionsResponse, translations)
170
171 const playerOptions: PeertubePlayerManagerOptions = {
172 common: {
173 // Autoplay in playlist mode
174 autoplay: alreadyHadPlayer ? true : this.autoplay,
175
176 controls: this.controls,
177 controlBar: this.controlBar,
178
179 muted: this.muted,
180 loop: this.loop,
181
182 p2pEnabled: this.p2pEnabled,
183
184 captions: videoCaptions.length !== 0,
185 subtitle: this.subtitle,
186
187 startTime: playlistTracker
188 ? playlistTracker.getCurrentElement().startTimestamp
189 : this.startTime,
190 stopTime: playlistTracker
191 ? playlistTracker.getCurrentElement().stopTimestamp
192 : this.stopTime,
193
194 videoCaptions,
195 inactivityTimeout: 2500,
196 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
197
198 videoShortUUID: video.shortUUID,
199 videoUUID: video.uuid,
200
201 playerElement: this.playerHTML.getPlayerElement(),
202 onPlayerElementChange: (element: HTMLVideoElement) => {
203 this.playerHTML.setPlayerElement(element)
204 },
205
206 videoDuration: video.duration,
207 enableHotkeys: true,
208 peertubeLink: this.peertubeLink,
209 poster: window.location.origin + video.previewPath,
210 theaterButton: false,
211
212 serverUrl: window.location.origin,
213 language: navigator.language,
214 embedUrl: window.location.origin + video.embedPath,
215 embedTitle: video.name,
216
217 errorNotifier: () => {
218 // Empty, we don't have a notifier in the embed
219 },
220
221 ...this.buildLiveOptions(video, live),
222
223 ...this.buildPlaylistOptions(options)
224 },
225
226 webtorrent: {
227 videoFiles: video.files
228 },
229
230 ...this.buildP2PMediaLoaderOptions(video),
231
232 pluginsManager: this.peertubePlugin.getPluginsManager()
233 }
234
235 return playerOptions
236 }
237
238 private buildLiveOptions (video: VideoDetails, live: LiveVideo) {
239 if (!video.isLive) return { isLive: false }
240
241 return {
242 isLive: true,
243 liveOptions: {
244 latencyMode: live.latencyMode
245 }
246 }
247 }
248
249 private buildPlaylistOptions (options: {
250 playlistTracker?: PlaylistTracker
251 playNextPlaylistVideo?: () => any
252 playPreviousPlaylistVideo?: () => any
253 onVideoUpdate?: (uuid: string) => any
254 }) {
255 const { playlistTracker, playNextPlaylistVideo, playPreviousPlaylistVideo, onVideoUpdate } = options
256
257 if (!playlistTracker) return {}
258
259 return {
260 playlist: {
261 elements: playlistTracker.getPlaylistElements(),
262 playlist: playlistTracker.getPlaylist(),
263
264 getCurrentPosition: () => playlistTracker.getCurrentPosition(),
265
266 onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
267 playlistTracker.setCurrentElement(videoPlaylistElement)
268
269 onVideoUpdate(videoPlaylistElement.video.uuid)
270 }
271 },
272
273 nextVideo: () => playNextPlaylistVideo(),
274 hasNextVideo: () => playlistTracker.hasNextPlaylistElement(),
275
276 previousVideo: () => playPreviousPlaylistVideo(),
277 hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement()
278 }
279 }
280
281 private buildP2PMediaLoaderOptions (video: VideoDetails) {
282 if (this.mode !== 'p2p-media-loader') return {}
283
284 const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
285
286 return {
287 p2pMediaLoader: {
288 playlistUrl: hlsPlaylist.playlistUrl,
289 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
290 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
291 trackerAnnounce: video.trackerUrls,
292 videoFiles: hlsPlaylist.files
293 } as P2PMediaLoaderOptions
294 }
295 }
296
297 // ---------------------------------------------------------------------------
298
299 private async buildCaptions (captionsResponse: Response, translations: Translations): Promise<VideoJSCaption[]> {
300 if (captionsResponse.ok) {
301 const { data } = await captionsResponse.json()
302
303 return data.map((c: VideoCaption) => ({
304 label: peertubeTranslate(c.language.label, translations),
305 language: c.language.id,
306 src: window.location.origin + c.captionPath
307 }))
308 }
309
310 return []
311 }
312
313 // ---------------------------------------------------------------------------
314
315 private isP2PEnabled (config: HTMLServerConfig, video: Video) {
316 const userP2PEnabled = getBoolOrDefault(
317 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
318 config.defaults.p2p.embed.enabled
319 )
320
321 return isP2PEnabled(video, config, userP2PEnabled)
322 }
323}
diff --git a/client/src/standalone/videos/shared/playlist-fetcher.ts b/client/src/standalone/videos/shared/playlist-fetcher.ts
new file mode 100644
index 000000000..a7e72c177
--- /dev/null
+++ b/client/src/standalone/videos/shared/playlist-fetcher.ts
@@ -0,0 +1,72 @@
1import { HttpStatusCode, ResultList, VideoPlaylistElement } from '../../../../../shared/models'
2import { AuthHTTP } from './auth-http'
3
4export class PlaylistFetcher {
5
6 constructor (private readonly http: AuthHTTP) {
7
8 }
9
10 async loadPlaylist (playlistId: string) {
11 const playlistPromise = this.loadPlaylistInfo(playlistId)
12 const playlistElementsPromise = this.loadPlaylistElements(playlistId)
13
14 let playlistResponse: Response
15 let isResponseOk: boolean
16
17 try {
18 playlistResponse = await playlistPromise
19 isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
20 } catch (err) {
21 console.error(err)
22 isResponseOk = false
23 }
24
25 if (!isResponseOk) {
26 if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
27 throw new Error('This playlist does not exist.')
28 }
29
30 throw new Error('We cannot fetch the playlist. Please try again later.')
31 }
32
33 return { playlistResponse, videosResponse: await playlistElementsPromise }
34 }
35
36 async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList<VideoPlaylistElement>) {
37 let elements = baseResult.data
38 let total = baseResult.total
39 let i = 0
40
41 while (total > elements.length && i < 10) {
42 const result = await this.loadPlaylistElements(playlistId, elements.length)
43
44 const json = await result.json()
45 total = json.total
46
47 elements = elements.concat(json.data)
48 i++
49 }
50
51 if (i === 10) {
52 console.error('Cannot fetch all playlists elements, there are too many!')
53 }
54
55 return elements
56 }
57
58 private loadPlaylistInfo (playlistId: string): Promise<Response> {
59 return this.http.fetch(this.getPlaylistUrl(playlistId), { optionalAuth: true })
60 }
61
62 private loadPlaylistElements (playlistId: string, start = 0): Promise<Response> {
63 const url = new URL(this.getPlaylistUrl(playlistId) + '/videos')
64 url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString()
65
66 return this.http.fetch(url.toString(), { optionalAuth: true })
67 }
68
69 private getPlaylistUrl (id: string) {
70 return window.location.origin + '/api/v1/video-playlists/' + id
71 }
72}
diff --git a/client/src/standalone/videos/shared/playlist-tracker.ts b/client/src/standalone/videos/shared/playlist-tracker.ts
new file mode 100644
index 000000000..75d10b4e2
--- /dev/null
+++ b/client/src/standalone/videos/shared/playlist-tracker.ts
@@ -0,0 +1,93 @@
1import { VideoPlaylist, VideoPlaylistElement } from '../../../../../shared/models'
2
3export class PlaylistTracker {
4 private currentPlaylistElement: VideoPlaylistElement
5
6 constructor (
7 private readonly playlist: VideoPlaylist,
8 private readonly playlistElements: VideoPlaylistElement[]
9 ) {
10
11 }
12
13 getPlaylist () {
14 return this.playlist
15 }
16
17 getPlaylistElements () {
18 return this.playlistElements
19 }
20
21 hasNextPlaylistElement (position?: number) {
22 return !!this.getNextPlaylistElement(position)
23 }
24
25 getNextPlaylistElement (position?: number): VideoPlaylistElement {
26 if (!position) position = this.currentPlaylistElement.position + 1
27
28 if (position > this.playlist.videosLength) {
29 return undefined
30 }
31
32 const next = this.playlistElements.find(e => e.position === position)
33
34 if (!next || !next.video) {
35 return this.getNextPlaylistElement(position + 1)
36 }
37
38 return next
39 }
40
41 hasPreviousPlaylistElement (position?: number) {
42 return !!this.getPreviousPlaylistElement(position)
43 }
44
45 getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
46 if (!position) position = this.currentPlaylistElement.position - 1
47
48 if (position < 1) {
49 return undefined
50 }
51
52 const prev = this.playlistElements.find(e => e.position === position)
53
54 if (!prev || !prev.video) {
55 return this.getNextPlaylistElement(position - 1)
56 }
57
58 return prev
59 }
60
61 nextVideoTitle () {
62 const next = this.getNextPlaylistElement()
63 if (!next) return ''
64
65 return next.video.name
66 }
67
68 setPosition (position: number) {
69 this.currentPlaylistElement = this.playlistElements.find(e => e.position === position)
70 if (!this.currentPlaylistElement || !this.currentPlaylistElement.video) {
71 console.error('Current playlist element is not valid.', this.currentPlaylistElement)
72 this.currentPlaylistElement = this.getNextPlaylistElement()
73 }
74
75 if (!this.currentPlaylistElement) {
76 throw new Error('This playlist does not have any valid element')
77 }
78 }
79
80 setCurrentElement (playlistElement: VideoPlaylistElement) {
81 this.currentPlaylistElement = playlistElement
82 }
83
84 getCurrentElement () {
85 return this.currentPlaylistElement
86 }
87
88 getCurrentPosition () {
89 if (!this.currentPlaylistElement) return -1
90
91 return this.currentPlaylistElement.position
92 }
93}
diff --git a/client/src/standalone/videos/shared/translations.ts b/client/src/standalone/videos/shared/translations.ts
new file mode 100644
index 000000000..146732495
--- /dev/null
+++ b/client/src/standalone/videos/shared/translations.ts
@@ -0,0 +1,5 @@
1type Translations = { [ id: string ]: string }
2
3export {
4 Translations
5}
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts
new file mode 100644
index 000000000..e78d38536
--- /dev/null
+++ b/client/src/standalone/videos/shared/video-fetcher.ts
@@ -0,0 +1,63 @@
1import { HttpStatusCode, LiveVideo, VideoDetails } from '../../../../../shared/models'
2import { AuthHTTP } from './auth-http'
3
4export class VideoFetcher {
5
6 constructor (private readonly http: AuthHTTP) {
7
8 }
9
10 async loadVideo (videoId: string) {
11 const videoPromise = this.loadVideoInfo(videoId)
12
13 let videoResponse: Response
14 let isResponseOk: boolean
15
16 try {
17 videoResponse = await videoPromise
18 isResponseOk = videoResponse.status === HttpStatusCode.OK_200
19 } catch (err) {
20 console.error(err)
21
22 isResponseOk = false
23 }
24
25 if (!isResponseOk) {
26 if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
27 throw new Error('This video does not exist.')
28 }
29
30 throw new Error('We cannot fetch the video. Please try again later.')
31 }
32
33 const captionsPromise = this.loadVideoCaptions(videoId)
34
35 return { captionsPromise, videoResponse }
36 }
37
38 loadVideoWithLive (video: VideoDetails) {
39 return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true })
40 .then(res => res.json())
41 .then((live: LiveVideo) => ({ video, live }))
42 }
43
44 getVideoViewsUrl (videoUUID: string) {
45 return this.getVideoUrl(videoUUID) + '/views'
46 }
47
48 private loadVideoInfo (videoId: string): Promise<Response> {
49 return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true })
50 }
51
52 private loadVideoCaptions (videoId: string): Promise<Response> {
53 return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true })
54 }
55
56 private getVideoUrl (id: string) {
57 return window.location.origin + '/api/v1/videos/' + id
58 }
59
60 private getLiveUrl (videoId: string) {
61 return window.location.origin + '/api/v1/videos/live/' + videoId
62 }
63}