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