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