aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/videos/embed.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone/videos/embed.ts')
-rw-r--r--client/src/standalone/videos/embed.ts859
1 files changed, 162 insertions, 697 deletions
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 1fc8e229b..c5d017d4a 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -3,83 +3,40 @@ import '../../assets/player/shared/dock/peertube-dock-component'
3import '../../assets/player/shared/dock/peertube-dock-plugin' 3import '../../assets/player/shared/dock/peertube-dock-plugin'
4import videojs from 'video.js' 4import videojs from 'video.js'
5import { peertubeTranslate } from '../../../../shared/core-utils/i18n' 5import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6import { 6import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models'
7 HTMLServerConfig,
8 HttpStatusCode,
9 LiveVideo,
10 OAuth2ErrorCode,
11 PublicServerSetting,
12 ResultList,
13 UserRefreshToken,
14 Video,
15 VideoCaption,
16 VideoDetails,
17 VideoPlaylist,
18 VideoPlaylistElement,
19 VideoStreamingPlaylistType
20} from '../../../../shared/models'
21import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../assets/player'
22import { TranslationsManager } from '../../assets/player/translations-manager' 7import { TranslationsManager } from '../../assets/player/translations-manager'
23import { getBoolOrDefault } from '../../root-helpers/local-storage-utils' 8import { getParamString } from '../../root-helpers'
24import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
25import { PluginInfo, PluginsManager } from '../../root-helpers/plugins-manager'
26import { UserLocalStorageKeys, UserTokens } from '../../root-helpers/users'
27import { objectToUrlEncoded } from '../../root-helpers/utils'
28import { isP2PEnabled } from '../../root-helpers/video'
29import { RegisterClientHelpers } from '../../types/register-client-option.model'
30import { PeerTubeEmbedApi } from './embed-api' 9import { PeerTubeEmbedApi } from './embed-api'
31 10import { AuthHTTP, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared'
32type Translations = { [ id: string ]: string } 11import { PlayerHTML } from './shared/player-html'
12import { PeertubePlayerManager } from '../../assets/player'
33 13
34export class PeerTubeEmbed { 14export class PeerTubeEmbed {
35 playerElement: HTMLVideoElement
36 player: videojs.Player 15 player: videojs.Player
37 api: PeerTubeEmbedApi = null 16 api: PeerTubeEmbedApi = null
38 17
39 autoplay: boolean
40
41 controls: boolean
42 controlBar: boolean
43
44 muted: boolean
45 loop: boolean
46 subtitle: string
47 enableApi = false
48 startTime: number | string = 0
49 stopTime: number | string
50
51 title: boolean
52 warningTitle: boolean
53 peertubeLink: boolean
54 p2pEnabled: boolean
55 bigPlayBackgroundColor: string
56 foregroundColor: string
57
58 mode: PlayerMode
59 scope = 'peertube'
60
61 userTokens: UserTokens
62 headers = new Headers()
63 LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
64 CLIENT_ID: 'client_id',
65 CLIENT_SECRET: 'client_secret'
66 }
67
68 config: HTMLServerConfig 18 config: HTMLServerConfig
69 19
70 private translationsPromise: Promise<{ [id: string]: string }> 20 private translationsPromise: Promise<{ [id: string]: string }>
71 private PeertubePlayerManagerModulePromise: Promise<any> 21 private PeertubePlayerManagerModulePromise: Promise<any>
72 22
73 private playlist: VideoPlaylist 23 private readonly http: AuthHTTP
74 private playlistElements: VideoPlaylistElement[] 24 private readonly videoFetcher: VideoFetcher
75 private currentPlaylistElement: VideoPlaylistElement 25 private readonly playlistFetcher: PlaylistFetcher
26 private readonly peertubePlugin: PeerTubePlugin
27 private readonly playerHTML: PlayerHTML
28 private readonly playerManagerOptions: PlayerManagerOptions
76 29
77 private readonly wrapperElement: HTMLElement 30 private playlistTracker: PlaylistTracker
78 31
79 private pluginsManager: PluginsManager 32 constructor (videoWrapperId: string) {
33 this.http = new AuthHTTP()
80 34
81 constructor (private readonly videoWrapperId: string) { 35 this.videoFetcher = new VideoFetcher(this.http)
82 this.wrapperElement = document.getElementById(this.videoWrapperId) 36 this.playlistFetcher = new PlaylistFetcher(this.http)
37 this.peertubePlugin = new PeerTubePlugin(this.http)
38 this.playerHTML = new PlayerHTML(videoWrapperId)
39 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
83 40
84 try { 41 try {
85 this.config = JSON.parse(window['PeerTubeServerConfig']) 42 this.config = JSON.parse(window['PeerTubeServerConfig'])
@@ -94,697 +51,268 @@ export class PeerTubeEmbed {
94 await embed.init() 51 await embed.init()
95 } 52 }
96 53
97 getVideoUrl (id: string) { 54 getPlayerElement () {
98 return window.location.origin + '/api/v1/videos/' + id 55 return this.playerHTML.getPlayerElement()
99 }
100
101 getLiveUrl (videoId: string) {
102 return window.location.origin + '/api/v1/videos/live/' + videoId
103 }
104
105 getPluginUrl () {
106 return window.location.origin + '/api/v1/plugins'
107 }
108
109 refreshFetch (url: string, options?: RequestInit) {
110 return fetch(url, options)
111 .then((res: Response) => {
112 if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
113
114 const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
115 const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
116 const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
117
118 const headers = new Headers()
119 headers.set('Content-Type', 'application/x-www-form-urlencoded')
120
121 const data = {
122 refresh_token: this.userTokens.refreshToken,
123 client_id: clientId,
124 client_secret: clientSecret,
125 response_type: 'code',
126 grant_type: 'refresh_token'
127 }
128
129 fetch('/api/v1/users/token', {
130 headers,
131 method: 'POST',
132 body: objectToUrlEncoded(data)
133 }).then(res => {
134 if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
135
136 return res.json()
137 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
138 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
139 UserTokens.flushLocalStorage(peertubeLocalStorage)
140 this.removeTokensFromHeaders()
141
142 return resolve()
143 }
144
145 this.userTokens.accessToken = obj.access_token
146 this.userTokens.refreshToken = obj.refresh_token
147 UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens)
148
149 this.setHeadersFromTokens()
150
151 resolve()
152 }).catch((refreshTokenError: any) => {
153 reject(refreshTokenError)
154 })
155 })
156
157 return refreshingTokenPromise
158 .catch(() => {
159 UserTokens.flushLocalStorage(peertubeLocalStorage)
160
161 this.removeTokensFromHeaders()
162 }).then(() => fetch(url, {
163 ...options,
164 headers: this.headers
165 }))
166 })
167 }
168
169 getPlaylistUrl (id: string) {
170 return window.location.origin + '/api/v1/video-playlists/' + id
171 } 56 }
172 57
173 loadVideoInfo (videoId: string): Promise<Response> { 58 getScope () {
174 return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers }) 59 return this.playerManagerOptions.getScope()
175 } 60 }
176 61
177 loadVideoCaptions (videoId: string): Promise<Response> { 62 // ---------------------------------------------------------------------------
178 return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
179 }
180 63
181 loadWithLive (video: VideoDetails) { 64 async init () {
182 return this.refreshFetch(this.getLiveUrl(video.uuid), { headers: this.headers }) 65 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
183 .then(res => res.json()) 66 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
184 .then((live: LiveVideo) => ({ video, live }))
185 }
186 67
187 loadPlaylistInfo (playlistId: string): Promise<Response> { 68 // Issue when we parsed config from HTML, fallback to API
188 return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers }) 69 if (!this.config) {
189 } 70 this.config = await this.http.fetch('/api/v1/config', { optionalAuth: false })
71 .then(res => res.json())
72 }
190 73
191 loadPlaylistElements (playlistId: string, start = 0): Promise<Response> { 74 const videoId = this.isPlaylistEmbed()
192 const url = new URL(this.getPlaylistUrl(playlistId) + '/videos') 75 ? await this.initPlaylist()
193 url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString() 76 : this.getResourceId()
194 77
195 return this.refreshFetch(url.toString(), { headers: this.headers }) 78 if (!videoId) return
196 }
197 79
198 removeElement (element: HTMLElement) { 80 return this.loadVideoAndBuildPlayer(videoId)
199 element.parentElement.removeChild(element)
200 } 81 }
201 82
202 displayError (text: string, translations?: Translations) { 83 private async initPlaylist () {
203 // Remove video element 84 const playlistId = this.getResourceId()
204 if (this.playerElement) {
205 this.removeElement(this.playerElement)
206 this.playerElement = undefined
207 }
208
209 const translatedText = peertubeTranslate(text, translations)
210 const translatedSorry = peertubeTranslate('Sorry', translations)
211 85
212 document.title = translatedSorry + ' - ' + translatedText 86 try {
87 const res = await this.playlistFetcher.loadPlaylist(playlistId)
213 88
214 const errorBlock = document.getElementById('error-block') 89 const [ playlist, playlistElementResult ] = await Promise.all([
215 errorBlock.style.display = 'flex' 90 res.playlistResponse.json() as Promise<VideoPlaylist>,
91 res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
92 ])
216 93
217 const errorTitle = document.getElementById('error-title') 94 const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
218 errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
219 95
220 const errorText = document.getElementById('error-content') 96 this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
221 errorText.innerHTML = translatedText
222 97
223 this.wrapperElement.style.display = 'none' 98 const params = new URL(window.location.toString()).searchParams
224 } 99 const playlistPositionParam = getParamString(params, 'playlistPosition')
225
226 videoNotFound (translations?: Translations) {
227 const text = 'This video does not exist.'
228 this.displayError(text, translations)
229 }
230 100
231 videoFetchError (translations?: Translations) { 101 const position = playlistPositionParam
232 const text = 'We cannot fetch the video. Please try again later.' 102 ? parseInt(playlistPositionParam + '', 10)
233 this.displayError(text, translations) 103 : 1
234 }
235 104
236 playlistNotFound (translations?: Translations) { 105 this.playlistTracker.setPosition(position)
237 const text = 'This playlist does not exist.' 106 } catch (err) {
238 this.displayError(text, translations) 107 this.playerHTML.displayError(err.message, await this.translationsPromise)
239 } 108 return undefined
109 }
240 110
241 playlistFetchError (translations?: Translations) { 111 return this.playlistTracker.getCurrentElement().video.uuid
242 const text = 'We cannot fetch the playlist. Please try again later.'
243 this.displayError(text, translations)
244 } 112 }
245 113
246 getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) { 114 private initializeApi () {
247 return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue 115 if (this.playerManagerOptions.hasAPIEnabled()) {
116 this.api = new PeerTubeEmbedApi(this)
117 this.api.initialize()
118 }
248 } 119 }
249 120
250 getParamString (params: URLSearchParams, name: string, defaultValue?: string) { 121 // ---------------------------------------------------------------------------
251 return params.has(name) ? params.get(name) : defaultValue
252 }
253 122
254 async playNextVideo () { 123 async playNextPlaylistVideo () {
255 const next = this.getNextPlaylistElement() 124 const next = this.playlistTracker.getNextPlaylistElement()
256 if (!next) { 125 if (!next) {
257 console.log('Next element not found in playlist.') 126 console.log('Next element not found in playlist.')
258 return 127 return
259 } 128 }
260 129
261 this.currentPlaylistElement = next 130 this.playlistTracker.setCurrentElement(next)
262 131
263 return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) 132 return this.loadVideoAndBuildPlayer(next.video.uuid)
264 } 133 }
265 134
266 async playPreviousVideo () { 135 async playPreviousPlaylistVideo () {
267 const previous = this.getPreviousPlaylistElement() 136 const previous = this.playlistTracker.getPreviousPlaylistElement()
268 if (!previous) { 137 if (!previous) {
269 console.log('Previous element not found in playlist.') 138 console.log('Previous element not found in playlist.')
270 return 139 return
271 } 140 }
272 141
273 this.currentPlaylistElement = previous 142 this.playlistTracker.setCurrentElement(previous)
274 143
275 await this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) 144 await this.loadVideoAndBuildPlayer(previous.video.uuid)
276 } 145 }
277 146
278 getCurrentPosition () { 147 getCurrentPlaylistPosition () {
279 if (!this.currentPlaylistElement) return -1 148 return this.playlistTracker.getCurrentPosition()
280
281 return this.currentPlaylistElement.position
282 }
283
284 async init () {
285 this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage)
286 await this.initCore()
287 }
288
289 private initializeApi () {
290 if (!this.enableApi) return
291
292 this.api = new PeerTubeEmbedApi(this)
293 this.api.initialize()
294 } 149 }
295 150
296 private loadParams (video: VideoDetails) { 151 // ---------------------------------------------------------------------------
297 try {
298 const params = new URL(window.location.toString()).searchParams
299
300 this.autoplay = this.getParamToggle(params, 'autoplay', false)
301
302 this.controls = this.getParamToggle(params, 'controls', true)
303 this.controlBar = this.getParamToggle(params, 'controlBar', true)
304
305 this.muted = this.getParamToggle(params, 'muted', undefined)
306 this.loop = this.getParamToggle(params, 'loop', false)
307 this.title = this.getParamToggle(params, 'title', true)
308 this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
309 this.warningTitle = this.getParamToggle(params, 'warningTitle', true)
310 this.peertubeLink = this.getParamToggle(params, 'peertubeLink', true)
311 this.p2pEnabled = this.getParamToggle(params, 'p2p', this.isP2PEnabled(video))
312
313 this.scope = this.getParamString(params, 'scope', this.scope)
314 this.subtitle = this.getParamString(params, 'subtitle')
315 this.startTime = this.getParamString(params, 'start')
316 this.stopTime = this.getParamString(params, 'stop')
317
318 this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor')
319 this.foregroundColor = this.getParamString(params, 'foregroundColor')
320
321 const modeParam = this.getParamString(params, 'mode')
322
323 if (modeParam) {
324 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
325 else this.mode = 'webtorrent'
326 } else {
327 if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
328 else this.mode = 'webtorrent'
329 }
330 } catch (err) {
331 console.error('Cannot get params from URL.', err)
332 }
333 }
334
335 private async loadAllPlaylistVideos (playlistId: string, baseResult: ResultList<VideoPlaylistElement>) {
336 let elements = baseResult.data
337 let total = baseResult.total
338 let i = 0
339
340 while (total > elements.length && i < 10) {
341 const result = await this.loadPlaylistElements(playlistId, elements.length)
342
343 const json = await result.json()
344 total = json.total
345
346 elements = elements.concat(json.data)
347 i++
348 }
349
350 if (i === 10) {
351 console.error('Cannot fetch all playlists elements, there are too many!')
352 }
353
354 return elements
355 }
356
357 private async loadPlaylist (playlistId: string) {
358 const playlistPromise = this.loadPlaylistInfo(playlistId)
359 const playlistElementsPromise = this.loadPlaylistElements(playlistId)
360
361 let playlistResponse: Response
362 let isResponseOk: boolean
363 152
153 private async loadVideoAndBuildPlayer (uuid: string) {
364 try { 154 try {
365 playlistResponse = await playlistPromise 155 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
366 isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
367 } catch (err) {
368 console.error(err)
369 isResponseOk = false
370 }
371
372 if (!isResponseOk) {
373 const serverTranslations = await this.translationsPromise
374
375 if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
376 this.playlistNotFound(serverTranslations)
377 return undefined
378 }
379
380 this.playlistFetchError(serverTranslations)
381 return undefined
382 }
383 156
384 return { playlistResponse, videosResponse: await playlistElementsPromise } 157 return this.buildVideoPlayer(videoResponse, captionsPromise)
385 }
386
387 private async loadVideo (videoId: string) {
388 const videoPromise = this.loadVideoInfo(videoId)
389
390 let videoResponse: Response
391 let isResponseOk: boolean
392
393 try {
394 videoResponse = await videoPromise
395 isResponseOk = videoResponse.status === HttpStatusCode.OK_200
396 } catch (err) { 158 } catch (err) {
397 console.error(err) 159 this.playerHTML.displayError(err.message, await this.translationsPromise)
398
399 isResponseOk = false
400 } 160 }
401
402 if (!isResponseOk) {
403 const serverTranslations = await this.translationsPromise
404
405 if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
406 this.videoNotFound(serverTranslations)
407 return undefined
408 }
409
410 this.videoFetchError(serverTranslations)
411 return undefined
412 }
413
414 const captionsPromise = this.loadVideoCaptions(videoId)
415
416 return { captionsPromise, videoResponse }
417 }
418
419 private async buildPlaylistManager () {
420 const translations = await this.translationsPromise
421
422 this.player.upnext({
423 timeout: 10000, // 10s
424 headText: peertubeTranslate('Up Next', translations),
425 cancelText: peertubeTranslate('Cancel', translations),
426 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
427 getTitle: () => this.nextVideoTitle(),
428 next: () => this.playNextVideo(),
429 condition: () => !!this.getNextPlaylistElement(),
430 suspended: () => false
431 })
432 }
433
434 private async loadVideoAndBuildPlayer (uuid: string) {
435 const res = await this.loadVideo(uuid)
436 if (res === undefined) return
437
438 return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
439 }
440
441 private nextVideoTitle () {
442 const next = this.getNextPlaylistElement()
443 if (!next) return ''
444
445 return next.video.name
446 }
447
448 private getNextPlaylistElement (position?: number): VideoPlaylistElement {
449 if (!position) position = this.currentPlaylistElement.position + 1
450
451 if (position > this.playlist.videosLength) {
452 return undefined
453 }
454
455 const next = this.playlistElements.find(e => e.position === position)
456
457 if (!next || !next.video) {
458 return this.getNextPlaylistElement(position + 1)
459 }
460
461 return next
462 }
463
464 private getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
465 if (!position) position = this.currentPlaylistElement.position - 1
466
467 if (position < 1) {
468 return undefined
469 }
470
471 const prev = this.playlistElements.find(e => e.position === position)
472
473 if (!prev || !prev.video) {
474 return this.getNextPlaylistElement(position - 1)
475 }
476
477 return prev
478 } 161 }
479 162
480 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { 163 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
481 let alreadyHadPlayer = false 164 const alreadyHadPlayer = this.resetPlayerElement()
482
483 if (this.player) {
484 this.player.dispose()
485 alreadyHadPlayer = true
486 }
487
488 this.playerElement = document.createElement('video')
489 this.playerElement.className = 'video-js vjs-peertube-skin'
490 this.playerElement.setAttribute('playsinline', 'true')
491 this.wrapperElement.appendChild(this.playerElement)
492
493 // Issue when we parsed config from HTML, fallback to API
494 if (!this.config) {
495 this.config = await this.refreshFetch('/api/v1/config')
496 .then(res => res.json())
497 }
498 165
499 const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json() 166 const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
500 .then((videoInfo: VideoDetails) => { 167 .then((videoInfo: VideoDetails) => {
501 this.loadParams(videoInfo) 168 this.playerManagerOptions.loadParams(this.config, videoInfo)
502 169
503 if (!alreadyHadPlayer && !this.autoplay) this.buildPlaceholder(videoInfo) 170 if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) {
171 this.playerHTML.buildPlaceholder(videoInfo)
172 }
504 173
505 if (!videoInfo.isLive) return { video: videoInfo } 174 if (!videoInfo.isLive) {
175 return { video: videoInfo }
176 }
506 177
507 return this.loadWithLive(videoInfo) 178 return this.videoFetcher.loadVideoWithLive(videoInfo)
508 }) 179 })
509 180
510 const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ 181 const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
511 videoInfoPromise, 182 videoInfoPromise,
512 this.translationsPromise, 183 this.translationsPromise,
513 captionsPromise, 184 captionsPromise,
514 this.PeertubePlayerManagerModulePromise 185 this.PeertubePlayerManagerModulePromise
515 ]) 186 ])
516 187
517 await this.loadPlugins(serverTranslations) 188 await this.peertubePlugin.loadPlugins(this.config, translations)
518
519 const { video: videoInfo, live } = videoInfoTmp
520
521 const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
522 const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
523
524 const liveOptions = videoInfo.isLive
525 ? { latencyMode: live.latencyMode }
526 : undefined
527
528 const playlistPlugin = this.currentPlaylistElement
529 ? {
530 elements: this.playlistElements,
531 playlist: this.playlist,
532
533 getCurrentPosition: () => this.currentPlaylistElement.position,
534
535 onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
536 this.currentPlaylistElement = videoPlaylistElement
537
538 this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
539 .catch(err => console.error(err))
540 }
541 }
542 : undefined
543
544 const options: PeertubePlayerManagerOptions = {
545 common: {
546 // Autoplay in playlist mode
547 autoplay: alreadyHadPlayer ? true : this.autoplay,
548
549 controls: this.controls,
550 controlBar: this.controlBar,
551
552 muted: this.muted,
553 loop: this.loop,
554 189
555 p2pEnabled: this.p2pEnabled, 190 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
556 191
557 captions: videoCaptions.length !== 0, 192 const options = await this.playerManagerOptions.getPlayerOptions({
558 subtitle: this.subtitle, 193 video,
194 captionsResponse,
195 alreadyHadPlayer,
196 translations,
197 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid),
559 198
560 startTime: this.playlist ? this.currentPlaylistElement.startTimestamp : this.startTime, 199 playlistTracker: this.playlistTracker,
561 stopTime: this.playlist ? this.currentPlaylistElement.stopTimestamp : this.stopTime, 200 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
201 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
562 202
563 nextVideo: this.playlist ? () => this.playNextVideo() : undefined, 203 live
564 hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined, 204 })
565
566 previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined,
567 hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined,
568
569 playlist: playlistPlugin,
570
571 videoCaptions,
572 inactivityTimeout: 2500,
573 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
574 videoShortUUID: videoInfo.shortUUID,
575 videoUUID: videoInfo.uuid,
576
577 isLive: videoInfo.isLive,
578 liveOptions,
579
580 playerElement: this.playerElement,
581 onPlayerElementChange: (element: HTMLVideoElement) => {
582 this.playerElement = element
583 },
584
585 videoDuration: videoInfo.duration,
586 enableHotkeys: true,
587 peertubeLink: this.peertubeLink,
588 poster: window.location.origin + videoInfo.previewPath,
589 theaterButton: false,
590
591 serverUrl: window.location.origin,
592 language: navigator.language,
593 embedUrl: window.location.origin + videoInfo.embedPath,
594 embedTitle: videoInfo.name,
595
596 errorNotifier: () => {
597 // Empty, we don't have a notifier in the embed
598 }
599 },
600
601 webtorrent: {
602 videoFiles: videoInfo.files
603 },
604
605 pluginsManager: this.pluginsManager
606 }
607
608 if (this.mode === 'p2p-media-loader') {
609 const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
610
611 Object.assign(options, {
612 p2pMediaLoader: {
613 playlistUrl: hlsPlaylist.playlistUrl,
614 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
615 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
616 trackerAnnounce: videoInfo.trackerUrls,
617 videoFiles: hlsPlaylist.files
618 } as P2PMediaLoaderOptions
619 })
620 }
621 205
622 this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: videojs.Player) => { 206 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), options, (player: videojs.Player) => {
623 this.player = player 207 this.player = player
624 }) 208 })
625 209
626 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) 210 this.player.on('customError', (event: any, data: any) => {
211 const message = data?.err?.message || ''
212 if (!message.includes('from xs param')) return
213
214 this.player.dispose()
215 this.playerHTML.removePlayerElement()
216 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations)
217 })
627 218
628 window['videojsPlayer'] = this.player 219 window['videojsPlayer'] = this.player
629 220
630 this.buildCSS() 221 this.buildCSS()
631 222 this.buildPlayerDock(video)
632 this.buildDock(videoInfo)
633
634 this.initializeApi() 223 this.initializeApi()
635 224
636 this.removePlaceholder() 225 this.playerHTML.removePlaceholder()
637 226
638 if (this.isPlaylistEmbed()) { 227 if (this.isPlaylistEmbed()) {
639 await this.buildPlaylistManager() 228 await this.buildPlayerPlaylistUpnext()
640 229
641 this.player.playlist().updateSelected() 230 this.player.playlist().updateSelected()
642 231
643 this.player.on('stopped', () => { 232 this.player.on('stopped', () => {
644 this.playNextVideo() 233 this.playNextPlaylistVideo()
645 }) 234 })
646 } 235 }
647 236
648 this.pluginsManager.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo }) 237 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
649 } 238 }
650 239
651 private async initCore () { 240 private resetPlayerElement () {
652 if (this.userTokens) this.setHeadersFromTokens() 241 let alreadyHadPlayer = false
653
654 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
655 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
656
657 let videoId: string
658
659 if (this.isPlaylistEmbed()) {
660 const playlistId = this.getResourceId()
661 const res = await this.loadPlaylist(playlistId)
662 if (!res) return undefined
663 242
664 this.playlist = await res.playlistResponse.json() 243 if (this.player) {
244 this.player.dispose()
245 alreadyHadPlayer = true
246 }
665 247
666 const playlistElementResult = await res.videosResponse.json() 248 const playerElement = document.createElement('video')
667 this.playlistElements = await this.loadAllPlaylistVideos(playlistId, playlistElementResult) 249 playerElement.className = 'video-js vjs-peertube-skin'
250 playerElement.setAttribute('playsinline', 'true')
668 251
669 const params = new URL(window.location.toString()).searchParams 252 this.playerHTML.setPlayerElement(playerElement)
670 const playlistPositionParam = this.getParamString(params, 'playlistPosition') 253 this.playerHTML.addPlayerElementToDOM()
671
672 let position = 1
673
674 if (playlistPositionParam) {
675 position = parseInt(playlistPositionParam + '', 10)
676 }
677
678 this.currentPlaylistElement = this.playlistElements.find(e => e.position === position)
679 if (!this.currentPlaylistElement || !this.currentPlaylistElement.video) {
680 console.error('Current playlist element is not valid.', this.currentPlaylistElement)
681 this.currentPlaylistElement = this.getNextPlaylistElement()
682 }
683
684 if (!this.currentPlaylistElement) {
685 console.error('This playlist does not have any valid element.')
686 const serverTranslations = await this.translationsPromise
687 this.playlistFetchError(serverTranslations)
688 return
689 }
690
691 videoId = this.currentPlaylistElement.video.uuid
692 } else {
693 videoId = this.getResourceId()
694 }
695 254
696 return this.loadVideoAndBuildPlayer(videoId) 255 return alreadyHadPlayer
697 } 256 }
698 257
699 private handleError (err: Error, translations?: { [ id: string ]: string }) { 258 private async buildPlayerPlaylistUpnext () {
700 if (err.message.includes('from xs param')) { 259 const translations = await this.translationsPromise
701 this.player.dispose() 260
702 this.playerElement = null 261 this.player.upnext({
703 this.displayError('This video is not available because the remote instance is not responding.', translations) 262 timeout: 10000, // 10s
704 } 263 headText: peertubeTranslate('Up Next', translations),
264 cancelText: peertubeTranslate('Cancel', translations),
265 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
266 getTitle: () => this.playlistTracker.nextVideoTitle(),
267 next: () => this.playNextPlaylistVideo(),
268 condition: () => !!this.playlistTracker.getNextPlaylistElement(),
269 suspended: () => false
270 })
705 } 271 }
706 272
707 private buildDock (videoInfo: VideoDetails) { 273 private buildPlayerDock (videoInfo: VideoDetails) {
708 if (!this.controls) return 274 if (!this.playerManagerOptions.hasControls()) return
709 275
710 // On webtorrent fallback, player may have been disposed 276 // On webtorrent fallback, player may have been disposed
711 if (!this.player.player_) return 277 if (!this.player.player_) return
712 278
713 const title = this.title ? videoInfo.name : undefined 279 const title = this.playerManagerOptions.hasTitle()
714 const description = this.warningTitle && this.p2pEnabled 280 ? videoInfo.name
281 : undefined
282
283 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
715 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' 284 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
716 : undefined 285 : undefined
717 286
287 if (!title && !description) return
288
718 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) 289 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
719 const avatar = availableAvatars.length !== 0 290 const avatar = availableAvatars.length !== 0
720 ? availableAvatars[0] 291 ? availableAvatars[0]
721 : undefined 292 : undefined
722 293
723 if (title || description) { 294 this.player.peertubeDock({
724 this.player.peertubeDock({ 295 title,
725 title, 296 description,
726 description, 297 avatarUrl: title && avatar
727 avatarUrl: title && avatar 298 ? avatar.path
728 ? avatar.path 299 : undefined
729 : undefined 300 })
730 })
731 }
732 } 301 }
733 302
734 private buildCSS () { 303 private buildCSS () {
735 const body = document.getElementById('custom-css') 304 const body = document.getElementById('custom-css')
736 305
737 if (this.bigPlayBackgroundColor) { 306 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
738 body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor) 307 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
739 } 308 }
740 309
741 if (this.foregroundColor) { 310 if (this.playerManagerOptions.hasForegroundColor()) {
742 body.style.setProperty('--embedForegroundColor', this.foregroundColor) 311 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
743 } 312 }
744 } 313 }
745 314
746 private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> { 315 // ---------------------------------------------------------------------------
747 if (captionsResponse.ok) {
748 const { data } = await captionsResponse.json()
749
750 return data.map((c: VideoCaption) => ({
751 label: peertubeTranslate(c.language.label, serverTranslations),
752 language: c.language.id,
753 src: window.location.origin + c.captionPath
754 }))
755 }
756
757 return []
758 }
759
760 private buildPlaceholder (video: VideoDetails) {
761 const placeholder = this.getPlaceholderElement()
762
763 const url = window.location.origin + video.previewPath
764 placeholder.style.backgroundImage = `url("${url}")`
765 placeholder.style.display = 'block'
766 }
767
768 private removePlaceholder () {
769 const placeholder = this.getPlaceholderElement()
770 placeholder.style.display = 'none'
771 }
772
773 private getPlaceholderElement () {
774 return document.getElementById('placeholder-preview')
775 }
776
777 private getHeaderTokenValue () {
778 return `${this.userTokens.tokenType} ${this.userTokens.accessToken}`
779 }
780
781 private setHeadersFromTokens () {
782 this.headers.set('Authorization', this.getHeaderTokenValue())
783 }
784
785 private removeTokensFromHeaders () {
786 this.headers.delete('Authorization')
787 }
788 316
789 private getResourceId () { 317 private getResourceId () {
790 const urlParts = window.location.pathname.split('/') 318 const urlParts = window.location.pathname.split('/')
@@ -794,69 +322,6 @@ export class PeerTubeEmbed {
794 private isPlaylistEmbed () { 322 private isPlaylistEmbed () {
795 return window.location.pathname.split('/')[1] === 'video-playlists' 323 return window.location.pathname.split('/')[1] === 'video-playlists'
796 } 324 }
797
798 private loadPlugins (translations?: { [ id: string ]: string }) {
799 this.pluginsManager = new PluginsManager({
800 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo, translations)
801 })
802
803 this.pluginsManager.loadPluginsList(this.config)
804
805 return this.pluginsManager.ensurePluginsAreLoaded('embed')
806 }
807
808 private buildPeerTubeHelpers (pluginInfo: PluginInfo, translations?: { [ id: string ]: string }): RegisterClientHelpers {
809 const unimplemented = () => {
810 throw new Error('This helper is not implemented in embed.')
811 }
812
813 return {
814 getBaseStaticRoute: unimplemented,
815 getBaseRouterRoute: unimplemented,
816 getBasePluginClientPath: unimplemented,
817
818 getSettings: () => {
819 const url = this.getPluginUrl() + '/' + pluginInfo.plugin.npmName + '/public-settings'
820
821 return this.refreshFetch(url, { headers: this.headers })
822 .then(res => res.json())
823 .then((obj: PublicServerSetting) => obj.publicSettings)
824 },
825
826 isLoggedIn: () => !!this.userTokens,
827 getAuthHeader: () => {
828 if (!this.userTokens) return undefined
829
830 return { Authorization: this.getHeaderTokenValue() }
831 },
832
833 notifier: {
834 info: unimplemented,
835 error: unimplemented,
836 success: unimplemented
837 },
838
839 showModal: unimplemented,
840
841 getServerConfig: unimplemented,
842
843 markdownRenderer: {
844 textMarkdownToHTML: unimplemented,
845 enhancedMarkdownToHTML: unimplemented
846 },
847
848 translate: (value: string) => Promise.resolve(peertubeTranslate(value, translations))
849 }
850 }
851
852 private isP2PEnabled (video: Video) {
853 const userP2PEnabled = getBoolOrDefault(
854 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
855 this.config.defaults.p2p.embed.enabled
856 )
857
858 return isP2PEnabled(video, this.config, userP2PEnabled)
859 }
860} 325}
861 326
862PeerTubeEmbed.main() 327PeerTubeEmbed.main()