diff options
Diffstat (limited to 'client/src/app')
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 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' | 2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' |
3 | import { VideoJsPlayer } from 'video.js' | ||
4 | import { PlatformLocation } from '@angular/common' | 3 | import { PlatformLocation } from '@angular/common' |
5 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
6 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -19,13 +18,13 @@ import { | |||
19 | UserService | 18 | UserService |
20 | } from '@app/core' | 19 | } from '@app/core' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 20 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 23 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 24 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 26 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled, videoRequiresUserAuth, videoRequiresFileToken } from '@root-helpers/video' | 27 | import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 28 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 29 | import { |
31 | HTMLServerConfig, | 30 | HTMLServerConfig, |
@@ -39,10 +38,10 @@ import { | |||
39 | VideoState | 38 | VideoState |
40 | } from '@shared/models' | 39 | } from '@shared/models' |
41 | import { | 40 | import { |
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 | |||
50 | import { environment } from '../../../environments/environment' | 49 | import { environment } from '../../../environments/environment' |
51 | import { VideoWatchPlaylistComponent } from './shared' | 50 | import { VideoWatchPlaylistComponent } from './shared' |
52 | 51 | ||
53 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 52 | type 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 } | |||
60 | export class VideoWatchComponent implements OnInit, OnDestroy { | 76 | export 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) |