]>
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' |
01771012 | 6 | import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } 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' |
d3f4689b | 11 | import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared' |
f1a0f3b7 | 12 | import { PlayerHTML } from './shared/player-html' |
202e7223 | 13 | |
5efab546 | 14 | export 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 | ||
356 | PeerTubeEmbed.main() | |
c21a0aa8 C |
357 | .catch(err => { |
358 | (window as any).displayIncompatibleBrowser() | |
359 | ||
42b40636 | 360 | logger.error('Cannot init embed.', err) |
c21a0aa8 | 361 | }) |