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