]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/standalone/videos/embed.ts
Translated using Weblate (Toki Pona (tok))
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
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 })