]>
Commit | Line | Data |
---|---|---|
202e7223 | 1 | import './embed.scss' |
57d65032 C |
2 | import '../../assets/player/shared/dock/peertube-dock-component' |
3 | import '../../assets/player/shared/dock/peertube-dock-plugin' | |
583eb04b | 4 | import videojs from 'video.js' |
bd45d503 | 5 | import { peertubeTranslate } from '../../../../shared/core-utils/i18n' |
c2419476 | 6 | import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' |
d3f4689b | 7 | import { PeertubePlayerManager } from '../../assets/player' |
583eb04b | 8 | import { TranslationsManager } from '../../assets/player/translations-manager' |
3545e72c | 9 | import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' |
aea0b0e7 | 10 | import { PeerTubeEmbedApi } from './embed-api' |
59a643aa C |
11 | import { |
12 | AuthHTTP, | |
13 | LiveManager, | |
14 | PeerTubePlugin, | |
15 | PlayerManagerOptions, | |
16 | PlaylistFetcher, | |
17 | PlaylistTracker, | |
18 | Translations, | |
19 | VideoFetcher | |
20 | } from './shared' | |
f1a0f3b7 | 21 | import { PlayerHTML } from './shared/player-html' |
202e7223 | 22 | |
5efab546 | 23 | export 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 { | |
54909304 | 55 | this.config = JSON.parse((window as any)['PeerTubeServerConfig']) |
aea0b0e7 | 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) | |
54909304 | 257 | }); |
99941732 | 258 | |
54909304 | 259 | (window as any)['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 | ||
406 | PeerTubeEmbed.main() | |
c21a0aa8 C |
407 | .catch(err => { |
408 | (window as any).displayIncompatibleBrowser() | |
409 | ||
42b40636 | 410 | logger.error('Cannot init embed.', err) |
c21a0aa8 | 411 | }) |