Reorganize client shared modules
[oweals/peertube.git] / client / src / app / videos / +video-watch / video-watch.component.ts
1 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
2 import { forkJoin, Observable, Subscription } from 'rxjs'
3 import { catchError } from 'rxjs/operators'
4 import { PlatformLocation } from '@angular/common'
5 import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
6 import { ActivatedRoute, Router } from '@angular/router'
7 import { AuthService, AuthUser, ConfirmService, MarkdownService, Notifier, RestExtractor, ServerService, UserService } from '@app/core'
8 import { HooksService } from '@app/core/plugins/hooks.service'
9 import { RedirectService } from '@app/core/routing/redirect.service'
10 import { isXPercentInViewport, peertubeLocalStorage, scrollToTop } from '@app/helpers'
11 import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
12 import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
13 import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
14 import { MetaService } from '@ngx-meta/core'
15 import { I18n } from '@ngx-translate/i18n-polyfill'
16 import { ServerConfig, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
17 import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
18 import {
19   CustomizationOptions,
20   P2PMediaLoaderOptions,
21   PeertubePlayerManager,
22   PeertubePlayerManagerOptions,
23   PlayerMode,
24   videojs
25 } from '../../../assets/player/peertube-player-manager'
26 import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
27 import { environment } from '../../../environments/environment'
28 import { VideoShareComponent } from './modal/video-share.component'
29 import { VideoSupportComponent } from './modal/video-support.component'
30 import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
31
32 @Component({
33   selector: 'my-video-watch',
34   templateUrl: './video-watch.component.html',
35   styleUrls: [ './video-watch.component.scss' ]
36 })
37 export class VideoWatchComponent implements OnInit, OnDestroy {
38   private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
39
40   @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
41   @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
42   @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
43   @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
44
45   player: any
46   playerElement: HTMLVideoElement
47   theaterEnabled = false
48   userRating: UserVideoRateType = null
49   descriptionLoading = false
50
51   video: VideoDetails = null
52   videoCaptions: VideoCaption[] = []
53
54   playlist: VideoPlaylist = null
55
56   completeDescriptionShown = false
57   completeVideoDescription: string
58   shortVideoDescription: string
59   videoHTMLDescription = ''
60   likesBarTooltipText = ''
61   hasAlreadyAcceptedPrivacyConcern = false
62   remoteServerDown = false
63   hotkeys: Hotkey[] = []
64
65   tooltipLike = ''
66   tooltipDislike = ''
67   tooltipSupport = ''
68   tooltipSaveToPlaylist = ''
69
70   private nextVideoUuid = ''
71   private nextVideoTitle = ''
72   private currentTime: number
73   private paramsSub: Subscription
74   private queryParamsSub: Subscription
75   private configSub: Subscription
76
77   private serverConfig: ServerConfig
78
79   constructor (
80     private elementRef: ElementRef,
81     private changeDetector: ChangeDetectorRef,
82     private route: ActivatedRoute,
83     private router: Router,
84     private videoService: VideoService,
85     private playlistService: VideoPlaylistService,
86     private confirmService: ConfirmService,
87     private metaService: MetaService,
88     private authService: AuthService,
89     private userService: UserService,
90     private serverService: ServerService,
91     private restExtractor: RestExtractor,
92     private notifier: Notifier,
93     private markdownService: MarkdownService,
94     private zone: NgZone,
95     private redirectService: RedirectService,
96     private videoCaptionService: VideoCaptionService,
97     private i18n: I18n,
98     private hotkeysService: HotkeysService,
99     private hooks: HooksService,
100     private location: PlatformLocation,
101     @Inject(LOCALE_ID) private localeId: string
102   ) {
103     this.tooltipLike = this.i18n('Like this video')
104     this.tooltipDislike = this.i18n('Dislike this video')
105     this.tooltipSupport = this.i18n('Support options for this video')
106     this.tooltipSaveToPlaylist = this.i18n('Save to playlist')
107   }
108
109   get user () {
110     return this.authService.getUser()
111   }
112
113   get anonymousUser () {
114     return this.userService.getAnonymousUser()
115   }
116
117   async ngOnInit () {
118     this.serverConfig = this.serverService.getTmpConfig()
119
120     this.configSub = this.serverService.getConfig()
121         .subscribe(config => {
122           this.serverConfig = config
123
124           if (
125             isWebRTCDisabled() ||
126             this.serverConfig.tracker.enabled === false ||
127             getStoredP2PEnabled() === false ||
128             peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
129           ) {
130             this.hasAlreadyAcceptedPrivacyConcern = true
131           }
132         })
133
134     this.paramsSub = this.route.params.subscribe(routeParams => {
135       const videoId = routeParams[ 'videoId' ]
136       if (videoId) this.loadVideo(videoId)
137
138       const playlistId = routeParams[ 'playlistId' ]
139       if (playlistId) this.loadPlaylist(playlistId)
140     })
141
142     this.queryParamsSub = this.route.queryParams.subscribe(async queryParams => {
143       const videoId = queryParams[ 'videoId' ]
144       if (videoId) this.loadVideo(videoId)
145
146       const start = queryParams[ 'start' ]
147       if (this.player && start) this.player.currentTime(parseInt(start, 10))
148     })
149
150     this.initHotkeys()
151
152     this.theaterEnabled = getStoredTheater()
153
154     this.hooks.runAction('action:video-watch.init', 'video-watch')
155   }
156
157   ngOnDestroy () {
158     this.flushPlayer()
159
160     // Unsubscribe subscriptions
161     if (this.paramsSub) this.paramsSub.unsubscribe()
162     if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
163
164     // Unbind hotkeys
165     this.hotkeysService.remove(this.hotkeys)
166   }
167
168   setLike () {
169     if (this.isUserLoggedIn() === false) return
170
171     // Already liked this video
172     if (this.userRating === 'like') this.setRating('none')
173     else this.setRating('like')
174   }
175
176   setDislike () {
177     if (this.isUserLoggedIn() === false) return
178
179     // Already disliked this video
180     if (this.userRating === 'dislike') this.setRating('none')
181     else this.setRating('dislike')
182   }
183
184   getRatePopoverText () {
185     if (this.isUserLoggedIn()) return undefined
186
187     return this.i18n('You need to be connected to rate this content.')
188   }
189
190   showMoreDescription () {
191     if (this.completeVideoDescription === undefined) {
192       return this.loadCompleteDescription()
193     }
194
195     this.updateVideoDescription(this.completeVideoDescription)
196     this.completeDescriptionShown = true
197   }
198
199   showLessDescription () {
200     this.updateVideoDescription(this.shortVideoDescription)
201     this.completeDescriptionShown = false
202   }
203
204   loadCompleteDescription () {
205     this.descriptionLoading = true
206
207     this.videoService.loadCompleteDescription(this.video.descriptionPath)
208         .subscribe(
209           description => {
210             this.completeDescriptionShown = true
211             this.descriptionLoading = false
212
213             this.shortVideoDescription = this.video.description
214             this.completeVideoDescription = description
215
216             this.updateVideoDescription(this.completeVideoDescription)
217           },
218
219           error => {
220             this.descriptionLoading = false
221             this.notifier.error(error.message)
222           }
223         )
224   }
225
226   showSupportModal () {
227     this.pausePlayer()
228
229     this.videoSupportModal.show()
230   }
231
232   showShareModal () {
233     this.pausePlayer()
234
235     this.videoShareModal.show(this.currentTime)
236   }
237
238   isUserLoggedIn () {
239     return this.authService.isLoggedIn()
240   }
241
242   getVideoTags () {
243     if (!this.video || Array.isArray(this.video.tags) === false) return []
244
245     return this.video.tags
246   }
247
248   onRecommendations (videos: Video[]) {
249     if (videos.length > 0) {
250       // The recommended videos's first element should be the next video
251       const video = videos[0]
252       this.nextVideoUuid = video.uuid
253       this.nextVideoTitle = video.name
254     }
255   }
256
257   onModalOpened () {
258     this.pausePlayer()
259   }
260
261   onVideoRemoved () {
262     this.redirectService.redirectToHomepage()
263   }
264
265   declinedPrivacyConcern () {
266     peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'false')
267     this.hasAlreadyAcceptedPrivacyConcern = false
268   }
269
270   acceptedPrivacyConcern () {
271     peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
272     this.hasAlreadyAcceptedPrivacyConcern = true
273   }
274
275   isVideoToTranscode () {
276     return this.video && this.video.state.id === VideoState.TO_TRANSCODE
277   }
278
279   isVideoToImport () {
280     return this.video && this.video.state.id === VideoState.TO_IMPORT
281   }
282
283   hasVideoScheduledPublication () {
284     return this.video && this.video.scheduledUpdate !== undefined
285   }
286
287   isVideoBlur (video: Video) {
288     return video.isVideoNSFWForUser(this.user, this.serverConfig)
289   }
290
291   isAutoPlayEnabled () {
292     return (
293       (this.user && this.user.autoPlayNextVideo) ||
294       this.anonymousUser.autoPlayNextVideo
295     )
296   }
297
298   handleTimestampClicked (timestamp: number) {
299     if (this.player) this.player.currentTime(timestamp)
300     scrollToTop()
301   }
302
303   isPlaylistAutoPlayEnabled () {
304     return (
305       (this.user && this.user.autoPlayNextVideoPlaylist) ||
306       this.anonymousUser.autoPlayNextVideoPlaylist
307     )
308   }
309
310   private loadVideo (videoId: string) {
311     // Video did not change
312     if (this.video && this.video.uuid === videoId) return
313
314     if (this.player) this.player.pause()
315
316     const videoObs = this.hooks.wrapObsFun(
317       this.videoService.getVideo.bind(this.videoService),
318       { videoId },
319       'video-watch',
320       'filter:api.video-watch.video.get.params',
321       'filter:api.video-watch.video.get.result'
322     )
323
324     // Video did change
325     forkJoin([
326       videoObs,
327       this.videoCaptionService.listCaptions(videoId)
328     ])
329       .pipe(
330         // If 401, the video is private or blocked so redirect to 404
331         catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
332       )
333       .subscribe(([ video, captionsResult ]) => {
334         const queryParams = this.route.snapshot.queryParams
335
336         const urlOptions = {
337           startTime: queryParams.start,
338           stopTime: queryParams.stop,
339
340           muted: queryParams.muted,
341           loop: queryParams.loop,
342           subtitle: queryParams.subtitle,
343
344           playerMode: queryParams.mode,
345           peertubeLink: false
346         }
347
348         this.onVideoFetched(video, captionsResult.data, urlOptions)
349             .catch(err => this.handleError(err))
350       })
351   }
352
353   private loadPlaylist (playlistId: string) {
354     // Playlist did not change
355     if (this.playlist && this.playlist.uuid === playlistId) return
356
357     this.playlistService.getVideoPlaylist(playlistId)
358       .pipe(
359         // If 401, the video is private or blocked so redirect to 404
360         catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
361       )
362       .subscribe(playlist => {
363         this.playlist = playlist
364
365         const videoId = this.route.snapshot.queryParams['videoId']
366         this.videoWatchPlaylist.loadPlaylistElements(playlist, !videoId)
367       })
368   }
369
370   private updateVideoDescription (description: string) {
371     this.video.description = description
372     this.setVideoDescriptionHTML()
373       .catch(err => console.error(err))
374   }
375
376   private async setVideoDescriptionHTML () {
377     const html = await this.markdownService.textMarkdownToHTML(this.video.description)
378     this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
379   }
380
381   private setVideoLikesBarTooltipText () {
382     this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
383       likesNumber: this.video.likes,
384       dislikesNumber: this.video.dislikes
385     })
386   }
387
388   private handleError (err: any) {
389     const errorMessage: string = typeof err === 'string' ? err : err.message
390     if (!errorMessage) return
391
392     // Display a message in the video player instead of a notification
393     if (errorMessage.indexOf('from xs param') !== -1) {
394       this.flushPlayer()
395       this.remoteServerDown = true
396       this.changeDetector.detectChanges()
397
398       return
399     }
400
401     this.notifier.error(errorMessage)
402   }
403
404   private checkUserRating () {
405     // Unlogged users do not have ratings
406     if (this.isUserLoggedIn() === false) return
407
408     this.videoService.getUserVideoRating(this.video.id)
409         .subscribe(
410           ratingObject => {
411             if (ratingObject) {
412               this.userRating = ratingObject.rating
413             }
414           },
415
416           err => this.notifier.error(err.message)
417         )
418   }
419
420   private async onVideoFetched (
421     video: VideoDetails,
422     videoCaptions: VideoCaption[],
423     urlOptions: CustomizationOptions & { playerMode: PlayerMode }
424   ) {
425     this.video = video
426     this.videoCaptions = videoCaptions
427
428     // Re init attributes
429     this.descriptionLoading = false
430     this.completeDescriptionShown = false
431     this.remoteServerDown = false
432     this.currentTime = undefined
433
434     this.videoWatchPlaylist.updatePlaylistIndex(video)
435
436     if (this.isVideoBlur(this.video)) {
437       const res = await this.confirmService.confirm(
438         this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
439         this.i18n('Mature or explicit content')
440       )
441       if (res === false) return this.location.back()
442     }
443
444     // Flush old player if needed
445     this.flushPlayer()
446
447     // Build video element, because videojs removes it on dispose
448     const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper')
449     this.playerElement = document.createElement('video')
450     this.playerElement.className = 'video-js vjs-peertube-skin'
451     this.playerElement.setAttribute('playsinline', 'true')
452     playerElementWrapper.appendChild(this.playerElement)
453
454     const params = {
455       video: this.video,
456       videoCaptions,
457       urlOptions,
458       user: this.user
459     }
460     const { playerMode, playerOptions } = await this.hooks.wrapFun(
461       this.buildPlayerManagerOptions.bind(this),
462       params,
463       'video-watch',
464       'filter:internal.video-watch.player.build-options.params',
465       'filter:internal.video-watch.player.build-options.result'
466     )
467
468     this.zone.runOutsideAngular(async () => {
469       this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player)
470       this.player.focus()
471
472       this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
473
474       this.player.on('timeupdate', () => {
475         this.currentTime = Math.floor(this.player.currentTime())
476       })
477
478       /**
479        * replaces this.player.one('ended')
480        * 'condition()': true to make the upnext functionality trigger,
481        *                false to disable the upnext functionality
482        * go to the next video in 'condition()' if you don't want of the timer.
483        * 'next': function triggered at the end of the timer.
484        * 'suspended': function used at each clic of the timer checking if we need
485        * to reset progress and wait until 'suspended' becomes truthy again.
486        */
487       this.player.upnext({
488         timeout: 10000, // 10s
489         headText: this.i18n('Up Next'),
490         cancelText: this.i18n('Cancel'),
491         suspendedText: this.i18n('Autoplay is suspended'),
492         getTitle: () => this.nextVideoTitle,
493         next: () => this.zone.run(() => this.autoplayNext()),
494         condition: () => {
495           if (this.playlist) {
496             if (this.isPlaylistAutoPlayEnabled()) {
497               // upnext will not trigger, and instead the next video will play immediately
498               this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
499             }
500           } else if (this.isAutoPlayEnabled()) {
501             return true // upnext will trigger
502           }
503           return false // upnext will not trigger, and instead leave the video stopping
504         },
505         suspended: () => {
506           return (
507             !isXPercentInViewport(this.player.el(), 80) ||
508             !document.getElementById('content').contains(document.activeElement)
509           )
510         }
511       })
512
513       this.player.one('stopped', () => {
514         if (this.playlist) {
515           if (this.isPlaylistAutoPlayEnabled()) this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
516         }
517       })
518
519       this.player.on('theaterChange', (_: any, enabled: boolean) => {
520         this.zone.run(() => this.theaterEnabled = enabled)
521       })
522
523       this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { player: this.player })
524     })
525
526     this.setVideoDescriptionHTML()
527     this.setVideoLikesBarTooltipText()
528
529     this.setOpenGraphTags()
530     this.checkUserRating()
531
532     this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', { videojs })
533   }
534
535   private autoplayNext () {
536     if (this.playlist) {
537       this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
538     } else if (this.nextVideoUuid) {
539       this.router.navigate([ '/videos/watch', this.nextVideoUuid ])
540     }
541   }
542
543   private setRating (nextRating: UserVideoRateType) {
544     const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
545       like: this.videoService.setVideoLike,
546       dislike: this.videoService.setVideoDislike,
547       none: this.videoService.unsetVideoLike
548     }
549
550     ratingMethods[nextRating].call(this.videoService, this.video.id)
551           .subscribe(
552             () => {
553               // Update the video like attribute
554               this.updateVideoRating(this.userRating, nextRating)
555               this.userRating = nextRating
556             },
557
558             (err: { message: string }) => this.notifier.error(err.message)
559           )
560   }
561
562   private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
563     let likesToIncrement = 0
564     let dislikesToIncrement = 0
565
566     if (oldRating) {
567       if (oldRating === 'like') likesToIncrement--
568       if (oldRating === 'dislike') dislikesToIncrement--
569     }
570
571     if (newRating === 'like') likesToIncrement++
572     if (newRating === 'dislike') dislikesToIncrement++
573
574     this.video.likes += likesToIncrement
575     this.video.dislikes += dislikesToIncrement
576
577     this.video.buildLikeAndDislikePercents()
578     this.setVideoLikesBarTooltipText()
579   }
580
581   private setOpenGraphTags () {
582     this.metaService.setTitle(this.video.name)
583
584     this.metaService.setTag('og:type', 'video')
585
586     this.metaService.setTag('og:title', this.video.name)
587     this.metaService.setTag('name', this.video.name)
588
589     this.metaService.setTag('og:description', this.video.description)
590     this.metaService.setTag('description', this.video.description)
591
592     this.metaService.setTag('og:image', this.video.previewPath)
593
594     this.metaService.setTag('og:duration', this.video.duration.toString())
595
596     this.metaService.setTag('og:site_name', 'PeerTube')
597
598     this.metaService.setTag('og:url', window.location.href)
599     this.metaService.setTag('url', window.location.href)
600   }
601
602   private isAutoplay () {
603     // We'll jump to the thread id, so do not play the video
604     if (this.route.snapshot.params['threadId']) return false
605
606     // Otherwise true by default
607     if (!this.user) return true
608
609     // Be sure the autoPlay is set to false
610     return this.user.autoPlayVideo !== false
611   }
612
613   private flushPlayer () {
614     // Remove player if it exists
615     if (this.player) {
616       try {
617         this.player.dispose()
618         this.player = undefined
619       } catch (err) {
620         console.error('Cannot dispose player.', err)
621       }
622     }
623   }
624
625   private buildPlayerManagerOptions (params: {
626     video: VideoDetails,
627     videoCaptions: VideoCaption[],
628     urlOptions: CustomizationOptions & { playerMode: PlayerMode },
629     user?: AuthUser
630   }) {
631     const { video, videoCaptions, urlOptions, user } = params
632     const getStartTime = () => {
633       const byUrl = urlOptions.startTime !== undefined
634       const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
635
636       if (byUrl) {
637         return timeToInt(urlOptions.startTime)
638       } else if (byHistory) {
639         return video.userHistory.currentTime
640       } else {
641         return 0
642       }
643     }
644
645     let startTime = getStartTime()
646     // If we are at the end of the video, reset the timer
647     if (video.duration - startTime <= 1) startTime = 0
648
649     const playerCaptions = videoCaptions.map(c => ({
650       label: c.language.label,
651       language: c.language.id,
652       src: environment.apiUrl + c.captionPath
653     }))
654
655     const options: PeertubePlayerManagerOptions = {
656       common: {
657         autoplay: this.isAutoplay(),
658         nextVideo: () => this.zone.run(() => this.autoplayNext()),
659
660         playerElement: this.playerElement,
661         onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
662
663         videoDuration: video.duration,
664         enableHotkeys: true,
665         inactivityTimeout: 2500,
666         poster: video.previewUrl,
667
668         startTime,
669         stopTime: urlOptions.stopTime,
670         controls: urlOptions.controls,
671         muted: urlOptions.muted,
672         loop: urlOptions.loop,
673         subtitle: urlOptions.subtitle,
674
675         peertubeLink: urlOptions.peertubeLink,
676
677         theaterButton: true,
678         captions: videoCaptions.length !== 0,
679
680         videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
681           ? this.videoService.getVideoViewUrl(video.uuid)
682           : null,
683         embedUrl: video.embedUrl,
684
685         language: this.localeId,
686
687         userWatching: user && user.videosHistoryEnabled === true ? {
688           url: this.videoService.getUserWatchingVideoUrl(video.uuid),
689           authorizationHeader: this.authService.getRequestHeaderValue()
690         } : undefined,
691
692         serverUrl: environment.apiUrl,
693
694         videoCaptions: playerCaptions
695       },
696
697       webtorrent: {
698         videoFiles: video.files
699       }
700     }
701
702     let mode: PlayerMode
703
704     if (urlOptions.playerMode) {
705       if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader'
706       else mode = 'webtorrent'
707     } else {
708       if (video.hasHlsPlaylist()) mode = 'p2p-media-loader'
709       else mode = 'webtorrent'
710     }
711
712     // p2p-media-loader needs TextEncoder, try to fallback on WebTorrent
713     if (typeof TextEncoder === 'undefined') {
714       mode = 'webtorrent'
715     }
716
717     if (mode === 'p2p-media-loader') {
718       const hlsPlaylist = video.getHlsPlaylist()
719
720       const p2pMediaLoader = {
721         playlistUrl: hlsPlaylist.playlistUrl,
722         segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
723         redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
724         trackerAnnounce: video.trackerUrls,
725         videoFiles: hlsPlaylist.files
726       } as P2PMediaLoaderOptions
727
728       Object.assign(options, { p2pMediaLoader })
729     }
730
731     return { playerMode: mode, playerOptions: options }
732   }
733
734   private pausePlayer () {
735     if (!this.player) return
736
737     this.player.pause()
738   }
739
740   private initHotkeys () {
741     this.hotkeys = [
742       // These hotkeys are managed by the player
743       new Hotkey('f', e => e, undefined, this.i18n('Enter/exit fullscreen (requires player focus)')),
744       new Hotkey('space', e => e, undefined, this.i18n('Play/Pause the video (requires player focus)')),
745       new Hotkey('m', e => e, undefined, this.i18n('Mute/unmute the video (requires player focus)')),
746
747       new Hotkey('0-9', e => e, undefined, this.i18n('Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus)')),
748
749       new Hotkey('up', e => e, undefined, this.i18n('Increase the volume (requires player focus)')),
750       new Hotkey('down', e => e, undefined, this.i18n('Decrease the volume (requires player focus)')),
751
752       new Hotkey('right', e => e, undefined, this.i18n('Seek the video forward (requires player focus)')),
753       new Hotkey('left', e => e, undefined, this.i18n('Seek the video backward (requires player focus)')),
754
755       new Hotkey('>', e => e, undefined, this.i18n('Increase playback rate (requires player focus)')),
756       new Hotkey('<', e => e, undefined, this.i18n('Decrease playback rate (requires player focus)')),
757
758       new Hotkey('.', e => e, undefined, this.i18n('Navigate in the video frame by frame (requires player focus)'))
759     ]
760
761     if (this.isUserLoggedIn()) {
762       this.hotkeys = this.hotkeys.concat([
763         new Hotkey('shift+l', () => {
764           this.setLike()
765           return false
766         }, undefined, this.i18n('Like the video')),
767
768         new Hotkey('shift+d', () => {
769           this.setDislike()
770           return false
771         }, undefined, this.i18n('Dislike the video')),
772
773         new Hotkey('shift+s', () => {
774           this.subscribeButton.subscribed ? this.subscribeButton.unsubscribe() : this.subscribeButton.subscribe()
775           return false
776         }, undefined, this.i18n('Subscribe to the account'))
777       ])
778     }
779
780     this.hotkeysService.add(this.hotkeys)
781   }
782 }