aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts16
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts463
-rw-r--r--client/src/app/helpers/utils/object.ts2
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-list.component.ts4
5 files changed, 249 insertions, 240 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
index ec85db0ff..97d71a510 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
@@ -152,12 +152,24 @@ export class VideoWatchPlaylistComponent {
152 this.onPlaylistVideosNearOfBottom(position) 152 this.onPlaylistVideosNearOfBottom(position)
153 } 153 }
154 154
155 // ---------------------------------------------------------------------------
156
155 hasPreviousVideo () { 157 hasPreviousVideo () {
156 return !!this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') 158 return !!this.getPreviousVideo()
159 }
160
161 getPreviousVideo () {
162 return this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous')
157 } 163 }
158 164
165 // ---------------------------------------------------------------------------
166
159 hasNextVideo () { 167 hasNextVideo () {
160 return !!this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') 168 return !!this.getNextVideo()
169 }
170
171 getNextVideo () {
172 return this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next')
161 } 173 }
162 174
163 navigateToPreviousPlaylistVideo () { 175 navigateToPreviousPlaylistVideo () {
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index 80fd6e40f..294ff4b3a 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -8,7 +8,7 @@
8 </div> 8 </div>
9 9
10 <div id="videojs-wrapper"> 10 <div id="videojs-wrapper">
11 <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> 11 <video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video>
12 </div> 12 </div>
13 13
14 <my-video-watch-playlist 14 <my-video-watch-playlist
@@ -51,7 +51,7 @@
51 </div> 51 </div>
52 52
53 <my-action-buttons 53 <my-action-buttons
54 [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions" 54 [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions"
55 [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" 55 [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()"
56 ></my-action-buttons> 56 ></my-action-buttons>
57 </div> 57 </div>
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 54e0649ba..aebec52fb 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -1,6 +1,5 @@
1import { Hotkey, HotkeysService } from 'angular2-hotkeys' 1import { Hotkey, HotkeysService } from 'angular2-hotkeys'
2import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' 2import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
3import { VideoJsPlayer } from 'video.js'
4import { PlatformLocation } from '@angular/common' 3import { PlatformLocation } from '@angular/common'
5import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
6import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
@@ -19,13 +18,13 @@ import {
19 UserService 18 UserService
20} from '@app/core' 19} from '@app/core'
21import { HooksService } from '@app/core/plugins/hooks.service' 20import { HooksService } from '@app/core/plugins/hooks.service'
22import { isXPercentInViewport, scrollToTop } from '@app/helpers' 21import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers'
23import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' 22import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main'
24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 23import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
25import { LiveVideoService } from '@app/shared/shared-video-live' 24import { LiveVideoService } from '@app/shared/shared-video-live'
26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 25import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
27import { logger } from '@root-helpers/logger' 26import { logger } from '@root-helpers/logger'
28import { isP2PEnabled, videoRequiresUserAuth, videoRequiresFileToken } from '@root-helpers/video' 27import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
29import { timeToInt } from '@shared/core-utils' 28import { timeToInt } from '@shared/core-utils'
30import { 29import {
31 HTMLServerConfig, 30 HTMLServerConfig,
@@ -39,10 +38,10 @@ import {
39 VideoState 38 VideoState
40} from '@shared/models' 39} from '@shared/models'
41import { 40import {
42 CustomizationOptions, 41 HLSOptions,
43 P2PMediaLoaderOptions, 42 PeerTubePlayer,
44 PeertubePlayerManager, 43 PeerTubePlayerContructorOptions,
45 PeertubePlayerManagerOptions, 44 PeerTubePlayerLoadOptions,
46 PlayerMode, 45 PlayerMode,
47 videojs 46 videojs
48} from '../../../assets/player' 47} from '../../../assets/player'
@@ -50,7 +49,24 @@ import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from
50import { environment } from '../../../environments/environment' 49import { environment } from '../../../environments/environment'
51import { VideoWatchPlaylistComponent } from './shared' 50import { VideoWatchPlaylistComponent } from './shared'
52 51
53type URLOptions = CustomizationOptions & { playerMode: PlayerMode } 52type URLOptions = {
53 playerMode: PlayerMode
54
55 startTime: number | string
56 stopTime: number | string
57
58 controls?: boolean
59 controlBar?: boolean
60
61 muted?: boolean
62 loop?: boolean
63 subtitle?: string
64 resume?: string
65
66 peertubeLink: boolean
67
68 playbackRate?: number | string
69}
54 70
55@Component({ 71@Component({
56 selector: 'my-video-watch', 72 selector: 'my-video-watch',
@@ -60,10 +76,9 @@ type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
60export class VideoWatchComponent implements OnInit, OnDestroy { 76export class VideoWatchComponent implements OnInit, OnDestroy {
61 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent 77 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
62 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 78 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
79 @ViewChild('playerElement') playerElement: ElementRef<HTMLVideoElement>
63 80
64 player: VideoJsPlayer 81 peertubePlayer: PeerTubePlayer
65 playerElement: HTMLVideoElement
66 playerPlaceholderImgSrc: string
67 theaterEnabled = false 82 theaterEnabled = false
68 83
69 video: VideoDetails = null 84 video: VideoDetails = null
@@ -78,8 +93,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
78 remoteServerDown = false 93 remoteServerDown = false
79 noPlaylistVideoFound = false 94 noPlaylistVideoFound = false
80 95
81 private nextVideoUUID = '' 96 private nextRecommendedVideoUUID = ''
82 private nextVideoTitle = '' 97 private nextRecommendedVideoTitle = ''
83 98
84 private videoFileToken: string 99 private videoFileToken: string
85 100
@@ -130,11 +145,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
130 return this.userService.getAnonymousUser() 145 return this.userService.getAnonymousUser()
131 } 146 }
132 147
133 ngOnInit () { 148 async ngOnInit () {
134 this.serverConfig = this.serverService.getHTMLConfig() 149 this.serverConfig = this.serverService.getHTMLConfig()
135 150
136 PeertubePlayerManager.initState()
137
138 this.loadRouteParams() 151 this.loadRouteParams()
139 this.loadRouteQuery() 152 this.loadRouteQuery()
140 153
@@ -143,10 +156,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
143 this.hooks.runAction('action:video-watch.init', 'video-watch') 156 this.hooks.runAction('action:video-watch.init', 'video-watch')
144 157
145 setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI 158 setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI
159
160 const constructorOptions = await this.hooks.wrapFun(
161 this.buildPeerTubePlayerConstructorOptions.bind(this),
162 { urlOptions: this.getUrlOptions() },
163 'video-watch',
164 'filter:internal.video-watch.player.build-options.params',
165 'filter:internal.video-watch.player.build-options.result'
166 )
167
168 this.peertubePlayer = new PeerTubePlayer(constructorOptions)
146 } 169 }
147 170
148 ngOnDestroy () { 171 ngOnDestroy () {
149 this.flushPlayer() 172 if (this.peertubePlayer) this.peertubePlayer.destroy()
150 173
151 // Unsubscribe subscriptions 174 // Unsubscribe subscriptions
152 if (this.paramsSub) this.paramsSub.unsubscribe() 175 if (this.paramsSub) this.paramsSub.unsubscribe()
@@ -171,14 +194,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
171 194
172 // The recommended videos's first element should be the next video 195 // The recommended videos's first element should be the next video
173 const video = videos[0] 196 const video = videos[0]
174 this.nextVideoUUID = video.uuid 197 this.nextRecommendedVideoUUID = video.uuid
175 this.nextVideoTitle = video.name 198 this.nextRecommendedVideoTitle = video.name
176 } 199 }
177 200
178 handleTimestampClicked (timestamp: number) { 201 handleTimestampClicked (timestamp: number) {
179 if (!this.player || this.video.isLive) return 202 if (!this.peertubePlayer || this.video.isLive) return
180 203
181 this.player.currentTime(timestamp) 204 this.peertubePlayer.getPlayer().currentTime(timestamp)
182 scrollToTop() 205 scrollToTop()
183 } 206 }
184 207
@@ -243,7 +266,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
243 this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) 266 this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition)
244 267
245 const start = queryParams['start'] 268 const start = queryParams['start']
246 if (this.player && start) this.player.currentTime(parseInt(start, 10)) 269 if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10))
247 }) 270 })
248 } 271 }
249 272
@@ -256,8 +279,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
256 279
257 if (this.isSameElement(this.video, videoId)) return 280 if (this.isSameElement(this.video, videoId)) return
258 281
259 if (this.player) this.player.pause()
260
261 this.video = undefined 282 this.video = undefined
262 283
263 const videoObs = this.hooks.wrapObsFun( 284 const videoObs = this.hooks.wrapObsFun(
@@ -291,23 +312,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
291 this.userService.getAnonymousOrLoggedUser() 312 this.userService.getAnonymousOrLoggedUser()
292 ]).subscribe({ 313 ]).subscribe({
293 next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => { 314 next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => {
294 const queryParams = this.route.snapshot.queryParams
295
296 const urlOptions = {
297 resume: queryParams.resume,
298
299 startTime: queryParams.start,
300 stopTime: queryParams.stop,
301
302 muted: queryParams.muted,
303 loop: queryParams.loop,
304 subtitle: queryParams.subtitle,
305
306 playerMode: queryParams.mode,
307 playbackRate: queryParams.playbackRate,
308 peertubeLink: false
309 }
310
311 this.onVideoFetched({ 315 this.onVideoFetched({
312 video, 316 video,
313 live, 317 live,
@@ -316,7 +320,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
316 videoFileToken, 320 videoFileToken,
317 videoPassword, 321 videoPassword,
318 loggedInOrAnonymousUser, 322 loggedInOrAnonymousUser,
319 urlOptions,
320 forceAutoplay 323 forceAutoplay
321 }).catch(err => { 324 }).catch(err => {
322 this.handleGlobalError(err) 325 this.handleGlobalError(err)
@@ -386,14 +389,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
386 const errorMessage: string = typeof err === 'string' ? err : err.message 389 const errorMessage: string = typeof err === 'string' ? err : err.message
387 if (!errorMessage) return 390 if (!errorMessage) return
388 391
389 // Display a message in the video player instead of a notification
390 if (errorMessage.includes('from xs param')) {
391 this.flushPlayer()
392 this.remoteServerDown = true
393
394 return
395 }
396
397 this.notifier.error(errorMessage) 392 this.notifier.error(errorMessage)
398 } 393 }
399 394
@@ -422,7 +417,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
422 videoFileToken: string 417 videoFileToken: string
423 videoPassword: string 418 videoPassword: string
424 419
425 urlOptions: URLOptions
426 loggedInOrAnonymousUser: User 420 loggedInOrAnonymousUser: User
427 forceAutoplay: boolean 421 forceAutoplay: boolean
428 }) { 422 }) {
@@ -431,7 +425,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
431 live, 425 live,
432 videoCaptions, 426 videoCaptions,
433 storyboards, 427 storyboards,
434 urlOptions,
435 videoFileToken, 428 videoFileToken,
436 videoPassword, 429 videoPassword,
437 loggedInOrAnonymousUser, 430 loggedInOrAnonymousUser,
@@ -448,7 +441,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
448 this.storyboards = storyboards 441 this.storyboards = storyboards
449 442
450 // Re init attributes 443 // Re init attributes
451 this.playerPlaceholderImgSrc = undefined
452 this.remoteServerDown = false 444 this.remoteServerDown = false
453 this.currentTime = undefined 445 this.currentTime = undefined
454 446
@@ -462,7 +454,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
462 454
463 this.buildHotkeysHelp(video) 455 this.buildHotkeysHelp(video)
464 456
465 this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) 457 this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay })
466 .catch(err => logger.error('Cannot build the player', err)) 458 .catch(err => logger.error('Cannot build the player', err))
467 459
468 this.setOpenGraphTags() 460 this.setOpenGraphTags()
@@ -475,28 +467,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
475 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) 467 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions)
476 } 468 }
477 469
478 private async buildPlayer (options: { 470 private async loadPlayer (options: {
479 urlOptions: URLOptions
480 loggedInOrAnonymousUser: User 471 loggedInOrAnonymousUser: User
481 forceAutoplay: boolean 472 forceAutoplay: boolean
482 }) { 473 }) {
483 const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options 474 const { loggedInOrAnonymousUser, forceAutoplay } = options
484
485 // Flush old player if needed
486 this.flushPlayer()
487 475
488 const videoState = this.video.state.id 476 const videoState = this.video.state.id
489 if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { 477 if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) {
490 this.playerPlaceholderImgSrc = this.video.previewPath 478 this.updatePlayerOnNoLive()
491 return 479 return
492 } 480 }
493 481
494 // Build video element, because videojs removes it on dispose 482 this.peertubePlayer?.enable()
495 const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper')
496 this.playerElement = document.createElement('video')
497 this.playerElement.className = 'video-js vjs-peertube-skin'
498 this.playerElement.setAttribute('playsinline', 'true')
499 playerElementWrapper.appendChild(this.playerElement)
500 483
501 const params = { 484 const params = {
502 video: this.video, 485 video: this.video,
@@ -505,86 +488,49 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
505 liveVideo: this.liveVideo, 488 liveVideo: this.liveVideo,
506 videoFileToken: this.videoFileToken, 489 videoFileToken: this.videoFileToken,
507 videoPassword: this.videoPassword, 490 videoPassword: this.videoPassword,
508 urlOptions, 491 urlOptions: this.getUrlOptions(),
509 loggedInOrAnonymousUser, 492 loggedInOrAnonymousUser,
510 forceAutoplay, 493 forceAutoplay,
511 user: this.user 494 user: this.user
512 } 495 }
513 const { playerMode, playerOptions } = await this.hooks.wrapFun( 496
514 this.buildPlayerManagerOptions.bind(this), 497 const loadOptions = await this.hooks.wrapFun(
498 this.buildPeerTubePlayerLoadOptions.bind(this),
515 params, 499 params,
516 'video-watch', 500 'video-watch',
517 'filter:internal.video-watch.player.build-options.params', 501 'filter:internal.video-watch.player.load-options.params',
518 'filter:internal.video-watch.player.build-options.result' 502 'filter:internal.video-watch.player.load-options.result'
519 ) 503 )
520 504
521 this.zone.runOutsideAngular(async () => { 505 this.zone.runOutsideAngular(async () => {
522 this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player) 506 await this.peertubePlayer.load(loadOptions)
523 507
524 this.player.on('customError', (_e, data: any) => { 508 const player = this.peertubePlayer.getPlayer()
525 this.zone.run(() => this.handleGlobalError(data.err))
526 })
527 509
528 this.player.on('timeupdate', () => { 510 player.on('timeupdate', () => {
529 // Don't need to trigger angular change for this variable, that is sent to children components on click 511 // Don't need to trigger angular change for this variable, that is sent to children components on click
530 this.currentTime = Math.floor(this.player.currentTime()) 512 this.currentTime = Math.floor(player.currentTime())
531 }) 513 })
532 514
533 /** 515 if (this.video.isLive) {
534 * condition: true to make the upnext functionality trigger, false to disable the upnext functionality 516 player.one('ended', () => {
535 * go to the next video in 'condition()' if you don't want of the timer. 517 this.zone.run(() => {
536 * next: function triggered at the end of the timer. 518 // We changed the video, it's not a live anymore
537 * suspended: function used at each click of the timer checking if we need to reset progress 519 if (!this.video.isLive) return
538 * and wait until suspended becomes truthy again.
539 */
540 this.player.upnext({
541 timeout: 5000, // 5s
542
543 headText: $localize`Up Next`,
544 cancelText: $localize`Cancel`,
545 suspendedText: $localize`Autoplay is suspended`,
546
547 getTitle: () => this.nextVideoTitle,
548 520
549 next: () => this.zone.run(() => this.playNextVideoInAngularZone()), 521 this.video.state.id = VideoState.LIVE_ENDED
550 condition: () => {
551 if (!this.playlist) return this.isAutoPlayNext()
552 522
553 // Don't wait timeout to play the next playlist video 523 this.updatePlayerOnNoLive()
554 if (this.isPlaylistAutoPlayNext()) { 524 })
555 this.playNextVideoInAngularZone() 525 })
556 return undefined 526 }
557 }
558
559 return false
560 },
561
562 suspended: () => {
563 return (
564 !isXPercentInViewport(this.player.el() as HTMLElement, 80) ||
565 !document.getElementById('content').contains(document.activeElement)
566 )
567 }
568 })
569
570 this.player.one('stopped', () => {
571 if (this.playlist && this.isPlaylistAutoPlayNext()) {
572 this.playNextVideoInAngularZone()
573 }
574 })
575
576 this.player.one('ended', () => {
577 if (this.video.isLive) {
578 this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED)
579 }
580 })
581 527
582 this.player.on('theaterChange', (_: any, enabled: boolean) => { 528 player.on('theater-change', (_: any, enabled: boolean) => {
583 this.zone.run(() => this.theaterEnabled = enabled) 529 this.zone.run(() => this.theaterEnabled = enabled)
584 }) 530 })
585 531
586 this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { 532 this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', {
587 player: this.player, 533 player,
588 playlist: this.playlist, 534 playlist: this.playlist,
589 playlistPosition: this.playlistPosition, 535 playlistPosition: this.playlistPosition,
590 videojs, 536 videojs,
@@ -601,15 +547,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
601 return true 547 return true
602 } 548 }
603 549
604 private playNextVideoInAngularZone () { 550 private getNextVideoTitle () {
605 if (this.playlist) { 551 if (this.playlist) {
606 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) 552 return this.videoWatchPlaylist.getNextVideo()?.video?.name || ''
607 return
608 } 553 }
609 554
610 if (this.nextVideoUUID) { 555 return this.nextRecommendedVideoTitle
611 this.router.navigate([ '/w', this.nextVideoUUID ]) 556 }
612 } 557
558 private playNextVideoInAngularZone () {
559 this.zone.run(() => {
560 if (this.playlist) {
561 this.videoWatchPlaylist.navigateToNextPlaylistVideo()
562 return
563 }
564
565 if (this.nextRecommendedVideoUUID) {
566 this.router.navigate([ '/w', this.nextRecommendedVideoUUID ])
567 }
568 })
613 } 569 }
614 570
615 private isAutoplay () { 571 private isAutoplay () {
@@ -637,19 +593,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
637 ) 593 )
638 } 594 }
639 595
640 private flushPlayer () { 596 private buildPeerTubePlayerConstructorOptions (options: {
641 // Remove player if it exists 597 urlOptions: URLOptions
642 if (!this.player) return 598 }): PeerTubePlayerContructorOptions {
599 const { urlOptions } = options
600
601 return {
602 playerElement: () => this.playerElement.nativeElement,
603
604 enableHotkeys: true,
605 inactivityTimeout: 2500,
606
607 theaterButton: true,
608
609 controls: urlOptions.controls,
610 controlBar: urlOptions.controlBar,
611
612 muted: urlOptions.muted,
613 loop: urlOptions.loop,
614
615 playbackRate: urlOptions.playbackRate,
616
617 instanceName: this.serverConfig.instance.name,
618 language: this.localeId,
619 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
620
621 videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS,
622 authorizationHeader: () => this.authService.getRequestHeaderValue(),
623
624 serverUrl: environment.originServerUrl || window.location.origin,
643 625
644 try { 626 errorNotifier: (message: string) => this.notifier.error(message),
645 this.player.dispose() 627
646 this.player = undefined 628 peertubeLink: () => false,
647 } catch (err) { 629
648 logger.error('Cannot dispose player.', err) 630 pluginsManager: this.pluginService.getPluginsManager()
649 } 631 }
650 } 632 }
651 633
652 private buildPlayerManagerOptions (params: { 634 private buildPeerTubePlayerLoadOptions (options: {
653 video: VideoDetails 635 video: VideoDetails
654 liveVideo: LiveVideo 636 liveVideo: LiveVideo
655 videoCaptions: VideoCaption[] 637 videoCaptions: VideoCaption[]
@@ -658,12 +640,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
658 videoFileToken: string 640 videoFileToken: string
659 videoPassword: string 641 videoPassword: string
660 642
661 urlOptions: CustomizationOptions & { playerMode: PlayerMode } 643 urlOptions: URLOptions
662 644
663 loggedInOrAnonymousUser: User 645 loggedInOrAnonymousUser: User
664 forceAutoplay: boolean 646 forceAutoplay: boolean
665 user?: AuthUser // Keep for plugins 647 user?: AuthUser // Keep for plugins
666 }) { 648 }): PeerTubePlayerLoadOptions {
667 const { 649 const {
668 video, 650 video,
669 liveVideo, 651 liveVideo,
@@ -674,7 +656,30 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
674 urlOptions, 656 urlOptions,
675 loggedInOrAnonymousUser, 657 loggedInOrAnonymousUser,
676 forceAutoplay 658 forceAutoplay
677 } = params 659 } = options
660
661 let mode: PlayerMode
662
663 if (urlOptions.playerMode) {
664 if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader'
665 else mode = 'web-video'
666 } else {
667 if (video.hasHlsPlaylist()) mode = 'p2p-media-loader'
668 else mode = 'web-video'
669 }
670
671 let hlsOptions: HLSOptions
672 if (video.hasHlsPlaylist()) {
673 const hlsPlaylist = video.getHlsPlaylist()
674
675 hlsOptions = {
676 playlistUrl: hlsPlaylist.playlistUrl,
677 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
678 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
679 trackerAnnounce: video.trackerUrls,
680 videoFiles: hlsPlaylist.files
681 }
682 }
678 683
679 const getStartTime = () => { 684 const getStartTime = () => {
680 const byUrl = urlOptions.startTime !== undefined 685 const byUrl = urlOptions.startTime !== undefined
@@ -714,118 +719,80 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
714 ? { latencyMode: liveVideo.latencyMode } 719 ? { latencyMode: liveVideo.latencyMode }
715 : undefined 720 : undefined
716 721
717 const options: PeertubePlayerManagerOptions = { 722 return {
718 common: { 723 mode,
719 autoplay: this.isAutoplay(),
720 forceAutoplay,
721 p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
722
723 hasNextVideo: () => this.hasNextVideo(),
724 nextVideo: () => this.playNextVideoInAngularZone(),
725
726 playerElement: this.playerElement,
727 onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
728 724
729 videoDuration: video.duration, 725 autoplay: this.isAutoplay(),
730 enableHotkeys: true, 726 forceAutoplay,
731 inactivityTimeout: 2500,
732 poster: video.previewUrl,
733
734 startTime,
735 stopTime: urlOptions.stopTime,
736 controlBar: urlOptions.controlBar,
737 controls: urlOptions.controls,
738 muted: urlOptions.muted,
739 loop: urlOptions.loop,
740 subtitle: urlOptions.subtitle,
741 playbackRate: urlOptions.playbackRate,
742
743 peertubeLink: urlOptions.peertubeLink,
744 727
745 theaterButton: true, 728 duration: this.video.duration,
746 captions: videoCaptions.length !== 0, 729 poster: video.previewUrl,
730 p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
747 731
748 embedUrl: video.embedUrl, 732 startTime,
749 embedTitle: video.name, 733 stopTime: urlOptions.stopTime,
750 instanceName: this.serverConfig.instance.name,
751 734
752 isLive: video.isLive, 735 embedUrl: video.embedUrl,
753 liveOptions, 736 embedTitle: video.name,
754 737
755 language: this.localeId, 738 isLive: video.isLive,
739 liveOptions,
756 740
757 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', 741 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
742 ? this.videoService.getVideoViewUrl(video.uuid)
743 : null,
758 744
759 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE 745 videoFileToken: () => videoFileToken,
760 ? this.videoService.getVideoViewUrl(video.uuid) 746 requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
761 : null, 747 requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
762 videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, 748 !video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
763 authorizationHeader: () => this.authService.getRequestHeaderValue(), 749 videoPassword: () => videoPassword,
764 750
765 serverUrl: environment.originServerUrl || window.location.origin, 751 videoCaptions: playerCaptions,
752 storyboard,
766 753
767 videoFileToken: () => videoFileToken, 754 videoShortUUID: video.shortUUID,
768 requiresUserAuth: videoRequiresUserAuth(video, videoPassword), 755 videoUUID: video.uuid,
769 requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
770 !video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
771 videoPassword: () => videoPassword,
772 756
773 videoCaptions: playerCaptions, 757 previousVideo: {
774 storyboard, 758 enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(),
775 759
776 videoShortUUID: video.shortUUID, 760 handler: this.playlist
777 videoUUID: video.uuid, 761 ? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo())
762 : undefined,
778 763
779 errorNotifier: (message: string) => this.notifier.error(message) 764 displayControlBarButton: !!this.playlist
780 }, 765 },
781 766
782 webtorrent: { 767 nextVideo: {
783 videoFiles: video.files 768 enabled: this.hasNextVideo(),
769 handler: () => this.playNextVideoInAngularZone(),
770 getVideoTitle: () => this.getNextVideoTitle(),
771 displayControlBarButton: this.hasNextVideo()
784 }, 772 },
785 773
786 pluginsManager: this.pluginService.getPluginsManager() 774 upnext: {
787 } 775 isEnabled: () => {
788 776 if (this.playlist) return this.isPlaylistAutoPlayNext()
789 // Only set this if we're in a playlist
790 if (this.playlist) {
791 options.common.hasPreviousVideo = () => this.videoWatchPlaylist.hasPreviousVideo()
792
793 options.common.previousVideo = () => {
794 this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo())
795 }
796 }
797
798 let mode: PlayerMode
799 777
800 if (urlOptions.playerMode) { 778 return this.isAutoPlayNext()
801 if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' 779 },
802 else mode = 'webtorrent'
803 } else {
804 if (video.hasHlsPlaylist()) mode = 'p2p-media-loader'
805 else mode = 'webtorrent'
806 }
807 780
808 // FIXME: remove, we don't support these old web browsers anymore 781 isSuspended: (player: videojs.Player) => {
809 // p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available 782 return !isXPercentInViewport(player.el() as HTMLElement, 80)
810 if (typeof TextEncoder === 'undefined') { 783 },
811 mode = 'webtorrent'
812 }
813 784
814 if (mode === 'p2p-media-loader') { 785 timeout: this.playlist
815 const hlsPlaylist = video.getHlsPlaylist() 786 ? 0 // Don't wait to play next video in playlist
787 : 5000 // 5 seconds for a recommended video
788 },
816 789
817 const p2pMediaLoader = { 790 hls: hlsOptions,
818 playlistUrl: hlsPlaylist.playlistUrl,
819 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
820 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
821 trackerAnnounce: video.trackerUrls,
822 videoFiles: hlsPlaylist.files
823 } as P2PMediaLoaderOptions
824 791
825 Object.assign(options, { p2pMediaLoader }) 792 webVideo: {
793 videoFiles: video.files
794 }
826 } 795 }
827
828 return { playerMode: mode, playerOptions: options }
829 } 796 }
830 797
831 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { 798 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
@@ -873,6 +840,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
873 this.video.viewers = newViewers 840 this.video.viewers = newViewers
874 } 841 }
875 842
843 private updatePlayerOnNoLive () {
844 this.peertubePlayer.unload()
845 this.peertubePlayer.disable()
846 this.peertubePlayer.setPoster(this.video.previewPath)
847 }
848
876 private buildHotkeysHelp (video: Video) { 849 private buildHotkeysHelp (video: Video) {
877 if (this.hotkeys.length !== 0) { 850 if (this.hotkeys.length !== 0) {
878 this.hotkeysService.remove(this.hotkeys) 851 this.hotkeysService.remove(this.hotkeys)
@@ -944,4 +917,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
944 this.metaService.setTag('og:url', window.location.href) 917 this.metaService.setTag('og:url', window.location.href)
945 this.metaService.setTag('url', window.location.href) 918 this.metaService.setTag('url', window.location.href)
946 } 919 }
920
921 private getUrlOptions (): URLOptions {
922 const queryParams = this.route.snapshot.queryParams
923
924 return {
925 resume: queryParams.resume,
926
927 startTime: queryParams.start,
928 stopTime: queryParams.stop,
929
930 muted: toBoolean(queryParams.muted),
931 loop: toBoolean(queryParams.loop),
932 subtitle: queryParams.subtitle,
933
934 playerMode: queryParams.mode,
935 playbackRate: queryParams.playbackRate,
936
937 controlBar: toBoolean(queryParams.controlBar),
938
939 peertubeLink: false
940 }
941 }
947} 942}
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts
index 69b2b18c0..b69e31edf 100644
--- a/client/src/app/helpers/utils/object.ts
+++ b/client/src/app/helpers/utils/object.ts
@@ -34,6 +34,8 @@ function toBoolean (value: any) {
34 34
35 if (value === 'true') return true 35 if (value === 'true') return true
36 if (value === 'false') return false 36 if (value === 'false') return false
37 if (value === '1') return true
38 if (value === '0') return false
37 39
38 return undefined 40 return undefined
39} 41}
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
index 45df0be38..14a5abd7a 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
@@ -241,7 +241,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
241 } 241 }
242 242
243 reloadVideos () { 243 reloadVideos () {
244 console.log('reload')
245 this.pagination.currentPage = 1 244 this.pagination.currentPage = 1
246 this.loadMoreVideos(true) 245 this.loadMoreVideos(true)
247 } 246 }
@@ -420,8 +419,9 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
420 this.lastQueryLength = data.length 419 this.lastQueryLength = data.length
421 420
422 if (reset) this.videos = [] 421 if (reset) this.videos = []
422
423 this.videos = this.videos.concat(data) 423 this.videos = this.videos.concat(data)
424 console.log('subscribe') 424
425 if (this.groupByDate) this.buildGroupedDateLabels() 425 if (this.groupByDate) this.buildGroupedDateLabels()
426 426
427 this.onDataSubject.next(data) 427 this.onDataSubject.next(data)