]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/videos/+video-watch/video-watch.component.ts
Cleaner videojs control bar implementation
[github/Chocobozzz/PeerTube.git] / client / src / app / videos / +video-watch / video-watch.component.ts
1 import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
2 import { ActivatedRoute, Router } from '@angular/router'
3 import { RedirectService } from '@app/core/routing/redirect.service'
4 import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
5 import { MetaService } from '@ngx-meta/core'
6 import { NotificationsService } from 'angular2-notifications'
7 import { Subscription } from 'rxjs/Subscription'
8 import * as videojs from 'video.js'
9 import 'videojs-hotkeys'
10 import { UserVideoRateType, VideoRateType } from '../../../../../shared'
11 import '../../../assets/player/peertube-videojs-plugin'
12 import { AuthService, ConfirmService } from '../../core'
13 import { VideoBlacklistService } from '../../shared'
14 import { Account } from '../../shared/account/account.model'
15 import { VideoDetails } from '../../shared/video/video-details.model'
16 import { Video } from '../../shared/video/video.model'
17 import { VideoService } from '../../shared/video/video.service'
18 import { MarkdownService } from '../shared'
19 import { VideoDownloadComponent } from './modal/video-download.component'
20 import { VideoReportComponent } from './modal/video-report.component'
21 import { VideoShareComponent } from './modal/video-share.component'
22
23 @Component({
24 selector: 'my-video-watch',
25 templateUrl: './video-watch.component.html',
26 styleUrls: [ './video-watch.component.scss' ]
27 })
28 export class VideoWatchComponent implements OnInit, OnDestroy {
29 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
30
31 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
32 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
33 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
34 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
35
36 otherVideosDisplayed: Video[] = []
37
38 error = false
39 player: videojs.Player
40 playerElement: HTMLVideoElement
41 userRating: UserVideoRateType = null
42 video: VideoDetails = null
43 videoPlayerLoaded = false
44 videoNotFound = false
45 descriptionLoading = false
46
47 completeDescriptionShown = false
48 completeVideoDescription: string
49 shortVideoDescription: string
50 videoHTMLDescription = ''
51 likesBarTooltipText = ''
52
53 private otherVideos: Video[] = []
54 private paramsSub: Subscription
55
56 constructor (
57 private elementRef: ElementRef,
58 private route: ActivatedRoute,
59 private router: Router,
60 private videoService: VideoService,
61 private videoBlacklistService: VideoBlacklistService,
62 private confirmService: ConfirmService,
63 private metaService: MetaService,
64 private authService: AuthService,
65 private notificationsService: NotificationsService,
66 private markdownService: MarkdownService,
67 private zone: NgZone,
68 private redirectService: RedirectService
69 ) {}
70
71 get user () {
72 return this.authService.getUser()
73 }
74
75 ngOnInit () {
76 this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
77 .subscribe(
78 data => {
79 this.otherVideos = data.videos
80 this.updateOtherVideosDisplayed()
81 },
82
83 err => console.error(err)
84 )
85
86 this.paramsSub = this.route.params.subscribe(routeParams => {
87 if (this.videoPlayerLoaded) {
88 this.player.pause()
89 }
90
91 const uuid = routeParams['uuid']
92 // Video did not changed
93 if (this.video && this.video.uuid === uuid) return
94
95 this.videoService.getVideo(uuid).subscribe(
96 video => this.onVideoFetched(video),
97
98 error => {
99 this.videoNotFound = true
100 console.error(error)
101 }
102 )
103 })
104 }
105
106 ngOnDestroy () {
107 // Remove player if it exists
108 if (this.videoPlayerLoaded === true) {
109 videojs(this.playerElement).dispose()
110 }
111
112 // Unsubscribe subscriptions
113 this.paramsSub.unsubscribe()
114 }
115
116 setLike () {
117 if (this.isUserLoggedIn() === false) return
118 if (this.userRating === 'like') {
119 // Already liked this video
120 this.setRating('none')
121 } else {
122 this.setRating('like')
123 }
124 }
125
126 setDislike () {
127 if (this.isUserLoggedIn() === false) return
128 if (this.userRating === 'dislike') {
129 // Already disliked this video
130 this.setRating('none')
131 } else {
132 this.setRating('dislike')
133 }
134 }
135
136 async blacklistVideo (event: Event) {
137 event.preventDefault()
138
139 const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist')
140 if (res === false) return
141
142 this.videoBlacklistService.blacklistVideo(this.video.id)
143 .subscribe(
144 status => {
145 this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
146 this.redirectService.redirectToHomepage()
147 },
148
149 error => this.notificationsService.error('Error', error.message)
150 )
151 }
152
153 showMoreDescription () {
154 if (this.completeVideoDescription === undefined) {
155 return this.loadCompleteDescription()
156 }
157
158 this.updateVideoDescription(this.completeVideoDescription)
159 this.completeDescriptionShown = true
160 }
161
162 showLessDescription () {
163 this.updateVideoDescription(this.shortVideoDescription)
164 this.completeDescriptionShown = false
165 }
166
167 loadCompleteDescription () {
168 this.descriptionLoading = true
169
170 this.videoService.loadCompleteDescription(this.video.descriptionPath)
171 .subscribe(
172 description => {
173 this.completeDescriptionShown = true
174 this.descriptionLoading = false
175
176 this.shortVideoDescription = this.video.description
177 this.completeVideoDescription = description
178
179 this.updateVideoDescription(this.completeVideoDescription)
180 },
181
182 error => {
183 this.descriptionLoading = false
184 this.notificationsService.error('Error', error.message)
185 }
186 )
187 }
188
189 showReportModal (event: Event) {
190 event.preventDefault()
191 this.videoReportModal.show()
192 }
193
194 showSupportModal () {
195 this.videoSupportModal.show()
196 }
197
198 showShareModal () {
199 this.videoShareModal.show()
200 }
201
202 showDownloadModal (event: Event) {
203 event.preventDefault()
204 this.videoDownloadModal.show()
205 }
206
207 isUserLoggedIn () {
208 return this.authService.isLoggedIn()
209 }
210
211 isVideoUpdatable () {
212 return this.video.isUpdatableBy(this.authService.getUser())
213 }
214
215 isVideoBlacklistable () {
216 return this.video.isBlackistableBy(this.user)
217 }
218
219 getAvatarPath () {
220 return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
221 }
222
223 getVideoPoster () {
224 if (!this.video) return ''
225
226 return this.video.previewUrl
227 }
228
229 getVideoTags () {
230 if (!this.video || Array.isArray(this.video.tags) === false) return []
231
232 return this.video.tags.join(', ')
233 }
234
235 isVideoRemovable () {
236 return this.video.isRemovableBy(this.authService.getUser())
237 }
238
239 async removeVideo (event: Event) {
240 event.preventDefault()
241
242 const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
243 if (res === false) return
244
245 this.videoService.removeVideo(this.video.id)
246 .subscribe(
247 status => {
248 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
249
250 // Go back to the video-list.
251 this.redirectService.redirectToHomepage()
252 },
253
254 error => this.notificationsService.error('Error', error.message)
255 )
256 }
257
258 private updateVideoDescription (description: string) {
259 this.video.description = description
260 this.setVideoDescriptionHTML()
261 }
262
263 private setVideoDescriptionHTML () {
264 if (!this.video.description) {
265 this.videoHTMLDescription = ''
266 return
267 }
268
269 this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description)
270 }
271
272 private setVideoLikesBarTooltipText () {
273 this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
274 }
275
276 private handleError (err: any) {
277 const errorMessage: string = typeof err === 'string' ? err : err.message
278 if (!errorMessage) return
279
280 let message = ''
281
282 if (errorMessage.indexOf('http error') !== -1) {
283 message = 'Cannot fetch video from server, maybe down.'
284 } else {
285 message = errorMessage
286 }
287
288 this.notificationsService.error('Error', message)
289 }
290
291 private checkUserRating () {
292 // Unlogged users do not have ratings
293 if (this.isUserLoggedIn() === false) return
294
295 this.videoService.getUserVideoRating(this.video.id)
296 .subscribe(
297 ratingObject => {
298 if (ratingObject) {
299 this.userRating = ratingObject.rating
300 }
301 },
302
303 err => this.notificationsService.error('Error', err.message)
304 )
305 }
306
307 private async onVideoFetched (video: VideoDetails) {
308 this.video = video
309
310 this.updateOtherVideosDisplayed()
311
312 if (this.video.isVideoNSFWForUser(this.user)) {
313 const res = await this.confirmService.confirm(
314 'This video contains mature or explicit content. Are you sure you want to watch it?',
315 'Mature or explicit content'
316 )
317 if (res === false) return this.redirectService.redirectToHomepage()
318 }
319
320 if (!this.hasAlreadyAcceptedPrivacyConcern()) {
321 const res = await this.confirmService.confirm(
322 'PeerTube uses P2P, other may know you are watching that video through your public IP address. ' +
323 'Are you okay with that?',
324 'Privacy concern',
325 'I accept!'
326 )
327 if (res === false) return this.redirectService.redirectToHomepage()
328 }
329
330 this.acceptedPrivacyConcern()
331
332 // Player was already loaded
333 if (this.videoPlayerLoaded !== true) {
334 this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
335
336 // If autoplay is true, we don't really need a poster
337 if (this.isAutoplay() === false) {
338 this.playerElement.poster = this.video.previewUrl
339 }
340
341 const videojsOptions = {
342 controls: true,
343 autoplay: this.isAutoplay(),
344 playbackRates: [ 0.5, 1, 1.5, 2 ],
345 plugins: {
346 peertube: {
347 videoFiles: this.video.files,
348 playerElement: this.playerElement,
349 videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
350 videoDuration: this.video.duration
351 },
352 hotkeys: {
353 enableVolumeScroll: false
354 }
355 },
356 controlBar: {
357 children: [
358 'playToggle',
359 'currentTimeDisplay',
360 'timeDivider',
361 'durationDisplay',
362 'liveDisplay',
363
364 'flexibleWidthSpacer',
365 'progressControl',
366
367 'webTorrentButton',
368
369 'playbackRateMenuButton',
370
371 'muteToggle',
372 'volumeControl',
373
374 'resolutionMenuButton',
375
376 'fullscreenToggle'
377 ]
378 }
379 }
380
381 this.videoPlayerLoaded = true
382
383 const self = this
384 this.zone.runOutsideAngular(() => {
385 videojs(this.playerElement, videojsOptions, function () {
386 self.player = this
387 this.on('customError', (event, data) => self.handleError(data.err))
388 })
389 })
390 } else {
391 const videoViewUrl = this.videoService.getVideoViewUrl(this.video.uuid)
392 this.player.peertube().setVideoFiles(this.video.files, videoViewUrl, this.video.duration)
393 }
394
395 this.setVideoDescriptionHTML()
396 this.setVideoLikesBarTooltipText()
397
398 this.setOpenGraphTags()
399 this.checkUserRating()
400 }
401
402 private setRating (nextRating) {
403 let method
404 switch (nextRating) {
405 case 'like':
406 method = this.videoService.setVideoLike
407 break
408 case 'dislike':
409 method = this.videoService.setVideoDislike
410 break
411 case 'none':
412 method = this.videoService.unsetVideoLike
413 break
414 }
415
416 method.call(this.videoService, this.video.id)
417 .subscribe(
418 () => {
419 // Update the video like attribute
420 this.updateVideoRating(this.userRating, nextRating)
421 this.userRating = nextRating
422 },
423 err => this.notificationsService.error('Error', err.message)
424 )
425 }
426
427 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
428 let likesToIncrement = 0
429 let dislikesToIncrement = 0
430
431 if (oldRating) {
432 if (oldRating === 'like') likesToIncrement--
433 if (oldRating === 'dislike') dislikesToIncrement--
434 }
435
436 if (newRating === 'like') likesToIncrement++
437 if (newRating === 'dislike') dislikesToIncrement++
438
439 this.video.likes += likesToIncrement
440 this.video.dislikes += dislikesToIncrement
441
442 this.video.buildLikeAndDislikePercents()
443 this.setVideoLikesBarTooltipText()
444 }
445
446 private updateOtherVideosDisplayed () {
447 if (this.video && this.otherVideos && this.otherVideos.length > 0) {
448 this.otherVideosDisplayed = this.otherVideos.filter(v => v.uuid !== this.video.uuid)
449 }
450 }
451
452 private setOpenGraphTags () {
453 this.metaService.setTitle(this.video.name)
454
455 this.metaService.setTag('og:type', 'video')
456
457 this.metaService.setTag('og:title', this.video.name)
458 this.metaService.setTag('name', this.video.name)
459
460 this.metaService.setTag('og:description', this.video.description)
461 this.metaService.setTag('description', this.video.description)
462
463 this.metaService.setTag('og:image', this.video.previewPath)
464
465 this.metaService.setTag('og:duration', this.video.duration.toString())
466
467 this.metaService.setTag('og:site_name', 'PeerTube')
468
469 this.metaService.setTag('og:url', window.location.href)
470 this.metaService.setTag('url', window.location.href)
471 }
472
473 private isAutoplay () {
474 // True by default
475 if (!this.user) return true
476
477 // Be sure the autoPlay is set to false
478 return this.user.autoPlayVideo !== false
479 }
480
481 private hasAlreadyAcceptedPrivacyConcern () {
482 return localStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
483 }
484
485 private acceptedPrivacyConcern () {
486 localStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
487 }
488 }