]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/standalone/videos/embed.ts
Translated using Weblate (Czech)
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
1 import './embed.scss'
2 import '../../assets/player/shared/dock/peertube-dock-component'
3 import '../../assets/player/shared/dock/peertube-dock-plugin'
4 import videojs from 'video.js'
5 import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6 import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models'
7 import { PeertubePlayerManager } from '../../assets/player'
8 import { TranslationsManager } from '../../assets/player/translations-manager'
9 import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
10 import { PeerTubeEmbedApi } from './embed-api'
11 import {
12 AuthHTTP,
13 LiveManager,
14 PeerTubePlugin,
15 PlayerManagerOptions,
16 PlaylistFetcher,
17 PlaylistTracker,
18 Translations,
19 VideoFetcher
20 } from './shared'
21 import { PlayerHTML } from './shared/player-html'
22
23 export class PeerTubeEmbed {
24 player: videojs.Player
25 api: PeerTubeEmbedApi = null
26
27 config: HTMLServerConfig
28
29 private translationsPromise: Promise<{ [id: string]: string }>
30 private PeertubePlayerManagerModulePromise: Promise<any>
31
32 private readonly http: AuthHTTP
33 private readonly videoFetcher: VideoFetcher
34 private readonly playlistFetcher: PlaylistFetcher
35 private readonly peertubePlugin: PeerTubePlugin
36 private readonly playerHTML: PlayerHTML
37 private readonly playerManagerOptions: PlayerManagerOptions
38 private readonly liveManager: LiveManager
39
40 private playlistTracker: PlaylistTracker
41
42 constructor (videoWrapperId: string) {
43 logger.registerServerSending(window.location.origin)
44
45 this.http = new AuthHTTP()
46
47 this.videoFetcher = new VideoFetcher(this.http)
48 this.playlistFetcher = new PlaylistFetcher(this.http)
49 this.peertubePlugin = new PeerTubePlugin(this.http)
50 this.playerHTML = new PlayerHTML(videoWrapperId)
51 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
52 this.liveManager = new LiveManager(this.playerHTML)
53
54 try {
55 this.config = JSON.parse(window['PeerTubeServerConfig'])
56 } catch (err) {
57 logger.error('Cannot parse HTML config.', err)
58 }
59 }
60
61 static async main () {
62 const videoContainerId = 'video-wrapper'
63 const embed = new PeerTubeEmbed(videoContainerId)
64 await embed.init()
65 }
66
67 getPlayerElement () {
68 return this.playerHTML.getPlayerElement()
69 }
70
71 getScope () {
72 return this.playerManagerOptions.getScope()
73 }
74
75 // ---------------------------------------------------------------------------
76
77 async init () {
78 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
79 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
80
81 // Issue when we parsed config from HTML, fallback to API
82 if (!this.config) {
83 this.config = await this.http.fetch('/api/v1/config', { optionalAuth: false })
84 .then(res => res.json())
85 }
86
87 const videoId = this.isPlaylistEmbed()
88 ? await this.initPlaylist()
89 : this.getResourceId()
90
91 if (!videoId) return
92
93 return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false })
94 }
95
96 private async initPlaylist () {
97 const playlistId = this.getResourceId()
98
99 try {
100 const res = await this.playlistFetcher.loadPlaylist(playlistId)
101
102 const [ playlist, playlistElementResult ] = await Promise.all([
103 res.playlistResponse.json() as Promise<VideoPlaylist>,
104 res.videosResponse.json() as Promise<ResultList<VideoPlaylistElement>>
105 ])
106
107 const allPlaylistElements = await this.playlistFetcher.loadAllPlaylistVideos(playlistId, playlistElementResult)
108
109 this.playlistTracker = new PlaylistTracker(playlist, allPlaylistElements)
110
111 const params = new URL(window.location.toString()).searchParams
112 const playlistPositionParam = getParamString(params, 'playlistPosition')
113
114 const position = playlistPositionParam
115 ? parseInt(playlistPositionParam + '', 10)
116 : 1
117
118 this.playlistTracker.setPosition(position)
119 } catch (err) {
120 this.playerHTML.displayError(err.message, await this.translationsPromise)
121 return undefined
122 }
123
124 return this.playlistTracker.getCurrentElement().video.uuid
125 }
126
127 private initializeApi () {
128 if (this.playerManagerOptions.hasAPIEnabled()) {
129 if (this.api) {
130 this.api.reInit()
131 return
132 }
133
134 this.api = new PeerTubeEmbedApi(this)
135 this.api.initialize()
136 }
137 }
138
139 // ---------------------------------------------------------------------------
140
141 async playNextPlaylistVideo () {
142 const next = this.playlistTracker.getNextPlaylistElement()
143 if (!next) {
144 logger.info('Next element not found in playlist.')
145 return
146 }
147
148 this.playlistTracker.setCurrentElement(next)
149
150 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false })
151 }
152
153 async playPreviousPlaylistVideo () {
154 const previous = this.playlistTracker.getPreviousPlaylistElement()
155 if (!previous) {
156 logger.info('Previous element not found in playlist.')
157 return
158 }
159
160 this.playlistTracker.setCurrentElement(previous)
161
162 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false })
163 }
164
165 getCurrentPlaylistPosition () {
166 return this.playlistTracker.getCurrentPosition()
167 }
168
169 // ---------------------------------------------------------------------------
170
171 private async loadVideoAndBuildPlayer (options: {
172 uuid: string
173 forceAutoplay: boolean
174 }) {
175 const { uuid, forceAutoplay } = options
176
177 try {
178 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid)
179
180 return this.buildVideoPlayer({ videoResponse, captionsPromise, forceAutoplay })
181 } catch (err) {
182 this.playerHTML.displayError(err.message, await this.translationsPromise)
183 }
184 }
185
186 private async buildVideoPlayer (options: {
187 videoResponse: Response
188 captionsPromise: Promise<Response>
189 forceAutoplay: boolean
190 }) {
191 const { videoResponse, captionsPromise, forceAutoplay } = options
192
193 const alreadyHadPlayer = this.resetPlayerElement()
194
195 const videoInfoPromise = videoResponse.json()
196 .then(async (videoInfo: VideoDetails) => {
197 this.playerManagerOptions.loadParams(this.config, videoInfo)
198
199 if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) {
200 this.playerHTML.buildPlaceholder(videoInfo)
201 }
202 const live = videoInfo.isLive
203 ? await this.videoFetcher.loadLive(videoInfo)
204 : undefined
205
206 const videoFileToken = videoRequiresAuth(videoInfo)
207 ? await this.videoFetcher.loadVideoToken(videoInfo)
208 : undefined
209
210 return { live, video: videoInfo, videoFileToken }
211 })
212
213 const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
214 videoInfoPromise,
215 this.translationsPromise,
216 captionsPromise,
217 this.PeertubePlayerManagerModulePromise
218 ])
219
220 await this.peertubePlugin.loadPlugins(this.config, translations)
221
222 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
223
224 const playerOptions = await this.playerManagerOptions.getPlayerOptions({
225 video,
226 captionsResponse,
227 alreadyHadPlayer,
228 translations,
229 serverConfig: this.config,
230
231 authorizationHeader: () => this.http.getHeaderTokenValue(),
232 videoFileToken: () => videoFileToken,
233
234 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
235
236 playlistTracker: this.playlistTracker,
237 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
238 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
239
240 live,
241 forceAutoplay
242 })
243
244 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => {
245 this.player = player
246 })
247
248 this.player.on('customError', (event: any, data: any) => {
249 const message = data?.err?.message || ''
250 if (!message.includes('from xs param')) return
251
252 this.player.dispose()
253 this.playerHTML.removePlayerElement()
254 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations)
255 })
256
257 window['videojsPlayer'] = this.player
258
259 this.buildCSS()
260 this.buildPlayerDock(video)
261 this.initializeApi()
262
263 this.playerHTML.removePlaceholder()
264
265 if (this.isPlaylistEmbed()) {
266 await this.buildPlayerPlaylistUpnext()
267
268 this.player.playlist().updateSelected()
269
270 this.player.on('stopped', () => {
271 this.playNextPlaylistVideo()
272 })
273 }
274
275 if (video.isLive) {
276 this.liveManager.listenForChanges({
277 video,
278 onPublishedVideo: () => {
279 this.liveManager.stopListeningForChanges(video)
280 this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true })
281 }
282 })
283
284 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
285 this.liveManager.displayInfo({ state: video.state.id, translations })
286
287 this.disablePlayer()
288 } else {
289 this.correctlyHandleLiveEnding(translations)
290 }
291 }
292
293 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
294 }
295
296 private resetPlayerElement () {
297 let alreadyHadPlayer = false
298
299 if (this.player) {
300 this.player.dispose()
301 this.player = undefined
302 alreadyHadPlayer = true
303 }
304
305 const playerElement = document.createElement('video')
306 playerElement.className = 'video-js vjs-peertube-skin'
307 playerElement.setAttribute('playsinline', 'true')
308
309 this.playerHTML.setPlayerElement(playerElement)
310 this.playerHTML.addPlayerElementToDOM()
311
312 return alreadyHadPlayer
313 }
314
315 private async buildPlayerPlaylistUpnext () {
316 const translations = await this.translationsPromise
317
318 this.player.upnext({
319 timeout: 10000, // 10s
320 headText: peertubeTranslate('Up Next', translations),
321 cancelText: peertubeTranslate('Cancel', translations),
322 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
323 getTitle: () => this.playlistTracker.nextVideoTitle(),
324 next: () => this.playNextPlaylistVideo(),
325 condition: () => !!this.playlistTracker.getNextPlaylistElement(),
326 suspended: () => false
327 })
328 }
329
330 private buildPlayerDock (videoInfo: VideoDetails) {
331 if (!this.playerManagerOptions.hasControls()) return
332
333 // On webtorrent fallback, player may have been disposed
334 if (!this.player.player_) return
335
336 const title = this.playerManagerOptions.hasTitle()
337 ? videoInfo.name
338 : undefined
339
340 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
341 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
342 : undefined
343
344 if (!title && !description) return
345
346 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
347 const avatar = availableAvatars.length !== 0
348 ? availableAvatars[0]
349 : undefined
350
351 this.player.peertubeDock({
352 title,
353 description,
354 avatarUrl: title && avatar
355 ? avatar.path
356 : undefined
357 })
358 }
359
360 private buildCSS () {
361 const body = document.getElementById('custom-css')
362
363 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) {
364 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor())
365 }
366
367 if (this.playerManagerOptions.hasForegroundColor()) {
368 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor())
369 }
370 }
371
372 // ---------------------------------------------------------------------------
373
374 private getResourceId () {
375 const urlParts = window.location.pathname.split('/')
376 return urlParts[urlParts.length - 1]
377 }
378
379 private isPlaylistEmbed () {
380 return window.location.pathname.split('/')[1] === 'video-playlists'
381 }
382
383 // ---------------------------------------------------------------------------
384
385 private correctlyHandleLiveEnding (translations: Translations) {
386 this.player.one('ended', () => {
387 // Display the live ended information
388 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
389
390 this.disablePlayer()
391 })
392 }
393
394 private disablePlayer () {
395 if (this.player.isFullscreen()) {
396 this.player.exitFullscreen()
397 }
398
399 // Disable player
400 this.player.hasStarted(false)
401 this.player.removeClass('vjs-has-autoplay')
402 this.player.bigPlayButton.hide();
403
404 (this.player.el() as HTMLElement).style.pointerEvents = 'none'
405 }
406
407 }
408
409 PeerTubeEmbed.main()
410 .catch(err => {
411 (window as any).displayIncompatibleBrowser()
412
413 logger.error('Cannot init embed.', err)
414 })