aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/videos')
-rw-r--r--client/src/app/videos/+video-watch/video-watch-routing.module.ts8
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html34
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss54
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts197
4 files changed, 258 insertions, 35 deletions
diff --git a/client/src/app/videos/+video-watch/video-watch-routing.module.ts b/client/src/app/videos/+video-watch/video-watch-routing.module.ts
index 0d7809044..ce9250bdc 100644
--- a/client/src/app/videos/+video-watch/video-watch-routing.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch-routing.module.ts
@@ -7,16 +7,16 @@ import { VideoWatchComponent } from './video-watch.component'
7 7
8const videoWatchRoutes: Routes = [ 8const videoWatchRoutes: Routes = [
9 { 9 {
10 path: 'playlist/:uuid', 10 path: 'playlist/:playlistId',
11 component: VideoWatchComponent, 11 component: VideoWatchComponent,
12 canActivate: [ MetaGuard ] 12 canActivate: [ MetaGuard ]
13 }, 13 },
14 { 14 {
15 path: ':uuid/comments/:commentId', 15 path: ':videoId/comments/:commentId',
16 redirectTo: ':uuid' 16 redirectTo: ':videoId'
17 }, 17 },
18 { 18 {
19 path: ':uuid', 19 path: ':videoId',
20 component: VideoWatchComponent, 20 component: VideoWatchComponent,
21 canActivate: [ MetaGuard ] 21 canActivate: [ MetaGuard ]
22 } 22 }
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 394c31f23..7f3d1cc2e 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -1,11 +1,39 @@
1<div class="root-row row"> 1<div class="root-row row">
2 <!-- We need the video container for videojs so we just hide it --> 2 <!-- We need the video container for videojs so we just hide it -->
3 <div id="video-element-wrapper"> 3 <div id="video-wrapper">
4 <div *ngIf="remoteServerDown" class="remote-server-down"> 4 <div *ngIf="remoteServerDown" class="remote-server-down">
5 Sorry, but this video is not available because the remote instance is not responding. 5 Sorry, but this video is not available because the remote instance is not responding.
6 <br /> 6 <br />
7 Please try again later. 7 Please try again later.
8 </div> 8 </div>
9
10 <div id="videojs-wrapper"></div>
11
12 <div *ngIf="playlist && video" class="playlist">
13 <div class="playlist-info">
14 <div class="playlist-display-name">
15 {{ playlist.displayName }}
16
17 <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span>
18 <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span>
19 <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span>
20 </div>
21
22 <div class="playlist-by-index">
23 <div class="playlist-by">{{ playlist.ownerBy }}</div>
24 <div class="playlist-index">
25 <span>{{currentPlaylistPosition}}</span><span>{{playlistPagination.totalItems}}</span>
26 </div>
27 </div>
28 </div>
29
30 <div *ngFor="let playlistVideo of playlistVideos" myInfiniteScroller [autoInit]="true" #elem [container]="elem" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
31 <my-video-playlist-element-miniature
32 [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
33 [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false"
34 ></my-video-playlist-element-miniature>
35 </div>
36 </div>
9 </div> 37 </div>
10 38
11 <div i18n class="alert alert-warning" *ngIf="isVideoToImport()"> 39 <div i18n class="alert alert-warning" *ngIf="isVideoToImport()">
@@ -20,6 +48,10 @@
20 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. 48 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
21 </div> 49 </div>
22 50
51 <div i18n class="alert alert-info" *ngIf="noPlaylistVideos">
52 This playlist does not have videos.
53 </div>
54
23 <div class="alert alert-danger" *ngIf="video?.blacklisted"> 55 <div class="alert alert-danger" *ngIf="video?.blacklisted">
24 <div class="blacklisted-label" i18n>This video is blacklisted.</div> 56 <div class="blacklisted-label" i18n>This video is blacklisted.</div>
25 {{ video.blacklistedReason }} 57 {{ video.blacklistedReason }}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 44040e90d..e1cb249ef 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -1,6 +1,7 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '_bootstrap-variables'; 3@import '_bootstrap-variables';
4@import '_miniature';
4 5
5$other-videos-width: 260px; 6$other-videos-width: 260px;
6 7
@@ -12,7 +13,7 @@ $other-videos-width: 260px;
12 font-weight: $font-semibold; 13 font-weight: $font-semibold;
13} 14}
14 15
15#video-element-wrapper { 16#video-wrapper {
16 background-color: #000; 17 background-color: #000;
17 display: flex; 18 display: flex;
18 justify-content: center; 19 justify-content: center;
@@ -39,6 +40,57 @@ $other-videos-width: 260px;
39 } 40 }
40 } 41 }
41 42
43 .playlist {
44 width: 400px;
45 height: 66vh;
46 background-color: #e4e4e4;
47 overflow-y: auto;
48
49 .playlist-info {
50 padding: 5px 30px;
51
52 .playlist-display-name {
53 font-size: 18px;
54 font-weight: $font-semibold;
55 margin-bottom: 5px;
56 }
57
58 .playlist-by-index {
59 color: $grey-foreground-color;
60 display: flex;
61
62 .playlist-by {
63 margin-right: 5px;
64 }
65
66 .playlist-index span:first-child::after {
67 content: '/';
68 margin: 0 3px;
69 }
70 }
71 }
72
73 my-video-playlist-element-miniature {
74 /deep/ {
75 .video {
76 .position {
77 margin-right: 0;
78 }
79
80 .video-info {
81 .video-info-name {
82 font-size: 15px;
83 }
84 }
85 }
86
87 my-video-thumbnail {
88 @include thumbnail-size-component(90px, 50px);
89 }
90 }
91 }
92 }
93
42 /deep/ .video-js { 94 /deep/ .video-js {
43 width: calc(66vh * 1.77); 95 width: calc(66vh * 1.77);
44 height: 66vh; 96 height: 66vh;
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'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPlaylistPrivacy, VideoPrivacy, VideoState } from '../../../../../shared'
12import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
13import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
14import { VideoDetails } from '../../shared/video/video-details.model' 14import { 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'
31import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
32import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
33import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
34import { 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}