1
0
Fork 0
peertube/client/src/app/+videos/+video-watch/video-watch.component.ts

936 lines
27 KiB
TypeScript
Raw Normal View History

2022-03-04 12:40:02 +00:00
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
2020-06-23 12:10:17 +00:00
import { PlatformLocation } from '@angular/common'
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import {
AuthService,
AuthUser,
ConfirmService,
Notifier,
PeerTubeSocket,
PluginService,
RestExtractor,
2021-02-02 09:37:52 +00:00
ScreenService,
ServerService,
Hotkey,
HotkeysService,
User,
UserService
} from '@app/core'
2020-06-23 12:10:17 +00:00
import { HooksService } from '@app/core/plugins/hooks.service'
2023-06-29 13:55:00 +00:00
import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers'
2023-08-28 08:55:04 +00:00
import { Video, VideoCaptionService, VideoChapterService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main'
2020-06-23 12:10:17 +00:00
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
2022-03-04 12:40:02 +00:00
import { LiveVideoService } from '@app/shared/shared-video-live'
2020-06-23 12:10:17 +00:00
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { timeToInt } from '@peertube/peertube-core-utils'
2021-07-16 08:42:24 +00:00
import {
HTMLServerConfig,
HttpStatusCode,
2022-03-04 12:40:02 +00:00
LiveVideo,
2021-07-16 08:42:24 +00:00
PeerTubeProblemDocument,
ServerErrorCode,
2023-06-01 12:51:16 +00:00
Storyboard,
2021-07-16 08:42:24 +00:00
VideoCaption,
2023-08-28 08:55:04 +00:00
VideoChapter,
2021-07-16 08:42:24 +00:00
VideoPrivacy,
VideoState,
VideoStateType
} from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
2019-02-06 09:39:50 +00:00
import {
2023-06-29 13:55:00 +00:00
HLSOptions,
PeerTubePlayer,
PeerTubePlayerContructorOptions,
PeerTubePlayerLoadOptions,
2020-06-23 12:10:17 +00:00
PlayerMode,
videojs
2022-02-02 10:16:23 +00:00
} from '../../../assets/player'
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
2020-06-23 12:10:17 +00:00
import { environment } from '../../../environments/environment'
2021-06-29 15:18:30 +00:00
import { VideoWatchPlaylistComponent } from './shared'
2016-03-14 12:50:19 +00:00
2023-06-29 13:55:00 +00:00
type URLOptions = {
playerMode: PlayerMode
startTime: number | string
stopTime: number | string
controls?: boolean
controlBar?: boolean
muted?: boolean
loop?: boolean
subtitle?: string
resume?: string
peertubeLink: boolean
playbackRate?: number | string
}
2016-03-14 12:50:19 +00:00
@Component({
selector: 'my-video-watch',
templateUrl: './video-watch.component.html',
styleUrls: [ './video-watch.component.scss' ]
2016-03-14 12:50:19 +00:00
})
2016-07-08 15:15:14 +00:00
export class VideoWatchComponent implements OnInit, OnDestroy {
2019-07-24 14:05:59 +00:00
@ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
2020-02-07 09:00:34 +00:00
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
2023-06-29 13:55:00 +00:00
@ViewChild('playerElement') playerElement: ElementRef<HTMLVideoElement>
2023-06-29 13:55:00 +00:00
peertubePlayer: PeerTubePlayer
theaterEnabled = false
2019-06-12 10:40:24 +00:00
video: VideoDetails = null
videoCaptions: VideoCaption[] = []
2023-08-28 08:55:04 +00:00
videoChapters: VideoChapter[] = []
2022-03-04 12:40:02 +00:00
liveVideo: LiveVideo
videoPassword: string
2023-06-01 12:51:16 +00:00
storyboards: Storyboard[] = []
2019-06-12 10:40:24 +00:00
playlistPosition: number
2019-03-13 13:18:58 +00:00
playlist: VideoPlaylist = null
remoteServerDown = false
noPlaylistVideoFound = false
2023-06-29 13:55:00 +00:00
private nextRecommendedVideoUUID = ''
private nextRecommendedVideoTitle = ''
private videoFileToken: string
2019-03-07 16:06:00 +00:00
private currentTime: number
private paramsSub: Subscription
2019-03-13 13:18:58 +00:00
private queryParamsSub: Subscription
2019-04-10 07:23:18 +00:00
private configSub: Subscription
private liveVideosSub: Subscription
2021-06-04 11:31:41 +00:00
private serverConfig: HTMLServerConfig
2019-12-18 14:31:54 +00:00
private hotkeys: Hotkey[] = []
private static VIEW_VIDEO_INTERVAL_MS = 5000
constructor (
2016-07-08 15:15:14 +00:00
private route: ActivatedRoute,
2017-04-04 19:37:03 +00:00
private router: Router,
2016-05-31 20:39:36 +00:00
private videoService: VideoService,
2019-03-13 13:18:58 +00:00
private playlistService: VideoPlaylistService,
2022-03-04 12:40:02 +00:00
private liveVideoService: LiveVideoService,
2017-04-04 19:37:03 +00:00
private confirmService: ConfirmService,
private authService: AuthService,
private userService: UserService,
private serverService: ServerService,
2018-05-31 09:35:01 +00:00
private restExtractor: RestExtractor,
private notifier: Notifier,
2018-03-01 12:57:29 +00:00
private zone: NgZone,
2018-07-13 16:21:19 +00:00
private videoCaptionService: VideoCaptionService,
2023-08-28 08:55:04 +00:00
private videoChapterService: VideoChapterService,
private hotkeysService: HotkeysService,
2019-07-22 13:40:13 +00:00
private hooks: HooksService,
private pluginService: PluginService,
private peertubeSocket: PeerTubeSocket,
2021-02-02 09:37:52 +00:00
private screenService: ScreenService,
private videoFileTokenService: VideoFileTokenService,
private location: PlatformLocation,
2018-06-06 12:23:40 +00:00
@Inject(LOCALE_ID) private localeId: string
2021-02-02 09:37:52 +00:00
) { }
2016-03-14 12:50:19 +00:00
2017-12-12 13:41:59 +00:00
get user () {
return this.authService.getUser()
}
get anonymousUser () {
return this.userService.getAnonymousUser()
}
2023-06-29 13:55:00 +00:00
async ngOnInit () {
this.serverConfig = this.serverService.getHTMLConfig()
this.loadRouteParams()
this.loadRouteQuery()
2019-06-11 14:26:48 +00:00
this.theaterEnabled = getStoredTheater()
2019-07-08 13:54:08 +00:00
2019-07-23 10:16:34 +00:00
this.hooks.runAction('action:video-watch.init', 'video-watch')
setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI
2023-06-29 13:55:00 +00:00
const constructorOptions = await this.hooks.wrapFun(
this.buildPeerTubePlayerConstructorOptions.bind(this),
{ urlOptions: this.getUrlOptions() },
'video-watch',
'filter:internal.video-watch.player.build-options.params',
'filter:internal.video-watch.player.build-options.result'
)
this.peertubePlayer = new PeerTubePlayer(constructorOptions)
}
ngOnDestroy () {
2023-06-29 13:55:00 +00:00
if (this.peertubePlayer) this.peertubePlayer.destroy()
2016-11-08 20:17:17 +00:00
// Unsubscribe subscriptions
2019-03-13 13:18:58 +00:00
if (this.paramsSub) this.paramsSub.unsubscribe()
if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
2020-08-04 09:42:06 +00:00
if (this.configSub) this.configSub.unsubscribe()
if (this.liveVideosSub) this.liveVideosSub.unsubscribe()
// Unbind hotkeys
this.hotkeysService.remove(this.hotkeys)
2016-03-14 12:50:19 +00:00
}
2016-03-14 21:16:43 +00:00
getCurrentTime () {
return this.currentTime
}
getCurrentPlaylistPosition () {
return this.videoWatchPlaylist.currentPlaylistPosition
2016-11-08 20:11:57 +00:00
}
onRecommendations (videos: Video[]) {
if (videos.length === 0) return
// The recommended videos's first element should be the next video
const video = videos[0]
2023-06-29 13:55:00 +00:00
this.nextRecommendedVideoUUID = video.uuid
this.nextRecommendedVideoTitle = video.name
}
handleTimestampClicked (timestamp: number) {
2023-06-29 13:55:00 +00:00
if (!this.peertubePlayer || this.video.isLive) return
2020-12-17 13:14:28 +00:00
2023-06-29 13:55:00 +00:00
this.peertubePlayer.getPlayer().currentTime(timestamp)
scrollToTop()
}
onPlaylistVideoFound (videoId: string) {
this.loadVideo({ videoId, forceAutoplay: false })
}
onPlaylistNoVideoFound () {
this.noPlaylistVideoFound = true
}
isUserLoggedIn () {
return this.authService.isLoggedIn()
}
isUserOwner () {
return this.video.isLocal === true && this.video.account.name === this.user?.username
}
isVideoBlur (video: Video) {
return video.isVideoNSFWForUser(this.user, this.serverConfig)
}
isChannelDisplayNameGeneric () {
const genericChannelDisplayName = [
`Main ${this.video.channel.ownerAccount.name} channel`,
`Default ${this.video.channel.ownerAccount.name} channel`
]
return genericChannelDisplayName.includes(this.video.channel.displayName)
}
displayOtherVideosAsRow () {
// Use the same value as in the SASS file
return this.screenService.getWindowInnerWidth() <= 1100
}
private loadRouteParams () {
this.paramsSub = this.route.params.subscribe(routeParams => {
2021-08-17 12:42:53 +00:00
const videoId = routeParams['videoId']
if (videoId) return this.loadVideo({ videoId, forceAutoplay: false })
2021-08-17 12:42:53 +00:00
const playlistId = routeParams['playlistId']
if (playlistId) return this.loadPlaylist(playlistId)
})
}
private loadRouteQuery () {
this.queryParamsSub = this.route.queryParams.subscribe(queryParams => {
// Handle the ?playlistPosition
2021-08-17 12:42:53 +00:00
const positionParam = queryParams['playlistPosition'] ?? 1
this.playlistPosition = positionParam === 'last'
? -1 // Handle the "last" index
: parseInt(positionParam + '', 10)
if (isNaN(this.playlistPosition)) {
logger.error(`playlistPosition query param '${positionParam}' was parsed as NaN, defaulting to 1.`)
this.playlistPosition = 1
}
this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition)
2021-08-17 12:42:53 +00:00
const start = queryParams['start']
2023-06-29 13:55:00 +00:00
if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10))
})
}
private loadVideo (options: {
videoId: string
forceAutoplay: boolean
videoPassword?: string
}) {
const { videoId, forceAutoplay, videoPassword } = options
if (this.isSameElement(this.video, videoId)) return
2019-03-13 13:18:58 +00:00
2022-07-29 08:32:56 +00:00
this.video = undefined
2019-07-22 13:40:13 +00:00
const videoObs = this.hooks.wrapObsFun(
this.videoService.getVideo.bind(this.videoService),
{ videoId, videoPassword },
2019-07-22 13:40:13 +00:00
'video-watch',
'filter:api.video-watch.video.get.params',
'filter:api.video-watch.video.get.result'
)
const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe(
2022-03-04 12:40:02 +00:00
switchMap(video => {
if (!video.isLive) return of({ video, live: undefined })
2022-03-04 12:40:02 +00:00
return this.liveVideoService.getVideoLive(video.uuid)
.pipe(map(live => ({ live, video })))
}),
switchMap(({ video, live }) => {
if (!videoRequiresFileToken(video)) return of({ video, live, videoFileToken: undefined })
return this.videoFileTokenService.getVideoFileToken({ videoUUID: video.uuid, videoPassword })
.pipe(map(({ token }) => ({ video, live, videoFileToken: token })))
2022-03-04 12:40:02 +00:00
})
)
forkJoin([
2022-03-04 12:40:02 +00:00
videoAndLiveObs,
this.videoCaptionService.listCaptions(videoId, videoPassword),
2023-08-28 08:55:04 +00:00
this.videoChapterService.getChapters({ videoId, videoPassword }),
this.videoService.getStoryboards(videoId, videoPassword),
this.userService.getAnonymousOrLoggedUser()
]).subscribe({
2023-08-28 08:55:04 +00:00
next: ([ { video, live, videoFileToken }, captionsResult, chaptersResult, storyboards, loggedInOrAnonymousUser ]) => {
this.onVideoFetched({
video,
live,
videoCaptions: captionsResult.data,
2023-08-28 08:55:04 +00:00
videoChapters: chaptersResult.chapters,
2023-06-01 12:51:16 +00:00
storyboards,
videoFileToken,
videoPassword,
loggedInOrAnonymousUser,
forceAutoplay
}).catch(err => {
this.handleGlobalError(err)
})
},
error: async err => {
if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD || err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) {
const { confirmed, password } = await this.handleVideoPasswordError(err)
if (confirmed === false) return this.location.back()
2019-05-31 09:48:28 +00:00
this.loadVideo({ ...options, videoPassword: password })
} else {
this.handleRequestError(err)
}
}
})
2019-03-13 13:18:58 +00:00
}
private loadPlaylist (playlistId: string) {
if (this.isSameElement(this.playlist, playlistId)) return
2019-03-13 13:18:58 +00:00
this.noPlaylistVideoFound = false
2019-03-13 13:18:58 +00:00
this.playlistService.getVideoPlaylist(playlistId)
2021-08-17 09:27:47 +00:00
.subscribe({
next: playlist => {
this.playlist = playlist
this.videoWatchPlaylist.loadPlaylistElements(playlist, !this.playlistPosition, this.playlistPosition)
},
2021-08-17 09:27:47 +00:00
error: err => this.handleRequestError(err)
})
}
2019-03-13 13:18:58 +00:00
private isSameElement (element: VideoDetails | VideoPlaylist, newId: string) {
if (!element) return false
return (element.id + '') === newId || element.uuid === newId || element.shortUUID === newId
}
private async handleRequestError (err: any) {
const errorBody = err.body as PeerTubeProblemDocument
2021-11-02 10:50:03 +00:00
if (errorBody?.code === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && errorBody.originUrl) {
const originUrl = errorBody.originUrl + (window.location.search ?? '')
const res = await this.confirmService.confirm(
2021-08-17 12:42:53 +00:00
// eslint-disable-next-line max-len
$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`
)
if (res === true) return window.location.href = originUrl
}
// If 400, 403 or 404, the video is private or blocked so redirect to 404
return this.restExtractor.redirectTo404IfNotFound(err, 'video', [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.FORBIDDEN_403,
HttpStatusCode.NOT_FOUND_404
])
2019-03-13 13:18:58 +00:00
}
private handleGlobalError (err: any) {
2017-07-23 09:07:30 +00:00
const errorMessage: string = typeof err === 'string' ? err : err.message
2018-02-26 08:55:23 +00:00
if (!errorMessage) return
this.notifier.error(errorMessage)
2017-07-23 09:07:30 +00:00
}
private handleVideoPasswordError (err: any) {
let isIncorrectPassword: boolean
if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) {
isIncorrectPassword = false
} else if (err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) {
this.videoPassword = undefined
isIncorrectPassword = true
}
return this.confirmService.confirmWithPassword({
message: $localize`You need a password to watch this video`,
title: $localize`This video is password protected`,
errorMessage: isIncorrectPassword ? $localize`Incorrect password, please enter a correct password` : ''
})
}
private async onVideoFetched (options: {
video: VideoDetails
2022-03-04 12:40:02 +00:00
live: LiveVideo
videoCaptions: VideoCaption[]
2023-08-28 08:55:04 +00:00
videoChapters: VideoChapter[]
2023-06-01 12:51:16 +00:00
storyboards: Storyboard[]
videoFileToken: string
videoPassword: string
loggedInOrAnonymousUser: User
forceAutoplay: boolean
}) {
2023-06-01 12:51:16 +00:00
const {
video,
live,
videoCaptions,
2023-08-28 08:55:04 +00:00
videoChapters,
2023-06-01 12:51:16 +00:00
storyboards,
videoFileToken,
videoPassword,
loggedInOrAnonymousUser,
forceAutoplay
} = options
this.subscribeToLiveEventsIfNeeded(this.video, video)
this.video = video
2019-06-12 10:40:24 +00:00
this.videoCaptions = videoCaptions
2023-08-28 08:55:04 +00:00
this.videoChapters = videoChapters
2022-03-04 12:40:02 +00:00
this.liveVideo = live
this.videoFileToken = videoFileToken
this.videoPassword = videoPassword
2023-06-01 12:51:16 +00:00
this.storyboards = storyboards
2017-04-04 19:37:03 +00:00
// Re init attributes
this.remoteServerDown = false
2019-03-07 16:06:00 +00:00
this.currentTime = undefined
2019-03-13 13:18:58 +00:00
if (this.isVideoBlur(this.video)) {
const res = await this.confirmService.confirm(
$localize`This video contains mature or explicit content. Are you sure you want to watch it?`,
$localize`Mature or explicit content`
)
if (res === false) return this.location.back()
2017-04-04 19:37:03 +00:00
}
this.buildHotkeysHelp(video)
2023-06-29 13:55:00 +00:00
this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay })
.catch(err => logger.error('Cannot build the player', err))
const hookOptions = {
videojs,
video: this.video,
playlist: this.playlist
}
this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions)
}
2023-06-29 13:55:00 +00:00
private async loadPlayer (options: {
loggedInOrAnonymousUser: User
forceAutoplay: boolean
}) {
2023-06-29 13:55:00 +00:00
const { loggedInOrAnonymousUser, forceAutoplay } = options
2018-04-03 15:33:39 +00:00
const videoState = this.video.state.id
if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) {
2023-06-29 13:55:00 +00:00
this.updatePlayerOnNoLive()
return
}
2023-06-29 13:55:00 +00:00
this.peertubePlayer?.enable()
2018-04-03 15:33:39 +00:00
2019-12-05 16:06:18 +00:00
const params = {
video: this.video,
videoCaptions: this.videoCaptions,
2023-08-28 08:55:04 +00:00
videoChapters: this.videoChapters,
2023-06-01 12:51:16 +00:00
storyboards: this.storyboards,
2022-03-04 12:40:02 +00:00
liveVideo: this.liveVideo,
videoFileToken: this.videoFileToken,
videoPassword: this.videoPassword,
2023-06-29 13:55:00 +00:00
urlOptions: this.getUrlOptions(),
loggedInOrAnonymousUser,
forceAutoplay,
2019-12-05 16:06:18 +00:00
user: this.user
2018-06-06 12:23:40 +00:00
}
2023-06-29 13:55:00 +00:00
const loadOptions = await this.hooks.wrapFun(
this.buildPeerTubePlayerLoadOptions.bind(this),
2019-12-05 16:06:18 +00:00
params,
2019-12-05 16:26:58 +00:00
'video-watch',
2023-06-29 13:55:00 +00:00
'filter:internal.video-watch.player.load-options.params',
'filter:internal.video-watch.player.load-options.result'
2019-12-05 16:06:18 +00:00
)
2018-06-06 12:23:40 +00:00
this.zone.runOutsideAngular(async () => {
2023-06-29 13:55:00 +00:00
await this.peertubePlayer.load(loadOptions)
2019-03-18 09:26:53 +00:00
2023-06-29 13:55:00 +00:00
const player = this.peertubePlayer.getPlayer()
2019-03-07 16:06:00 +00:00
2023-06-29 13:55:00 +00:00
player.on('timeupdate', () => {
// Don't need to trigger angular change for this variable, that is sent to children components on click
2023-06-29 13:55:00 +00:00
this.currentTime = Math.floor(player.currentTime())
2019-03-07 16:06:00 +00:00
})
2019-03-13 13:18:58 +00:00
2023-06-29 13:55:00 +00:00
if (this.video.isLive) {
player.one('ended', () => {
this.zone.run(() => {
// We changed the video, it's not a live anymore
if (!this.video.isLive) return
2023-06-29 13:55:00 +00:00
this.video.state.id = VideoState.LIVE_ENDED
2023-06-29 13:55:00 +00:00
this.updatePlayerOnNoLive()
})
})
}
2023-06-29 13:55:00 +00:00
player.on('theater-change', (_: any, enabled: boolean) => {
2019-03-18 09:26:53 +00:00
this.zone.run(() => this.theaterEnabled = enabled)
})
this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', {
2023-06-29 13:55:00 +00:00
player,
playlist: this.playlist,
playlistPosition: this.playlistPosition,
videojs,
video: this.video
})
2018-04-03 15:33:39 +00:00
})
2017-04-04 19:37:03 +00:00
}
2022-01-07 13:25:23 +00:00
private hasNextVideo () {
if (this.playlist) {
return this.videoWatchPlaylist.hasNextVideo()
}
return true
}
2023-06-29 13:55:00 +00:00
private getNextVideoTitle () {
if (this.playlist) {
2023-06-29 13:55:00 +00:00
return this.videoWatchPlaylist.getNextVideo()?.video?.name || ''
}
2016-11-04 16:37:44 +00:00
2023-06-29 13:55:00 +00:00
return this.nextRecommendedVideoTitle
}
private playNextVideoInAngularZone () {
this.zone.run(() => {
if (this.playlist) {
this.videoWatchPlaylist.navigateToNextPlaylistVideo()
return
}
if (this.nextRecommendedVideoUUID) {
this.router.navigate([ '/w', this.nextRecommendedVideoUUID ])
}
})
2016-11-04 16:37:44 +00:00
}
2017-11-30 08:21:11 +00:00
private isAutoplay () {
// We'll jump to the thread id, so do not play the video
if (this.route.snapshot.params['threadId']) return false
2023-07-13 12:40:06 +00:00
if (this.user) return this.user.autoPlayVideo
2023-07-13 12:40:06 +00:00
if (this.anonymousUser) return this.anonymousUser.autoPlayVideo
throw new Error('Cannot guess autoplay because user and anonymousUser are not defined')
}
2018-04-03 16:06:58 +00:00
private isAutoPlayNext () {
return (
2021-08-17 12:42:53 +00:00
(this.user?.autoPlayNextVideo) ||
this.anonymousUser.autoPlayNextVideo
)
}
private isPlaylistAutoPlayNext () {
return (
2021-08-17 12:42:53 +00:00
(this.user?.autoPlayNextVideoPlaylist) ||
this.anonymousUser.autoPlayNextVideoPlaylist
)
}
2023-06-29 13:55:00 +00:00
private buildPeerTubePlayerConstructorOptions (options: {
urlOptions: URLOptions
}): PeerTubePlayerContructorOptions {
const { urlOptions } = options
return {
playerElement: () => this.playerElement.nativeElement,
enableHotkeys: true,
inactivityTimeout: 2500,
theaterButton: true,
controls: urlOptions.controls,
controlBar: urlOptions.controlBar,
muted: urlOptions.muted,
loop: urlOptions.loop,
playbackRate: urlOptions.playbackRate,
instanceName: this.serverConfig.instance.name,
language: this.localeId,
metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS,
authorizationHeader: () => this.authService.getRequestHeaderValue(),
serverUrl: environment.originServerUrl || window.location.origin,
2023-06-29 13:55:00 +00:00
errorNotifier: (message: string) => this.notifier.error(message),
peertubeLink: () => false,
2023-08-18 07:48:45 +00:00
pluginsManager: this.pluginService.getPluginsManager(),
autoPlayerRatio: {
cssRatioVariable: '--player-ratio',
cssPlayerPortraitModeVariable: '--player-portrait-mode'
}
2018-04-03 16:06:58 +00:00
}
}
2023-06-29 13:55:00 +00:00
private buildPeerTubePlayerLoadOptions (options: {
2021-08-17 12:42:53 +00:00
video: VideoDetails
2022-03-04 12:40:02 +00:00
liveVideo: LiveVideo
2021-08-17 12:42:53 +00:00
videoCaptions: VideoCaption[]
2023-08-28 08:55:04 +00:00
videoChapters: VideoChapter[]
2023-06-01 12:51:16 +00:00
storyboards: Storyboard[]
videoFileToken: string
videoPassword: string
2023-06-29 13:55:00 +00:00
urlOptions: URLOptions
loggedInOrAnonymousUser: User
forceAutoplay: boolean
2022-04-05 12:03:52 +00:00
user?: AuthUser // Keep for plugins
2023-06-29 13:55:00 +00:00
}): PeerTubePlayerLoadOptions {
2023-06-01 12:51:16 +00:00
const {
video,
liveVideo,
videoCaptions,
2023-08-28 08:55:04 +00:00
videoChapters,
2023-06-01 12:51:16 +00:00
storyboards,
videoFileToken,
videoPassword,
urlOptions,
loggedInOrAnonymousUser,
forceAutoplay
2023-06-29 13:55:00 +00:00
} = options
let mode: PlayerMode
if (urlOptions.playerMode) {
if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader'
else mode = 'web-video'
} else {
if (video.hasHlsPlaylist()) mode = 'p2p-media-loader'
else mode = 'web-video'
}
let hlsOptions: HLSOptions
if (video.hasHlsPlaylist()) {
const hlsPlaylist = video.getHlsPlaylist()
hlsOptions = {
playlistUrl: hlsPlaylist.playlistUrl,
segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
trackerAnnounce: video.trackerUrls,
videoFiles: hlsPlaylist.files
}
}
const getStartTime = () => {
const byUrl = urlOptions.startTime !== undefined
const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
const byLocalStorage = getStoredVideoWatchHistory(video.uuid)
2020-08-18 14:04:03 +00:00
if (byUrl) return timeToInt(urlOptions.startTime)
let startTime = 0
if (byHistory) startTime = video.userHistory.currentTime
if (byLocalStorage) startTime = byLocalStorage.duration
2019-12-05 16:06:18 +00:00
// If we are at the end of the video, reset the timer
if (video.duration - startTime <= 1) startTime = 0
return startTime
}
2020-08-18 14:04:03 +00:00
const startTime = getStartTime()
2019-12-05 16:06:18 +00:00
const playerCaptions = videoCaptions.map(c => ({
label: c.language.label,
language: c.language.id,
src: environment.apiUrl + c.captionPath
}))
2023-06-01 12:51:16 +00:00
const storyboard = storyboards.length !== 0
? {
url: environment.apiUrl + storyboards[0].storyboardPath,
height: storyboards[0].spriteHeight,
width: storyboards[0].spriteWidth,
interval: storyboards[0].spriteDuration
}
: undefined
2022-03-04 12:40:02 +00:00
const liveOptions = video.isLive
? { latencyMode: liveVideo.latencyMode }
: undefined
2023-06-29 13:55:00 +00:00
return {
mode,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
autoplay: this.isAutoplay(),
forceAutoplay,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
duration: this.video.duration,
poster: video.previewUrl,
p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
startTime,
stopTime: urlOptions.stopTime,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
embedUrl: video.embedUrl,
embedTitle: video.name,
2020-11-10 13:21:26 +00:00
2023-06-29 13:55:00 +00:00
isLive: video.isLive,
liveOptions,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
? this.videoService.getVideoViewUrl(video.uuid)
: null,
2023-06-29 13:55:00 +00:00
videoFileToken: () => videoFileToken,
requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
!video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
videoPassword: () => videoPassword,
2023-06-29 13:55:00 +00:00
videoCaptions: playerCaptions,
2023-08-28 08:55:04 +00:00
videoChapters,
2023-06-29 13:55:00 +00:00
storyboard,
2023-06-29 13:55:00 +00:00
videoShortUUID: video.shortUUID,
videoUUID: video.uuid,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
previousVideo: {
enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(),
2023-06-29 13:55:00 +00:00
handler: this.playlist
? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo())
: undefined,
2022-02-02 10:16:23 +00:00
2023-06-29 13:55:00 +00:00
displayControlBarButton: !!this.playlist
2019-12-05 16:06:18 +00:00
},
2023-06-29 13:55:00 +00:00
nextVideo: {
enabled: this.hasNextVideo(),
handler: () => this.playNextVideoInAngularZone(),
getVideoTitle: () => this.getNextVideoTitle(),
displayControlBarButton: this.hasNextVideo()
},
2023-06-29 13:55:00 +00:00
upnext: {
isEnabled: () => {
if (this.playlist) return this.isPlaylistAutoPlayNext()
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
return this.isAutoPlayNext()
},
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
isSuspended: (player: videojs.Player) => {
return !isXPercentInViewport(player.el() as HTMLElement, 80)
},
2020-05-13 08:39:54 +00:00
2023-06-29 13:55:00 +00:00
timeout: this.playlist
? 0 // Don't wait to play next video in playlist
: 5000 // 5 seconds for a recommended video
},
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
hls: hlsOptions,
2019-12-05 16:06:18 +00:00
2023-06-29 13:55:00 +00:00
webVideo: {
videoFiles: video.files
}
2019-12-05 16:06:18 +00:00
}
}
private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
if (!this.liveVideosSub) {
2020-12-09 14:00:02 +00:00
this.liveVideosSub = this.buildLiveEventsSubscription()
}
if (oldVideo && oldVideo.id !== newVideo.id) {
2021-08-25 14:14:11 +00:00
this.peertubeSocket.unsubscribeLiveVideos(oldVideo.id)
}
if (!newVideo.isLive) return
await this.peertubeSocket.subscribeToLiveVideosSocket(newVideo.id)
}
2020-12-09 14:00:02 +00: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.viewers)
2020-12-09 14:00:02 +00:00
})
}
private handleLiveStateChange (newState: VideoStateType) {
2020-12-09 14:00:02 +00:00
if (newState !== VideoState.PUBLISHED) return
logger.info('Loading video after live update.')
2020-12-09 14:00:02 +00:00
const videoUUID = this.video.uuid
// Reset to force refresh the video
2020-12-09 14:00:02 +00:00
this.video = undefined
this.loadVideo({ videoId: videoUUID, forceAutoplay: true })
2020-12-09 14:00:02 +00:00
}
private handleLiveViewsChange (newViewers: number) {
2020-12-09 14:00:02 +00:00
if (!this.video) {
logger.error('Cannot update video live views because video is no defined.')
2020-12-09 14:00:02 +00:00
return
}
logger.info('Updating live views.')
2020-12-10 08:37:53 +00:00
this.video.viewers = newViewers
2020-12-09 14:00:02 +00:00
}
2023-06-29 13:55:00 +00:00
private updatePlayerOnNoLive () {
this.peertubePlayer.unload()
this.peertubePlayer.disable()
this.peertubePlayer.setPoster(this.video.previewPath)
}
private buildHotkeysHelp (video: Video) {
if (this.hotkeys.length !== 0) {
this.hotkeysService.remove(this.hotkeys)
}
2019-12-06 08:55:36 +00:00
this.hotkeys = [
// These hotkeys are managed by the player
new Hotkey('f', e => e, $localize`Enter/exit fullscreen`),
new Hotkey('space', e => e, $localize`Play/Pause the video`),
new Hotkey('m', e => e, $localize`Mute/unmute the video`),
2019-12-06 08:55:36 +00:00
new Hotkey('up', e => e, $localize`Increase the volume`),
new Hotkey('down', e => e, $localize`Decrease the volume`),
2019-12-06 08:55:36 +00:00
2022-01-12 15:01:39 +00:00
new Hotkey('t', e => {
this.theaterEnabled = !this.theaterEnabled
return false
}, $localize`Toggle theater mode`)
2019-12-06 08:55:36 +00:00
]
if (!video.isLive) {
this.hotkeys = this.hotkeys.concat([
// These hotkeys are also managed by the player but only for VOD
new Hotkey('0-9', e => e, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`),
new Hotkey('right', e => e, $localize`Seek the video forward`),
new Hotkey('left', e => e, $localize`Seek the video backward`),
new Hotkey('>', e => e, $localize`Increase playback rate`),
new Hotkey('<', e => e, $localize`Decrease playback rate`),
new Hotkey(',', e => e, $localize`Navigate in the video to the previous frame`),
new Hotkey('.', e => e, $localize`Navigate in the video to the next frame`)
])
}
if (this.isUserLoggedIn()) {
this.hotkeys = this.hotkeys.concat([
new Hotkey('shift+s', () => {
2021-08-17 12:42:53 +00:00
if (this.subscribeButton.isSubscribedToAll()) this.subscribeButton.unsubscribe()
else this.subscribeButton.subscribe()
2021-07-12 08:03:46 +00:00
return false
}, $localize`Subscribe to the account`)
])
}
this.hotkeysService.add(this.hotkeys)
2019-12-06 08:55:36 +00:00
}
2023-06-29 13:55:00 +00:00
private getUrlOptions (): URLOptions {
const queryParams = this.route.snapshot.queryParams
return {
resume: queryParams.resume,
startTime: queryParams.start,
stopTime: queryParams.stop,
muted: toBoolean(queryParams.muted),
loop: toBoolean(queryParams.loop),
subtitle: queryParams.subtitle,
playerMode: queryParams.mode,
playbackRate: queryParams.playbackRate,
controlBar: toBoolean(queryParams.controlBar),
peertubeLink: false
}
}
2016-03-14 12:50:19 +00:00
}