diff options
Diffstat (limited to 'client/src/app/videos/+video-watch/video-watch.component.ts')
-rw-r--r-- | client/src/app/videos/+video-watch/video-watch.component.ts | 197 |
1 files changed, 168 insertions, 29 deletions
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 359217f3b..ddd0f1766 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -8,7 +8,7 @@ import { MetaService } from '@ngx-meta/core' | |||
8 | import { Notifier, ServerService } from '@app/core' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Subscription } from 'rxjs' |
10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' | 11 | import { UserVideoRateType, VideoCaption, VideoPlaylistPrivacy, VideoPrivacy, VideoState } from '../../../../../shared' |
12 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
13 | import { RestExtractor, VideoBlacklistService } from '../../shared' | 13 | import { RestExtractor, VideoBlacklistService } from '../../shared' |
14 | import { VideoDetails } from '../../shared/video/video-details.model' | 14 | import { VideoDetails } from '../../shared/video/video-details.model' |
@@ -28,6 +28,10 @@ import { | |||
28 | PeertubePlayerManagerOptions, | 28 | PeertubePlayerManagerOptions, |
29 | PlayerMode | 29 | PlayerMode |
30 | } from '../../../assets/player/peertube-player-manager' | 30 | } from '../../../assets/player/peertube-player-manager' |
31 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
32 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
33 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
34 | import { Video } from '@app/shared/video/video.model' | ||
31 | 35 | ||
32 | @Component({ | 36 | @Component({ |
33 | selector: 'my-video-watch', | 37 | selector: 'my-video-watch', |
@@ -50,6 +54,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
50 | video: VideoDetails = null | 54 | video: VideoDetails = null |
51 | descriptionLoading = false | 55 | descriptionLoading = false |
52 | 56 | ||
57 | playlist: VideoPlaylist = null | ||
58 | playlistVideos: Video[] = [] | ||
59 | playlistPagination: ComponentPagination = { | ||
60 | currentPage: 1, | ||
61 | itemsPerPage: 10, | ||
62 | totalItems: null | ||
63 | } | ||
64 | noPlaylistVideos = false | ||
65 | currentPlaylistPosition = 1 | ||
66 | |||
53 | completeDescriptionShown = false | 67 | completeDescriptionShown = false |
54 | completeVideoDescription: string | 68 | completeVideoDescription: string |
55 | shortVideoDescription: string | 69 | shortVideoDescription: string |
@@ -61,6 +75,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
61 | 75 | ||
62 | private currentTime: number | 76 | private currentTime: number |
63 | private paramsSub: Subscription | 77 | private paramsSub: Subscription |
78 | private queryParamsSub: Subscription | ||
64 | 79 | ||
65 | constructor ( | 80 | constructor ( |
66 | private elementRef: ElementRef, | 81 | private elementRef: ElementRef, |
@@ -68,6 +83,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
68 | private route: ActivatedRoute, | 83 | private route: ActivatedRoute, |
69 | private router: Router, | 84 | private router: Router, |
70 | private videoService: VideoService, | 85 | private videoService: VideoService, |
86 | private playlistService: VideoPlaylistService, | ||
71 | private videoBlacklistService: VideoBlacklistService, | 87 | private videoBlacklistService: VideoBlacklistService, |
72 | private confirmService: ConfirmService, | 88 | private confirmService: ConfirmService, |
73 | private metaService: MetaService, | 89 | private metaService: MetaService, |
@@ -97,31 +113,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
97 | } | 113 | } |
98 | 114 | ||
99 | this.paramsSub = this.route.params.subscribe(routeParams => { | 115 | this.paramsSub = this.route.params.subscribe(routeParams => { |
100 | const uuid = routeParams[ 'uuid' ] | 116 | const videoId = routeParams[ 'videoId' ] |
117 | if (videoId) this.loadVideo(videoId) | ||
101 | 118 | ||
102 | // Video did not change | 119 | const playlistId = routeParams[ 'playlistId' ] |
103 | if (this.video && this.video.uuid === uuid) return | 120 | if (playlistId) this.loadPlaylist(playlistId) |
104 | 121 | }) | |
105 | if (this.player) this.player.pause() | ||
106 | 122 | ||
107 | // Video did change | 123 | this.queryParamsSub = this.route.queryParams.subscribe(queryParams => { |
108 | forkJoin( | 124 | const videoId = queryParams[ 'videoId' ] |
109 | this.videoService.getVideo(uuid), | 125 | if (videoId) this.loadVideo(videoId) |
110 | this.videoCaptionService.listCaptions(uuid) | ||
111 | ) | ||
112 | .pipe( | ||
113 | // If 401, the video is private or blacklisted so redirect to 404 | ||
114 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) | ||
115 | ) | ||
116 | .subscribe(([ video, captionsResult ]) => { | ||
117 | const startTime = this.route.snapshot.queryParams.start | ||
118 | const stopTime = this.route.snapshot.queryParams.stop | ||
119 | const subtitle = this.route.snapshot.queryParams.subtitle | ||
120 | const playerMode = this.route.snapshot.queryParams.mode | ||
121 | |||
122 | this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode }) | ||
123 | .catch(err => this.handleError(err)) | ||
124 | }) | ||
125 | }) | 126 | }) |
126 | 127 | ||
127 | this.hotkeys = [ | 128 | this.hotkeys = [ |
@@ -147,7 +148,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
147 | this.flushPlayer() | 148 | this.flushPlayer() |
148 | 149 | ||
149 | // Unsubscribe subscriptions | 150 | // Unsubscribe subscriptions |
150 | this.paramsSub.unsubscribe() | 151 | if (this.paramsSub) this.paramsSub.unsubscribe() |
152 | if (this.queryParamsSub) this.queryParamsSub.unsubscribe() | ||
151 | 153 | ||
152 | // Unbind hotkeys | 154 | // Unbind hotkeys |
153 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) | 155 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) |
@@ -219,8 +221,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
219 | } | 221 | } |
220 | 222 | ||
221 | showShareModal () { | 223 | showShareModal () { |
222 | const currentTime = this.player ? this.player.currentTime() : undefined | ||
223 | |||
224 | this.videoShareModal.show(this.currentTime) | 224 | this.videoShareModal.show(this.currentTime) |
225 | } | 225 | } |
226 | 226 | ||
@@ -322,6 +322,107 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
322 | return this.video && this.video.scheduledUpdate !== undefined | 322 | return this.video && this.video.scheduledUpdate !== undefined |
323 | } | 323 | } |
324 | 324 | ||
325 | isVideoBlur (video: Video) { | ||
326 | return video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) | ||
327 | } | ||
328 | |||
329 | isPlaylistOwned () { | ||
330 | return this.playlist.isLocal === true && this.playlist.ownerAccount.name === this.user.username | ||
331 | } | ||
332 | |||
333 | isUnlistedPlaylist () { | ||
334 | return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED | ||
335 | } | ||
336 | |||
337 | isPrivatePlaylist () { | ||
338 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE | ||
339 | } | ||
340 | |||
341 | isPublicPlaylist () { | ||
342 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC | ||
343 | } | ||
344 | |||
345 | onPlaylistVideosNearOfBottom () { | ||
346 | // Last page | ||
347 | if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return | ||
348 | |||
349 | this.playlistPagination.currentPage += 1 | ||
350 | this.loadPlaylistElements(false) | ||
351 | } | ||
352 | |||
353 | onElementRemoved (video: Video) { | ||
354 | this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id) | ||
355 | |||
356 | this.playlistPagination.totalItems-- | ||
357 | } | ||
358 | |||
359 | private loadVideo (videoId: string) { | ||
360 | // Video did not change | ||
361 | if (this.video && this.video.uuid === videoId) return | ||
362 | |||
363 | if (this.player) this.player.pause() | ||
364 | |||
365 | // Video did change | ||
366 | forkJoin( | ||
367 | this.videoService.getVideo(videoId), | ||
368 | this.videoCaptionService.listCaptions(videoId) | ||
369 | ) | ||
370 | .pipe( | ||
371 | // If 401, the video is private or blacklisted so redirect to 404 | ||
372 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) | ||
373 | ) | ||
374 | .subscribe(([ video, captionsResult ]) => { | ||
375 | const queryParams = this.route.snapshot.queryParams | ||
376 | const startTime = queryParams.start | ||
377 | const stopTime = queryParams.stop | ||
378 | const subtitle = queryParams.subtitle | ||
379 | const playerMode = queryParams.mode | ||
380 | |||
381 | this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode }) | ||
382 | .catch(err => this.handleError(err)) | ||
383 | }) | ||
384 | } | ||
385 | |||
386 | private loadPlaylist (playlistId: string) { | ||
387 | // Playlist did not change | ||
388 | if (this.playlist && this.playlist.uuid === playlistId) return | ||
389 | |||
390 | this.playlistService.getVideoPlaylist(playlistId) | ||
391 | .pipe( | ||
392 | // If 401, the video is private or blacklisted so redirect to 404 | ||
393 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) | ||
394 | ) | ||
395 | .subscribe(playlist => { | ||
396 | this.playlist = playlist | ||
397 | |||
398 | const videoId = this.route.snapshot.queryParams['videoId'] | ||
399 | this.loadPlaylistElements(!videoId) | ||
400 | }) | ||
401 | } | ||
402 | |||
403 | private loadPlaylistElements (redirectToFirst = false) { | ||
404 | this.videoService.getPlaylistVideos(this.playlist.id, this.playlistPagination) | ||
405 | .subscribe(({ totalVideos, videos }) => { | ||
406 | this.playlistVideos = this.playlistVideos.concat(videos) | ||
407 | this.playlistPagination.totalItems = totalVideos | ||
408 | |||
409 | if (totalVideos === 0) { | ||
410 | this.noPlaylistVideos = true | ||
411 | return | ||
412 | } | ||
413 | |||
414 | this.updatePlaylistIndex() | ||
415 | |||
416 | if (redirectToFirst) { | ||
417 | const extras = { | ||
418 | queryParams: { videoId: this.playlistVideos[ 0 ].uuid }, | ||
419 | replaceUrl: true | ||
420 | } | ||
421 | this.router.navigate([], extras) | ||
422 | } | ||
423 | }) | ||
424 | } | ||
425 | |||
325 | private updateVideoDescription (description: string) { | 426 | private updateVideoDescription (description: string) { |
326 | this.video.description = description | 427 | this.video.description = description |
327 | this.setVideoDescriptionHTML() | 428 | this.setVideoDescriptionHTML() |
@@ -383,11 +484,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
383 | this.remoteServerDown = false | 484 | this.remoteServerDown = false |
384 | this.currentTime = undefined | 485 | this.currentTime = undefined |
385 | 486 | ||
487 | this.updatePlaylistIndex() | ||
488 | |||
386 | let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) | 489 | let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) |
387 | // If we are at the end of the video, reset the timer | 490 | // If we are at the end of the video, reset the timer |
388 | if (this.video.duration - startTime <= 1) startTime = 0 | 491 | if (this.video.duration - startTime <= 1) startTime = 0 |
389 | 492 | ||
390 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { | 493 | if (this.isVideoBlur(this.video)) { |
391 | const res = await this.confirmService.confirm( | 494 | const res = await this.confirmService.confirm( |
392 | this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'), | 495 | this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'), |
393 | this.i18n('Mature or explicit content') | 496 | this.i18n('Mature or explicit content') |
@@ -399,7 +502,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
399 | this.flushPlayer() | 502 | this.flushPlayer() |
400 | 503 | ||
401 | // Build video element, because videojs remove it on dispose | 504 | // Build video element, because videojs remove it on dispose |
402 | const playerElementWrapper = this.elementRef.nativeElement.querySelector('#video-element-wrapper') | 505 | const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper') |
403 | this.playerElement = document.createElement('video') | 506 | this.playerElement = document.createElement('video') |
404 | this.playerElement.className = 'video-js vjs-peertube-skin' | 507 | this.playerElement.className = 'video-js vjs-peertube-skin' |
405 | this.playerElement.setAttribute('playsinline', 'true') | 508 | this.playerElement.setAttribute('playsinline', 'true') |
@@ -474,6 +577,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
474 | this.player.on('timeupdate', () => { | 577 | this.player.on('timeupdate', () => { |
475 | this.currentTime = Math.floor(this.player.currentTime()) | 578 | this.currentTime = Math.floor(this.player.currentTime()) |
476 | }) | 579 | }) |
580 | |||
581 | this.player.one('ended', () => { | ||
582 | if (this.playlist) { | ||
583 | this.zone.run(() => this.navigateToNextPlaylistVideo()) | ||
584 | } | ||
585 | }) | ||
586 | |||
587 | this.player.one('stopped', () => { | ||
588 | if (this.playlist) { | ||
589 | this.zone.run(() => this.navigateToNextPlaylistVideo()) | ||
590 | } | ||
591 | }) | ||
477 | }) | 592 | }) |
478 | 593 | ||
479 | this.setVideoDescriptionHTML() | 594 | this.setVideoDescriptionHTML() |
@@ -528,6 +643,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
528 | this.setVideoLikesBarTooltipText() | 643 | this.setVideoLikesBarTooltipText() |
529 | } | 644 | } |
530 | 645 | ||
646 | private updatePlaylistIndex () { | ||
647 | if (this.playlistVideos.length === 0 || !this.video) return | ||
648 | |||
649 | for (const video of this.playlistVideos) { | ||
650 | if (video.id === this.video.id) { | ||
651 | this.currentPlaylistPosition = video.playlistElement.position | ||
652 | return | ||
653 | } | ||
654 | } | ||
655 | |||
656 | // Load more videos to find our video | ||
657 | this.onPlaylistVideosNearOfBottom() | ||
658 | } | ||
659 | |||
531 | private setOpenGraphTags () { | 660 | private setOpenGraphTags () { |
532 | this.metaService.setTitle(this.video.name) | 661 | this.metaService.setTitle(this.video.name) |
533 | 662 | ||
@@ -567,4 +696,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
567 | this.player = undefined | 696 | this.player = undefined |
568 | } | 697 | } |
569 | } | 698 | } |
699 | |||
700 | private navigateToNextPlaylistVideo () { | ||
701 | if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { | ||
702 | const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1) | ||
703 | |||
704 | const start = next.playlistElement.startTimestamp | ||
705 | const stop = next.playlistElement.stopTimestamp | ||
706 | this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } }) | ||
707 | } | ||
708 | } | ||
570 | } | 709 | } |