aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/videos/embed.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone/videos/embed.ts')
-rw-r--r--client/src/standalone/videos/embed.ts276
1 files changed, 132 insertions, 144 deletions
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cc4274b99..78b812ffd 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -1,18 +1,26 @@
1import './embed.scss' 1import './embed.scss'
2import '../../assets/player/shared/dock/peertube-dock-component' 2import '../../assets/player/shared/dock/peertube-dock-component'
3import '../../assets/player/shared/dock/peertube-dock-plugin' 3import '../../assets/player/shared/dock/peertube-dock-plugin'
4import { PeerTubeServerError } from 'src/types'
4import videojs from 'video.js' 5import videojs from 'video.js'
5import { peertubeTranslate } from '../../../../shared/core-utils/i18n' 6import {
6import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' 7 HTMLServerConfig,
7import { PeertubePlayerManager } from '../../assets/player' 8 ResultList,
9 ServerErrorCode,
10 VideoDetails,
11 VideoPlaylist,
12 VideoPlaylistElement,
13 VideoState
14} from '../../../../shared/models'
15import { PeerTubePlayer } from '../../assets/player/peertube-player'
8import { TranslationsManager } from '../../assets/player/translations-manager' 16import { TranslationsManager } from '../../assets/player/translations-manager'
9import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' 17import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers'
10import { PeerTubeEmbedApi } from './embed-api' 18import { PeerTubeEmbedApi } from './embed-api'
11import { 19import {
12 AuthHTTP, 20 AuthHTTP,
13 LiveManager, 21 LiveManager,
14 PeerTubePlugin, 22 PeerTubePlugin,
15 PlayerManagerOptions, 23 PlayerOptionsBuilder,
16 PlaylistFetcher, 24 PlaylistFetcher,
17 PlaylistTracker, 25 PlaylistTracker,
18 Translations, 26 Translations,
@@ -27,18 +35,26 @@ export class PeerTubeEmbed {
27 config: HTMLServerConfig 35 config: HTMLServerConfig
28 36
29 private translationsPromise: Promise<{ [id: string]: string }> 37 private translationsPromise: Promise<{ [id: string]: string }>
30 private PeertubePlayerManagerModulePromise: Promise<any> 38 private PeerTubePlayerManagerModulePromise: Promise<any>
31 39
32 private readonly http: AuthHTTP 40 private readonly http: AuthHTTP
33 private readonly videoFetcher: VideoFetcher 41 private readonly videoFetcher: VideoFetcher
34 private readonly playlistFetcher: PlaylistFetcher 42 private readonly playlistFetcher: PlaylistFetcher
35 private readonly peertubePlugin: PeerTubePlugin 43 private readonly peertubePlugin: PeerTubePlugin
36 private readonly playerHTML: PlayerHTML 44 private readonly playerHTML: PlayerHTML
37 private readonly playerManagerOptions: PlayerManagerOptions 45 private readonly playerOptionsBuilder: PlayerOptionsBuilder
38 private readonly liveManager: LiveManager 46 private readonly liveManager: LiveManager
39 47
48 private peertubePlayer: PeerTubePlayer
49
40 private playlistTracker: PlaylistTracker 50 private playlistTracker: PlaylistTracker
41 51
52 private alreadyInitialized = false
53 private alreadyPlayed = false
54
55 private videoPassword: string
56 private requiresPassword: boolean
57
42 constructor (videoWrapperId: string) { 58 constructor (videoWrapperId: string) {
43 logger.registerServerSending(window.location.origin) 59 logger.registerServerSending(window.location.origin)
44 60
@@ -48,8 +64,9 @@ export class PeerTubeEmbed {
48 this.playlistFetcher = new PlaylistFetcher(this.http) 64 this.playlistFetcher = new PlaylistFetcher(this.http)
49 this.peertubePlugin = new PeerTubePlugin(this.http) 65 this.peertubePlugin = new PeerTubePlugin(this.http)
50 this.playerHTML = new PlayerHTML(videoWrapperId) 66 this.playerHTML = new PlayerHTML(videoWrapperId)
51 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) 67 this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
52 this.liveManager = new LiveManager(this.playerHTML) 68 this.liveManager = new LiveManager(this.playerHTML)
69 this.requiresPassword = false
53 70
54 try { 71 try {
55 this.config = JSON.parse((window as any)['PeerTubeServerConfig']) 72 this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
@@ -69,14 +86,14 @@ export class PeerTubeEmbed {
69 } 86 }
70 87
71 getScope () { 88 getScope () {
72 return this.playerManagerOptions.getScope() 89 return this.playerOptionsBuilder.getScope()
73 } 90 }
74 91
75 // --------------------------------------------------------------------------- 92 // ---------------------------------------------------------------------------
76 93
77 async init () { 94 async init () {
78 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) 95 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
79 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') 96 this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player')
80 97
81 // Issue when we parsed config from HTML, fallback to API 98 // Issue when we parsed config from HTML, fallback to API
82 if (!this.config) { 99 if (!this.config) {
@@ -90,7 +107,7 @@ export class PeerTubeEmbed {
90 107
91 if (!videoId) return 108 if (!videoId) return
92 109
93 return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) 110 return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false })
94 } 111 }
95 112
96 private async initPlaylist () { 113 private async initPlaylist () {
@@ -125,7 +142,7 @@ export class PeerTubeEmbed {
125 } 142 }
126 143
127 private initializeApi () { 144 private initializeApi () {
128 if (this.playerManagerOptions.hasAPIEnabled()) { 145 if (this.playerOptionsBuilder.hasAPIEnabled()) {
129 if (this.api) { 146 if (this.api) {
130 this.api.reInit() 147 this.api.reInit()
131 return 148 return
@@ -147,7 +164,7 @@ export class PeerTubeEmbed {
147 164
148 this.playlistTracker.setCurrentElement(next) 165 this.playlistTracker.setCurrentElement(next)
149 166
150 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 167 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false })
151 } 168 }
152 169
153 async playPreviousPlaylistVideo () { 170 async playPreviousPlaylistVideo () {
@@ -159,7 +176,7 @@ export class PeerTubeEmbed {
159 176
160 this.playlistTracker.setCurrentElement(previous) 177 this.playlistTracker.setCurrentElement(previous)
161 178
162 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 179 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false })
163 } 180 }
164 181
165 getCurrentPlaylistPosition () { 182 getCurrentPlaylistPosition () {
@@ -170,123 +187,124 @@ export class PeerTubeEmbed {
170 187
171 private async loadVideoAndBuildPlayer (options: { 188 private async loadVideoAndBuildPlayer (options: {
172 uuid: string 189 uuid: string
173 autoplayFromPreviousVideo: boolean
174 forceAutoplay: boolean 190 forceAutoplay: boolean
175 }) { 191 }) {
176 const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options 192 const { uuid, forceAutoplay } = options
177 193
178 try { 194 try {
179 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid) 195 const {
196 videoResponse,
197 captionsPromise,
198 storyboardsPromise
199 } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
180 200
181 return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay }) 201 return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, forceAutoplay })
182 } catch (err) { 202 } catch (err) {
183 this.playerHTML.displayError(err.message, await this.translationsPromise) 203
204 if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
205 else this.playerHTML.displayError(err.message, await this.translationsPromise)
184 } 206 }
185 } 207 }
186 208
187 private async buildVideoPlayer (options: { 209 private async buildVideoPlayer (options: {
188 videoResponse: Response 210 videoResponse: Response
211 storyboardsPromise: Promise<Response>
189 captionsPromise: Promise<Response> 212 captionsPromise: Promise<Response>
190 autoplayFromPreviousVideo: boolean
191 forceAutoplay: boolean 213 forceAutoplay: boolean
192 }) { 214 }) {
193 const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options 215 const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options
194
195 this.resetPlayerElement()
196 216
197 const videoInfoPromise = videoResponse.json() 217 const videoInfoPromise = videoResponse.json()
198 .then(async (videoInfo: VideoDetails) => { 218 .then(async (videoInfo: VideoDetails) => {
199 this.playerManagerOptions.loadParams(this.config, videoInfo) 219 this.playerOptionsBuilder.loadParams(this.config, videoInfo)
200 220
201 if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
202 this.playerHTML.buildPlaceholder(videoInfo)
203 }
204 const live = videoInfo.isLive 221 const live = videoInfo.isLive
205 ? await this.videoFetcher.loadLive(videoInfo) 222 ? await this.videoFetcher.loadLive(videoInfo)
206 : undefined 223 : undefined
207 224
208 const videoFileToken = videoRequiresAuth(videoInfo) 225 const videoFileToken = videoRequiresFileToken(videoInfo)
209 ? await this.videoFetcher.loadVideoToken(videoInfo) 226 ? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
210 : undefined 227 : undefined
211 228
212 return { live, video: videoInfo, videoFileToken } 229 return { live, video: videoInfo, videoFileToken }
213 }) 230 })
214 231
215 const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ 232 const [
233 { video, live, videoFileToken },
234 translations,
235 captionsResponse,
236 storyboardsResponse
237 ] = await Promise.all([
216 videoInfoPromise, 238 videoInfoPromise,
217 this.translationsPromise, 239 this.translationsPromise,
218 captionsPromise, 240 captionsPromise,
219 this.PeertubePlayerManagerModulePromise 241 storyboardsPromise,
242 this.buildPlayerIfNeeded()
220 ]) 243 ])
221 244
222 await this.peertubePlugin.loadPlugins(this.config, translations) 245 // If already played, we are in a playlist so we don't want to display the poster between videos
246 if (!this.alreadyPlayed) {
247 this.peertubePlayer.setPoster(window.location.origin + video.previewPath)
248 }
249
250 const playlist = this.playlistTracker
251 ? {
252 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
223 253
224 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager 254 playlistTracker: this.playlistTracker,
255 playNext: () => this.playNextPlaylistVideo(),
256 playPrevious: () => this.playPreviousPlaylistVideo()
257 }
258 : undefined
225 259
226 const playerOptions = await this.playerManagerOptions.getPlayerOptions({ 260 const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({
227 video, 261 video,
228 captionsResponse, 262 captionsResponse,
229 autoplayFromPreviousVideo,
230 translations, 263 translations,
231 serverConfig: this.config,
232 264
233 authorizationHeader: () => this.http.getHeaderTokenValue(), 265 storyboardsResponse,
234 videoFileToken: () => videoFileToken,
235 266
236 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), 267 videoFileToken: () => videoFileToken,
268 videoPassword: () => this.videoPassword,
269 requiresPassword: this.requiresPassword,
237 270
238 playlistTracker: this.playlistTracker, 271 playlist,
239 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
240 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
241 272
242 live, 273 live,
243 forceAutoplay 274 forceAutoplay,
275 alreadyPlayed: this.alreadyPlayed
244 }) 276 })
277 await this.peertubePlayer.load(loadOptions)
245 278
246 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { 279 if (!this.alreadyInitialized) {
247 this.player = player 280 this.player = this.peertubePlayer.getPlayer();
248 })
249 281
250 this.player.on('customError', (event: any, data: any) => { 282 (window as any)['videojsPlayer'] = this.player
251 const message = data?.err?.message || ''
252 if (!message.includes('from xs param')) return
253 283
254 this.player.dispose() 284 this.buildCSS()
255 this.playerHTML.removePlayerElement() 285 this.initializeApi()
256 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations) 286 }
257 });
258
259 (window as any)['videojsPlayer'] = this.player
260
261 this.buildCSS()
262 this.buildPlayerDock(video)
263 this.initializeApi()
264 287
265 this.playerHTML.removePlaceholder() 288 this.alreadyInitialized = true
266 289
267 if (this.isPlaylistEmbed()) { 290 this.player.one('play', () => {
268 await this.buildPlayerPlaylistUpnext() 291 this.alreadyPlayed = true
292 })
269 293
270 this.player.playlist().updateSelected() 294 if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
271
272 this.player.on('stopped', () => {
273 this.playNextPlaylistVideo()
274 })
275 }
276 295
277 if (video.isLive) { 296 if (video.isLive) {
278 this.liveManager.listenForChanges({ 297 this.liveManager.listenForChanges({
279 video, 298 video,
280 onPublishedVideo: () => { 299 onPublishedVideo: () => {
281 this.liveManager.stopListeningForChanges(video) 300 this.liveManager.stopListeningForChanges(video)
282 this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) 301 this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true })
283 } 302 }
284 }) 303 })
285 304
286 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { 305 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
287 this.liveManager.displayInfo({ state: video.state.id, translations }) 306 this.liveManager.displayInfo({ state: video.state.id, translations })
288 307 this.peertubePlayer.disable()
289 this.disablePlayer()
290 } else { 308 } else {
291 this.correctlyHandleLiveEnding(translations) 309 this.correctlyHandleLiveEnding(translations)
292 } 310 }
@@ -295,74 +313,15 @@ export class PeerTubeEmbed {
295 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) 313 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
296 } 314 }
297 315
298 private resetPlayerElement () {
299 if (this.player) {
300 this.player.dispose()
301 this.player = undefined
302 }
303
304 const playerElement = document.createElement('video')
305 playerElement.className = 'video-js vjs-peertube-skin'
306 playerElement.setAttribute('playsinline', 'true')
307
308 this.playerHTML.setPlayerElement(playerElement)
309 this.playerHTML.addPlayerElementToDOM()
310 }
311
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 })
325 }
326
327 private buildPlayerDock (videoInfo: VideoDetails) {
328 if (!this.playerManagerOptions.hasControls()) return
329
330 // On webtorrent fallback, player may have been disposed
331 if (!this.player.player_) return
332
333 const title = this.playerManagerOptions.hasTitle()
334 ? videoInfo.name
335 : undefined
336
337 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
338 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
339 : undefined
340
341 if (!title && !description) return
342
343 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
344 const avatar = availableAvatars.length !== 0
345 ? availableAvatars[0]
346 : undefined
347
348 this.player.peertubeDock({
349 title,
350 description,
351 avatarUrl: title && avatar
352 ? avatar.path
353 : undefined
354 })
355 }
356
357 private buildCSS () { 316 private buildCSS () {
358 const body = document.getElementById('custom-css') 317 const body = document.getElementById('custom-css')
359 318
360 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) { 319 if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) {
361 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor()) 320 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor())
362 } 321 }
363 322
364 if (this.playerManagerOptions.hasForegroundColor()) { 323 if (this.playerOptionsBuilder.hasForegroundColor()) {
365 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor()) 324 body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor())
366 } 325 }
367 } 326 }
368 327
@@ -384,23 +343,52 @@ export class PeerTubeEmbed {
384 // Display the live ended information 343 // Display the live ended information
385 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) 344 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
386 345
387 this.disablePlayer() 346 this.peertubePlayer.disable()
388 }) 347 })
389 } 348 }
390 349
391 private disablePlayer () { 350 private async handlePasswordError (err: PeerTubeServerError) {
392 if (this.player.isFullscreen()) { 351 let incorrectPassword: boolean = null
393 this.player.exitFullscreen() 352 if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
394 } 353 else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true
395 354
396 // Disable player 355 if (incorrectPassword === null) return false
397 this.player.hasStarted(false)
398 this.player.removeClass('vjs-has-autoplay')
399 this.player.bigPlayButton.hide();
400 356
401 (this.player.el() as HTMLElement).style.pointerEvents = 'none' 357 this.requiresPassword = true
358 this.videoPassword = await this.playerHTML.askVideoPassword({
359 incorrectPassword,
360 translations: await this.translationsPromise
361 })
362 return true
402 } 363 }
403 364
365 private async buildPlayerIfNeeded () {
366 if (this.peertubePlayer) {
367 this.peertubePlayer.enable()
368
369 return
370 }
371
372 const playerElement = document.createElement('video')
373 playerElement.className = 'video-js vjs-peertube-skin'
374 playerElement.setAttribute('playsinline', 'true')
375
376 this.playerHTML.setPlayerElement(playerElement)
377 this.playerHTML.addPlayerElementToDOM()
378
379 const [ { PeerTubePlayer } ] = await Promise.all([
380 this.PeerTubePlayerManagerModulePromise,
381 this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise)
382 ])
383
384 const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({
385 serverConfig: this.config,
386 authorizationHeader: () => this.http.getHeaderTokenValue()
387 })
388 this.peertubePlayer = new PeerTubePlayer(constructorOptions)
389
390 this.player = this.peertubePlayer.getPlayer()
391 }
404} 392}
405 393
406PeerTubeEmbed.main() 394PeerTubeEmbed.main()