2020-06-23 14:10:17 +02:00
import { Hotkey , HotkeysService } from 'angular2-hotkeys'
import { forkJoin , Observable , Subscription } from 'rxjs'
2018-09-18 11:59:05 +02:00
import { catchError } from 'rxjs/operators'
2020-06-23 14:10:17 +02:00
import { PlatformLocation } from '@angular/common'
2018-07-17 17:06:34 +02:00
import { ChangeDetectorRef , Component , ElementRef , Inject , LOCALE_ID , NgZone , OnDestroy , OnInit , ViewChild } from '@angular/core'
2017-06-16 14:32:15 +02:00
import { ActivatedRoute , Router } from '@angular/router'
2020-09-25 10:04:21 +02:00
import {
AuthService ,
AuthUser ,
ConfirmService ,
MarkdownService ,
2021-05-14 16:12:45 +02:00
MetaService ,
2020-09-25 10:04:21 +02:00
Notifier ,
PeerTubeSocket ,
RestExtractor ,
2021-02-02 10:37:52 +01:00
ScreenService ,
2020-09-25 10:04:21 +02:00
ServerService ,
UserService
} from '@app/core'
2020-06-23 14:10:17 +02:00
import { HooksService } from '@app/core/plugins/hooks.service'
2018-03-01 13:57:29 +01:00
import { RedirectService } from '@app/core/routing/redirect.service'
2020-08-03 18:06:49 +02:00
import { isXPercentInViewport , scrollToTop } from '@app/helpers'
2020-06-23 14:10:17 +02:00
import { Video , VideoCaptionService , VideoDetails , VideoService } from '@app/shared/shared-main'
2020-08-07 16:29:30 +02:00
import { VideoShareComponent } from '@app/shared/shared-share-modal'
2021-03-29 15:56:01 +02:00
import { SupportModalComponent } from '@app/shared/shared-support-modal'
2020-06-23 14:10:17 +02:00
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
2020-11-05 10:56:23 +01:00
import { VideoActionsDisplayType , VideoDownloadComponent } from '@app/shared/shared-video-miniature'
2020-06-23 14:10:17 +02:00
import { VideoPlaylist , VideoPlaylistService } from '@app/shared/shared-video-playlist'
2020-08-07 16:29:30 +02:00
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
2020-12-09 15:00:02 +01:00
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
2020-08-26 09:14:14 +02:00
import { ServerConfig , ServerErrorCode , UserVideoRateType , VideoCaption , VideoPrivacy , VideoState } from '@shared/models'
2021-04-07 17:01:29 +02:00
import {
cleanupVideoWatch ,
getStoredP2PEnabled ,
getStoredTheater ,
getStoredVideoWatchHistory
} from '../../../assets/player/peertube-player-local-storage'
2019-02-06 10:39:50 +01:00
import {
2019-06-11 15:59:10 +02:00
CustomizationOptions ,
2019-02-06 10:39:50 +01:00
P2PMediaLoaderOptions ,
PeertubePlayerManager ,
PeertubePlayerManagerOptions ,
2020-06-23 14:10:17 +02:00
PlayerMode ,
videojs
2019-02-06 10:39:50 +01:00
} from '../../../assets/player/peertube-player-manager'
2019-06-11 15:59:10 +02:00
import { isWebRTCDisabled , timeToInt } from '../../../assets/player/utils'
2020-06-23 14:10:17 +02:00
import { environment } from '../../../environments/environment'
import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
2016-03-14 13:50:19 +01:00
2020-09-25 10:04:21 +02:00
type URLOptions = CustomizationOptions & { playerMode : PlayerMode }
2016-03-14 13:50:19 +01:00
@Component ( {
selector : 'my-video-watch' ,
2016-09-19 22:49:31 +02:00
templateUrl : './video-watch.component.html' ,
styleUrls : [ './video-watch.component.scss' ]
2016-03-14 13:50:19 +01:00
} )
2016-07-08 17:15:14 +02:00
export class VideoWatchComponent implements OnInit , OnDestroy {
2018-02-28 15:33:45 +01:00
private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
2019-07-24 16:05:59 +02:00
@ViewChild ( 'videoWatchPlaylist' , { static : true } ) videoWatchPlaylist : VideoWatchPlaylistComponent
2020-02-07 10:00:34 +01:00
@ViewChild ( 'videoShareModal' ) videoShareModal : VideoShareComponent
2021-03-29 15:56:01 +02:00
@ViewChild ( 'supportModal' ) supportModal : SupportModalComponent
2020-02-07 10:00:34 +01:00
@ViewChild ( 'subscribeButton' ) subscribeButton : SubscribeButtonComponent
2020-08-03 21:06:45 +02:00
@ViewChild ( 'videoDownloadModal' ) videoDownloadModal : VideoDownloadComponent
2017-06-16 14:32:15 +02:00
2019-01-23 15:36:45 +01:00
player : any
2017-12-20 09:57:00 +01:00
playerElement : HTMLVideoElement
2021-02-22 10:53:25 +01:00
2019-03-18 10:26:53 +01:00
theaterEnabled = false
2021-02-22 10:53:25 +01:00
2017-06-17 11:28:11 +02:00
userRating : UserVideoRateType = null
2021-02-22 10:53:25 +01:00
playerPlaceholderImgSrc : string
2017-10-30 20:26:06 +01:00
2019-06-12 12:40:24 +02:00
video : VideoDetails = null
videoCaptions : VideoCaption [ ] = [ ]
2020-08-19 09:21:46 +02:00
playlistPosition : number
2019-03-13 14:18:58 +01:00
playlist : VideoPlaylist = null
2021-02-22 10:53:25 +01:00
descriptionLoading = false
2017-10-30 20:26:06 +01:00
completeDescriptionShown = false
completeVideoDescription : string
shortVideoDescription : string
2017-10-26 15:01:47 +02:00
videoHTMLDescription = ''
2021-02-22 10:53:25 +01:00
2017-12-21 10:49:52 +01:00
likesBarTooltipText = ''
2021-02-22 10:53:25 +01:00
2018-03-08 12:01:55 +01:00
hasAlreadyAcceptedPrivacyConcern = false
2018-07-16 19:15:20 +02:00
remoteServerDown = false
2021-02-22 10:53:25 +01:00
2019-12-06 11:07:30 +01:00
hotkeys : Hotkey [ ] = [ ]
2017-06-16 14:32:15 +02:00
2019-12-12 19:42:37 +01:00
tooltipLike = ''
tooltipDislike = ''
tooltipSupport = ''
tooltipSaveToPlaylist = ''
2020-11-05 10:56:23 +01:00
videoActionsOptions : VideoActionsDisplayType = {
playlist : false ,
download : true ,
update : true ,
blacklist : true ,
delete : true ,
report : true ,
duplicate : true ,
mute : true ,
liveInfo : true
}
2019-09-24 08:48:01 +02:00
private nextVideoUuid = ''
2019-12-17 16:49:33 +01:00
private nextVideoTitle = ''
2019-03-07 17:06:00 +01:00
private currentTime : number
2017-06-16 14:32:15 +02:00
private paramsSub : Subscription
2019-03-13 14:18:58 +01:00
private queryParamsSub : Subscription
2019-04-10 09:23:18 +02:00
private configSub : Subscription
2020-09-25 10:04:21 +02:00
private liveVideosSub : Subscription
2017-06-16 14:32:15 +02:00
2019-12-18 15:31:54 +01:00
private serverConfig : ServerConfig
2017-06-16 14:32:15 +02:00
constructor (
2016-05-27 17:49:18 +02:00
private elementRef : ElementRef ,
2018-07-17 17:06:34 +02:00
private changeDetector : ChangeDetectorRef ,
2016-07-08 17:15:14 +02:00
private route : ActivatedRoute ,
2017-04-04 21:37:03 +02:00
private router : Router ,
2016-05-31 22:39:36 +02:00
private videoService : VideoService ,
2019-03-13 14:18:58 +01:00
private playlistService : VideoPlaylistService ,
2017-04-04 21:37:03 +02:00
private confirmService : ConfirmService ,
2016-11-04 17:37:44 +01:00
private metaService : MetaService ,
2017-01-27 16:14:11 +01:00
private authService : AuthService ,
2020-02-28 13:52:21 +01:00
private userService : UserService ,
2018-04-19 11:01:34 +02:00
private serverService : ServerService ,
2018-05-31 11:35:01 +02:00
private restExtractor : RestExtractor ,
2018-12-19 16:04:34 +01:00
private notifier : Notifier ,
2018-01-10 17:36:35 +01:00
private markdownService : MarkdownService ,
2018-03-01 13:57:29 +01:00
private zone : NgZone ,
2018-05-31 18:12:15 +02:00
private redirectService : RedirectService ,
2018-07-13 18:21:19 +02:00
private videoCaptionService : VideoCaptionService ,
2018-09-02 20:54:23 +02:00
private hotkeysService : HotkeysService ,
2019-07-22 15:40:13 +02:00
private hooks : HooksService ,
2020-09-25 10:04:21 +02:00
private peertubeSocket : PeerTubeSocket ,
2021-02-02 10:37:52 +01:00
private screenService : ScreenService ,
2019-08-22 17:13:58 +02:00
private location : PlatformLocation ,
2018-06-06 14:23:40 +02:00
@Inject ( LOCALE_ID ) private localeId : string
2021-02-02 10:37:52 +01:00
) { }
2016-03-14 13:50:19 +01:00
2017-12-12 14:41:59 +01:00
get user ( ) {
return this . authService . getUser ( )
}
2020-02-28 13:52:21 +01:00
get anonymousUser ( ) {
return this . userService . getAnonymousUser ( )
}
2019-07-08 15:54:08 +02:00
async ngOnInit ( ) {
2021-02-02 10:37:52 +01:00
// Hide the tooltips for unlogged users in mobile view, this adds confusion with the popover
if ( this . user || ! this . screenService . isInMobileView ( ) ) {
this . tooltipLike = $localize ` Like this video `
this . tooltipDislike = $localize ` Dislike this video `
this . tooltipSupport = $localize ` Support options for this video `
this . tooltipSaveToPlaylist = $localize ` Save to playlist `
}
2020-08-27 08:39:44 +02:00
PeertubePlayerManager . initState ( )
2019-12-18 15:31:54 +01:00
this . serverConfig = this . serverService . getTmpConfig ( )
this . configSub = this . serverService . getConfig ( )
. subscribe ( config = > {
this . serverConfig = config
2019-04-10 09:23:18 +02:00
if (
isWebRTCDisabled ( ) ||
2019-12-18 15:31:54 +01:00
this . serverConfig . tracker . enabled === false ||
2020-01-03 10:07:41 +01:00
getStoredP2PEnabled ( ) === false ||
2019-04-10 09:23:18 +02:00
peertubeLocalStorage . getItem ( VideoWatchComponent . LOCAL_STORAGE_PRIVACY_CONCERN_KEY ) === 'true'
) {
this . hasAlreadyAcceptedPrivacyConcern = true
}
} )
2018-03-07 17:35:30 +01:00
2017-01-29 18:35:19 +01:00
this . paramsSub = this . route . params . subscribe ( routeParams = > {
2019-03-13 14:18:58 +01:00
const videoId = routeParams [ 'videoId' ]
if ( videoId ) this . loadVideo ( videoId )
2018-05-31 11:35:01 +02:00
2019-03-13 14:18:58 +01:00
const playlistId = routeParams [ 'playlistId' ]
if ( playlistId ) this . loadPlaylist ( playlistId )
} )
2018-06-14 11:25:49 +02:00
2020-08-19 09:21:46 +02:00
this . queryParamsSub = this . route . queryParams . subscribe ( queryParams = > {
2021-04-26 11:01:29 +02:00
// Handle the ?playlistPosition
2021-04-27 09:02:35 +02:00
const positionParam = queryParams [ 'playlistPosition' ] ? ? 1
2021-04-26 11:01:29 +02:00
this . playlistPosition = positionParam === 'last'
? - 1 // Handle the "last" index
2021-04-27 09:02:35 +02:00
: parseInt ( positionParam + '' , 10 )
2021-04-26 11:01:29 +02:00
if ( isNaN ( this . playlistPosition ) ) {
console . error ( ` playlistPosition query param ' ${ positionParam } ' was parsed as NaN, defaulting to 1. ` )
this . playlistPosition = 1
}
2020-08-19 09:21:46 +02:00
this . videoWatchPlaylist . updatePlaylistIndex ( this . playlistPosition )
2019-12-16 16:21:42 +01:00
const start = queryParams [ 'start' ]
if ( this . player && start ) this . player . currentTime ( parseInt ( start , 10 ) )
2017-06-16 14:32:15 +02:00
} )
2018-09-02 20:54:23 +02:00
2019-05-17 14:34:21 +02:00
this . initHotkeys ( )
2019-06-11 16:26:48 +02:00
this . theaterEnabled = getStoredTheater ( )
2019-07-08 15:54:08 +02:00
2019-07-23 12:16:34 +02:00
this . hooks . runAction ( 'action:video-watch.init' , 'video-watch' )
2021-03-31 11:26:32 +02:00
setTimeout ( cleanupVideoWatch , 1500 ) // Run in timeout to ensure we're not blocking the UI
2016-11-04 16:04:50 +01:00
}
2017-06-16 14:32:15 +02:00
ngOnDestroy ( ) {
2018-04-03 18:06:58 +02:00
this . flushPlayer ( )
2016-11-08 21:17:17 +01:00
2017-01-29 18:35:19 +01:00
// Unsubscribe subscriptions
2019-03-13 14:18:58 +01:00
if ( this . paramsSub ) this . paramsSub . unsubscribe ( )
if ( this . queryParamsSub ) this . queryParamsSub . unsubscribe ( )
2020-08-04 11:42:06 +02:00
if ( this . configSub ) this . configSub . unsubscribe ( )
2020-09-25 10:04:21 +02:00
if ( this . liveVideosSub ) this . liveVideosSub . unsubscribe ( )
2018-09-02 20:54:23 +02:00
// Unbind hotkeys
2019-12-06 11:07:30 +01:00
this . hotkeysService . remove ( this . hotkeys )
2016-03-14 13:50:19 +01:00
}
2016-03-14 22:16:43 +01:00
2017-06-16 14:32:15 +02:00
setLike ( ) {
if ( this . isUserLoggedIn ( ) === false ) return
2019-05-31 11:48:28 +02:00
// Already liked this video
if ( this . userRating === 'like' ) this . setRating ( 'none' )
else this . setRating ( 'like' )
2017-03-08 21:35:43 +01:00
}
2017-06-16 14:32:15 +02:00
setDislike ( ) {
if ( this . isUserLoggedIn ( ) === false ) return
2019-05-31 11:48:28 +02:00
// Already disliked this video
if ( this . userRating === 'dislike' ) this . setRating ( 'none' )
else this . setRating ( 'dislike' )
2017-03-08 21:35:43 +01:00
}
2019-12-05 16:28:05 +01:00
getRatePopoverText ( ) {
if ( this . isUserLoggedIn ( ) ) return undefined
2020-10-30 11:30:21 +01:00
return $localize ` You need to be <a href="/login">logged in</a> to rate this video. `
2019-12-05 16:28:05 +01:00
}
2017-10-30 20:26:06 +01:00
showMoreDescription ( ) {
if ( this . completeVideoDescription === undefined ) {
return this . loadCompleteDescription ( )
}
this . updateVideoDescription ( this . completeVideoDescription )
2017-11-28 15:40:53 +01:00
this . completeDescriptionShown = true
2017-10-30 20:26:06 +01:00
}
showLessDescription ( ) {
this . updateVideoDescription ( this . shortVideoDescription )
2017-11-28 15:40:53 +01:00
this . completeDescriptionShown = false
2017-10-30 20:26:06 +01:00
}
2020-08-03 21:06:45 +02:00
showDownloadModal ( ) {
this . videoDownloadModal . show ( this . video , this . videoCaptions )
}
isVideoDownloadable ( ) {
2020-10-28 10:49:20 +01:00
return this . video && this . video instanceof VideoDetails && this . video . downloadEnabled && ! this . video . isLive
2020-08-03 21:06:45 +02:00
}
2017-10-30 20:26:06 +01:00
loadCompleteDescription ( ) {
2017-11-28 15:40:53 +01:00
this . descriptionLoading = true
2017-10-30 20:26:06 +01:00
this . videoService . loadCompleteDescription ( this . video . descriptionPath )
2018-06-12 20:04:58 +02:00
. subscribe (
description = > {
this . completeDescriptionShown = true
this . descriptionLoading = false
this . shortVideoDescription = this . video . description
this . completeVideoDescription = description
this . updateVideoDescription ( this . completeVideoDescription )
} ,
error = > {
this . descriptionLoading = false
2018-12-19 16:04:34 +01:00
this . notifier . error ( error . message )
2018-06-12 20:04:58 +02:00
}
)
2017-10-30 20:26:06 +01:00
}
2018-02-20 16:13:05 +01:00
showSupportModal ( ) {
2021-04-08 13:37:57 +02:00
this . supportModal . show ( )
2018-02-20 16:13:05 +01:00
}
2017-06-16 14:32:15 +02:00
showShareModal ( ) {
2020-08-07 13:43:48 +02:00
this . videoShareModal . show ( this . currentTime , this . videoWatchPlaylist . currentPlaylistPosition )
2016-11-08 21:11:57 +01:00
}
2017-06-16 14:32:15 +02:00
isUserLoggedIn ( ) {
return this . authService . isLoggedIn ( )
2017-01-20 19:22:15 +01:00
}
2021-04-10 16:48:13 +02:00
getVideoUrl ( ) {
if ( ! this . video . url ) {
return this . video . originInstanceUrl + VideoDetails . buildClientUrl ( this . video . uuid )
}
return this . video . url
}
2017-12-06 17:15:59 +01:00
getVideoTags ( ) {
if ( ! this . video || Array . isArray ( this . video . tags ) === false ) return [ ]
2018-07-24 11:40:04 +02:00
return this . video . tags
2017-12-06 17:15:59 +01:00
}
2019-09-24 08:48:01 +02:00
onRecommendations ( videos : Video [ ] ) {
if ( videos . length > 0 ) {
2019-12-17 16:49:33 +01:00
// The recommended videos's first element should be the next video
const video = videos [ 0 ]
this . nextVideoUuid = video . uuid
this . nextVideoTitle = video . name
2019-09-24 08:48:01 +02:00
}
}
2019-04-05 10:52:27 +02:00
onVideoRemoved ( ) {
this . redirectService . redirectToHomepage ( )
2017-12-20 17:53:58 +01:00
}
2020-02-28 13:52:21 +01:00
declinedPrivacyConcern ( ) {
peertubeLocalStorage . setItem ( VideoWatchComponent . LOCAL_STORAGE_PRIVACY_CONCERN_KEY , 'false' )
this . hasAlreadyAcceptedPrivacyConcern = false
}
2018-03-08 12:01:55 +01:00
acceptedPrivacyConcern ( ) {
2018-03-23 14:26:20 +01:00
peertubeLocalStorage . setItem ( VideoWatchComponent . LOCAL_STORAGE_PRIVACY_CONCERN_KEY , 'true' )
2018-03-08 12:01:55 +01:00
this . hasAlreadyAcceptedPrivacyConcern = true
}
2018-06-12 20:04:58 +02:00
isVideoToTranscode ( ) {
return this . video && this . video . state . id === VideoState . TO_TRANSCODE
}
2018-08-03 09:43:00 +02:00
isVideoToImport ( ) {
return this . video && this . video . state . id === VideoState . TO_IMPORT
}
2018-06-15 16:52:15 +02:00
hasVideoScheduledPublication ( ) {
return this . video && this . video . scheduledUpdate !== undefined
}
2020-09-25 10:04:21 +02:00
isLive ( ) {
return ! ! ( this . video ? . isLive )
}
isWaitingForLive ( ) {
return this . video ? . state . id === VideoState . WAITING_FOR_LIVE
}
isLiveEnded ( ) {
return this . video ? . state . id === VideoState . LIVE_ENDED
}
2019-03-13 14:18:58 +01:00
isVideoBlur ( video : Video ) {
2019-12-18 15:31:54 +01:00
return video . isVideoNSFWForUser ( this . user , this . serverConfig )
2019-03-13 14:18:58 +01:00
}
2019-12-12 18:11:55 +01:00
isAutoPlayEnabled ( ) {
return (
2019-12-13 09:32:36 +01:00
( this . user && this . user . autoPlayNextVideo ) ||
2020-02-28 13:52:21 +01:00
this . anonymousUser . autoPlayNextVideo
2019-12-12 18:11:55 +01:00
)
2019-12-16 16:21:42 +01:00
}
handleTimestampClicked ( timestamp : number ) {
2020-12-17 14:14:28 +01:00
if ( ! this . player || this . video . isLive ) return
this . player . currentTime ( timestamp )
2019-12-16 16:21:42 +01:00
scrollToTop ( )
2019-12-12 18:11:55 +01:00
}
isPlaylistAutoPlayEnabled ( ) {
return (
2019-12-13 09:32:36 +01:00
( this . user && this . user . autoPlayNextVideoPlaylist ) ||
2020-02-28 13:52:21 +01:00
this . anonymousUser . autoPlayNextVideoPlaylist
2019-12-12 18:11:55 +01:00
)
}
2020-07-24 08:49:59 +02:00
isChannelDisplayNameGeneric ( ) {
const genericChannelDisplayName = [
` Main ${ this . video . channel . ownerAccount . name } channel ` ,
` Default ${ this . video . channel . ownerAccount . name } channel `
]
return genericChannelDisplayName . includes ( this . video . channel . displayName )
}
2020-08-19 09:21:46 +02:00
onPlaylistVideoFound ( videoId : string ) {
this . loadVideo ( videoId )
}
2021-04-01 11:10:27 +02:00
displayOtherVideosAsRow ( ) {
// Use the same value as in the SASS file
return this . screenService . getWindowInnerWidth ( ) <= 1100
}
2019-03-13 14:18:58 +01:00
private loadVideo ( videoId : string ) {
// Video did not change
if ( this . video && this . video . uuid === videoId ) return
if ( this . player ) this . player . pause ( )
2019-07-22 15:40:13 +02:00
const videoObs = this . hooks . wrapObsFun (
this . videoService . getVideo . bind ( this . videoService ) ,
{ videoId } ,
'video-watch' ,
'filter:api.video-watch.video.get.params' ,
'filter:api.video-watch.video.get.result'
)
2019-03-13 14:18:58 +01:00
// Video did change
2019-07-25 16:23:44 +02:00
forkJoin ( [
2019-07-22 15:40:13 +02:00
videoObs ,
2019-03-13 14:18:58 +01:00
this . videoCaptionService . listCaptions ( videoId )
2019-07-25 16:23:44 +02:00
] )
2019-03-13 14:18:58 +01:00
. pipe (
2021-01-24 03:02:04 +01:00
// If 400, 403 or 404, the video is private or blocked so redirect to 404
2020-08-26 09:14:14 +02:00
catchError ( err = > {
if ( err . body . errorCode === ServerErrorCode . DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err . body . originUrl ) {
const search = window . location . search
let originUrl = err . body . originUrl
if ( search ) originUrl += search
this . confirmService . confirm (
$localize ` This video is not available on this instance. Do you want to be redirected on the origin instance: <a href=" ${ originUrl } "> ${ originUrl } </a>? ` ,
$localize ` Redirection `
) . then ( res = > {
2020-12-08 21:16:10 +01:00
if ( res === false ) {
2021-01-24 03:02:04 +01:00
return this . restExtractor . redirectTo404IfNotFound ( err , 'video' , [
2020-12-08 21:16:10 +01:00
HttpStatusCode . BAD_REQUEST_400 ,
HttpStatusCode . FORBIDDEN_403 ,
HttpStatusCode . NOT_FOUND_404
] )
}
2020-08-26 09:14:14 +02:00
return window . location . href = originUrl
} )
}
2021-01-24 03:02:04 +01:00
return this . restExtractor . redirectTo404IfNotFound ( err , 'video' , [
2020-12-08 21:16:10 +01:00
HttpStatusCode . BAD_REQUEST_400 ,
HttpStatusCode . FORBIDDEN_403 ,
HttpStatusCode . NOT_FOUND_404
] )
2020-08-26 09:14:14 +02:00
} )
2019-03-13 14:18:58 +01:00
)
. subscribe ( ( [ video , captionsResult ] ) = > {
const queryParams = this . route . snapshot . queryParams
2019-05-31 11:48:28 +02:00
const urlOptions = {
2020-08-18 16:04:03 +02:00
resume : queryParams.resume ,
2019-05-31 11:48:28 +02:00
startTime : queryParams.start ,
stopTime : queryParams.stop ,
2019-06-11 15:59:10 +02:00
muted : queryParams.muted ,
loop : queryParams.loop ,
2019-05-31 11:48:28 +02:00
subtitle : queryParams.subtitle ,
2019-06-11 15:59:10 +02:00
playerMode : queryParams.mode ,
peertubeLink : false
2019-05-31 11:48:28 +02:00
}
this . onVideoFetched ( video , captionsResult . data , urlOptions )
2019-03-13 14:18:58 +01:00
. catch ( err = > this . handleError ( err ) )
} )
}
private loadPlaylist ( playlistId : string ) {
// Playlist did not change
if ( this . playlist && this . playlist . uuid === playlistId ) return
this . playlistService . getVideoPlaylist ( playlistId )
. pipe (
2021-01-24 03:02:04 +01:00
// If 400 or 403, the video is private or blocked so redirect to 404
catchError ( err = > this . restExtractor . redirectTo404IfNotFound ( err , 'video' , [
2020-12-08 21:16:10 +01:00
HttpStatusCode . BAD_REQUEST_400 ,
HttpStatusCode . FORBIDDEN_403 ,
HttpStatusCode . NOT_FOUND_404
] ) )
2019-03-13 14:18:58 +01:00
)
. subscribe ( playlist = > {
this . playlist = playlist
2020-08-19 09:21:46 +02:00
this . videoWatchPlaylist . loadPlaylistElements ( playlist , ! this . playlistPosition , this . playlistPosition )
2019-03-13 14:18:58 +01:00
} )
}
2017-10-30 20:26:06 +01:00
private updateVideoDescription ( description : string ) {
this . video . description = description
this . setVideoDescriptionHTML ( )
2019-05-31 11:48:28 +02:00
. catch ( err = > console . error ( err ) )
2017-10-30 20:26:06 +01:00
}
2019-02-15 15:52:18 +01:00
private async setVideoDescriptionHTML ( ) {
2019-10-02 21:17:10 +02:00
const html = await this . markdownService . textMarkdownToHTML ( this . video . description )
2021-05-27 15:59:55 +02:00
this . videoHTMLDescription = this . markdownService . processVideoTimestamps ( html )
2017-10-30 20:26:06 +01:00
}
2017-12-21 10:49:52 +01:00
private setVideoLikesBarTooltipText ( ) {
2020-08-12 10:40:04 +02:00
this . likesBarTooltipText = ` ${ this . video . likes } likes / ${ this . video . dislikes } dislikes `
2017-12-21 10:49:52 +01:00
}
2017-07-23 11:07:30 +02:00
private handleError ( err : any ) {
const errorMessage : string = typeof err === 'string' ? err : err.message
2018-02-26 09:55:23 +01:00
if ( ! errorMessage ) return
2018-07-16 19:15:20 +02:00
// Display a message in the video player instead of a notification
2018-07-17 14:44:19 +02:00
if ( errorMessage . indexOf ( 'from xs param' ) !== - 1 ) {
2018-07-16 19:15:20 +02:00
this . flushPlayer ( )
this . remoteServerDown = true
2018-07-17 17:06:34 +02:00
this . changeDetector . detectChanges ( )
2018-07-16 19:15:20 +02:00
return
2017-07-23 11:07:30 +02:00
}
2018-12-19 16:04:34 +01:00
this . notifier . error ( errorMessage )
2017-07-23 11:07:30 +02:00
}
2017-06-16 14:32:15 +02:00
private checkUserRating ( ) {
2017-03-08 21:35:43 +01:00
// Unlogged users do not have ratings
2017-06-16 14:32:15 +02:00
if ( this . isUserLoggedIn ( ) === false ) return
2017-03-08 21:35:43 +01:00
this . videoService . getUserVideoRating ( this . video . id )
2018-06-12 20:04:58 +02:00
. subscribe (
ratingObject = > {
if ( ratingObject ) {
this . userRating = ratingObject . rating
}
} ,
2018-12-19 16:04:34 +01:00
err = > this . notifier . error ( err . message )
2018-06-12 20:04:58 +02:00
)
2017-03-08 21:35:43 +01:00
}
2019-02-07 15:56:17 +01:00
private async onVideoFetched (
video : VideoDetails ,
videoCaptions : VideoCaption [ ] ,
2020-09-25 10:04:21 +02:00
urlOptions : URLOptions
2019-02-07 15:56:17 +01:00
) {
2020-09-25 10:04:21 +02:00
this . subscribeToLiveEventsIfNeeded ( this . video , video )
2017-06-16 14:32:15 +02:00
this . video = video
2019-06-12 12:40:24 +02:00
this . videoCaptions = videoCaptions
2017-04-04 21:37:03 +02:00
2018-04-04 09:04:34 +02:00
// Re init attributes
2021-02-22 10:53:25 +01:00
this . playerPlaceholderImgSrc = undefined
2018-04-04 09:04:34 +02:00
this . descriptionLoading = false
this . completeDescriptionShown = false
2021-02-12 09:13:15 +01:00
this . completeVideoDescription = undefined
2018-07-16 19:15:20 +02:00
this . remoteServerDown = false
2019-03-07 17:06:00 +01:00
this . currentTime = undefined
2018-04-04 09:04:34 +02:00
2019-03-13 14:18:58 +01:00
if ( this . isVideoBlur ( this . video ) ) {
2018-02-28 15:33:45 +01:00
const res = await this . confirmService . confirm (
2020-08-12 10:40:04 +02:00
$localize ` This video contains mature or explicit content. Are you sure you want to watch it? ` ,
$localize ` Mature or explicit content `
2017-10-27 08:51:40 +02:00
)
2019-08-22 17:13:58 +02:00
if ( res === false ) return this . location . back ( )
2017-04-04 21:37:03 +02:00
}
2021-02-22 10:46:52 +01:00
this . buildPlayer ( urlOptions )
. catch ( err = > console . error ( 'Cannot build the player' , err ) )
this . setVideoDescriptionHTML ( )
this . setVideoLikesBarTooltipText ( )
this . setOpenGraphTags ( )
this . checkUserRating ( )
2021-04-09 10:54:34 +02:00
const hookOptions = {
videojs ,
video : this.video ,
playlist : this.playlist
}
this . hooks . runAction ( 'action:video-watch.video.loaded' , 'video-watch' , hookOptions )
2021-02-22 10:46:52 +01:00
}
private async buildPlayer ( urlOptions : URLOptions ) {
2018-04-03 18:06:58 +02:00
// Flush old player if needed
this . flushPlayer ( )
2018-04-03 17:33:39 +02:00
2021-02-22 10:53:25 +01:00
const videoState = this . video . state . id
if ( videoState === VideoState . LIVE_ENDED || videoState === VideoState . WAITING_FOR_LIVE ) {
this . playerPlaceholderImgSrc = this . video . previewPath
return
}
2019-08-22 17:13:58 +02:00
// Build video element, because videojs removes it on dispose
2019-03-13 14:18:58 +01:00
const playerElementWrapper = this . elementRef . nativeElement . querySelector ( '#videojs-wrapper' )
2018-04-03 17:33:39 +02:00
this . playerElement = document . createElement ( 'video' )
this . playerElement . className = 'video-js vjs-peertube-skin'
2018-05-22 09:16:05 +02:00
this . playerElement . setAttribute ( 'playsinline' , 'true' )
2018-04-03 17:33:39 +02:00
playerElementWrapper . appendChild ( this . playerElement )
2019-12-05 17:06:18 +01:00
const params = {
video : this.video ,
2021-02-22 10:46:52 +01:00
videoCaptions : this.videoCaptions ,
2019-12-05 17:06:18 +01:00
urlOptions ,
user : this.user
2018-06-06 14:23:40 +02:00
}
2019-12-05 17:06:18 +01:00
const { playerMode , playerOptions } = await this . hooks . wrapFun (
this . buildPlayerManagerOptions . bind ( this ) ,
params ,
2019-12-05 17:26:58 +01:00
'video-watch' ,
'filter:internal.video-watch.player.build-options.params' ,
2019-12-05 17:06:18 +01:00
'filter:internal.video-watch.player.build-options.result'
)
2018-06-06 14:23:40 +02:00
this . zone . runOutsideAngular ( async ( ) = > {
2019-12-05 17:06:18 +01:00
this . player = await PeertubePlayerManager . initialize ( playerMode , playerOptions , player = > this . player = player )
2019-03-18 10:26:53 +01:00
2019-01-23 15:36:45 +01:00
this . player . on ( 'customError' , ( { err } : { err : any } ) = > this . handleError ( err ) )
2019-03-07 17:06:00 +01:00
this . player . on ( 'timeupdate' , ( ) = > {
this . currentTime = Math . floor ( this . player . currentTime ( ) )
} )
2019-03-13 14:18:58 +01:00
2019-12-17 16:49:33 +01:00
/ * *
* replaces this . player . one ( 'ended' )
2019-12-20 17:49:57 +01:00
* 'condition()' : true to make the upnext functionality trigger ,
* false to disable the upnext functionality
* go to the next video in 'condition()' if you don ' t want of the timer .
* 'next' : function triggered at the end of the timer .
* 'suspended' : function used at each clic of the timer checking if we need
* to reset progress and wait until 'suspended' becomes truthy again .
2019-12-17 16:49:33 +01:00
* /
this . player . upnext ( {
2019-12-17 16:59:43 +01:00
timeout : 10000 , // 10s
2020-08-12 10:40:04 +02:00
headText : $localize ` Up Next ` ,
cancelText : $localize ` Cancel ` ,
suspendedText : $localize ` Autoplay is suspended ` ,
2019-12-17 16:49:33 +01:00
getTitle : ( ) = > this . nextVideoTitle ,
next : ( ) = > this . zone . run ( ( ) = > this . autoplayNext ( ) ) ,
condition : ( ) = > {
if ( this . playlist ) {
if ( this . isPlaylistAutoPlayEnabled ( ) ) {
// upnext will not trigger, and instead the next video will play immediately
this . zone . run ( ( ) = > this . videoWatchPlaylist . navigateToNextPlaylistVideo ( ) )
}
} else if ( this . isAutoPlayEnabled ( ) ) {
return true // upnext will trigger
}
return false // upnext will not trigger, and instead leave the video stopping
2019-12-20 17:49:57 +01:00
} ,
suspended : ( ) = > {
return (
! isXPercentInViewport ( this . player . el ( ) , 80 ) ||
! document . getElementById ( 'content' ) . contains ( document . activeElement )
)
2019-03-13 14:18:58 +01:00
}
} )
this . player . one ( 'stopped' , ( ) = > {
if ( this . playlist ) {
2019-12-12 18:11:55 +01:00
if ( this . isPlaylistAutoPlayEnabled ( ) ) this . zone . run ( ( ) = > this . videoWatchPlaylist . navigateToNextPlaylistVideo ( ) )
2019-03-13 14:18:58 +01:00
}
} )
2019-03-18 10:26:53 +01:00
2020-12-04 15:29:18 +01:00
this . player . one ( 'ended' , ( ) = > {
if ( this . video . isLive ) {
2021-05-25 15:27:43 +02:00
this . zone . run ( ( ) = > this . video . state . id = VideoState . LIVE_ENDED )
2020-12-04 15:29:18 +01:00
}
} )
2019-03-18 10:26:53 +01:00
this . player . on ( 'theaterChange' , ( _ : any , enabled : boolean ) = > {
this . zone . run ( ( ) = > this . theaterEnabled = enabled )
} )
2019-11-18 09:55:23 +01:00
2020-08-21 14:45:57 +02:00
this . hooks . runAction ( 'action:video-watch.player.loaded' , 'video-watch' , { player : this.player , videojs , video : this.video } )
2018-04-03 17:33:39 +02:00
} )
2017-04-04 21:37:03 +02:00
}
2019-09-24 08:48:01 +02:00
private autoplayNext ( ) {
2020-03-17 15:05:28 +01:00
if ( this . playlist ) {
this . zone . run ( ( ) = > this . videoWatchPlaylist . navigateToNextPlaylistVideo ( ) )
} else if ( this . nextVideoUuid ) {
2021-05-28 11:36:33 +02:00
this . router . navigate ( [ '/w' , this . nextVideoUuid ] )
2019-09-24 08:48:01 +02:00
}
}
2018-11-14 15:01:28 +01:00
private setRating ( nextRating : UserVideoRateType ) {
2019-05-31 11:48:28 +02:00
const ratingMethods : { [ id in UserVideoRateType ] : ( id : number ) = > Observable < any > } = {
like : this.videoService.setVideoLike ,
dislike : this.videoService.setVideoDislike ,
none : this.videoService.unsetVideoLike
2018-01-07 14:48:10 +01:00
}
2019-05-31 11:48:28 +02:00
ratingMethods [ nextRating ] . call ( this . videoService , this . video . id )
2018-06-12 20:04:58 +02:00
. subscribe (
( ) = > {
// Update the video like attribute
this . updateVideoRating ( this . userRating , nextRating )
this . userRating = nextRating
} ,
2018-12-19 16:04:34 +01:00
( err : { message : string } ) = > this . notifier . error ( err . message )
2018-06-12 20:04:58 +02:00
)
2018-01-07 14:48:10 +01:00
}
2018-11-14 15:01:28 +01:00
private updateVideoRating ( oldRating : UserVideoRateType , newRating : UserVideoRateType ) {
2017-06-16 14:32:15 +02:00
let likesToIncrement = 0
let dislikesToIncrement = 0
2017-03-08 21:35:43 +01:00
if ( oldRating ) {
2017-06-16 14:32:15 +02:00
if ( oldRating === 'like' ) likesToIncrement --
if ( oldRating === 'dislike' ) dislikesToIncrement --
2017-03-08 21:35:43 +01:00
}
2017-06-16 14:32:15 +02:00
if ( newRating === 'like' ) likesToIncrement ++
if ( newRating === 'dislike' ) dislikesToIncrement ++
2017-03-08 21:35:43 +01:00
2017-06-16 14:32:15 +02:00
this . video . likes += likesToIncrement
this . video . dislikes += dislikesToIncrement
2018-02-28 09:49:40 +01:00
2018-02-28 15:33:45 +01:00
this . video . buildLikeAndDislikePercents ( )
2018-02-28 09:49:40 +01:00
this . setVideoLikesBarTooltipText ( )
2017-03-08 21:35:43 +01:00
}
2017-06-16 14:32:15 +02:00
private setOpenGraphTags ( ) {
this . metaService . setTitle ( this . video . name )
2017-03-10 10:33:36 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:type' , 'video' )
2016-11-04 17:37:44 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:title' , this . video . name )
this . metaService . setTag ( 'name' , this . video . name )
2016-11-04 17:37:44 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:description' , this . video . description )
this . metaService . setTag ( 'description' , this . video . description )
2016-11-04 17:37:44 +01:00
2017-07-12 12:19:39 +02:00
this . metaService . setTag ( 'og:image' , this . video . previewPath )
2016-11-04 17:37:44 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:duration' , this . video . duration . toString ( ) )
2016-11-04 17:37:44 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:site_name' , 'PeerTube' )
2016-11-04 17:37:44 +01:00
2017-06-16 14:32:15 +02:00
this . metaService . setTag ( 'og:url' , window . location . href )
this . metaService . setTag ( 'url' , window . location . href )
2016-11-04 17:37:44 +01:00
}
2017-11-30 09:21:11 +01:00
2017-12-19 14:01:34 +01:00
private isAutoplay ( ) {
2018-06-14 11:25:49 +02:00
// We'll jump to the thread id, so do not play the video
if ( this . route . snapshot . params [ 'threadId' ] ) return false
// Otherwise true by default
2017-12-19 14:01:34 +01:00
if ( ! this . user ) return true
// Be sure the autoPlay is set to false
return this . user . autoPlayVideo !== false
}
2018-04-03 18:06:58 +02:00
private flushPlayer ( ) {
// Remove player if it exists
if ( this . player ) {
2019-05-16 16:55:34 +02:00
try {
this . player . dispose ( )
this . player = undefined
} catch ( err ) {
console . error ( 'Cannot dispose player.' , err )
}
2018-04-03 18:06:58 +02:00
}
}
2019-05-17 14:34:21 +02:00
2019-12-05 17:06:18 +01:00
private buildPlayerManagerOptions ( params : {
video : VideoDetails ,
videoCaptions : VideoCaption [ ] ,
urlOptions : CustomizationOptions & { playerMode : PlayerMode } ,
user? : AuthUser
} ) {
const { video , videoCaptions , urlOptions , user } = params
2019-12-12 18:11:55 +01:00
const getStartTime = ( ) = > {
const byUrl = urlOptions . startTime !== undefined
2019-12-18 23:39:07 +01:00
const byHistory = video . userHistory && ( ! this . playlist || urlOptions . resume !== undefined )
2021-03-31 11:26:32 +02:00
const byLocalStorage = getStoredVideoWatchHistory ( video . uuid )
2019-12-12 18:11:55 +01:00
2020-08-18 16:04:03 +02:00
if ( byUrl ) return timeToInt ( urlOptions . startTime )
if ( byHistory ) return video . userHistory . currentTime
2021-03-31 11:26:32 +02:00
if ( byLocalStorage ) return byLocalStorage . duration
2020-08-18 16:04:03 +02:00
return 0
2019-12-12 18:11:55 +01:00
}
2019-12-05 17:06:18 +01:00
2019-12-12 18:11:55 +01:00
let startTime = getStartTime ( )
2020-08-18 16:04:03 +02:00
2019-12-05 17:06:18 +01:00
// If we are at the end of the video, reset the timer
if ( video . duration - startTime <= 1 ) startTime = 0
const playerCaptions = videoCaptions . map ( c = > ( {
label : c.language.label ,
language : c.language.id ,
src : environment.apiUrl + c . captionPath
} ) )
const options : PeertubePlayerManagerOptions = {
common : {
autoplay : this.isAutoplay ( ) ,
2019-12-19 21:34:45 +01:00
nextVideo : ( ) = > this . zone . run ( ( ) = > this . autoplayNext ( ) ) ,
2019-12-05 17:06:18 +01:00
playerElement : this.playerElement ,
onPlayerElementChange : ( element : HTMLVideoElement ) = > this . playerElement = element ,
videoDuration : video.duration ,
enableHotkeys : true ,
inactivityTimeout : 2500 ,
poster : video.previewUrl ,
startTime ,
stopTime : urlOptions.stopTime ,
controls : urlOptions.controls ,
muted : urlOptions.muted ,
loop : urlOptions.loop ,
subtitle : urlOptions.subtitle ,
peertubeLink : urlOptions.peertubeLink ,
theaterButton : true ,
captions : videoCaptions.length !== 0 ,
videoViewUrl : video.privacy.id !== VideoPrivacy . PRIVATE
? this . videoService . getVideoViewUrl ( video . uuid )
: null ,
embedUrl : video.embedUrl ,
2021-03-31 08:32:05 +02:00
embedTitle : video.name ,
2019-12-05 17:06:18 +01:00
2020-11-10 14:21:26 +01:00
isLive : video.isLive ,
2019-12-05 17:06:18 +01:00
language : this.localeId ,
userWatching : user && user . videosHistoryEnabled === true ? {
url : this.videoService.getUserWatchingVideoUrl ( video . uuid ) ,
authorizationHeader : this.authService.getRequestHeaderValue ( )
} : undefined ,
serverUrl : environment.apiUrl ,
2021-03-31 11:26:32 +02:00
videoCaptions : playerCaptions ,
2021-04-21 15:28:12 +02:00
videoUUID : video.uuid
2019-12-05 17:06:18 +01:00
} ,
webtorrent : {
videoFiles : video.files
}
}
2021-04-24 14:04:48 +02:00
// Only set this if we're in a playlist
2021-04-24 16:01:36 +02:00
if ( this . playlist ) {
options . common . previousVideo = ( ) = > {
this . zone . run ( ( ) = > this . videoWatchPlaylist . navigateToPreviousPlaylistVideo ( ) )
}
2021-04-24 14:04:48 +02:00
}
2019-12-05 17:06:18 +01:00
let mode : PlayerMode
if ( urlOptions . playerMode ) {
if ( urlOptions . playerMode === 'p2p-media-loader' ) mode = 'p2p-media-loader'
else mode = 'webtorrent'
} else {
if ( video . hasHlsPlaylist ( ) ) mode = 'p2p-media-loader'
else mode = 'webtorrent'
}
2020-05-13 10:39:54 +02:00
// p2p-media-loader needs TextEncoder, try to fallback on WebTorrent
if ( typeof TextEncoder === 'undefined' ) {
mode = 'webtorrent'
}
2019-12-05 17:06:18 +01:00
if ( mode === 'p2p-media-loader' ) {
const hlsPlaylist = video . getHlsPlaylist ( )
const p2pMediaLoader = {
playlistUrl : hlsPlaylist.playlistUrl ,
segmentsSha256Url : hlsPlaylist.segmentsSha256Url ,
redundancyBaseUrls : hlsPlaylist.redundancies.map ( r = > r . baseUrl ) ,
trackerAnnounce : video.trackerUrls ,
videoFiles : hlsPlaylist.files
} as P2PMediaLoaderOptions
Object . assign ( options , { p2pMediaLoader } )
}
return { playerMode : mode , playerOptions : options }
}
2020-09-25 10:04:21 +02:00
private async subscribeToLiveEventsIfNeeded ( oldVideo : VideoDetails , newVideo : VideoDetails ) {
if ( ! this . liveVideosSub ) {
2020-12-09 15:00:02 +01:00
this . liveVideosSub = this . buildLiveEventsSubscription ( )
2020-09-25 10:04:21 +02:00
}
if ( oldVideo && oldVideo . id !== newVideo . id ) {
await this . peertubeSocket . unsubscribeLiveVideos ( oldVideo . id )
}
if ( ! newVideo . isLive ) return
await this . peertubeSocket . subscribeToLiveVideosSocket ( newVideo . id )
}
2020-12-09 15:00:02 +01:00
private buildLiveEventsSubscription ( ) {
return this . peertubeSocket . getLiveVideosObservable ( )
. subscribe ( ( { type , payload } ) = > {
if ( type === 'state-change' ) return this . handleLiveStateChange ( payload . state )
if ( type === 'views-change' ) return this . handleLiveViewsChange ( payload . views )
} )
}
private handleLiveStateChange ( newState : VideoState ) {
if ( newState !== VideoState . PUBLISHED ) return
const videoState = this . video . state . id
if ( videoState !== VideoState . WAITING_FOR_LIVE && videoState !== VideoState . LIVE_ENDED ) return
console . log ( 'Loading video after live update.' )
const videoUUID = this . video . uuid
// Reset to refetch the video
this . video = undefined
this . loadVideo ( videoUUID )
}
private handleLiveViewsChange ( newViews : number ) {
if ( ! this . video ) {
console . error ( 'Cannot update video live views because video is no defined.' )
return
}
2020-12-10 09:37:53 +01:00
console . log ( 'Updating live views.' )
2020-12-09 15:00:02 +01:00
this . video . views = newViews
}
2019-12-06 09:55:36 +01:00
private initHotkeys ( ) {
this . hotkeys = [
// These hotkeys are managed by the player
2020-08-12 10:40:04 +02:00
new Hotkey ( 'f' , e = > e , undefined , $localize ` Enter/exit fullscreen (requires player focus) ` ) ,
new Hotkey ( 'space' , e = > e , undefined , $localize ` Play/Pause the video (requires player focus) ` ) ,
new Hotkey ( 'm' , e = > e , undefined , $localize ` Mute/unmute the video (requires player focus) ` ) ,
2019-12-06 09:55:36 +01:00
2020-08-12 10:40:04 +02:00
new Hotkey ( '0-9' , e = > e , undefined , $localize ` Skip to a percentage of the video: 0 is 0% and 9 is 90% (requires player focus) ` ) ,
2019-12-06 09:55:36 +01:00
2020-08-12 10:40:04 +02:00
new Hotkey ( 'up' , e = > e , undefined , $localize ` Increase the volume (requires player focus) ` ) ,
new Hotkey ( 'down' , e = > e , undefined , $localize ` Decrease the volume (requires player focus) ` ) ,
2019-12-06 09:55:36 +01:00
2020-08-12 10:40:04 +02:00
new Hotkey ( 'right' , e = > e , undefined , $localize ` Seek the video forward (requires player focus) ` ) ,
new Hotkey ( 'left' , e = > e , undefined , $localize ` Seek the video backward (requires player focus) ` ) ,
2019-12-06 09:55:36 +01:00
2020-08-12 10:40:04 +02:00
new Hotkey ( '>' , e = > e , undefined , $localize ` Increase playback rate (requires player focus) ` ) ,
new Hotkey ( '<' , e = > e , undefined , $localize ` Decrease playback rate (requires player focus) ` ) ,
2019-12-06 09:55:36 +01:00
2020-08-12 10:40:04 +02:00
new Hotkey ( '.' , e = > e , undefined , $localize ` Navigate in the video frame by frame (requires player focus) ` )
2019-12-06 09:55:36 +01:00
]
2019-12-06 11:07:30 +01:00
if ( this . isUserLoggedIn ( ) ) {
this . hotkeys = this . hotkeys . concat ( [
new Hotkey ( 'shift+l' , ( ) = > {
this . setLike ( )
return false
2020-08-12 10:40:04 +02:00
} , undefined , $localize ` Like the video ` ) ,
2019-12-06 11:07:30 +01:00
new Hotkey ( 'shift+d' , ( ) = > {
this . setDislike ( )
return false
2020-08-12 10:40:04 +02:00
} , undefined , $localize ` Dislike the video ` ) ,
2019-12-06 11:07:30 +01:00
new Hotkey ( 'shift+s' , ( ) = > {
this . subscribeButton . subscribed ? this . subscribeButton . unsubscribe ( ) : this . subscribeButton . subscribe ( )
return false
2020-08-12 10:40:04 +02:00
} , undefined , $localize ` Subscribe to the account ` )
2019-12-06 11:07:30 +01:00
] )
}
this . hotkeysService . add ( this . hotkeys )
2019-12-06 09:55:36 +01:00
}
2016-03-14 13:50:19 +01:00
}