aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/videos
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone/videos')
-rw-r--r--client/src/standalone/videos/embed-api.ts3
-rw-r--r--client/src/standalone/videos/embed.html5
-rw-r--r--client/src/standalone/videos/embed.scss5
-rw-r--r--client/src/standalone/videos/embed.ts241
4 files changed, 219 insertions, 35 deletions
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts
index a9263555d..efc23a1fc 100644
--- a/client/src/standalone/videos/embed-api.ts
+++ b/client/src/standalone/videos/embed-api.ts
@@ -26,7 +26,7 @@ export class PeerTubeEmbedApi {
26 } 26 }
27 27
28 private get element () { 28 private get element () {
29 return this.embed.videoElement 29 return this.embed.playerElement
30 } 30 }
31 31
32 private constructChannel () { 32 private constructChannel () {
@@ -108,7 +108,6 @@ export class PeerTubeEmbedApi {
108 setInterval(() => { 108 setInterval(() => {
109 const position = this.element.currentTime 109 const position = this.element.currentTime
110 const volume = this.element.volume 110 const volume = this.element.volume
111 const duration = this.element.duration
112 111
113 this.channel.notify({ 112 this.channel.notify({
114 method: 'playbackStatusUpdate', 113 method: 'playbackStatusUpdate',
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index 6edf71f48..908aad940 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -19,10 +19,9 @@
19 <div id="error-content"></div> 19 <div id="error-content"></div>
20 </div> 20 </div>
21 21
22 <video playsinline="true" id="video-container" class="video-js vjs-peertube-skin"> 22 <div id="video-wrapper"></div>
23 </video>
24 23
25 <div id="placeholder-preview" /> 24 <div id="placeholder-preview"></div>
26 25
27 </body> 26 </body>
28</html> 27</html>
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss
index 95573dabe..cbe6bdd01 100644
--- a/client/src/standalone/videos/embed.scss
+++ b/client/src/standalone/videos/embed.scss
@@ -27,6 +27,11 @@ html, body {
27 background-color: #000; 27 background-color: #000;
28} 28}
29 29
30#video-wrapper {
31 width: 100%;
32 height: 100%;
33}
34
30.video-js.vjs-peertube-skin { 35.video-js.vjs-peertube-skin {
31 width: 100%; 36 width: 100%;
32 height: 100%; 37 height: 100%;
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'
14import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' 16import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
@@ -19,9 +21,10 @@ import { PeerTubeEmbedApi } from './embed-api'
19type Translations = { [ id: string ]: string } 21type Translations = { [ id: string ]: string }
20 22
21export class PeerTubeEmbed { 23export 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
392PeerTubeEmbed.main() 573PeerTubeEmbed.main()