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