]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/standalone/videos/embed.ts
Add control bar option for peertube player
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
CommitLineData
202e7223 1import './embed.scss'
57d65032
C
2import '../../assets/player/shared/dock/peertube-dock-component'
3import '../../assets/player/shared/dock/peertube-dock-plugin'
583eb04b 4import videojs from 'video.js'
bd45d503 5import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
3f9c4955 6import {
aea0b0e7 7 HTMLServerConfig,
c0e8b12e 8 HttpStatusCode,
f443a746 9 LiveVideo,
e030bfb5 10 OAuth2ErrorCode,
5302f77d 11 PublicServerSetting,
3f9c4955 12 ResultList,
583eb04b 13 UserRefreshToken,
a9bfa85d 14 Video,
583eb04b 15 VideoCaption,
4504f09f 16 VideoDetails,
5abc96fc
C
17 VideoPlaylist,
18 VideoPlaylistElement,
aea0b0e7 19 VideoStreamingPlaylistType
583eb04b 20} from '../../../../shared/models'
57d65032 21import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../assets/player'
583eb04b 22import { TranslationsManager } from '../../assets/player/translations-manager'
a9bfa85d 23import { getBoolOrDefault } from '../../root-helpers/local-storage-utils'
aea0b0e7 24import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
5302f77d 25import { PluginInfo, PluginsManager } from '../../root-helpers/plugins-manager'
a9bfa85d 26import { UserLocalStorageKeys, UserTokens } from '../../root-helpers/users'
f9562863 27import { objectToUrlEncoded } from '../../root-helpers/utils'
57d65032 28import { isP2PEnabled } from '../../root-helpers/video'
f9562863 29import { RegisterClientHelpers } from '../../types/register-client-option.model'
aea0b0e7 30import { PeerTubeEmbedApi } from './embed-api'
abb3097e
C
31
32type Translations = { [ id: string ]: string }
202e7223 33
5efab546 34export class PeerTubeEmbed {
5abc96fc 35 playerElement: HTMLVideoElement
7e37e111 36 player: videojs.Player
902aa3a0 37 api: PeerTubeEmbedApi = null
5abc96fc 38
3b019808 39 autoplay: boolean
60f013e1 40
3b019808 41 controls: boolean
60f013e1
C
42 controlBar: boolean
43
3b019808
C
44 muted: boolean
45 loop: boolean
46 subtitle: string
902aa3a0 47 enableApi = false
1f6824c9 48 startTime: number | string = 0
f0a39880 49 stopTime: number | string
5efab546
C
50
51 title: boolean
52 warningTitle: boolean
08d9ba0f 53 peertubeLink: boolean
85302118 54 p2pEnabled: boolean
5efab546
C
55 bigPlayBackgroundColor: string
56 foregroundColor: string
57
3b6f205c 58 mode: PlayerMode
902aa3a0
C
59 scope = 'peertube'
60
a9bfa85d 61 userTokens: UserTokens
71ab65d0 62 headers = new Headers()
4504f09f
RK
63 LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
64 CLIENT_ID: 'client_id',
65 CLIENT_SECRET: 'client_secret'
66 }
71ab65d0 67
aea0b0e7
C
68 config: HTMLServerConfig
69
5abc96fc 70 private translationsPromise: Promise<{ [id: string]: string }>
5abc96fc
C
71 private PeertubePlayerManagerModulePromise: Promise<any>
72
73 private playlist: VideoPlaylist
74 private playlistElements: VideoPlaylistElement[]
75 private currentPlaylistElement: VideoPlaylistElement
76
9df52d66 77 private readonly wrapperElement: HTMLElement
5abc96fc 78
72f611ca 79 private pluginsManager: PluginsManager
f9562863 80
9df52d66 81 constructor (private readonly videoWrapperId: string) {
5abc96fc 82 this.wrapperElement = document.getElementById(this.videoWrapperId)
aea0b0e7
C
83
84 try {
85 this.config = JSON.parse(window['PeerTubeServerConfig'])
86 } catch (err) {
87 console.error('Cannot parse HTML config.', err)
88 }
902aa3a0
C
89 }
90
9df52d66
C
91 static async main () {
92 const videoContainerId = 'video-wrapper'
93 const embed = new PeerTubeEmbed(videoContainerId)
94 await embed.init()
95 }
96
99941732
WL
97 getVideoUrl (id: string) {
98 return window.location.origin + '/api/v1/videos/' + id
99 }
d4f3fea6 100
f443a746
C
101 getLiveUrl (videoId: string) {
102 return window.location.origin + '/api/v1/videos/live/' + videoId
103 }
104
5302f77d
C
105 getPluginUrl () {
106 return window.location.origin + '/api/v1/plugins'
107 }
108
207612df 109 refreshFetch (url: string, options?: RequestInit) {
4504f09f
RK
110 return fetch(url, options)
111 .then((res: Response) => {
f2eb23cd 112 if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
4504f09f 113
72aa835e 114 const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
4504f09f
RK
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)
207612df 117
4504f09f
RK
118 const headers = new Headers()
119 headers.set('Content-Type', 'application/x-www-form-urlencoded')
207612df 120
4504f09f 121 const data = {
a4ff3100 122 refresh_token: this.userTokens.refreshToken,
4504f09f
RK
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)
496d784d 133 }).then(res => {
f2eb23cd 134 if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
a4ff3100 135
496d784d 136 return res.json()
e030bfb5
C
137 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
138 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
a9bfa85d 139 UserTokens.flushLocalStorage(peertubeLocalStorage)
496d784d 140 this.removeTokensFromHeaders()
a4ff3100 141
496d784d
C
142 return resolve()
143 }
144
145 this.userTokens.accessToken = obj.access_token
146 this.userTokens.refreshToken = obj.refresh_token
a9bfa85d 147 UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens)
496d784d
C
148
149 this.setHeadersFromTokens()
150
151 resolve()
a7299d9d
C
152 }).catch((refreshTokenError: any) => {
153 reject(refreshTokenError)
496d784d 154 })
4504f09f
RK
155 })
156
157 return refreshingTokenPromise
496d784d 158 .catch(() => {
a9bfa85d 159 UserTokens.flushLocalStorage(peertubeLocalStorage)
496d784d
C
160
161 this.removeTokensFromHeaders()
162 }).then(() => fetch(url, {
4504f09f
RK
163 ...options,
164 headers: this.headers
165 }))
166 })
167 }
168
5abc96fc
C
169 getPlaylistUrl (id: string) {
170 return window.location.origin + '/api/v1/video-playlists/' + id
171 }
172
99941732 173 loadVideoInfo (videoId: string): Promise<Response> {
4504f09f 174 return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers })
99941732 175 }
d4f3fea6 176
16f7022b 177 loadVideoCaptions (videoId: string): Promise<Response> {
be59656c 178 return this.refreshFetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
16f7022b
C
179 }
180
f443a746
C
181 loadWithLive (video: VideoDetails) {
182 return this.refreshFetch(this.getLiveUrl(video.uuid), { headers: this.headers })
183 .then(res => res.json())
184 .then((live: LiveVideo) => ({ video, live }))
185 }
186
5abc96fc 187 loadPlaylistInfo (playlistId: string): Promise<Response> {
be59656c 188 return this.refreshFetch(this.getPlaylistUrl(playlistId), { headers: this.headers })
5abc96fc
C
189 }
190
fb13852d
C
191 loadPlaylistElements (playlistId: string, start = 0): Promise<Response> {
192 const url = new URL(this.getPlaylistUrl(playlistId) + '/videos')
193 url.search = new URLSearchParams({ start: '' + start, count: '100' }).toString()
194
be59656c 195 return this.refreshFetch(url.toString(), { headers: this.headers })
5abc96fc
C
196 }
197
99941732
WL
198 removeElement (element: HTMLElement) {
199 element.parentElement.removeChild(element)
200 }
d4f3fea6 201
abb3097e 202 displayError (text: string, translations?: Translations) {
99941732 203 // Remove video element
5abc96fc
C
204 if (this.playerElement) {
205 this.removeElement(this.playerElement)
206 this.playerElement = undefined
207 }
99941732 208
ad3fa0c5
C
209 const translatedText = peertubeTranslate(text, translations)
210 const translatedSorry = peertubeTranslate('Sorry', translations)
211
212 document.title = translatedSorry + ' - ' + translatedText
99941732
WL
213
214 const errorBlock = document.getElementById('error-block')
215 errorBlock.style.display = 'flex'
216
ad3fa0c5
C
217 const errorTitle = document.getElementById('error-title')
218 errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
219
99941732 220 const errorText = document.getElementById('error-content')
ad3fa0c5 221 errorText.innerHTML = translatedText
2a71d286
C
222
223 this.wrapperElement.style.display = 'none'
99941732
WL
224 }
225
abb3097e 226 videoNotFound (translations?: Translations) {
99941732 227 const text = 'This video does not exist.'
ad3fa0c5 228 this.displayError(text, translations)
99941732
WL
229 }
230
abb3097e 231 videoFetchError (translations?: Translations) {
99941732 232 const text = 'We cannot fetch the video. Please try again later.'
ad3fa0c5 233 this.displayError(text, translations)
99941732
WL
234 }
235
5abc96fc
C
236 playlistNotFound (translations?: Translations) {
237 const text = 'This playlist does not exist.'
238 this.displayError(text, translations)
239 }
240
241 playlistFetchError (translations?: Translations) {
242 const text = 'We cannot fetch the playlist. Please try again later.'
243 this.displayError(text, translations)
244 }
245
3b019808 246 getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
99941732
WL
247 return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
248 }
d4f3fea6 249
3b019808 250 getParamString (params: URLSearchParams, name: string, defaultValue?: string) {
99941732
WL
251 return params.has(name) ? params.get(name) : defaultValue
252 }
da99ccf2 253
9054a8b6
C
254 async playNextVideo () {
255 const next = this.getNextPlaylistElement()
256 if (!next) {
257 console.log('Next element not found in playlist.')
258 return
259 }
260
261 this.currentPlaylistElement = next
262
263 return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
264 }
265
266 async playPreviousVideo () {
267 const previous = this.getPreviousPlaylistElement()
268 if (!previous) {
269 console.log('Previous element not found in playlist.')
270 return
271 }
272
273 this.currentPlaylistElement = previous
274
275 await this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
276 }
277
278 getCurrentPosition () {
279 if (!this.currentPlaylistElement) return -1
280
281 return this.currentPlaylistElement.position
282 }
283
902aa3a0 284 async init () {
a9bfa85d 285 this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage)
c21a0aa8 286 await this.initCore()
99941732
WL
287 }
288
902aa3a0
C
289 private initializeApi () {
290 if (!this.enableApi) return
291
292 this.api = new PeerTubeEmbedApi(this)
293 this.api.initialize()
294 }
295
0f2f274c 296 private loadParams (video: VideoDetails) {
da99ccf2 297 try {
c4710631 298 const params = new URL(window.location.toString()).searchParams
99941732 299
31b6ddf8 300 this.autoplay = this.getParamToggle(params, 'autoplay', false)
60f013e1 301
31b6ddf8 302 this.controls = this.getParamToggle(params, 'controls', true)
60f013e1
C
303 this.controlBar = this.getParamToggle(params, 'controlBar', true)
304
64645512 305 this.muted = this.getParamToggle(params, 'muted', undefined)
31b6ddf8 306 this.loop = this.getParamToggle(params, 'loop', false)
5efab546 307 this.title = this.getParamToggle(params, 'title', true)
99941732 308 this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
5efab546 309 this.warningTitle = this.getParamToggle(params, 'warningTitle', true)
08d9ba0f 310 this.peertubeLink = this.getParamToggle(params, 'peertubeLink', true)
85302118 311 this.p2pEnabled = this.getParamToggle(params, 'p2p', this.isP2PEnabled(video))
f37bad63 312
3b019808
C
313 this.scope = this.getParamString(params, 'scope', this.scope)
314 this.subtitle = this.getParamString(params, 'subtitle')
315 this.startTime = this.getParamString(params, 'start')
f0a39880 316 this.stopTime = this.getParamString(params, 'stop')
3b6f205c 317
5efab546
C
318 this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor')
319 this.foregroundColor = this.getParamString(params, 'foregroundColor')
320
0f2f274c
C
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 }
da99ccf2
C
330 } catch (err) {
331 console.error('Cannot get params from URL.', err)
332 }
99941732
WL
333 }
334
fb13852d
C
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
9df52d66 343 const json = await result.json()
fb13852d
C
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
5abc96fc
C
357 private async loadPlaylist (playlistId: string) {
358 const playlistPromise = this.loadPlaylistInfo(playlistId)
359 const playlistElementsPromise = this.loadPlaylistElements(playlistId)
99941732 360
be59656c
C
361 let playlistResponse: Response
362 let isResponseOk: boolean
5abc96fc 363
be59656c
C
364 try {
365 playlistResponse = await playlistPromise
f2eb23cd 366 isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
be59656c
C
367 } catch (err) {
368 console.error(err)
369 isResponseOk = false
370 }
371
372 if (!isResponseOk) {
5abc96fc 373 const serverTranslations = await this.translationsPromise
71ab65d0 374
f2eb23cd 375 if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
5abc96fc
C
376 this.playlistNotFound(serverTranslations)
377 return undefined
378 }
379
380 this.playlistFetchError(serverTranslations)
381 return undefined
382 }
383
384 return { playlistResponse, videosResponse: await playlistElementsPromise }
385 }
386
387 private async loadVideo (videoId: string) {
3f9c4955 388 const videoPromise = this.loadVideoInfo(videoId)
3f9c4955 389
be59656c
C
390 let videoResponse: Response
391 let isResponseOk: boolean
392
393 try {
394 videoResponse = await videoPromise
f2eb23cd 395 isResponseOk = videoResponse.status === HttpStatusCode.OK_200
be59656c
C
396 } catch (err) {
397 console.error(err)
398
399 isResponseOk = false
400 }
99941732 401
be59656c 402 if (!isResponseOk) {
5abc96fc
C
403 const serverTranslations = await this.translationsPromise
404
f2eb23cd 405 if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
5abc96fc
C
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 }
3f9c4955 418
5abc96fc
C
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(),
a950e4c8 428 next: () => this.playNextVideo(),
5abc96fc
C
429 condition: () => !!this.getNextPlaylistElement(),
430 suspended: () => false
431 })
432 }
99941732 433
4572c3d0
C
434 private async loadVideoAndBuildPlayer (uuid: string) {
435 const res = await this.loadVideo(uuid)
5abc96fc
C
436 if (res === undefined) return
437
438 return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
439 }
3f9c4955 440
5abc96fc
C
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
a950e4c8 464 private getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
2a71d286 465 if (!position) position = this.currentPlaylistElement.position - 1
a950e4c8
C
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 }
479
5abc96fc
C
480 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
481 let alreadyHadPlayer = false
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
aea0b0e7
C
493 // Issue when we parsed config from HTML, fallback to API
494 if (!this.config) {
495 this.config = await this.refreshFetch('/api/v1/config')
9df52d66 496 .then(res => res.json())
aea0b0e7
C
497 }
498
f443a746 499 const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json()
5abc96fc 500 .then((videoInfo: VideoDetails) => {
200eaf51
C
501 this.loadParams(videoInfo)
502
f443a746 503 if (!alreadyHadPlayer && !this.autoplay) this.buildPlaceholder(videoInfo)
5abc96fc 504
f443a746
C
505 if (!videoInfo.isLive) return { video: videoInfo }
506
507 return this.loadWithLive(videoInfo)
5abc96fc
C
508 })
509
aea0b0e7 510 const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
5abc96fc
C
511 videoInfoPromise,
512 this.translationsPromise,
513 captionsPromise,
5abc96fc
C
514 this.PeertubePlayerManagerModulePromise
515 ])
3f9c4955 516
72f611ca 517 await this.loadPlugins(serverTranslations)
f9562863 518
f443a746 519 const { video: videoInfo, live } = videoInfoTmp
6fad8e51 520
3f9c4955 521 const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
5efab546 522 const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
99941732 523
f443a746
C
524 const liveOptions = videoInfo.isLive
525 ? { latencyMode: live.latencyMode }
526 : undefined
527
4572c3d0
C
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)
9df52d66 539 .catch(err => console.error(err))
3e0e8d4a 540 }
4572c3d0
C
541 }
542 : undefined
543
2adfc7ea
C
544 const options: PeertubePlayerManagerOptions = {
545 common: {
5abc96fc
C
546 // Autoplay in playlist mode
547 autoplay: alreadyHadPlayer ? true : this.autoplay,
60f013e1 548
2adfc7ea 549 controls: this.controls,
60f013e1
C
550 controlBar: this.controlBar,
551
2adfc7ea
C
552 muted: this.muted,
553 loop: this.loop,
1a8c2d74 554
85302118 555 p2pEnabled: this.p2pEnabled,
a9bfa85d 556
2adfc7ea 557 captions: videoCaptions.length !== 0,
2adfc7ea
C
558 subtitle: this.subtitle,
559
1a8c2d74
C
560 startTime: this.playlist ? this.currentPlaylistElement.startTimestamp : this.startTime,
561 stopTime: this.playlist ? this.currentPlaylistElement.stopTimestamp : this.stopTime,
562
a950e4c8
C
563 nextVideo: this.playlist ? () => this.playNextVideo() : undefined,
564 hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined,
565
566 previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined,
567 hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined,
568
4572c3d0 569 playlist: playlistPlugin,
5abc96fc 570
2adfc7ea 571 videoCaptions,
35f0a5e6 572 inactivityTimeout: 2500,
5abc96fc 573 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
29837f88 574 videoShortUUID: videoInfo.shortUUID,
58b9ce30 575 videoUUID: videoInfo.uuid,
6ec0b75b 576
25b7c847 577 isLive: videoInfo.isLive,
f443a746 578 liveOptions,
25b7c847 579
5abc96fc 580 playerElement: this.playerElement,
9df52d66
C
581 onPlayerElementChange: (element: HTMLVideoElement) => {
582 this.playerElement = element
583 },
6ec0b75b 584
2adfc7ea
C
585 videoDuration: videoInfo.duration,
586 enableHotkeys: true,
08d9ba0f 587 peertubeLink: this.peertubeLink,
2adfc7ea 588 poster: window.location.origin + videoInfo.previewPath,
3d9a63d3 589 theaterButton: false,
2adfc7ea
C
590
591 serverUrl: window.location.origin,
592 language: navigator.language,
4097c6d6 593 embedUrl: window.location.origin + videoInfo.embedPath,
c4207f97
C
594 embedTitle: videoInfo.name,
595
596 errorNotifier: () => {
597 // Empty, we don't have a notifier in the embed
598 }
6ec0b75b
C
599 },
600
601 webtorrent: {
602 videoFiles: videoInfo.files
72f611ca 603 },
604
605 pluginsManager: this.pluginsManager
3b6f205c 606 }
2adfc7ea 607
3b6f205c 608 if (this.mode === 'p2p-media-loader') {
09209296
C
609 const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
610
3b6f205c
C
611 Object.assign(options, {
612 p2pMediaLoader: {
b9da21bd 613 playlistUrl: hlsPlaylist.playlistUrl,
09209296 614 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
b9da21bd 615 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
09209296 616 trackerAnnounce: videoInfo.trackerUrls,
5a71acd2 617 videoFiles: hlsPlaylist.files
09209296 618 } as P2PMediaLoaderOptions
3b6f205c 619 })
2adfc7ea 620 }
202e7223 621
9df52d66
C
622 this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: videojs.Player) => {
623 this.player = player
624 })
625
2adfc7ea 626 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
99941732 627
9df52d66 628 window['videojsPlayer'] = this.player
902aa3a0 629
5efab546
C
630 this.buildCSS()
631
98ab5dc8 632 this.buildDock(videoInfo)
5efab546
C
633
634 this.initializeApi()
3f9c4955
C
635
636 this.removePlaceholder()
5abc96fc
C
637
638 if (this.isPlaylistEmbed()) {
639 await this.buildPlaylistManager()
1a8c2d74 640
4572c3d0 641 this.player.playlist().updateSelected()
1a8c2d74
C
642
643 this.player.on('stopped', () => {
644 this.playNextVideo()
645 })
5abc96fc 646 }
f9562863 647
72f611ca 648 this.pluginsManager.runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video: videoInfo })
5abc96fc
C
649 }
650
651 private async initCore () {
652 if (this.userTokens) this.setHeadersFromTokens()
653
5abc96fc
C
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
664 this.playlist = await res.playlistResponse.json()
665
666 const playlistElementResult = await res.videosResponse.json()
fb13852d 667 this.playlistElements = await this.loadAllPlaylistVideos(playlistId, playlistElementResult)
5abc96fc 668
2a71d286
C
669 const params = new URL(window.location.toString()).searchParams
670 const playlistPositionParam = this.getParamString(params, 'playlistPosition')
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
5abc96fc
C
691 videoId = this.currentPlaylistElement.video.uuid
692 } else {
693 videoId = this.getResourceId()
694 }
695
4572c3d0 696 return this.loadVideoAndBuildPlayer(videoId)
5efab546
C
697 }
698
699 private handleError (err: Error, translations?: { [ id: string ]: string }) {
e5a818d3 700 if (err.message.includes('from xs param')) {
5efab546 701 this.player.dispose()
5abc96fc 702 this.playerElement = null
5efab546 703 this.displayError('This video is not available because the remote instance is not responding.', translations)
5efab546
C
704 }
705 }
706
98ab5dc8 707 private buildDock (videoInfo: VideoDetails) {
abb3097e 708 if (!this.controls) return
5efab546 709
818c449b
C
710 // On webtorrent fallback, player may have been disposed
711 if (!this.player.player_) return
5efab546 712
abb3097e 713 const title = this.title ? videoInfo.name : undefined
85302118 714 const description = this.warningTitle && this.p2pEnabled
abb3097e
C
715 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
716 : undefined
717
01dd04cd
C
718 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
719 const avatar = availableAvatars.length !== 0
720 ? availableAvatars[0]
721 : undefined
722
b9da21bd 723 if (title || description) {
01dd04cd 724 this.player.peertubeDock({
b9da21bd 725 title,
01dd04cd
C
726 description,
727 avatarUrl: title && avatar
728 ? avatar.path
729 : undefined
b9da21bd
C
730 })
731 }
5efab546 732 }
16f7022b 733
5efab546
C
734 private buildCSS () {
735 const body = document.getElementById('custom-css')
736
737 if (this.bigPlayBackgroundColor) {
738 body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor)
739 }
740
741 if (this.foregroundColor) {
742 body.style.setProperty('--embedForegroundColor', this.foregroundColor)
743 }
99941732 744 }
6d88de72 745
5efab546
C
746 private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> {
747 if (captionsResponse.ok) {
9df52d66 748 const { data } = await captionsResponse.json()
5efab546 749
9df52d66 750 return data.map((c: VideoCaption) => ({
5efab546
C
751 label: peertubeTranslate(c.language.label, serverTranslations),
752 language: c.language.id,
753 src: window.location.origin + c.captionPath
754 }))
6d88de72 755 }
5efab546
C
756
757 return []
6d88de72 758 }
3f9c4955 759
f443a746 760 private buildPlaceholder (video: VideoDetails) {
3f9c4955
C
761 const placeholder = this.getPlaceholderElement()
762
763 const url = window.location.origin + video.previewPath
764 placeholder.style.backgroundImage = `url("${url}")`
5abc96fc 765 placeholder.style.display = 'block'
3f9c4955
C
766 }
767
768 private removePlaceholder () {
769 const placeholder = this.getPlaceholderElement()
5abc96fc 770 placeholder.style.display = 'none'
3f9c4955
C
771 }
772
773 private getPlaceholderElement () {
774 return document.getElementById('placeholder-preview')
775 }
a4ff3100 776
5302f77d
C
777 private getHeaderTokenValue () {
778 return `${this.userTokens.tokenType} ${this.userTokens.accessToken}`
779 }
780
a4ff3100 781 private setHeadersFromTokens () {
5302f77d 782 this.headers.set('Authorization', this.getHeaderTokenValue())
a4ff3100 783 }
5abc96fc 784
207612df
C
785 private removeTokensFromHeaders () {
786 this.headers.delete('Authorization')
787 }
788
5abc96fc
C
789 private getResourceId () {
790 const urlParts = window.location.pathname.split('/')
9df52d66 791 return urlParts[urlParts.length - 1]
5abc96fc
C
792 }
793
794 private isPlaylistEmbed () {
795 return window.location.pathname.split('/')[1] === 'video-playlists'
796 }
f9562863 797
72f611ca 798 private loadPlugins (translations?: { [ id: string ]: string }) {
799 this.pluginsManager = new PluginsManager({
5302f77d 800 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo, translations)
72f611ca 801 })
f9562863 802
72f611ca 803 this.pluginsManager.loadPluginsList(this.config)
f9562863 804
72f611ca 805 return this.pluginsManager.ensurePluginsAreLoaded('embed')
f9562863
C
806 }
807
5302f77d 808 private buildPeerTubeHelpers (pluginInfo: PluginInfo, translations?: { [ id: string ]: string }): RegisterClientHelpers {
9df52d66 809 const unimplemented = () => {
f9562863
C
810 throw new Error('This helper is not implemented in embed.')
811 }
812
813 return {
814 getBaseStaticRoute: unimplemented,
9777fe9e 815 getBaseRouterRoute: unimplemented,
d63e6d46 816 getBasePluginClientPath: unimplemented,
9777fe9e 817
5302f77d
C
818 getSettings: () => {
819 const url = this.getPluginUrl() + '/' + pluginInfo.plugin.npmName + '/public-settings'
f9562863 820
5302f77d
C
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 },
f9562863
C
832
833 notifier: {
834 info: unimplemented,
835 error: unimplemented,
836 success: unimplemented
837 },
838
839 showModal: unimplemented,
840
5aa7abf1
C
841 getServerConfig: unimplemented,
842
f9562863
C
843 markdownRenderer: {
844 textMarkdownToHTML: unimplemented,
845 enhancedMarkdownToHTML: unimplemented
846 },
847
9df52d66 848 translate: (value: string) => Promise.resolve(peertubeTranslate(value, translations))
f9562863
C
849 }
850 }
a9bfa85d
C
851
852 private isP2PEnabled (video: Video) {
853 const userP2PEnabled = getBoolOrDefault(
854 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
b65de1be 855 this.config.defaults.p2p.embed.enabled
a9bfa85d
C
856 )
857
858 return isP2PEnabled(video, this.config, userP2PEnabled)
859 }
99941732
WL
860}
861
862PeerTubeEmbed.main()
c21a0aa8
C
863 .catch(err => {
864 (window as any).displayIncompatibleBrowser()
865
866 console.error('Cannot init embed.', err)
867 })