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