diff options
Diffstat (limited to 'client/src/standalone/videos/embed.ts')
-rw-r--r-- | client/src/standalone/videos/embed.ts | 241 |
1 files changed, 211 insertions, 30 deletions
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 8b00be790..71bd04e76 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -9,6 +9,8 @@ import { | |||
9 | UserRefreshToken, | 9 | UserRefreshToken, |
10 | VideoCaption, | 10 | VideoCaption, |
11 | VideoDetails, | 11 | VideoDetails, |
12 | VideoPlaylist, | ||
13 | VideoPlaylistElement, | ||
12 | VideoStreamingPlaylistType | 14 | VideoStreamingPlaylistType |
13 | } from '../../../../shared/models' | 15 | } from '../../../../shared/models' |
14 | import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' | 16 | import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' |
@@ -19,9 +21,10 @@ import { PeerTubeEmbedApi } from './embed-api' | |||
19 | type Translations = { [ id: string ]: string } | 21 | type Translations = { [ id: string ]: string } |
20 | 22 | ||
21 | export class PeerTubeEmbed { | 23 | export class PeerTubeEmbed { |
22 | videoElement: HTMLVideoElement | 24 | playerElement: HTMLVideoElement |
23 | player: videojs.Player | 25 | player: videojs.Player |
24 | api: PeerTubeEmbedApi = null | 26 | api: PeerTubeEmbedApi = null |
27 | |||
25 | autoplay: boolean | 28 | autoplay: boolean |
26 | controls: boolean | 29 | controls: boolean |
27 | muted: boolean | 30 | muted: boolean |
@@ -47,14 +50,24 @@ export class PeerTubeEmbed { | |||
47 | CLIENT_SECRET: 'client_secret' | 50 | CLIENT_SECRET: 'client_secret' |
48 | } | 51 | } |
49 | 52 | ||
53 | private translationsPromise: Promise<{ [id: string]: string }> | ||
54 | private configPromise: Promise<ServerConfig> | ||
55 | private PeertubePlayerManagerModulePromise: Promise<any> | ||
56 | |||
57 | private playlist: VideoPlaylist | ||
58 | private playlistElements: VideoPlaylistElement[] | ||
59 | private currentPlaylistElement: VideoPlaylistElement | ||
60 | |||
61 | private wrapperElement: HTMLElement | ||
62 | |||
50 | static async main () { | 63 | static async main () { |
51 | const videoContainerId = 'video-container' | 64 | const videoContainerId = 'video-wrapper' |
52 | const embed = new PeerTubeEmbed(videoContainerId) | 65 | const embed = new PeerTubeEmbed(videoContainerId) |
53 | await embed.init() | 66 | await embed.init() |
54 | } | 67 | } |
55 | 68 | ||
56 | constructor (private videoContainerId: string) { | 69 | constructor (private videoWrapperId: string) { |
57 | this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement | 70 | this.wrapperElement = document.getElementById(this.videoWrapperId) |
58 | } | 71 | } |
59 | 72 | ||
60 | getVideoUrl (id: string) { | 73 | getVideoUrl (id: string) { |
@@ -114,6 +127,10 @@ export class PeerTubeEmbed { | |||
114 | }) | 127 | }) |
115 | } | 128 | } |
116 | 129 | ||
130 | getPlaylistUrl (id: string) { | ||
131 | return window.location.origin + '/api/v1/video-playlists/' + id | ||
132 | } | ||
133 | |||
117 | loadVideoInfo (videoId: string): Promise<Response> { | 134 | loadVideoInfo (videoId: string): Promise<Response> { |
118 | return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers }) | 135 | return this.refreshFetch(this.getVideoUrl(videoId), { headers: this.headers }) |
119 | } | 136 | } |
@@ -122,8 +139,17 @@ export class PeerTubeEmbed { | |||
122 | return fetch(this.getVideoUrl(videoId) + '/captions') | 139 | return fetch(this.getVideoUrl(videoId) + '/captions') |
123 | } | 140 | } |
124 | 141 | ||
125 | loadConfig (): Promise<Response> { | 142 | loadPlaylistInfo (playlistId: string): Promise<Response> { |
143 | return fetch(this.getPlaylistUrl(playlistId)) | ||
144 | } | ||
145 | |||
146 | loadPlaylistElements (playlistId: string): Promise<Response> { | ||
147 | return fetch(this.getPlaylistUrl(playlistId) + '/videos') | ||
148 | } | ||
149 | |||
150 | loadConfig (): Promise<ServerConfig> { | ||
126 | return fetch('/api/v1/config') | 151 | return fetch('/api/v1/config') |
152 | .then(res => res.json()) | ||
127 | } | 153 | } |
128 | 154 | ||
129 | removeElement (element: HTMLElement) { | 155 | removeElement (element: HTMLElement) { |
@@ -132,7 +158,10 @@ export class PeerTubeEmbed { | |||
132 | 158 | ||
133 | displayError (text: string, translations?: Translations) { | 159 | displayError (text: string, translations?: Translations) { |
134 | // Remove video element | 160 | // Remove video element |
135 | if (this.videoElement) this.removeElement(this.videoElement) | 161 | if (this.playerElement) { |
162 | this.removeElement(this.playerElement) | ||
163 | this.playerElement = undefined | ||
164 | } | ||
136 | 165 | ||
137 | const translatedText = peertubeTranslate(text, translations) | 166 | const translatedText = peertubeTranslate(text, translations) |
138 | const translatedSorry = peertubeTranslate('Sorry', translations) | 167 | const translatedSorry = peertubeTranslate('Sorry', translations) |
@@ -159,6 +188,16 @@ export class PeerTubeEmbed { | |||
159 | this.displayError(text, translations) | 188 | this.displayError(text, translations) |
160 | } | 189 | } |
161 | 190 | ||
191 | playlistNotFound (translations?: Translations) { | ||
192 | const text = 'This playlist does not exist.' | ||
193 | this.displayError(text, translations) | ||
194 | } | ||
195 | |||
196 | playlistFetchError (translations?: Translations) { | ||
197 | const text = 'We cannot fetch the playlist. Please try again later.' | ||
198 | this.displayError(text, translations) | ||
199 | } | ||
200 | |||
162 | getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) { | 201 | getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) { |
163 | return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue | 202 | return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue |
164 | } | 203 | } |
@@ -218,34 +257,129 @@ export class PeerTubeEmbed { | |||
218 | } | 257 | } |
219 | } | 258 | } |
220 | 259 | ||
221 | private async initCore () { | 260 | private async loadPlaylist (playlistId: string) { |
222 | const urlParts = window.location.pathname.split('/') | 261 | const playlistPromise = this.loadPlaylistInfo(playlistId) |
223 | const videoId = urlParts[ urlParts.length - 1 ] | 262 | const playlistElementsPromise = this.loadPlaylistElements(playlistId) |
224 | 263 | ||
225 | if (this.userTokens) this.setHeadersFromTokens() | 264 | const playlistResponse = await playlistPromise |
265 | |||
266 | if (!playlistResponse.ok) { | ||
267 | const serverTranslations = await this.translationsPromise | ||
226 | 268 | ||
269 | if (playlistResponse.status === 404) { | ||
270 | this.playlistNotFound(serverTranslations) | ||
271 | return undefined | ||
272 | } | ||
273 | |||
274 | this.playlistFetchError(serverTranslations) | ||
275 | return undefined | ||
276 | } | ||
277 | |||
278 | return { playlistResponse, videosResponse: await playlistElementsPromise } | ||
279 | } | ||
280 | |||
281 | private async loadVideo (videoId: string) { | ||
227 | const videoPromise = this.loadVideoInfo(videoId) | 282 | const videoPromise = this.loadVideoInfo(videoId) |
228 | const captionsPromise = this.loadVideoCaptions(videoId) | ||
229 | const configPromise = this.loadConfig() | ||
230 | 283 | ||
231 | const translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) | ||
232 | const videoResponse = await videoPromise | 284 | const videoResponse = await videoPromise |
233 | 285 | ||
234 | if (!videoResponse.ok) { | 286 | if (!videoResponse.ok) { |
235 | const serverTranslations = await translationsPromise | 287 | const serverTranslations = await this.translationsPromise |
288 | |||
289 | if (videoResponse.status === 404) { | ||
290 | this.videoNotFound(serverTranslations) | ||
291 | return undefined | ||
292 | } | ||
293 | |||
294 | this.videoFetchError(serverTranslations) | ||
295 | return undefined | ||
296 | } | ||
297 | |||
298 | const captionsPromise = this.loadVideoCaptions(videoId) | ||
299 | |||
300 | return { captionsPromise, videoResponse } | ||
301 | } | ||
236 | 302 | ||
237 | if (videoResponse.status === 404) return this.videoNotFound(serverTranslations) | 303 | private async buildPlaylistManager () { |
304 | const translations = await this.translationsPromise | ||
305 | |||
306 | this.player.upnext({ | ||
307 | timeout: 10000, // 10s | ||
308 | headText: peertubeTranslate('Up Next', translations), | ||
309 | cancelText: peertubeTranslate('Cancel', translations), | ||
310 | suspendedText: peertubeTranslate('Autoplay is suspended', translations), | ||
311 | getTitle: () => this.nextVideoTitle(), | ||
312 | next: () => this.autoplayNext(), | ||
313 | condition: () => !!this.getNextPlaylistElement(), | ||
314 | suspended: () => false | ||
315 | }) | ||
316 | } | ||
238 | 317 | ||
239 | return this.videoFetchError(serverTranslations) | 318 | private async autoplayNext () { |
319 | const next = this.getNextPlaylistElement() | ||
320 | if (!next) { | ||
321 | console.log('Next element not found in playlist.') | ||
322 | return | ||
240 | } | 323 | } |
241 | 324 | ||
242 | const videoInfo: VideoDetails = await videoResponse.json() | 325 | this.currentPlaylistElement = next |
243 | this.loadPlaceholder(videoInfo) | ||
244 | 326 | ||
245 | const PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') | 327 | const res = await this.loadVideo(this.currentPlaylistElement.video.uuid) |
328 | if (res === undefined) return | ||
329 | |||
330 | return this.buildVideoPlayer(res.videoResponse, res.captionsPromise) | ||
331 | } | ||
246 | 332 | ||
247 | const promises = [ translationsPromise, captionsPromise, configPromise, PeertubePlayerManagerModulePromise ] | 333 | private nextVideoTitle () { |
248 | const [ serverTranslations, captionsResponse, configResponse, PeertubePlayerManagerModule ] = await Promise.all(promises) | 334 | const next = this.getNextPlaylistElement() |
335 | if (!next) return '' | ||
336 | |||
337 | return next.video.name | ||
338 | } | ||
339 | |||
340 | private getNextPlaylistElement (position?: number): VideoPlaylistElement { | ||
341 | if (!position) position = this.currentPlaylistElement.position + 1 | ||
342 | |||
343 | if (position > this.playlist.videosLength) { | ||
344 | return undefined | ||
345 | } | ||
346 | |||
347 | const next = this.playlistElements.find(e => e.position === position) | ||
348 | |||
349 | if (!next || !next.video) { | ||
350 | return this.getNextPlaylistElement(position + 1) | ||
351 | } | ||
352 | |||
353 | return next | ||
354 | } | ||
355 | |||
356 | private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { | ||
357 | let alreadyHadPlayer = false | ||
358 | |||
359 | if (this.player) { | ||
360 | this.player.dispose() | ||
361 | alreadyHadPlayer = true | ||
362 | } | ||
363 | |||
364 | this.playerElement = document.createElement('video') | ||
365 | this.playerElement.className = 'video-js vjs-peertube-skin' | ||
366 | this.playerElement.setAttribute('playsinline', 'true') | ||
367 | this.wrapperElement.appendChild(this.playerElement) | ||
368 | |||
369 | const videoInfoPromise = videoResponse.json() | ||
370 | .then((videoInfo: VideoDetails) => { | ||
371 | if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo) | ||
372 | |||
373 | return videoInfo | ||
374 | }) | ||
375 | |||
376 | const [ videoInfo, serverTranslations, captionsResponse, config, PeertubePlayerManagerModule ] = await Promise.all([ | ||
377 | videoInfoPromise, | ||
378 | this.translationsPromise, | ||
379 | captionsPromise, | ||
380 | this.configPromise, | ||
381 | this.PeertubePlayerManagerModulePromise | ||
382 | ]) | ||
249 | 383 | ||
250 | const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager | 384 | const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager |
251 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) | 385 | const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) |
@@ -254,7 +388,8 @@ export class PeerTubeEmbed { | |||
254 | 388 | ||
255 | const options: PeertubePlayerManagerOptions = { | 389 | const options: PeertubePlayerManagerOptions = { |
256 | common: { | 390 | common: { |
257 | autoplay: this.autoplay, | 391 | // Autoplay in playlist mode |
392 | autoplay: alreadyHadPlayer ? true : this.autoplay, | ||
258 | controls: this.controls, | 393 | controls: this.controls, |
259 | muted: this.muted, | 394 | muted: this.muted, |
260 | loop: this.loop, | 395 | loop: this.loop, |
@@ -263,12 +398,14 @@ export class PeerTubeEmbed { | |||
263 | stopTime: this.stopTime, | 398 | stopTime: this.stopTime, |
264 | subtitle: this.subtitle, | 399 | subtitle: this.subtitle, |
265 | 400 | ||
401 | nextVideo: () => this.autoplayNext(), | ||
402 | |||
266 | videoCaptions, | 403 | videoCaptions, |
267 | inactivityTimeout: 2500, | 404 | inactivityTimeout: 2500, |
268 | videoViewUrl: this.getVideoUrl(videoId) + '/views', | 405 | videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', |
269 | 406 | ||
270 | playerElement: this.videoElement, | 407 | playerElement: this.playerElement, |
271 | onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element, | 408 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, |
272 | 409 | ||
273 | videoDuration: videoInfo.duration, | 410 | videoDuration: videoInfo.duration, |
274 | enableHotkeys: true, | 411 | enableHotkeys: true, |
@@ -307,23 +444,58 @@ export class PeerTubeEmbed { | |||
307 | 444 | ||
308 | this.buildCSS() | 445 | this.buildCSS() |
309 | 446 | ||
310 | await this.buildDock(videoInfo, configResponse) | 447 | await this.buildDock(videoInfo, config) |
311 | 448 | ||
312 | this.initializeApi() | 449 | this.initializeApi() |
313 | 450 | ||
314 | this.removePlaceholder() | 451 | this.removePlaceholder() |
452 | |||
453 | if (this.isPlaylistEmbed()) { | ||
454 | await this.buildPlaylistManager() | ||
455 | } | ||
456 | } | ||
457 | |||
458 | private async initCore () { | ||
459 | if (this.userTokens) this.setHeadersFromTokens() | ||
460 | |||
461 | this.configPromise = this.loadConfig() | ||
462 | this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) | ||
463 | this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') | ||
464 | |||
465 | let videoId: string | ||
466 | |||
467 | if (this.isPlaylistEmbed()) { | ||
468 | const playlistId = this.getResourceId() | ||
469 | const res = await this.loadPlaylist(playlistId) | ||
470 | if (!res) return undefined | ||
471 | |||
472 | this.playlist = await res.playlistResponse.json() | ||
473 | |||
474 | const playlistElementResult = await res.videosResponse.json() | ||
475 | this.playlistElements = playlistElementResult.data | ||
476 | |||
477 | this.currentPlaylistElement = this.playlistElements[0] | ||
478 | videoId = this.currentPlaylistElement.video.uuid | ||
479 | } else { | ||
480 | videoId = this.getResourceId() | ||
481 | } | ||
482 | |||
483 | const res = await this.loadVideo(videoId) | ||
484 | if (res === undefined) return | ||
485 | |||
486 | return this.buildVideoPlayer(res.videoResponse, res.captionsPromise) | ||
315 | } | 487 | } |
316 | 488 | ||
317 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | 489 | private handleError (err: Error, translations?: { [ id: string ]: string }) { |
318 | if (err.message.indexOf('from xs param') !== -1) { | 490 | if (err.message.indexOf('from xs param') !== -1) { |
319 | this.player.dispose() | 491 | this.player.dispose() |
320 | this.videoElement = null | 492 | this.playerElement = null |
321 | this.displayError('This video is not available because the remote instance is not responding.', translations) | 493 | this.displayError('This video is not available because the remote instance is not responding.', translations) |
322 | return | 494 | return |
323 | } | 495 | } |
324 | } | 496 | } |
325 | 497 | ||
326 | private async buildDock (videoInfo: VideoDetails, configResponse: Response) { | 498 | private async buildDock (videoInfo: VideoDetails, config: ServerConfig) { |
327 | if (!this.controls) return | 499 | if (!this.controls) return |
328 | 500 | ||
329 | // On webtorrent fallback, player may have been disposed | 501 | // On webtorrent fallback, player may have been disposed |
@@ -331,7 +503,6 @@ export class PeerTubeEmbed { | |||
331 | 503 | ||
332 | const title = this.title ? videoInfo.name : undefined | 504 | const title = this.title ? videoInfo.name : undefined |
333 | 505 | ||
334 | const config: ServerConfig = await configResponse.json() | ||
335 | const description = config.tracker.enabled && this.warningTitle | 506 | const description = config.tracker.enabled && this.warningTitle |
336 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | 507 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' |
337 | : undefined | 508 | : undefined |
@@ -373,11 +544,12 @@ export class PeerTubeEmbed { | |||
373 | 544 | ||
374 | const url = window.location.origin + video.previewPath | 545 | const url = window.location.origin + video.previewPath |
375 | placeholder.style.backgroundImage = `url("${url}")` | 546 | placeholder.style.backgroundImage = `url("${url}")` |
547 | placeholder.style.display = 'block' | ||
376 | } | 548 | } |
377 | 549 | ||
378 | private removePlaceholder () { | 550 | private removePlaceholder () { |
379 | const placeholder = this.getPlaceholderElement() | 551 | const placeholder = this.getPlaceholderElement() |
380 | placeholder.parentElement.removeChild(placeholder) | 552 | placeholder.style.display = 'none' |
381 | } | 553 | } |
382 | 554 | ||
383 | private getPlaceholderElement () { | 555 | private getPlaceholderElement () { |
@@ -387,6 +559,15 @@ export class PeerTubeEmbed { | |||
387 | private setHeadersFromTokens () { | 559 | private setHeadersFromTokens () { |
388 | this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`) | 560 | this.headers.set('Authorization', `${this.userTokens.tokenType} ${this.userTokens.accessToken}`) |
389 | } | 561 | } |
562 | |||
563 | private getResourceId () { | ||
564 | const urlParts = window.location.pathname.split('/') | ||
565 | return urlParts[ urlParts.length - 1 ] | ||
566 | } | ||
567 | |||
568 | private isPlaylistEmbed () { | ||
569 | return window.location.pathname.split('/')[1] === 'video-playlists' | ||
570 | } | ||
390 | } | 571 | } |
391 | 572 | ||
392 | PeerTubeEmbed.main() | 573 | PeerTubeEmbed.main() |