diff --git a/client/package.json b/client/package.json
index 564e56ae7..149322192 100644
--- a/client/package.json
+++ b/client/package.json
@@ -71,7 +71,6 @@
"@types/sanitize-html": "2.6.2",
"@types/sha.js": "^2.4.0",
"@types/video.js": "^7.3.40",
- "@types/webtorrent": "^0.109.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@wdio/browserstack-service": "^8.10.5",
@@ -85,14 +84,12 @@
"babel-loader": "^9.1.0",
"bootstrap": "^5.1.3",
"buffer": "^6.0.3",
- "cache-chunk-store": "^3.0.0",
"chart.js": "^4.3.0",
"chartjs-plugin-zoom": "~2.0.1",
"chromedriver": "^113.0.0",
"core-js": "^3.22.8",
"css-loader": "^6.2.0",
"debug": "^4.3.1",
- "dexie": "^3.2.2",
"eslint": "^8.28.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsdoc": "^44.2.4",
@@ -103,7 +100,6 @@
"hls.js": "~1.3",
"html-loader": "^4.1.0",
"html-webpack-plugin": "^5.3.1",
- "https-browserify": "^1.0.0",
"intl-messageformat": "^10.1.0",
"jschannel": "^1.0.2",
"linkify-html": "^4.0.2",
@@ -115,9 +111,7 @@
"path-browserify": "^1.0.0",
"postcss": "^8.4.14",
"primeng": "^16.0.0-rc.2",
- "process": "^0.11.10",
"purify-css": "^1.2.5",
- "querystring": "^0.2.1",
"raw-loader": "^4.0.2",
"rxjs": "^7.3.0",
"sanitize-html": "^2.1.2",
@@ -125,23 +119,17 @@
"sass-loader": "^13.2.0",
"sha.js": "^2.4.11",
"socket.io-client": "^4.5.4",
- "stream-browserify": "^3.0.0",
- "stream-http": "^3.0.0",
"stylelint": "^15.1.0",
"stylelint-config-sass-guidelines": "^10.0.0",
"ts-loader": "^9.3.0",
"tslib": "^2.4.0",
"typescript": "~4.9.5",
- "url": "^0.11.0",
"video.js": "^7.19.2",
- "videostream": "~3.2.1",
"wdio-chromedriver-service": "^8.1.1",
"wdio-geckodriver-service": "^5.0.1",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^5.0.1",
- "webtorrent": "1.8.26",
- "whatwg-fetch": "^3.0.0",
"zone.js": "~0.13.0"
},
"dependencies": {}
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
index ec85db0ff..97d71a510 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts
@@ -152,12 +152,24 @@ export class VideoWatchPlaylistComponent {
this.onPlaylistVideosNearOfBottom(position)
}
+ // ---------------------------------------------------------------------------
+
hasPreviousVideo () {
- return !!this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous')
+ return !!this.getPreviousVideo()
}
+ getPreviousVideo () {
+ return this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous')
+ }
+
+ // ---------------------------------------------------------------------------
+
hasNextVideo () {
- return !!this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next')
+ return !!this.getNextVideo()
+ }
+
+ getNextVideo () {
+ return this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next')
}
navigateToPreviousPlaylistVideo () {
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index 80fd6e40f..294ff4b3a 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -8,7 +8,7 @@
-
+
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 54e0649ba..aebec52fb 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -1,6 +1,5 @@
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
-import { VideoJsPlayer } from 'video.js'
import { PlatformLocation } from '@angular/common'
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
@@ -19,13 +18,13 @@ import {
UserService
} from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
-import { isXPercentInViewport, scrollToTop } from '@app/helpers'
+import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers'
import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { logger } from '@root-helpers/logger'
-import { isP2PEnabled, videoRequiresUserAuth, videoRequiresFileToken } from '@root-helpers/video'
+import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
import { timeToInt } from '@shared/core-utils'
import {
HTMLServerConfig,
@@ -39,10 +38,10 @@ import {
VideoState
} from '@shared/models'
import {
- CustomizationOptions,
- P2PMediaLoaderOptions,
- PeertubePlayerManager,
- PeertubePlayerManagerOptions,
+ HLSOptions,
+ PeerTubePlayer,
+ PeerTubePlayerContructorOptions,
+ PeerTubePlayerLoadOptions,
PlayerMode,
videojs
} from '../../../assets/player'
@@ -50,7 +49,24 @@ import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from
import { environment } from '../../../environments/environment'
import { VideoWatchPlaylistComponent } from './shared'
-type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
+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
+}
@Component({
selector: 'my-video-watch',
@@ -60,10 +76,9 @@ type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
export class VideoWatchComponent implements OnInit, OnDestroy {
@ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
+ @ViewChild('playerElement') playerElement: ElementRef
- player: VideoJsPlayer
- playerElement: HTMLVideoElement
- playerPlaceholderImgSrc: string
+ peertubePlayer: PeerTubePlayer
theaterEnabled = false
video: VideoDetails = null
@@ -78,8 +93,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
remoteServerDown = false
noPlaylistVideoFound = false
- private nextVideoUUID = ''
- private nextVideoTitle = ''
+ private nextRecommendedVideoUUID = ''
+ private nextRecommendedVideoTitle = ''
private videoFileToken: string
@@ -130,11 +145,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.userService.getAnonymousUser()
}
- ngOnInit () {
+ async ngOnInit () {
this.serverConfig = this.serverService.getHTMLConfig()
- PeertubePlayerManager.initState()
-
this.loadRouteParams()
this.loadRouteQuery()
@@ -143,10 +156,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.hooks.runAction('action:video-watch.init', 'video-watch')
setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI
+
+ 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 () {
- this.flushPlayer()
+ if (this.peertubePlayer) this.peertubePlayer.destroy()
// Unsubscribe subscriptions
if (this.paramsSub) this.paramsSub.unsubscribe()
@@ -171,14 +194,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
// The recommended videos's first element should be the next video
const video = videos[0]
- this.nextVideoUUID = video.uuid
- this.nextVideoTitle = video.name
+ this.nextRecommendedVideoUUID = video.uuid
+ this.nextRecommendedVideoTitle = video.name
}
handleTimestampClicked (timestamp: number) {
- if (!this.player || this.video.isLive) return
+ if (!this.peertubePlayer || this.video.isLive) return
- this.player.currentTime(timestamp)
+ this.peertubePlayer.getPlayer().currentTime(timestamp)
scrollToTop()
}
@@ -243,7 +266,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition)
const start = queryParams['start']
- if (this.player && start) this.player.currentTime(parseInt(start, 10))
+ if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10))
})
}
@@ -256,8 +279,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (this.isSameElement(this.video, videoId)) return
- if (this.player) this.player.pause()
-
this.video = undefined
const videoObs = this.hooks.wrapObsFun(
@@ -291,23 +312,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.userService.getAnonymousOrLoggedUser()
]).subscribe({
next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => {
- const queryParams = this.route.snapshot.queryParams
-
- const urlOptions = {
- resume: queryParams.resume,
-
- startTime: queryParams.start,
- stopTime: queryParams.stop,
-
- muted: queryParams.muted,
- loop: queryParams.loop,
- subtitle: queryParams.subtitle,
-
- playerMode: queryParams.mode,
- playbackRate: queryParams.playbackRate,
- peertubeLink: false
- }
-
this.onVideoFetched({
video,
live,
@@ -316,7 +320,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoFileToken,
videoPassword,
loggedInOrAnonymousUser,
- urlOptions,
forceAutoplay
}).catch(err => {
this.handleGlobalError(err)
@@ -386,14 +389,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
const errorMessage: string = typeof err === 'string' ? err : err.message
if (!errorMessage) return
- // Display a message in the video player instead of a notification
- if (errorMessage.includes('from xs param')) {
- this.flushPlayer()
- this.remoteServerDown = true
-
- return
- }
-
this.notifier.error(errorMessage)
}
@@ -422,7 +417,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoFileToken: string
videoPassword: string
- urlOptions: URLOptions
loggedInOrAnonymousUser: User
forceAutoplay: boolean
}) {
@@ -431,7 +425,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
live,
videoCaptions,
storyboards,
- urlOptions,
videoFileToken,
videoPassword,
loggedInOrAnonymousUser,
@@ -448,7 +441,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.storyboards = storyboards
// Re init attributes
- this.playerPlaceholderImgSrc = undefined
this.remoteServerDown = false
this.currentTime = undefined
@@ -462,7 +454,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.buildHotkeysHelp(video)
- this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay })
+ this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay })
.catch(err => logger.error('Cannot build the player', err))
this.setOpenGraphTags()
@@ -475,28 +467,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions)
}
- private async buildPlayer (options: {
- urlOptions: URLOptions
+ private async loadPlayer (options: {
loggedInOrAnonymousUser: User
forceAutoplay: boolean
}) {
- const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options
-
- // Flush old player if needed
- this.flushPlayer()
+ const { loggedInOrAnonymousUser, forceAutoplay } = options
const videoState = this.video.state.id
if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) {
- this.playerPlaceholderImgSrc = this.video.previewPath
+ this.updatePlayerOnNoLive()
return
}
- // Build video element, because videojs removes it on dispose
- const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper')
- this.playerElement = document.createElement('video')
- this.playerElement.className = 'video-js vjs-peertube-skin'
- this.playerElement.setAttribute('playsinline', 'true')
- playerElementWrapper.appendChild(this.playerElement)
+ this.peertubePlayer?.enable()
const params = {
video: this.video,
@@ -505,86 +488,49 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
liveVideo: this.liveVideo,
videoFileToken: this.videoFileToken,
videoPassword: this.videoPassword,
- urlOptions,
+ urlOptions: this.getUrlOptions(),
loggedInOrAnonymousUser,
forceAutoplay,
user: this.user
}
- const { playerMode, playerOptions } = await this.hooks.wrapFun(
- this.buildPlayerManagerOptions.bind(this),
+
+ const loadOptions = await this.hooks.wrapFun(
+ this.buildPeerTubePlayerLoadOptions.bind(this),
params,
'video-watch',
- 'filter:internal.video-watch.player.build-options.params',
- 'filter:internal.video-watch.player.build-options.result'
+ 'filter:internal.video-watch.player.load-options.params',
+ 'filter:internal.video-watch.player.load-options.result'
)
this.zone.runOutsideAngular(async () => {
- this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player)
+ await this.peertubePlayer.load(loadOptions)
- this.player.on('customError', (_e, data: any) => {
- this.zone.run(() => this.handleGlobalError(data.err))
- })
+ const player = this.peertubePlayer.getPlayer()
- this.player.on('timeupdate', () => {
+ player.on('timeupdate', () => {
// Don't need to trigger angular change for this variable, that is sent to children components on click
- this.currentTime = Math.floor(this.player.currentTime())
+ this.currentTime = Math.floor(player.currentTime())
})
- /**
- * 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 click of the timer checking if we need to reset progress
- * and wait until suspended becomes truthy again.
- */
- this.player.upnext({
- timeout: 5000, // 5s
+ 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
- headText: $localize`Up Next`,
- cancelText: $localize`Cancel`,
- suspendedText: $localize`Autoplay is suspended`,
+ this.video.state.id = VideoState.LIVE_ENDED
- getTitle: () => this.nextVideoTitle,
+ this.updatePlayerOnNoLive()
+ })
+ })
+ }
- next: () => this.zone.run(() => this.playNextVideoInAngularZone()),
- condition: () => {
- if (!this.playlist) return this.isAutoPlayNext()
-
- // Don't wait timeout to play the next playlist video
- if (this.isPlaylistAutoPlayNext()) {
- this.playNextVideoInAngularZone()
- return undefined
- }
-
- return false
- },
-
- suspended: () => {
- return (
- !isXPercentInViewport(this.player.el() as HTMLElement, 80) ||
- !document.getElementById('content').contains(document.activeElement)
- )
- }
- })
-
- this.player.one('stopped', () => {
- if (this.playlist && this.isPlaylistAutoPlayNext()) {
- this.playNextVideoInAngularZone()
- }
- })
-
- this.player.one('ended', () => {
- if (this.video.isLive) {
- this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED)
- }
- })
-
- this.player.on('theaterChange', (_: any, enabled: boolean) => {
+ player.on('theater-change', (_: any, enabled: boolean) => {
this.zone.run(() => this.theaterEnabled = enabled)
})
this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', {
- player: this.player,
+ player,
playlist: this.playlist,
playlistPosition: this.playlistPosition,
videojs,
@@ -601,15 +547,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return true
}
- private playNextVideoInAngularZone () {
+ private getNextVideoTitle () {
if (this.playlist) {
- this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
- return
+ return this.videoWatchPlaylist.getNextVideo()?.video?.name || ''
}
- if (this.nextVideoUUID) {
- this.router.navigate([ '/w', this.nextVideoUUID ])
- }
+ return this.nextRecommendedVideoTitle
+ }
+
+ private playNextVideoInAngularZone () {
+ this.zone.run(() => {
+ if (this.playlist) {
+ this.videoWatchPlaylist.navigateToNextPlaylistVideo()
+ return
+ }
+
+ if (this.nextRecommendedVideoUUID) {
+ this.router.navigate([ '/w', this.nextRecommendedVideoUUID ])
+ }
+ })
}
private isAutoplay () {
@@ -637,19 +593,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
}
- private flushPlayer () {
- // Remove player if it exists
- if (!this.player) return
+ private buildPeerTubePlayerConstructorOptions (options: {
+ urlOptions: URLOptions
+ }): PeerTubePlayerContructorOptions {
+ const { urlOptions } = options
- try {
- this.player.dispose()
- this.player = undefined
- } catch (err) {
- logger.error('Cannot dispose player.', err)
+ 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,
+
+ errorNotifier: (message: string) => this.notifier.error(message),
+
+ peertubeLink: () => false,
+
+ pluginsManager: this.pluginService.getPluginsManager()
}
}
- private buildPlayerManagerOptions (params: {
+ private buildPeerTubePlayerLoadOptions (options: {
video: VideoDetails
liveVideo: LiveVideo
videoCaptions: VideoCaption[]
@@ -658,12 +640,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoFileToken: string
videoPassword: string
- urlOptions: CustomizationOptions & { playerMode: PlayerMode }
+ urlOptions: URLOptions
loggedInOrAnonymousUser: User
forceAutoplay: boolean
user?: AuthUser // Keep for plugins
- }) {
+ }): PeerTubePlayerLoadOptions {
const {
video,
liveVideo,
@@ -674,7 +656,30 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
urlOptions,
loggedInOrAnonymousUser,
forceAutoplay
- } = params
+ } = 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
@@ -714,118 +719,80 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
? { latencyMode: liveVideo.latencyMode }
: undefined
- const options: PeertubePlayerManagerOptions = {
- common: {
- autoplay: this.isAutoplay(),
- forceAutoplay,
- p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
+ return {
+ mode,
- hasNextVideo: () => this.hasNextVideo(),
- nextVideo: () => this.playNextVideoInAngularZone(),
+ autoplay: this.isAutoplay(),
+ forceAutoplay,
- playerElement: this.playerElement,
- onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
+ duration: this.video.duration,
+ poster: video.previewUrl,
+ p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
- videoDuration: video.duration,
- enableHotkeys: true,
- inactivityTimeout: 2500,
- poster: video.previewUrl,
+ startTime,
+ stopTime: urlOptions.stopTime,
- startTime,
- stopTime: urlOptions.stopTime,
- controlBar: urlOptions.controlBar,
- controls: urlOptions.controls,
- muted: urlOptions.muted,
- loop: urlOptions.loop,
- subtitle: urlOptions.subtitle,
- playbackRate: urlOptions.playbackRate,
+ embedUrl: video.embedUrl,
+ embedTitle: video.name,
- peertubeLink: urlOptions.peertubeLink,
+ isLive: video.isLive,
+ liveOptions,
- theaterButton: true,
- captions: videoCaptions.length !== 0,
+ videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
+ ? this.videoService.getVideoViewUrl(video.uuid)
+ : null,
- embedUrl: video.embedUrl,
- embedTitle: video.name,
- instanceName: this.serverConfig.instance.name,
+ videoFileToken: () => videoFileToken,
+ requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
+ requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
+ !video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
+ videoPassword: () => videoPassword,
- isLive: video.isLive,
- liveOptions,
+ videoCaptions: playerCaptions,
+ storyboard,
- language: this.localeId,
+ videoShortUUID: video.shortUUID,
+ videoUUID: video.uuid,
- metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
+ previousVideo: {
+ enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(),
- videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
- ? this.videoService.getVideoViewUrl(video.uuid)
- : null,
- videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS,
- authorizationHeader: () => this.authService.getRequestHeaderValue(),
+ handler: this.playlist
+ ? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo())
+ : undefined,
- serverUrl: environment.originServerUrl || window.location.origin,
-
- videoFileToken: () => videoFileToken,
- requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
- requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
- !video.canAccessPasswordProtectedVideoWithoutPassword(this.user),
- videoPassword: () => videoPassword,
-
- videoCaptions: playerCaptions,
- storyboard,
-
- videoShortUUID: video.shortUUID,
- videoUUID: video.uuid,
-
- errorNotifier: (message: string) => this.notifier.error(message)
+ displayControlBarButton: !!this.playlist
},
- webtorrent: {
+ nextVideo: {
+ enabled: this.hasNextVideo(),
+ handler: () => this.playNextVideoInAngularZone(),
+ getVideoTitle: () => this.getNextVideoTitle(),
+ displayControlBarButton: this.hasNextVideo()
+ },
+
+ upnext: {
+ isEnabled: () => {
+ if (this.playlist) return this.isPlaylistAutoPlayNext()
+
+ return this.isAutoPlayNext()
+ },
+
+ isSuspended: (player: videojs.Player) => {
+ return !isXPercentInViewport(player.el() as HTMLElement, 80)
+ },
+
+ timeout: this.playlist
+ ? 0 // Don't wait to play next video in playlist
+ : 5000 // 5 seconds for a recommended video
+ },
+
+ hls: hlsOptions,
+
+ webVideo: {
videoFiles: video.files
- },
-
- pluginsManager: this.pluginService.getPluginsManager()
- }
-
- // Only set this if we're in a playlist
- if (this.playlist) {
- options.common.hasPreviousVideo = () => this.videoWatchPlaylist.hasPreviousVideo()
-
- options.common.previousVideo = () => {
- this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo())
}
}
-
- 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'
- }
-
- // FIXME: remove, we don't support these old web browsers anymore
- // p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available
- if (typeof TextEncoder === 'undefined') {
- mode = 'webtorrent'
- }
-
- 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 }
}
private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
@@ -873,6 +840,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.video.viewers = newViewers
}
+ 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)
@@ -944,4 +917,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.metaService.setTag('og:url', window.location.href)
this.metaService.setTag('url', window.location.href)
}
+
+ 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
+ }
+ }
}
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts
index 69b2b18c0..b69e31edf 100644
--- a/client/src/app/helpers/utils/object.ts
+++ b/client/src/app/helpers/utils/object.ts
@@ -34,6 +34,8 @@ function toBoolean (value: any) {
if (value === 'true') return true
if (value === 'false') return false
+ if (value === '1') return true
+ if (value === '0') return false
return undefined
}
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
index 45df0be38..14a5abd7a 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
@@ -241,7 +241,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
}
reloadVideos () {
- console.log('reload')
this.pagination.currentPage = 1
this.loadMoreVideos(true)
}
@@ -420,8 +419,9 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
this.lastQueryLength = data.length
if (reset) this.videos = []
+
this.videos = this.videos.concat(data)
- console.log('subscribe')
+
if (this.groupByDate) this.buildGroupedDateLabels()
this.onDataSubject.next(data)
diff --git a/client/src/assets/player/index.ts b/client/src/assets/player/index.ts
index 9b87afc4a..d34188ea7 100644
--- a/client/src/assets/player/index.ts
+++ b/client/src/assets/player/index.ts
@@ -1,2 +1,2 @@
-export * from './peertube-player-manager'
+export * from './peertube-player'
export * from './types'
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
deleted file mode 100644
index 66d9c7298..000000000
--- a/client/src/assets/player/peertube-player-manager.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-import '@peertube/videojs-contextmenu'
-import './shared/upnext/end-card'
-import './shared/upnext/upnext-plugin'
-import './shared/stats/stats-card'
-import './shared/stats/stats-plugin'
-import './shared/bezels/bezels-plugin'
-import './shared/peertube/peertube-plugin'
-import './shared/resolutions/peertube-resolutions-plugin'
-import './shared/control-bar/storyboard-plugin'
-import './shared/control-bar/next-previous-video-button'
-import './shared/control-bar/p2p-info-button'
-import './shared/control-bar/peertube-link-button'
-import './shared/control-bar/peertube-load-progress-bar'
-import './shared/control-bar/theater-button'
-import './shared/control-bar/peertube-live-display'
-import './shared/settings/resolution-menu-button'
-import './shared/settings/resolution-menu-item'
-import './shared/settings/settings-dialog'
-import './shared/settings/settings-menu-button'
-import './shared/settings/settings-menu-item'
-import './shared/settings/settings-panel'
-import './shared/settings/settings-panel-child'
-import './shared/playlist/playlist-plugin'
-import './shared/mobile/peertube-mobile-plugin'
-import './shared/mobile/peertube-mobile-buttons'
-import './shared/hotkeys/peertube-hotkeys-plugin'
-import './shared/metrics/metrics-plugin'
-import videojs from 'video.js'
-import { logger } from '@root-helpers/logger'
-import { PluginsManager } from '@root-helpers/plugins-manager'
-import { isMobile } from '@root-helpers/web-browser'
-import { saveAverageBandwidth } from './peertube-player-local-storage'
-import { ManagerOptionsBuilder } from './shared/manager-options'
-import { TranslationsManager } from './translations-manager'
-import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode, PlayerNetworkInfo } from './types'
-
-// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
-(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
-
-const CaptionsButton = videojs.getComponent('CaptionsButton') as any
-// Change Captions to Subtitles/CC
-CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
-// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
-CaptionsButton.prototype.label_ = ' '
-
-// TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged
-const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any
-if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) {
- PlayProgressBar.prototype.options_.children.push('timeTooltip')
-}
-
-export class PeertubePlayerManager {
- private static playerElementClassName: string
- private static playerElementAttributes: { name: string, value: string }[] = []
-
- private static onPlayerChange: (player: videojs.Player) => void
- private static alreadyPlayed = false
- private static pluginsManager: PluginsManager
-
- private static videojsDecodeErrors = 0
-
- private static p2pMediaLoaderModule: any
-
- static initState () {
- this.alreadyPlayed = false
- }
-
- static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
- this.pluginsManager = options.pluginsManager
-
- this.onPlayerChange = onPlayerChange
-
- this.playerElementClassName = options.common.playerElement.className
-
- for (const name of options.common.playerElement.getAttributeNames()) {
- this.playerElementAttributes.push({ name, value: options.common.playerElement.getAttribute(name) })
- }
-
- if (mode === 'webtorrent') await import('./shared/webtorrent/webtorrent-plugin')
- if (mode === 'p2p-media-loader') {
- const [ p2pMediaLoaderModule ] = await Promise.all([
- import('@peertube/p2p-media-loader-hlsjs'),
- import('./shared/p2p-media-loader/p2p-media-loader-plugin')
- ])
-
- this.p2pMediaLoaderModule = p2pMediaLoaderModule
- }
-
- await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
-
- return this.buildPlayer(mode, options)
- }
-
- private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise {
- const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
-
- const videojsOptions = await this.pluginsManager.runHook(
- 'filter:internal.player.videojs.options.result',
- videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed)
- )
-
- const self = this
- return new Promise(res => {
- videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
- const player = this
-
- if (!isNaN(+options.common.playbackRate)) {
- player.playbackRate(+options.common.playbackRate)
- }
-
- let alreadyFallback = false
-
- const handleError = () => {
- if (alreadyFallback) return
- alreadyFallback = true
-
- if (mode === 'p2p-media-loader') {
- self.tryToRecoverHLSError(player.error(), player, options)
- } else {
- self.maybeFallbackToWebTorrent(mode, player, options)
- }
- }
-
- player.one('error', () => handleError())
-
- player.one('play', () => {
- self.alreadyPlayed = true
- })
-
- self.addContextMenu(videojsOptionsBuilder, player, options.common)
-
- if (isMobile()) player.peertubeMobile()
- if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive })
- if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden')
-
- player.bezels()
-
- player.stats({
- videoUUID: options.common.videoUUID,
- videoIsLive: options.common.isLive,
- mode,
- p2pEnabled: options.common.p2pEnabled
- })
-
- if (options.common.storyboard) {
- player.storyboard(options.common.storyboard)
- }
-
- player.on('p2pInfo', (_, data: PlayerNetworkInfo) => {
- if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return
-
- saveAverageBandwidth(data.bandwidthEstimate)
- })
-
- const offlineNotificationElem = document.createElement('div')
- offlineNotificationElem.classList.add('vjs-peertube-offline-notification')
- offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work')
-
- let offlineNotificationElemAdded = false
-
- const handleOnline = () => {
- if (!offlineNotificationElemAdded) return
-
- player.el().removeChild(offlineNotificationElem)
- offlineNotificationElemAdded = false
-
- logger.info('The browser is online')
- }
-
- const handleOffline = () => {
- if (offlineNotificationElemAdded) return
-
- player.el().appendChild(offlineNotificationElem)
- offlineNotificationElemAdded = true
-
- logger.info('The browser is offline')
- }
-
- window.addEventListener('online', handleOnline)
- window.addEventListener('offline', handleOffline)
-
- player.on('dispose', () => {
- window.removeEventListener('online', handleOnline)
- window.removeEventListener('offline', handleOffline)
- })
-
- return res(player)
- })
- })
- }
-
- private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) {
- if (err.code === MediaError.MEDIA_ERR_DECODE) {
-
- // Display a notification to user
- if (this.videojsDecodeErrors === 0) {
- options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.'))
- }
-
- if (this.videojsDecodeErrors === 20) {
- this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
- return
- }
-
- logger.info('Fast forwarding HLS to recover from an error.')
-
- this.videojsDecodeErrors++
-
- options.common.startTime = currentPlayer.currentTime() + 2
- options.common.autoplay = true
- this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
-
- const newPlayer = await this.buildPlayer('p2p-media-loader', options)
- this.onPlayerChange(newPlayer)
- } else {
- this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
- }
- }
-
- private static async maybeFallbackToWebTorrent (
- currentMode: PlayerMode,
- currentPlayer: videojs.Player,
- options: PeertubePlayerManagerOptions
- ) {
- if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') {
- currentPlayer.peertube().displayFatalError()
- return
- }
-
- logger.info('Fallback to webtorrent.')
-
- this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
-
- await import('./shared/webtorrent/webtorrent-plugin')
-
- const newPlayer = await this.buildPlayer('webtorrent', options)
- this.onPlayerChange(newPlayer)
- }
-
- private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) {
- const newVideoElement = document.createElement('video')
-
- // Reset class
- newVideoElement.className = this.playerElementClassName
-
- // Reapply attributes
- for (const { name, value } of this.playerElementAttributes) {
- newVideoElement.setAttribute(name, value)
- }
-
- // VideoJS wraps our video element inside a div
- let currentParentPlayerElement = commonOptions.playerElement.parentNode
- // Fix on IOS, don't ask me why
- if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode
-
- currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
-
- commonOptions.playerElement = newVideoElement
- commonOptions.onPlayerElementChange(newVideoElement)
-
- player.dispose()
-
- return newVideoElement
- }
-
- private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
- const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
-
- player.contextmenuUI(options)
- }
-}
-
-// ############################################################################
-
-export {
- videojs
-}
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts
new file mode 100644
index 000000000..a7a2b4065
--- /dev/null
+++ b/client/src/assets/player/peertube-player.ts
@@ -0,0 +1,522 @@
+import '@peertube/videojs-contextmenu'
+import './shared/upnext/end-card'
+import './shared/upnext/upnext-plugin'
+import './shared/stats/stats-card'
+import './shared/stats/stats-plugin'
+import './shared/bezels/bezels-plugin'
+import './shared/peertube/peertube-plugin'
+import './shared/resolutions/peertube-resolutions-plugin'
+import './shared/control-bar/storyboard-plugin'
+import './shared/control-bar/next-previous-video-button'
+import './shared/control-bar/p2p-info-button'
+import './shared/control-bar/peertube-link-button'
+import './shared/control-bar/theater-button'
+import './shared/control-bar/peertube-live-display'
+import './shared/settings/resolution-menu-button'
+import './shared/settings/resolution-menu-item'
+import './shared/settings/settings-dialog'
+import './shared/settings/settings-menu-button'
+import './shared/settings/settings-menu-item'
+import './shared/settings/settings-panel'
+import './shared/settings/settings-panel-child'
+import './shared/playlist/playlist-plugin'
+import './shared/mobile/peertube-mobile-plugin'
+import './shared/mobile/peertube-mobile-buttons'
+import './shared/hotkeys/peertube-hotkeys-plugin'
+import './shared/metrics/metrics-plugin'
+import videojs, { VideoJsPlayer } from 'video.js'
+import { logger } from '@root-helpers/logger'
+import { PluginsManager } from '@root-helpers/plugins-manager'
+import { copyToClipboard } from '@root-helpers/utils'
+import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
+import { isMobile } from '@root-helpers/web-browser'
+import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@shared/core-utils'
+import { saveAverageBandwidth } from './peertube-player-local-storage'
+import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder'
+import { TranslationsManager } from './translations-manager'
+import { PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types'
+
+// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
+(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
+
+const CaptionsButton = videojs.getComponent('CaptionsButton') as any
+// Change Captions to Subtitles/CC
+CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
+// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
+CaptionsButton.prototype.label_ = ' '
+
+// TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged
+const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any
+if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) {
+ PlayProgressBar.prototype.options_.children.push('timeTooltip')
+}
+
+export class PeerTubePlayer {
+ private pluginsManager: PluginsManager
+
+ private videojsDecodeErrors = 0
+
+ private p2pMediaLoaderModule: any
+
+ private player: VideoJsPlayer
+
+ private currentLoadOptions: PeerTubePlayerLoadOptions
+
+ private moduleLoaded = {
+ webVideo: false,
+ p2pMediaLoader: false
+ }
+
+ constructor (private options: PeerTubePlayerContructorOptions) {
+ this.pluginsManager = options.pluginsManager
+ }
+
+ unload () {
+ if (!this.player) return
+
+ this.disposeDynamicPluginsIfNeeded()
+
+ this.player.reset()
+ }
+
+ async load (loadOptions: PeerTubePlayerLoadOptions) {
+ this.currentLoadOptions = loadOptions
+
+ this.setPoster('')
+
+ this.disposeDynamicPluginsIfNeeded()
+
+ await this.lazyLoadModulesIfNeeded()
+ await this.buildPlayerIfNeeded()
+
+ if (this.currentLoadOptions.mode === 'p2p-media-loader') {
+ await this.loadP2PMediaLoader()
+ } else {
+ this.loadWebVideo()
+ }
+
+ this.loadDynamicPlugins()
+
+ if (this.options.controlBar === false) this.player.controlBar.hide()
+ else this.player.controlBar.show()
+
+ this.player.autoplay(this.getAutoPlayValue(this.currentLoadOptions.autoplay))
+
+ this.player.trigger('video-change')
+ }
+
+ getPlayer () {
+ return this.player
+ }
+
+ destroy () {
+ if (this.player) this.player.dispose()
+ }
+
+ setPoster (url: string) {
+ this.player?.poster(url)
+ this.options.playerElement().poster = url
+ }
+
+ enable () {
+ if (!this.player) return
+
+ (this.player.el() as HTMLElement).style.pointerEvents = 'auto'
+ }
+
+ disable () {
+ if (!this.player) return
+
+ if (this.player.isFullscreen()) {
+ this.player.exitFullscreen()
+ }
+
+ // Disable player
+ this.player.hasStarted(false)
+ this.player.removeClass('vjs-has-autoplay')
+ this.player.bigPlayButton.hide();
+
+ (this.player.el() as HTMLElement).style.pointerEvents = 'none'
+ }
+
+ private async loadP2PMediaLoader () {
+ const hlsOptionsBuilder = new HLSOptionsBuilder({
+ ...pick(this.options, [ 'pluginsManager', 'serverUrl', 'authorizationHeader' ]),
+ ...pick(this.currentLoadOptions, [
+ 'videoPassword',
+ 'requiresUserAuth',
+ 'videoFileToken',
+ 'requiresPassword',
+ 'isLive',
+ 'p2pEnabled',
+ 'liveOptions',
+ 'hls'
+ ])
+ }, this.p2pMediaLoaderModule)
+
+ const { hlsjs, p2pMediaLoader } = await hlsOptionsBuilder.getPluginOptions()
+
+ this.player.hlsjs(hlsjs)
+ this.player.p2pMediaLoader(p2pMediaLoader)
+ }
+
+ private loadWebVideo () {
+ const webVideoOptionsBuilder = new WebVideoOptionsBuilder(pick(this.currentLoadOptions, [
+ 'videoFileToken',
+ 'webVideo',
+ 'hls',
+ 'startTime'
+ ]))
+
+ this.player.webVideo(webVideoOptionsBuilder.getPluginOptions())
+ }
+
+ private async buildPlayerIfNeeded () {
+ if (this.player) return
+
+ await TranslationsManager.loadLocaleInVideoJS(this.options.serverUrl, this.options.language, videojs)
+
+ const videojsOptions = await this.pluginsManager.runHook(
+ 'filter:internal.player.videojs.options.result',
+ this.getVideojsOptions()
+ )
+
+ this.player = videojs(this.options.playerElement(), videojsOptions)
+
+ this.player.ready(() => {
+ if (!isNaN(+this.options.playbackRate)) {
+ this.player.playbackRate(+this.options.playbackRate)
+ }
+
+ let alreadyFallback = false
+
+ const handleError = () => {
+ if (alreadyFallback) return
+ alreadyFallback = true
+
+ if (this.currentLoadOptions.mode === 'p2p-media-loader') {
+ this.tryToRecoverHLSError(this.player.error())
+ } else {
+ this.maybeFallbackToWebVideo()
+ }
+ }
+
+ this.player.one('error', () => handleError())
+
+ this.player.on('p2p-info', (_, data: PlayerNetworkInfo) => {
+ if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return
+
+ saveAverageBandwidth(data.bandwidthEstimate)
+ })
+
+ this.player.contextmenuUI(this.getContextMenuOptions())
+
+ this.displayNotificationWhenOffline()
+ })
+ }
+
+ private disposeDynamicPluginsIfNeeded () {
+ if (!this.player) return
+
+ if (this.player.usingPlugin('peertubeMobile')) this.player.peertubeMobile().dispose()
+ if (this.player.usingPlugin('peerTubeHotkeysPlugin')) this.player.peerTubeHotkeysPlugin().dispose()
+ if (this.player.usingPlugin('playlist')) this.player.playlist().dispose()
+ if (this.player.usingPlugin('bezels')) this.player.bezels().dispose()
+ if (this.player.usingPlugin('upnext')) this.player.upnext().dispose()
+ if (this.player.usingPlugin('stats')) this.player.stats().dispose()
+ if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose()
+
+ if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose()
+
+ if (this.player.usingPlugin('p2pMediaLoader')) this.player.p2pMediaLoader().dispose()
+ if (this.player.usingPlugin('hlsjs')) this.player.hlsjs().dispose()
+
+ if (this.player.usingPlugin('webVideo')) this.player.webVideo().dispose()
+ }
+
+ private loadDynamicPlugins () {
+ if (isMobile()) this.player.peertubeMobile()
+
+ this.player.bezels()
+
+ this.player.stats({
+ videoUUID: this.currentLoadOptions.videoUUID,
+ videoIsLive: this.currentLoadOptions.isLive,
+ mode: this.currentLoadOptions.mode,
+ p2pEnabled: this.currentLoadOptions.p2pEnabled
+ })
+
+ if (this.options.enableHotkeys === true) {
+ this.player.peerTubeHotkeysPlugin({ isLive: this.currentLoadOptions.isLive })
+ }
+
+ if (this.currentLoadOptions.playlist) {
+ this.player.playlist(this.currentLoadOptions.playlist)
+ }
+
+ if (this.currentLoadOptions.upnext) {
+ this.player.upnext({
+ timeout: this.currentLoadOptions.upnext.timeout,
+
+ getTitle: () => this.currentLoadOptions.nextVideo.getVideoTitle(),
+
+ next: () => this.currentLoadOptions.nextVideo.handler(),
+ isDisplayed: () => this.currentLoadOptions.nextVideo.enabled && this.currentLoadOptions.upnext.isEnabled(),
+
+ isSuspended: () => this.currentLoadOptions.upnext.isSuspended(this.player)
+ })
+ }
+
+ if (this.currentLoadOptions.storyboard) {
+ this.player.storyboard(this.currentLoadOptions.storyboard)
+ }
+
+ if (this.currentLoadOptions.dock) {
+ this.player.peertubeDock(this.currentLoadOptions.dock)
+ }
+ }
+
+ private async lazyLoadModulesIfNeeded () {
+ if (this.currentLoadOptions.mode === 'web-video' && this.moduleLoaded.webVideo !== true) {
+ await import('./shared/web-video/web-video-plugin')
+ }
+
+ if (this.currentLoadOptions.mode === 'p2p-media-loader' && this.moduleLoaded.p2pMediaLoader !== true) {
+ const [ p2pMediaLoaderModule ] = await Promise.all([
+ import('@peertube/p2p-media-loader-hlsjs'),
+ import('./shared/p2p-media-loader/hls-plugin'),
+ import('./shared/p2p-media-loader/p2p-media-loader-plugin')
+ ])
+
+ this.p2pMediaLoaderModule = p2pMediaLoaderModule
+ }
+ }
+
+ private async tryToRecoverHLSError (err: any) {
+ if (err.code === MediaError.MEDIA_ERR_DECODE) {
+
+ // Display a notification to user
+ if (this.videojsDecodeErrors === 0) {
+ this.options.errorNotifier(this.player.localize('The video failed to play, will try to fast forward.'))
+ }
+
+ if (this.videojsDecodeErrors === 20) {
+ this.maybeFallbackToWebVideo()
+ return
+ }
+
+ logger.info('Fast forwarding HLS to recover from an error.')
+
+ this.videojsDecodeErrors++
+
+ await this.load({
+ ...this.currentLoadOptions,
+
+ mode: 'p2p-media-loader',
+ startTime: this.player.currentTime() + 2,
+ autoplay: true
+ })
+ } else {
+ this.maybeFallbackToWebVideo()
+ }
+ }
+
+ private async maybeFallbackToWebVideo () {
+ if (this.currentLoadOptions.webVideo.videoFiles.length === 0 || this.currentLoadOptions.mode === 'web-video') {
+ this.player.peertube().displayFatalError()
+ return
+ }
+
+ logger.info('Fallback to web-video.')
+
+ await this.load({
+ ...this.currentLoadOptions,
+
+ mode: 'web-video',
+ startTime: this.player.currentTime(),
+ autoplay: true
+ })
+ }
+
+ getVideojsOptions (): videojs.PlayerOptions {
+ const html5 = {
+ preloadTextTracks: false
+ }
+
+ const plugins: VideoJSPluginOptions = {
+ peertube: {
+ hasAutoplay: () => this.getAutoPlayValue(this.currentLoadOptions.autoplay),
+
+ videoViewUrl: () => this.currentLoadOptions.videoViewUrl,
+ videoViewIntervalMs: this.options.videoViewIntervalMs,
+
+ authorizationHeader: this.options.authorizationHeader,
+
+ videoDuration: () => this.currentLoadOptions.duration,
+
+ startTime: () => this.currentLoadOptions.startTime,
+ stopTime: () => this.currentLoadOptions.stopTime,
+
+ videoCaptions: () => this.currentLoadOptions.videoCaptions,
+ isLive: () => this.currentLoadOptions.isLive,
+ videoUUID: () => this.currentLoadOptions.videoUUID,
+ subtitle: () => this.currentLoadOptions.subtitle
+ },
+ metrics: {
+ mode: () => this.currentLoadOptions.mode,
+
+ metricsUrl: () => this.options.metricsUrl,
+ videoUUID: () => this.currentLoadOptions.videoUUID
+ }
+ }
+
+ const controlBarOptionsBuilder = new ControlBarOptionsBuilder({
+ ...this.options,
+
+ videoShortUUID: () => this.currentLoadOptions.videoShortUUID,
+ p2pEnabled: () => this.currentLoadOptions.p2pEnabled,
+
+ nextVideo: () => this.currentLoadOptions.nextVideo,
+ previousVideo: () => this.currentLoadOptions.previousVideo
+ })
+
+ const videojsOptions = {
+ html5,
+
+ // We don't use text track settings for now
+ textTrackSettings: false as any, // FIXME: typings
+ controls: this.options.controls !== undefined ? this.options.controls : true,
+ loop: this.options.loop !== undefined ? this.options.loop : false,
+
+ muted: this.options.muted !== undefined
+ ? this.options.muted
+ : undefined, // Undefined so the player knows it has to check the local storage
+
+ autoplay: this.getAutoPlayValue(this.currentLoadOptions.autoplay),
+
+ poster: this.currentLoadOptions.poster,
+ inactivityTimeout: this.options.inactivityTimeout,
+ playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
+
+ plugins,
+
+ controlBar: {
+ children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings
+ },
+
+ language: this.options.language && !isDefaultLocale(this.options.language)
+ ? this.options.language
+ : undefined
+ }
+
+ return videojsOptions
+ }
+
+ private getAutoPlayValue (autoplay: boolean): videojs.Autoplay {
+ if (autoplay !== true) return false
+
+ return this.currentLoadOptions.forceAutoplay
+ ? 'any'
+ : 'play'
+ }
+
+ private displayNotificationWhenOffline () {
+ const offlineNotificationElem = document.createElement('div')
+ offlineNotificationElem.classList.add('vjs-peertube-offline-notification')
+ offlineNotificationElem.innerText = this.player.localize('You seem to be offline and the video may not work')
+
+ let offlineNotificationElemAdded = false
+
+ const handleOnline = () => {
+ if (!offlineNotificationElemAdded) return
+
+ this.player.el().removeChild(offlineNotificationElem)
+ offlineNotificationElemAdded = false
+
+ logger.info('The browser is online')
+ }
+
+ const handleOffline = () => {
+ if (offlineNotificationElemAdded) return
+
+ this.player.el().appendChild(offlineNotificationElem)
+ offlineNotificationElemAdded = true
+
+ logger.info('The browser is offline')
+ }
+
+ window.addEventListener('online', handleOnline)
+ window.addEventListener('offline', handleOffline)
+
+ this.player.on('dispose', () => {
+ window.removeEventListener('online', handleOnline)
+ window.removeEventListener('offline', handleOffline)
+ })
+ }
+
+ private getContextMenuOptions () {
+
+ const content = () => {
+ const self = this
+ const player = this.player
+
+ const shortUUID = self.currentLoadOptions.videoShortUUID
+ const isLoopEnabled = player.options_['loop']
+
+ const items = [
+ {
+ icon: 'repeat',
+ label: player.localize('Play in loop') + (isLoopEnabled ? '' : ''),
+ listener: function () {
+ player.options_['loop'] = !isLoopEnabled
+ }
+ },
+ {
+ label: player.localize('Copy the video URL'),
+ listener: function () {
+ copyToClipboard(buildVideoLink({ shortUUID }))
+ }
+ },
+ {
+ label: player.localize('Copy the video URL at the current time'),
+ listener: function () {
+ const url = buildVideoLink({ shortUUID })
+
+ copyToClipboard(decorateVideoLink({ url, startTime: player.currentTime() }))
+ }
+ },
+ {
+ icon: 'code',
+ label: player.localize('Copy embed code'),
+ listener: () => {
+ copyToClipboard(buildVideoOrPlaylistEmbed({
+ embedUrl: self.currentLoadOptions.embedUrl,
+ embedTitle: self.currentLoadOptions.embedTitle
+ }))
+ }
+ }
+ ]
+
+ items.push({
+ icon: 'info',
+ label: player.localize('Stats for nerds'),
+ listener: () => {
+ player.stats().show()
+ }
+ })
+
+ return items.map(i => ({
+ ...i,
+ label: `` + i.label
+ }))
+ }
+
+ return { content }
+ }
+}
+
+// ############################################################################
+
+export {
+ videojs
+}
diff --git a/client/src/assets/player/shared/bezels/bezels-plugin.ts b/client/src/assets/player/shared/bezels/bezels-plugin.ts
index ca88bc1f9..6afb2c6a3 100644
--- a/client/src/assets/player/shared/bezels/bezels-plugin.ts
+++ b/client/src/assets/player/shared/bezels/bezels-plugin.ts
@@ -1,5 +1,5 @@
import videojs from 'video.js'
-import './pause-bezel'
+import { PauseBezel } from './pause-bezel'
const Plugin = videojs.getPlugin('plugin')
@@ -12,7 +12,7 @@ class BezelsPlugin extends Plugin {
player.addClass('vjs-bezels')
})
- player.addChild('PauseBezel', options)
+ player.addChild(new PauseBezel(player, options))
}
}
diff --git a/client/src/assets/player/shared/bezels/pause-bezel.ts b/client/src/assets/player/shared/bezels/pause-bezel.ts
index e35c39a5f..d364ad0dd 100644
--- a/client/src/assets/player/shared/bezels/pause-bezel.ts
+++ b/client/src/assets/player/shared/bezels/pause-bezel.ts
@@ -32,26 +32,61 @@ function getPlayBezel () {
}
const Component = videojs.getComponent('Component')
-class PauseBezel extends Component {
+export class PauseBezel extends Component {
container: HTMLDivElement
+ private firstPlayDone = false
+ private paused = false
+
+ private playerPauseHandler: () => void
+ private playerPlayHandler: () => void
+ private videoChangeHandler: () => void
+
constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
super(player, options)
// Hide bezels on mobile since we already have our mobile overlay
if (isMobile()) return
- player.on('pause', (_: any) => {
- if (player.seeking() || player.ended()) return
+ this.playerPauseHandler = () => {
+ if (player.seeking()) return
+
+ this.paused = true
+
+ if (player.ended()) return
+
this.container.innerHTML = getPauseBezel()
this.showBezel()
- })
+ }
+
+ this.playerPlayHandler = () => {
+ if (player.seeking() || !this.firstPlayDone || !this.paused) {
+ this.firstPlayDone = true
+ return
+ }
+
+ this.paused = false
+ this.firstPlayDone = true
- player.on('play', (_: any) => {
- if (player.seeking()) return
this.container.innerHTML = getPlayBezel()
this.showBezel()
- })
+ }
+
+ this.videoChangeHandler = () => {
+ this.firstPlayDone = false
+ }
+
+ player.on('video-change', () => this.videoChangeHandler)
+ player.on('pause', this.playerPauseHandler)
+ player.on('play', this.playerPlayHandler)
+ }
+
+ dispose () {
+ if (this.playerPauseHandler) this.player().off('pause', this.playerPauseHandler)
+ if (this.playerPlayHandler) this.player().off('play', this.playerPlayHandler)
+ if (this.videoChangeHandler) this.player().off('video-change', this.videoChangeHandler)
+
+ super.dispose()
}
createEl () {
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts
index 24877c267..9307027f6 100644
--- a/client/src/assets/player/shared/control-bar/index.ts
+++ b/client/src/assets/player/shared/control-bar/index.ts
@@ -2,6 +2,5 @@ export * from './next-previous-video-button'
export * from './p2p-info-button'
export * from './peertube-link-button'
export * from './peertube-live-display'
-export * from './peertube-load-progress-bar'
export * from './storyboard-plugin'
export * from './theater-button'
diff --git a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts
index b7b986806..18a107f52 100644
--- a/client/src/assets/player/shared/control-bar/next-previous-video-button.ts
+++ b/client/src/assets/player/shared/control-bar/next-previous-video-button.ts
@@ -4,14 +4,18 @@ import { NextPreviousVideoButtonOptions } from '../../types'
const Button = videojs.getComponent('Button')
class NextPreviousVideoButton extends Button {
- private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions
+ options_: NextPreviousVideoButtonOptions & videojs.ComponentOptions
- constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) {
- super(player, options as any)
+ constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions & videojs.ComponentOptions) {
+ super(player, options)
- this.nextPreviousVideoButtonOptions = options
+ this.player().on('video-change', () => {
+ this.updateDisabled()
+ this.updateShowing()
+ })
- this.update()
+ this.updateDisabled()
+ this.updateShowing()
}
createEl () {
@@ -35,15 +39,20 @@ class NextPreviousVideoButton extends Button {
}
handleClick () {
- this.nextPreviousVideoButtonOptions.handler()
+ this.options_.handler()
}
- update () {
- const disabled = this.nextPreviousVideoButtonOptions.isDisabled()
+ updateDisabled () {
+ const disabled = this.options_.isDisabled()
if (disabled) this.addClass('vjs-disabled')
else this.removeClass('vjs-disabled')
}
+
+ updateShowing () {
+ if (this.options_.isDisplayed()) this.show()
+ else this.hide()
+ }
}
videojs.registerComponent('NextVideoButton', NextPreviousVideoButton)
diff --git a/client/src/assets/player/shared/control-bar/p2p-info-button.ts b/client/src/assets/player/shared/control-bar/p2p-info-button.ts
index 1979654ad..4177b3280 100644
--- a/client/src/assets/player/shared/control-bar/p2p-info-button.ts
+++ b/client/src/assets/player/shared/control-bar/p2p-info-button.ts
@@ -1,71 +1,44 @@
import videojs from 'video.js'
-import { PeerTubeP2PInfoButtonOptions, PlayerNetworkInfo } from '../../types'
+import { PlayerNetworkInfo } from '../../types'
import { bytes } from '../common'
const Button = videojs.getComponent('Button')
-class P2pInfoButton extends Button {
-
- constructor (player: videojs.Player, options?: PeerTubeP2PInfoButtonOptions) {
- super(player, options as any)
- }
+class P2PInfoButton extends Button {
+ el_: HTMLElement
createEl () {
- const div = videojs.dom.createEl('div', {
- className: 'vjs-peertube'
- })
- const subDivWebtorrent = videojs.dom.createEl('div', {
+ const div = videojs.dom.createEl('div', { className: 'vjs-peertube' })
+ const subDivP2P = videojs.dom.createEl('div', {
className: 'vjs-peertube-hidden' // Hide the stats before we get the info
}) as HTMLDivElement
- div.appendChild(subDivWebtorrent)
+ div.appendChild(subDivP2P)
- // Stop here if P2P is not enabled
- const p2pEnabled = (this.options_ as PeerTubeP2PInfoButtonOptions).p2pEnabled
- if (!p2pEnabled) return div as HTMLButtonElement
+ const downloadIcon = videojs.dom.createEl('span', { className: 'icon icon-download' })
+ subDivP2P.appendChild(downloadIcon)
- const downloadIcon = videojs.dom.createEl('span', {
- className: 'icon icon-download'
- })
- subDivWebtorrent.appendChild(downloadIcon)
-
- const downloadSpeedText = videojs.dom.createEl('span', {
- className: 'download-speed-text'
- })
- const downloadSpeedNumber = videojs.dom.createEl('span', {
- className: 'download-speed-number'
- })
+ const downloadSpeedText = videojs.dom.createEl('span', { className: 'download-speed-text' })
+ const downloadSpeedNumber = videojs.dom.createEl('span', { className: 'download-speed-number' })
const downloadSpeedUnit = videojs.dom.createEl('span')
downloadSpeedText.appendChild(downloadSpeedNumber)
downloadSpeedText.appendChild(downloadSpeedUnit)
- subDivWebtorrent.appendChild(downloadSpeedText)
+ subDivP2P.appendChild(downloadSpeedText)
- const uploadIcon = videojs.dom.createEl('span', {
- className: 'icon icon-upload'
- })
- subDivWebtorrent.appendChild(uploadIcon)
+ const uploadIcon = videojs.dom.createEl('span', { className: 'icon icon-upload' })
+ subDivP2P.appendChild(uploadIcon)
- const uploadSpeedText = videojs.dom.createEl('span', {
- className: 'upload-speed-text'
- })
- const uploadSpeedNumber = videojs.dom.createEl('span', {
- className: 'upload-speed-number'
- })
+ const uploadSpeedText = videojs.dom.createEl('span', { className: 'upload-speed-text' })
+ const uploadSpeedNumber = videojs.dom.createEl('span', { className: 'upload-speed-number' })
const uploadSpeedUnit = videojs.dom.createEl('span')
uploadSpeedText.appendChild(uploadSpeedNumber)
uploadSpeedText.appendChild(uploadSpeedUnit)
- subDivWebtorrent.appendChild(uploadSpeedText)
+ subDivP2P.appendChild(uploadSpeedText)
- const peersText = videojs.dom.createEl('span', {
- className: 'peers-text'
- })
- const peersNumber = videojs.dom.createEl('span', {
- className: 'peers-number'
- })
- subDivWebtorrent.appendChild(peersNumber)
- subDivWebtorrent.appendChild(peersText)
+ const peersText = videojs.dom.createEl('span', { className: 'peers-text' })
+ const peersNumber = videojs.dom.createEl('span', { className: 'peers-number' })
+ subDivP2P.appendChild(peersNumber)
+ subDivP2P.appendChild(peersText)
- const subDivHttp = videojs.dom.createEl('div', {
- className: 'vjs-peertube-hidden'
- })
+ const subDivHttp = videojs.dom.createEl('div', { className: 'vjs-peertube-hidden' }) as HTMLElement
const subDivHttpText = videojs.dom.createEl('span', {
className: 'http-fallback',
textContent: 'HTTP'
@@ -74,14 +47,9 @@ class P2pInfoButton extends Button {
subDivHttp.appendChild(subDivHttpText)
div.appendChild(subDivHttp)
- this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
- // We are in HTTP fallback
- if (!data) {
- subDivHttp.className = 'vjs-peertube-displayed'
- subDivWebtorrent.className = 'vjs-peertube-hidden'
-
- return
- }
+ this.player_.on('p2p-info', (_event: any, data: PlayerNetworkInfo) => {
+ subDivP2P.className = 'vjs-peertube-displayed'
+ subDivHttp.className = 'vjs-peertube-hidden'
const p2pStats = data.p2p
const httpStats = data.http
@@ -92,17 +60,17 @@ class P2pInfoButton extends Button {
const totalUploaded = bytes(p2pStats.uploaded)
const numPeers = p2pStats.numPeers
- subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n'
+ subDivP2P.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n'
if (data.source === 'p2p-media-loader') {
const downloadedFromServer = bytes(httpStats.downloaded).join(' ')
const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
- subDivWebtorrent.title +=
+ subDivP2P.title +=
' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' +
' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n'
}
- subDivWebtorrent.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ')
+ subDivP2P.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ')
downloadSpeedNumber.textContent = downloadSpeed[0]
downloadSpeedUnit.textContent = ' ' + downloadSpeed[1]
@@ -114,11 +82,24 @@ class P2pInfoButton extends Button {
peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer'))
subDivHttp.className = 'vjs-peertube-hidden'
- subDivWebtorrent.className = 'vjs-peertube-displayed'
+ subDivP2P.className = 'vjs-peertube-displayed'
+ })
+
+ this.player_.on('http-info', (_event, data: PlayerNetworkInfo) => {
+ // We are in HTTP fallback
+ subDivHttp.className = 'vjs-peertube-displayed'
+ subDivP2P.className = 'vjs-peertube-hidden'
+
+ subDivHttp.title = this.player().localize('Total downloaded: ') + bytes(data.http.downloaded).join(' ')
+ })
+
+ this.player_.on('video-change', () => {
+ subDivP2P.className = 'vjs-peertube-hidden'
+ subDivHttp.className = 'vjs-peertube-hidden'
})
return div as HTMLButtonElement
}
}
-videojs.registerComponent('P2PInfoButton', P2pInfoButton)
+videojs.registerComponent('P2PInfoButton', P2PInfoButton)
diff --git a/client/src/assets/player/shared/control-bar/peertube-link-button.ts b/client/src/assets/player/shared/control-bar/peertube-link-button.ts
index 45d7ac42f..8242b9cea 100644
--- a/client/src/assets/player/shared/control-bar/peertube-link-button.ts
+++ b/client/src/assets/player/shared/control-bar/peertube-link-button.ts
@@ -3,37 +3,58 @@ import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
import { PeerTubeLinkButtonOptions } from '../../types'
const Component = videojs.getComponent('Component')
-class PeerTubeLinkButton extends Component {
- constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) {
- super(player, options as any)
+class PeerTubeLinkButton extends Component {
+ private mouseEnterHandler: () => void
+ private clickHandler: () => void
+
+ options_: PeerTubeLinkButtonOptions & videojs.ComponentOptions
+
+ constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions & videojs.ComponentOptions) {
+ super(player, options)
+
+ this.updateShowing()
+ this.player().on('video-change', () => this.updateShowing())
+ }
+
+ dispose () {
+ if (this.el()) return
+
+ this.el().removeEventListener('mouseenter', this.mouseEnterHandler)
+ this.el().removeEventListener('click', this.clickHandler)
+
+ super.dispose()
}
createEl () {
- return this.buildElement()
+ const el = videojs.dom.createEl('a', {
+ href: this.buildLink(),
+ innerHTML: this.options_.instanceName,
+ title: this.player().localize('Video page (new window)'),
+ className: 'vjs-peertube-link',
+ target: '_blank'
+ })
+
+ this.mouseEnterHandler = () => this.updateHref()
+ this.clickHandler = () => this.player().pause()
+
+ el.addEventListener('mouseenter', this.mouseEnterHandler)
+ el.addEventListener('click', this.clickHandler)
+
+ return el
+ }
+
+ updateShowing () {
+ if (this.options_.isDisplayed()) this.show()
+ else this.hide()
}
updateHref () {
this.el().setAttribute('href', this.buildLink())
}
- private buildElement () {
- const el = videojs.dom.createEl('a', {
- href: this.buildLink(),
- innerHTML: (this.options_ as PeerTubeLinkButtonOptions).instanceName,
- title: this.player().localize('Video page (new window)'),
- className: 'vjs-peertube-link',
- target: '_blank'
- })
-
- el.addEventListener('mouseenter', () => this.updateHref())
- el.addEventListener('click', () => this.player().pause())
-
- return el as HTMLButtonElement
- }
-
private buildLink () {
- const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID })
+ const url = buildVideoLink({ shortUUID: this.options_.shortUUID() })
return decorateVideoLink({ url, startTime: this.player().currentTime() })
}
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts
index 649eb0b00..f9f6bf12f 100644
--- a/client/src/assets/player/shared/control-bar/peertube-live-display.ts
+++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts
@@ -13,7 +13,6 @@ class PeerTubeLiveDisplay extends ClickableComponent {
this.interval = this.setInterval(() => this.updateClass(), 1000)
- this.show()
this.updateSync(true)
}
@@ -30,7 +29,7 @@ class PeerTubeLiveDisplay extends ClickableComponent {
createEl () {
const el = super.createEl('div', {
- className: 'vjs-live-control vjs-control'
+ className: 'vjs-pt-live-control vjs-control'
})
this.contentEl_ = videojs.dom.createEl('div', {
@@ -83,10 +82,9 @@ class PeerTubeLiveDisplay extends ClickableComponent {
}
private getHLSJS () {
- const p2pMediaLoader = this.player()?.p2pMediaLoader
- if (!p2pMediaLoader) return undefined
+ if (!this.player()?.usingPlugin('p2pMediaLoader')) return
- return p2pMediaLoader().getHLSJS()
+ return this.player().p2pMediaLoader().getHLSJS()
}
}
diff --git a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts b/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts
deleted file mode 100644
index 623e70eb2..000000000
--- a/client/src/assets/player/shared/control-bar/peertube-load-progress-bar.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import videojs from 'video.js'
-
-const Component = videojs.getComponent('Component')
-
-class PeerTubeLoadProgressBar extends Component {
-
- constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
- super(player, options)
-
- this.on(player, 'progress', this.update)
- }
-
- createEl () {
- return super.createEl('div', {
- className: 'vjs-load-progress',
- innerHTML: `${this.localize('Loaded')}: 0%`
- })
- }
-
- dispose () {
- super.dispose()
- }
-
- update () {
- const torrent = this.player().webtorrent().getTorrent()
- if (!torrent) return
-
- (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%'
- }
-
-}
-
-Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar)
diff --git a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts
index 81ab60842..80c69b5f2 100644
--- a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts
+++ b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts
@@ -24,6 +24,8 @@ class StoryboardPlugin extends Plugin {
private readonly boundedHijackMouseTooltip: typeof StoryboardPlugin.prototype.hijackMouseTooltip
+ private onReadyOrLoadstartHandler: (event: { type: 'ready' }) => void
+
constructor (player: videojs.Player, options: videojs.ComponentOptions & StoryboardOptions) {
super(player, options)
@@ -54,7 +56,7 @@ class StoryboardPlugin extends Plugin {
this.spritePlaceholder = videojs.dom.createEl('div', { className: 'vjs-storyboard-sprite-placeholder' }) as HTMLElement
this.seekBar?.el()?.appendChild(this.spritePlaceholder)
- this.player.on([ 'ready', 'loadstart' ], event => {
+ this.onReadyOrLoadstartHandler = event => {
if (event.type !== 'ready') {
const spriteSource = this.player.currentSources().find(source => {
return Object.prototype.hasOwnProperty.call(source, 'storyboard')
@@ -72,7 +74,18 @@ class StoryboardPlugin extends Plugin {
this.cached = !!this.sprites[this.url]
this.load()
- })
+ }
+
+ this.player.on([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler)
+ }
+
+ dispose () {
+ if (this.onReadyOrLoadstartHandler) this.player.off([ 'ready', 'loadstart' ], this.onReadyOrLoadstartHandler)
+ if (this.progress) this.progress.off([ 'mousemove', 'touchmove' ], this.boundedHijackMouseTooltip)
+
+ this.seekBar?.el()?.removeChild(this.spritePlaceholder)
+
+ super.dispose()
}
private load () {
diff --git a/client/src/assets/player/shared/control-bar/theater-button.ts b/client/src/assets/player/shared/control-bar/theater-button.ts
index 56c349d6b..a5feb56ee 100644
--- a/client/src/assets/player/shared/control-bar/theater-button.ts
+++ b/client/src/assets/player/shared/control-bar/theater-button.ts
@@ -1,14 +1,19 @@
import videojs from 'video.js'
import { getStoredTheater, saveTheaterInStore } from '../../peertube-player-local-storage'
+import { TheaterButtonOptions } from '../../types'
const Button = videojs.getComponent('Button')
class TheaterButton extends Button {
private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled'
- constructor (player: videojs.Player, options: videojs.ComponentOptions) {
+ private theaterButtonOptions: TheaterButtonOptions
+
+ constructor (player: videojs.Player, options: TheaterButtonOptions & videojs.ComponentOptions) {
super(player, options)
+ this.theaterButtonOptions = options
+
const enabled = getStoredTheater()
if (enabled === true) {
this.player().addClass(TheaterButton.THEATER_MODE_CLASS)
@@ -19,6 +24,9 @@ class TheaterButton extends Button {
this.controlText('Theater mode')
this.player().theaterEnabled = enabled
+
+ this.updateShowing()
+ this.player().on('video-change', () => this.updateShowing())
}
buildCSSClass () {
@@ -36,7 +44,7 @@ class TheaterButton extends Button {
saveTheaterInStore(theaterEnabled)
- this.player_.trigger('theaterChange', theaterEnabled)
+ this.player_.trigger('theater-change', theaterEnabled)
}
handleClick () {
@@ -48,6 +56,11 @@ class TheaterButton extends Button {
private isTheaterEnabled () {
return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS)
}
+
+ private updateShowing () {
+ if (this.theaterButtonOptions.isDisplayed()) this.show()
+ else this.hide()
+ }
}
videojs.registerComponent('TheaterButton', TheaterButton)
diff --git a/client/src/assets/player/shared/dock/peertube-dock-component.ts b/client/src/assets/player/shared/dock/peertube-dock-component.ts
index 183c7a00f..c13ca647b 100644
--- a/client/src/assets/player/shared/dock/peertube-dock-component.ts
+++ b/client/src/assets/player/shared/dock/peertube-dock-component.ts
@@ -10,17 +10,20 @@ export type PeerTubeDockComponentOptions = {
class PeerTubeDockComponent extends Component {
+ options_: videojs.ComponentOptions & PeerTubeDockComponentOptions
+
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
+ constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeDockComponentOptions) {
+ super(player, options)
+ }
+
createEl () {
- const options = this.options_ as PeerTubeDockComponentOptions
+ const el = super.createEl('div', { className: 'peertube-dock' })
- const el = super.createEl('div', {
- className: 'peertube-dock'
- })
-
- if (options.avatarUrl) {
+ if (this.options_.avatarUrl) {
const avatar = videojs.dom.createEl('img', {
className: 'peertube-dock-avatar',
- src: options.avatarUrl
+ src: this.options_.avatarUrl
})
el.appendChild(avatar)
@@ -30,27 +33,27 @@ class PeerTubeDockComponent extends Component {
className: 'peertube-dock-title-description'
})
- if (options.title) {
+ if (this.options_.title) {
const title = videojs.dom.createEl('div', {
className: 'peertube-dock-title',
- title: options.title,
- innerHTML: options.title
+ title: this.options_.title,
+ innerHTML: this.options_.title
})
elWrapperTitleDescription.appendChild(title)
}
- if (options.description) {
+ if (this.options_.description) {
const description = videojs.dom.createEl('div', {
className: 'peertube-dock-description',
- title: options.description,
- innerHTML: options.description
+ title: this.options_.description,
+ innerHTML: this.options_.description
})
elWrapperTitleDescription.appendChild(description)
}
- if (options.title || options.description) {
+ if (this.options_.title || this.options_.description) {
el.appendChild(elWrapperTitleDescription)
}
diff --git a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts
index 245981692..fc71a8c4b 100644
--- a/client/src/assets/player/shared/dock/peertube-dock-plugin.ts
+++ b/client/src/assets/player/shared/dock/peertube-dock-plugin.ts
@@ -10,14 +10,25 @@ export type PeerTubeDockPluginOptions = {
}
class PeerTubeDockPlugin extends Plugin {
+ private dockComponent: PeerTubeDockComponent
+
constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeDockPluginOptions) {
super(player, options)
- this.player.addClass('peertube-dock')
-
- this.player.ready(() => {
- this.player.addChild('PeerTubeDockComponent', options) as PeerTubeDockComponent
+ player.ready(() => {
+ player.addClass('peertube-dock')
})
+
+ this.dockComponent = new PeerTubeDockComponent(player, options)
+ player.addChild(this.dockComponent)
+ }
+
+ dispose () {
+ this.dockComponent?.dispose()
+ this.player.removeChild(this.dockComponent)
+ this.player.removeClass('peertube-dock')
+
+ super.dispose()
}
}
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
index 2742b21a1..e77b7dc6d 100644
--- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
+++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
@@ -31,6 +31,8 @@ class PeerTubeHotkeysPlugin extends Plugin {
dispose () {
document.removeEventListener('keydown', this.handleKeyFunction)
+
+ super.dispose()
}
private onKeyDown (event: KeyboardEvent) {
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
deleted file mode 100644
index 26f923e92..000000000
--- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import {
- CommonOptions,
- NextPreviousVideoButtonOptions,
- PeerTubeLinkButtonOptions,
- PeertubePlayerManagerOptions,
- PlayerMode
-} from '../../types'
-
-export class ControlBarOptionsBuilder {
- private options: CommonOptions
-
- constructor (
- globalOptions: PeertubePlayerManagerOptions,
- private mode: PlayerMode
- ) {
- this.options = globalOptions.common
- }
-
- getChildrenOptions () {
- const children = {}
-
- if (this.options.previousVideo) {
- Object.assign(children, this.getPreviousVideo())
- }
-
- Object.assign(children, { playToggle: {} })
-
- if (this.options.nextVideo) {
- Object.assign(children, this.getNextVideo())
- }
-
- Object.assign(children, {
- ...this.getTimeControls(),
-
- flexibleWidthSpacer: {},
-
- ...this.getProgressControl(),
-
- p2PInfoButton: {
- p2pEnabled: this.options.p2pEnabled
- },
-
- muteToggle: {},
- volumeControl: {},
-
- ...this.getSettingsButton()
- })
-
- if (this.options.peertubeLink === true) {
- Object.assign(children, {
- peerTubeLinkButton: {
- shortUUID: this.options.videoShortUUID,
- instanceName: this.options.instanceName
- } as PeerTubeLinkButtonOptions
- })
- }
-
- if (this.options.theaterButton === true) {
- Object.assign(children, {
- theaterButton: {}
- })
- }
-
- Object.assign(children, {
- fullscreenToggle: {}
- })
-
- return children
- }
-
- private getSettingsButton () {
- const settingEntries: string[] = []
-
- if (!this.options.isLive) {
- settingEntries.push('playbackRateMenuButton')
- }
-
- if (this.options.captions === true) settingEntries.push('captionsButton')
-
- settingEntries.push('resolutionMenuButton')
-
- return {
- settingsButton: {
- setup: {
- maxHeightOffset: 40
- },
- entries: settingEntries
- }
- }
- }
-
- private getTimeControls () {
- if (this.options.isLive) {
- return {
- peerTubeLiveDisplay: {}
- }
- }
-
- return {
- currentTimeDisplay: {},
- timeDivider: {},
- durationDisplay: {}
- }
- }
-
- private getProgressControl () {
- if (this.options.isLive) return {}
-
- const loadProgressBar = this.mode === 'webtorrent'
- ? 'peerTubeLoadProgressBar'
- : 'loadProgressBar'
-
- return {
- progressControl: {
- children: {
- seekBar: {
- children: {
- [loadProgressBar]: {},
- mouseTimeDisplay: {},
- playProgressBar: {}
- }
- }
- }
- }
- }
- }
-
- private getPreviousVideo () {
- const buttonOptions: NextPreviousVideoButtonOptions = {
- type: 'previous',
- handler: this.options.previousVideo,
- isDisabled: () => {
- if (!this.options.hasPreviousVideo) return false
-
- return !this.options.hasPreviousVideo()
- }
- }
-
- return { previousVideoButton: buttonOptions }
- }
-
- private getNextVideo () {
- const buttonOptions: NextPreviousVideoButtonOptions = {
- type: 'next',
- handler: this.options.nextVideo,
- isDisabled: () => {
- if (!this.options.hasNextVideo) return false
-
- return !this.options.hasNextVideo()
- }
- }
-
- return { nextVideoButton: buttonOptions }
- }
-}
diff --git a/client/src/assets/player/shared/manager-options/index.ts b/client/src/assets/player/shared/manager-options/index.ts
deleted file mode 100644
index 4934d8302..000000000
--- a/client/src/assets/player/shared/manager-options/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './manager-options-builder'
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts
deleted file mode 100644
index 5d3ee4c4a..000000000
--- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import videojs from 'video.js'
-import { copyToClipboard } from '@root-helpers/utils'
-import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
-import { isIOS, isSafari } from '@root-helpers/web-browser'
-import { buildVideoLink, decorateVideoLink, pick } from '@shared/core-utils'
-import { isDefaultLocale } from '@shared/core-utils/i18n'
-import { VideoJSPluginOptions } from '../../types'
-import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../types/manager-options'
-import { ControlBarOptionsBuilder } from './control-bar-options-builder'
-import { HLSOptionsBuilder } from './hls-options-builder'
-import { WebTorrentOptionsBuilder } from './webtorrent-options-builder'
-
-export class ManagerOptionsBuilder {
-
- constructor (
- private mode: PlayerMode,
- private options: PeertubePlayerManagerOptions,
- private p2pMediaLoaderModule?: any
- ) {
-
- }
-
- async getVideojsOptions (alreadyPlayed: boolean): Promise {
- const commonOptions = this.options.common
-
- let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
- const html5 = {
- preloadTextTracks: false
- }
-
- const plugins: VideoJSPluginOptions = {
- peertube: {
- mode: this.mode,
- autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
-
- ...pick(commonOptions, [
- 'videoViewUrl',
- 'videoViewIntervalMs',
- 'authorizationHeader',
- 'startTime',
- 'videoDuration',
- 'subtitle',
- 'videoCaptions',
- 'stopTime',
- 'isLive',
- 'videoUUID'
- ])
- },
- metrics: {
- mode: this.mode,
-
- ...pick(commonOptions, [
- 'metricsUrl',
- 'videoUUID'
- ])
- }
- }
-
- if (commonOptions.playlist) {
- plugins.playlist = commonOptions.playlist
- }
-
- if (this.mode === 'p2p-media-loader') {
- const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule)
- const options = await hlsOptionsBuilder.getPluginOptions()
-
- Object.assign(plugins, pick(options, [ 'hlsjs', 'p2pMediaLoader' ]))
- Object.assign(html5, options.html5)
- } else if (this.mode === 'webtorrent') {
- const webtorrentOptionsBuilder = new WebTorrentOptionsBuilder(this.options, this.getAutoPlayValue(autoplay, alreadyPlayed))
-
- Object.assign(plugins, webtorrentOptionsBuilder.getPluginOptions())
-
- // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
- autoplay = false
- }
-
- const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode)
-
- const videojsOptions = {
- html5,
-
- // We don't use text track settings for now
- textTrackSettings: false as any, // FIXME: typings
- controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
- loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
-
- muted: commonOptions.muted !== undefined
- ? commonOptions.muted
- : undefined, // Undefined so the player knows it has to check the local storage
-
- autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
-
- poster: commonOptions.poster,
- inactivityTimeout: commonOptions.inactivityTimeout,
- playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
-
- plugins,
-
- controlBar: {
- children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings
- }
- }
-
- if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
- Object.assign(videojsOptions, { language: commonOptions.language })
- }
-
- return videojsOptions
- }
-
- private getAutoPlayValue (autoplay: videojs.Autoplay, alreadyPlayed: boolean) {
- if (autoplay !== true) return autoplay
-
- // On first play, disable autoplay to avoid issues
- // But if the player already played videos, we can safely autoplay next ones
- if (isIOS() || isSafari()) {
- return alreadyPlayed ? 'play' : false
- }
-
- return this.options.common.forceAutoplay
- ? 'any'
- : 'play'
- }
-
- getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
- const content = () => {
- const isLoopEnabled = player.options_['loop']
-
- const items = [
- {
- icon: 'repeat',
- label: player.localize('Play in loop') + (isLoopEnabled ? '' : ''),
- listener: function () {
- player.options_['loop'] = !isLoopEnabled
- }
- },
- {
- label: player.localize('Copy the video URL'),
- listener: function () {
- copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
- }
- },
- {
- label: player.localize('Copy the video URL at the current time'),
- listener: function (this: videojs.Player) {
- const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
-
- copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
- }
- },
- {
- icon: 'code',
- label: player.localize('Copy embed code'),
- listener: () => {
- copyToClipboard(buildVideoOrPlaylistEmbed({ embedUrl: commonOptions.embedUrl, embedTitle: commonOptions.embedTitle }))
- }
- }
- ]
-
- if (this.mode === 'webtorrent') {
- items.push({
- label: player.localize('Copy magnet URI'),
- listener: function (this: videojs.Player) {
- copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
- }
- })
- }
-
- items.push({
- icon: 'info',
- label: player.localize('Stats for nerds'),
- listener: () => {
- player.stats().show()
- }
- })
-
- return items.map(i => ({
- ...i,
- label: `` + i.label
- }))
- }
-
- return { content }
- }
-}
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
deleted file mode 100644
index 80eec02cf..000000000
--- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { addQueryParams } from '../../../../../../shared/core-utils'
-import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types'
-
-export class WebTorrentOptionsBuilder {
-
- constructor (
- private options: PeertubePlayerManagerOptions,
- private autoPlayValue: any
- ) {
-
- }
-
- getPluginOptions () {
- const commonOptions = this.options.common
- const webtorrentOptions = this.options.webtorrent
- const p2pMediaLoaderOptions = this.options.p2pMediaLoader
-
- const autoplay = this.autoPlayValue === 'play'
-
- const webtorrent: WebtorrentPluginOptions = {
- autoplay,
-
- playerRefusedP2P: commonOptions.p2pEnabled === false,
- videoDuration: commonOptions.videoDuration,
- playerElement: commonOptions.playerElement,
-
- videoFileToken: commonOptions.videoFileToken,
-
- requiresUserAuth: commonOptions.requiresUserAuth,
-
- buildWebSeedUrls: file => {
- if (!commonOptions.requiresUserAuth && !commonOptions.requiresPassword) return []
-
- return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ]
- },
-
- videoFiles: webtorrentOptions.videoFiles.length !== 0
- ? webtorrentOptions.videoFiles
- // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
- : p2pMediaLoaderOptions?.videoFiles || [],
-
- startTime: commonOptions.startTime
- }
-
- return { webtorrent }
- }
-}
diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts
index 2aae3e90a..48363a724 100644
--- a/client/src/assets/player/shared/metrics/metrics-plugin.ts
+++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts
@@ -1,14 +1,15 @@
+import debug from 'debug'
import videojs from 'video.js'
-import { PlaybackMetricCreate } from '../../../../../../shared/models'
-import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types'
import { logger } from '@root-helpers/logger'
+import { PlaybackMetricCreate } from '../../../../../../shared/models'
+import { MetricsPluginOptions, PlayerNetworkInfo } from '../../types'
+
+const debugLogger = debug('peertube:player:metrics')
const Plugin = videojs.getPlugin('plugin')
class MetricsPlugin extends Plugin {
- private readonly metricsUrl: string
- private readonly videoUUID: string
- private readonly mode: PlayerMode
+ options_: MetricsPluginOptions
private downloadedBytesP2P = 0
private downloadedBytesHTTP = 0
@@ -28,29 +29,54 @@ class MetricsPlugin extends Plugin {
constructor (player: videojs.Player, options: MetricsPluginOptions) {
super(player)
- this.metricsUrl = options.metricsUrl
- this.videoUUID = options.videoUUID
- this.mode = options.mode
+ this.options_ = options
- this.player.one('play', () => {
- this.runMetricsInterval()
+ this.trackBytes()
+ this.trackResolutionChange()
+ this.trackErrors()
- this.trackBytes()
- this.trackResolutionChange()
- this.trackErrors()
+ this.one('play', () => {
+ this.player.on('video-change', () => {
+ this.runMetricsIntervalOnPlay()
+ })
})
+
+ this.runMetricsIntervalOnPlay()
}
dispose () {
if (this.metricsInterval) clearInterval(this.metricsInterval)
+
+ super.dispose()
+ }
+
+ private runMetricsIntervalOnPlay () {
+ this.downloadedBytesP2P = 0
+ this.downloadedBytesHTTP = 0
+ this.uploadedBytesP2P = 0
+
+ this.resolutionChanges = 0
+ this.errors = 0
+
+ this.lastPlayerNetworkInfo = undefined
+
+ debugLogger('Will track metrics on next play')
+
+ this.player.one('play', () => {
+ debugLogger('Tracking metrics')
+
+ this.runMetricsInterval()
+ })
}
private runMetricsInterval () {
+ if (this.metricsInterval) clearInterval(this.metricsInterval)
+
this.metricsInterval = setInterval(() => {
let resolution: number
let fps: number
- if (this.mode === 'p2p-media-loader') {
+ if (this.player.usingPlugin('p2pMediaLoader')) {
const level = this.player.p2pMediaLoader().getCurrentLevel()
if (!level) return
@@ -60,21 +86,23 @@ class MetricsPlugin extends Plugin {
fps = framerate
? parseInt(framerate, 10)
: undefined
- } else { // webtorrent
- const videoFile = this.player.webtorrent().getCurrentVideoFile()
+ } else if (this.player.usingPlugin('webVideo')) {
+ const videoFile = this.player.webVideo().getCurrentVideoFile()
if (!videoFile) return
resolution = videoFile.resolution.id
fps = videoFile.fps && videoFile.fps !== -1
? videoFile.fps
: undefined
+ } else {
+ return
}
const body: PlaybackMetricCreate = {
resolution,
fps,
- playerMode: this.mode,
+ playerMode: this.options_.mode(),
resolutionChanges: this.resolutionChanges,
@@ -85,7 +113,7 @@ class MetricsPlugin extends Plugin {
uploadedBytesP2P: this.uploadedBytesP2P,
- videoId: this.videoUUID
+ videoId: this.options_.videoUUID()
}
this.resolutionChanges = 0
@@ -99,15 +127,13 @@ class MetricsPlugin extends Plugin {
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
- return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers })
+ return fetch(this.options_.metricsUrl(), { method: 'POST', body: JSON.stringify(body), headers })
.catch(err => logger.error('Cannot send metrics to the server.', err))
}, this.CONSTANTS.METRICS_INTERVAL)
}
private trackBytes () {
- this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => {
- if (!data) return
-
+ this.player.on('p2p-info', (_event, data: PlayerNetworkInfo) => {
this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0)
this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0)
@@ -115,10 +141,18 @@ class MetricsPlugin extends Plugin {
this.lastPlayerNetworkInfo = data
})
+
+ this.player.on('http-info', (_event, data: PlayerNetworkInfo) => {
+ this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0)
+ })
}
private trackResolutionChange () {
- this.player.on('engineResolutionChange', () => {
+ this.player.on('engine-resolution-change', () => {
+ this.resolutionChanges++
+ })
+
+ this.player.on('user-resolution-change', () => {
this.resolutionChanges++
})
}
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts
index 09cb98f2e..1bc3ca38d 100644
--- a/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts
+++ b/client/src/assets/player/shared/mobile/peertube-mobile-buttons.ts
@@ -2,22 +2,20 @@ import videojs from 'video.js'
const Component = videojs.getComponent('Component')
class PeerTubeMobileButtons extends Component {
+ private mainButton: HTMLDivElement
private rewind: Element
private forward: Element
private rewindText: Element
private forwardText: Element
+ private touchStartHandler: (e: TouchEvent) => void
+
createEl () {
- const container = super.createEl('div', {
- className: 'vjs-mobile-buttons-overlay'
- }) as HTMLDivElement
+ const container = super.createEl('div', { className: 'vjs-mobile-buttons-overlay' }) as HTMLDivElement
+ this.mainButton = super.createEl('div', { className: 'main-button' }) as HTMLDivElement
- const mainButton = super.createEl('div', {
- className: 'main-button'
- }) as HTMLDivElement
-
- mainButton.addEventListener('touchstart', e => {
+ this.touchStartHandler = e => {
e.stopPropagation()
if (this.player_.paused() || this.player_.ended()) {
@@ -26,7 +24,9 @@ class PeerTubeMobileButtons extends Component {
}
this.player_.pause()
- })
+ }
+
+ this.mainButton.addEventListener('touchstart', this.touchStartHandler, { passive: true })
this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' })
this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' })
@@ -40,12 +40,18 @@ class PeerTubeMobileButtons extends Component {
this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' }))
container.appendChild(this.rewind)
- container.appendChild(mainButton)
+ container.appendChild(this.mainButton)
container.appendChild(this.forward)
return container
}
+ dispose () {
+ if (this.touchStartHandler) this.mainButton.removeEventListener('touchstart', this.touchStartHandler)
+
+ super.dispose()
+ }
+
displayFastSeek (amount: number) {
if (amount === 0) {
this.hideRewind()
diff --git a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts
index 646e9f8c6..f31fa7ddb 100644
--- a/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts
+++ b/client/src/assets/player/shared/mobile/peertube-mobile-plugin.ts
@@ -21,6 +21,15 @@ class PeerTubeMobilePlugin extends Plugin {
private setCurrentTimeTimeout: ReturnType
+ private onPlayHandler: () => void
+ private onFullScreenChangeHandler: () => void
+ private onTouchStartHandler: (event: TouchEvent) => void
+ private onMobileButtonTouchStartHandler: (event: TouchEvent) => void
+ private sliderActiveHandler: () => void
+ private sliderInactiveHandler: () => void
+
+ private seekBar: videojs.Component
+
constructor (player: videojs.Player, options: videojs.PlayerOptions) {
super(player, options)
@@ -36,18 +45,38 @@ class PeerTubeMobilePlugin extends Plugin {
(this.player.options_.userActions as any).click = false
this.player.options_.userActions.doubleClick = false
- this.player.one('play', () => {
- this.initTouchStartEvents()
- })
+ this.onPlayHandler = () => this.initTouchStartEvents()
+ this.player.one('play', this.onPlayHandler)
+
+ this.seekBar = this.player.getDescendant([ 'controlBar', 'progressControl', 'seekBar' ])
+
+ this.sliderActiveHandler = () => this.player.addClass('vjs-mobile-sliding')
+ this.sliderInactiveHandler = () => this.player.removeClass('vjs-mobile-sliding')
+
+ this.seekBar.on('slideractive', this.sliderActiveHandler)
+ this.seekBar.on('sliderinactive', this.sliderInactiveHandler)
+ }
+
+ dispose () {
+ if (this.onPlayHandler) this.player.off('play', this.onPlayHandler)
+ if (this.onFullScreenChangeHandler) this.player.off('fullscreenchange', this.onFullScreenChangeHandler)
+ if (this.onTouchStartHandler) this.player.off('touchstart', this.onFullScreenChangeHandler)
+ if (this.onMobileButtonTouchStartHandler) {
+ this.peerTubeMobileButtons?.el().removeEventListener('touchstart', this.onMobileButtonTouchStartHandler)
+ }
+
+ super.dispose()
}
private handleFullscreenRotation () {
- this.player.on('fullscreenchange', () => {
+ this.onFullScreenChangeHandler = () => {
if (!this.player.isFullscreen() || this.isPortraitVideo()) return
screen.orientation.lock('landscape')
.catch(err => logger.error('Cannot lock screen to landscape.', err))
- })
+ }
+
+ this.player.on('fullscreenchange', this.onFullScreenChangeHandler)
}
private isPortraitVideo () {
@@ -80,19 +109,22 @@ class PeerTubeMobilePlugin extends Plugin {
this.lastTapEvent = event
}
- this.player.on('touchstart', (event: TouchEvent) => {
+ this.onTouchStartHandler = event => {
// Only enable user active on player touch, we listen event on peertube mobile buttons to disable it
if (this.player.userActive()) return
handleTouchStart(event)
- })
+ }
+ this.player.on('touchstart', this.onTouchStartHandler)
- this.peerTubeMobileButtons.el().addEventListener('touchstart', (event: TouchEvent) => {
+ this.onMobileButtonTouchStartHandler = event => {
// Prevent mousemove/click events firing on the player, that conflict with our user active logic
event.preventDefault()
handleTouchStart(event)
- }, { passive: false })
+ }
+
+ this.peerTubeMobileButtons.el().addEventListener('touchstart', this.onMobileButtonTouchStartHandler, { passive: false })
}
private onDoubleTap (event: TouchEvent) {
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
index d05d6193c..d83ec625a 100644
--- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
@@ -14,6 +14,10 @@ type Metadata = {
levels: Level[]
}
+// ---------------------------------------------------------------------------
+// Source handler registration
+// ---------------------------------------------------------------------------
+
type HookFn = (player: videojs.Player, hljs: Hlsjs) => void
const registerSourceHandler = function (vjs: typeof videojs) {
@@ -25,10 +29,13 @@ const registerSourceHandler = function (vjs: typeof videojs) {
const html5 = vjs.getTech('Html5')
if (!html5) {
- logger.error('No Hml5 tech found in videojs')
+ logger.error('No "Html5" tech found in videojs')
return
}
+ // Already registered
+ if ((html5 as any).canPlaySource({ type: 'application/x-mpegURL' })) return
+
// FIXME: typings
(html5 as any).registerSourceHandler({
canHandleSource: function (source: videojs.Tech.SourceObject) {
@@ -56,32 +63,55 @@ const registerSourceHandler = function (vjs: typeof videojs) {
(vjs as any).Html5Hlsjs = Html5Hlsjs
}
-function hlsjsConfigHandler (this: videojs.Player, options: HlsjsConfigHandlerOptions) {
- const player = this
+// ---------------------------------------------------------------------------
+// HLS options plugin
+// ---------------------------------------------------------------------------
- if (!options) return
+const Plugin = videojs.getPlugin('plugin')
- if (!player.srOptions_) {
- player.srOptions_ = {}
+class HLSJSConfigHandler extends Plugin {
+
+ constructor (player: videojs.Player, options: HlsjsConfigHandlerOptions) {
+ super(player, options)
+
+ if (!options) return
+
+ if (!player.srOptions_) {
+ player.srOptions_ = {}
+ }
+
+ if (!player.srOptions_.hlsjsConfig) {
+ player.srOptions_.hlsjsConfig = options.hlsjsConfig
+ }
+
+ if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) {
+ player.srOptions_.levelLabelHandler = options.levelLabelHandler
+ }
+
+ registerSourceHandler(videojs)
}
- if (!player.srOptions_.hlsjsConfig) {
- player.srOptions_.hlsjsConfig = options.hlsjsConfig
- }
+ dispose () {
+ this.player.srOptions_ = undefined
- if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) {
- player.srOptions_.levelLabelHandler = options.levelLabelHandler
+ const tech = this.player.tech(true) as any
+ if (tech.hlsProvider) {
+ tech.hlsProvider.dispose()
+ tech.hlsProvider = undefined
+ }
+
+ super.dispose()
}
}
-const registerConfigPlugin = function (vjs: typeof videojs) {
- // Used in Brightcove since we don't pass options directly there
- const registerVjsPlugin = vjs.registerPlugin || vjs.plugin
- registerVjsPlugin('hlsjs', hlsjsConfigHandler)
-}
+videojs.registerPlugin('hlsjs', HLSJSConfigHandler)
-class Html5Hlsjs {
- private static readonly hooks: { [id: string]: HookFn[] } = {}
+// ---------------------------------------------------------------------------
+// HLS JS source handler
+// ---------------------------------------------------------------------------
+
+export class Html5Hlsjs {
+ private static hooks: { [id: string]: HookFn[] } = {}
private readonly videoElement: HTMLVideoElement
private readonly errorCounts: ErrorCounts = {}
@@ -101,8 +131,9 @@ class Html5Hlsjs {
private dvrDuration: number = null
private edgeMargin: number = null
- private handlers: { [ id in 'play' ]: EventListener } = {
- play: null
+ private handlers: { [ id in 'play' | 'error' ]: EventListener } = {
+ play: null,
+ error: null
}
constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) {
@@ -115,7 +146,7 @@ class Html5Hlsjs {
this.videoElement = tech.el() as HTMLVideoElement
this.player = vjs((tech.options_ as any).playerId)
- this.videoElement.addEventListener('error', event => {
+ this.handlers.error = event => {
let errorTxt: string
const mediaError = ((event.currentTarget || event.target) as HTMLVideoElement).error
@@ -143,7 +174,8 @@ class Html5Hlsjs {
}
logger.error(`MEDIA_ERROR: ${errorTxt}`)
- })
+ }
+ this.videoElement.addEventListener('error', this.handlers.error)
this.initialize()
}
@@ -174,6 +206,7 @@ class Html5Hlsjs {
// See comment for `initialize` method.
dispose () {
this.videoElement.removeEventListener('play', this.handlers.play)
+ this.videoElement.removeEventListener('error', this.handlers.error)
// FIXME: https://github.com/video-dev/hls.js/issues/4092
const untypedHLS = this.hls as any
@@ -200,6 +233,10 @@ class Html5Hlsjs {
return true
}
+ static removeAllHooks () {
+ Html5Hlsjs.hooks = {}
+ }
+
private _executeHooksFor (type: string) {
if (Html5Hlsjs.hooks[type] === undefined) {
return
@@ -421,7 +458,7 @@ class Html5Hlsjs {
? data.level
: -1
- this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, byEngine: true })
+ this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, fireCallback: false })
})
this.hls.attachMedia(this.videoElement)
@@ -433,9 +470,3 @@ class Html5Hlsjs {
this._initHlsjs()
}
}
-
-export {
- Html5Hlsjs,
- registerSourceHandler,
- registerConfigPlugin
-}
diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
index e6f525fea..fe967a730 100644
--- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -3,19 +3,12 @@ import videojs from 'video.js'
import { Events, Segment } from '@peertube/p2p-media-loader-core'
import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
import { logger } from '@root-helpers/logger'
-import { addQueryParams, timeToInt } from '@shared/core-utils'
+import { addQueryParams } from '@shared/core-utils'
import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
-import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
-
-registerConfigPlugin(videojs)
-registerSourceHandler(videojs)
+import { SettingsButton } from '../settings/settings-menu-button'
const Plugin = videojs.getPlugin('plugin')
class P2pMediaLoaderPlugin extends Plugin {
-
- private readonly CONSTANTS = {
- INFO_SCHEDULER: 1000 // Don't change this
- }
private readonly options: P2PMediaLoaderPluginOptions
private hlsjs: Hlsjs
@@ -31,7 +24,6 @@ class P2pMediaLoaderPlugin extends Plugin {
pendingDownload: [] as number[],
totalDownload: 0
}
- private startTime: number
private networkInfoInterval: any
@@ -39,7 +31,6 @@ class P2pMediaLoaderPlugin extends Plugin {
super(player)
this.options = options
- this.startTime = timeToInt(options.startTime)
// FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
if (!(videojs as any).Html5Hlsjs) {
@@ -77,17 +68,22 @@ class P2pMediaLoaderPlugin extends Plugin {
})
player.ready(() => {
- this.initializeCore()
-
this.initializePlugin()
})
}
dispose () {
- if (this.hlsjs) this.hlsjs.destroy()
- if (this.p2pEngine) this.p2pEngine.destroy()
+ this.p2pEngine?.removeAllListeners()
+ this.p2pEngine?.destroy()
+
+ this.hlsjs?.destroy()
+ this.options.segmentValidator?.destroy();
+
+ (videojs as any).Html5Hlsjs?.removeAllHooks()
clearInterval(this.networkInfoInterval)
+
+ super.dispose()
}
getCurrentLevel () {
@@ -104,18 +100,6 @@ class P2pMediaLoaderPlugin extends Plugin {
return this.hlsjs
}
- private initializeCore () {
- this.player.one('play', () => {
- this.player.addClass('vjs-has-big-play-button-clicked')
- })
-
- this.player.one('canplay', () => {
- if (this.startTime) {
- this.player.currentTime(this.startTime)
- }
- })
- }
-
private initializePlugin () {
initHlsJsPlayer(this.hlsjs)
@@ -133,7 +117,7 @@ class P2pMediaLoaderPlugin extends Plugin {
this.runStats()
- this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange'))
+ this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engine-resolution-change'))
}
private runStats () {
@@ -167,7 +151,7 @@ class P2pMediaLoaderPlugin extends Plugin {
this.statsP2PBytes.pendingUpload = []
this.statsHTTPBytes.pendingDownload = []
- return this.player.trigger('p2pInfo', {
+ return this.player.trigger('p2p-info', {
source: 'p2p-media-loader',
http: {
downloadSpeed: httpDownloadSpeed,
@@ -182,7 +166,7 @@ class P2pMediaLoaderPlugin extends Plugin {
},
bandwidthEstimate: (this.hlsjs as any).bandwidthEstimate / 8
} as PlayerNetworkInfo)
- }, this.CONSTANTS.INFO_SCHEDULER)
+ }, 1000)
}
private arraySum (data: number[]) {
@@ -190,10 +174,7 @@ class P2pMediaLoaderPlugin extends Plugin {
}
private fallbackToBuiltInIOS () {
- logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.');
-
- // Workaround to force video.js to not re create a video element
- (this.player as any).playerElIngest_ = this.player.el().parentNode
+ logger.info('HLS.js does not seem to be supported. Fallback to built-in HLS.')
this.player.src({
type: this.options.type,
@@ -203,9 +184,14 @@ class P2pMediaLoaderPlugin extends Plugin {
})
})
- this.player.ready(() => {
- this.initializeCore()
- })
+ // Resolution button is not supported in built-in HLS player
+ this.getResolutionButton().hide()
+ }
+
+ private getResolutionButton () {
+ const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
+
+ return settingsButton.menu.getChild('resolutionMenuButton')
}
}
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
index e86d3d159..a2f7e676d 100644
--- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
@@ -9,30 +9,29 @@ type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string
const maxRetries = 10
-function segmentValidatorFactory (options: {
- serverUrl: string
- segmentsSha256Url: string
- authorizationHeader: () => string
- requiresUserAuth: boolean
- requiresPassword: boolean
- videoPassword: () => string
-}) {
- const { serverUrl, segmentsSha256Url, authorizationHeader, requiresUserAuth, requiresPassword, videoPassword } = options
+export class SegmentValidator {
- let segmentsJSON = fetchSha256Segments({
- serverUrl,
- segmentsSha256Url,
- authorizationHeader,
- requiresUserAuth,
- requiresPassword,
- videoPassword
- })
- const regex = /bytes=(\d+)-(\d+)/
+ private readonly bytesRangeRegex = /bytes=(\d+)-(\d+)/
+
+ private destroyed = false
+
+ constructor (private readonly options: {
+ serverUrl: string
+ segmentsSha256Url: string
+ authorizationHeader: () => string
+ requiresUserAuth: boolean
+ requiresPassword: boolean
+ videoPassword: () => string
+ }) {
+
+ }
+
+ async validate (segment: Segment, _method: string, _peerId: string, retry = 1) {
+ if (this.destroyed) return
- return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) {
const filename = basename(removeQueryParams(segment.url))
- const segmentValue = (await segmentsJSON)[filename]
+ const segmentValue = (await this.fetchSha256Segments())[filename]
if (!segmentValue && retry > maxRetries) {
throw new Error(`Unknown segment name ${filename} in segment validator`)
@@ -43,15 +42,7 @@ function segmentValidatorFactory (options: {
await wait(500)
- segmentsJSON = fetchSha256Segments({
- serverUrl,
- segmentsSha256Url,
- authorizationHeader,
- requiresUserAuth,
- requiresPassword,
- videoPassword
- })
- await segmentValidator(segment, _method, _peerId, retry + 1)
+ await this.validate(segment, _method, _peerId, retry + 1)
return
}
@@ -62,7 +53,7 @@ function segmentValidatorFactory (options: {
if (typeof segmentValue === 'string') {
hashShouldBe = segmentValue
} else {
- const captured = regex.exec(segment.range)
+ const captured = this.bytesRangeRegex.exec(segment.range)
range = captured[1] + '-' + captured[2]
hashShouldBe = segmentValue[range]
@@ -72,7 +63,7 @@ function segmentValidatorFactory (options: {
throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
}
- const calculatedSha = await sha256Hex(segment.data)
+ const calculatedSha = await this.sha256Hex(segment.data)
if (calculatedSha !== hashShouldBe) {
throw new Error(
`Hashes does not correspond for segment ${filename}/${range}` +
@@ -80,65 +71,53 @@ function segmentValidatorFactory (options: {
)
}
}
-}
-// ---------------------------------------------------------------------------
-
-export {
- segmentValidatorFactory
-}
-
-// ---------------------------------------------------------------------------
-
-function fetchSha256Segments (options: {
- serverUrl: string
- segmentsSha256Url: string
- authorizationHeader: () => string
- requiresUserAuth: boolean
- requiresPassword: boolean
- videoPassword: () => string
-}): Promise {
- const { serverUrl, segmentsSha256Url, requiresUserAuth, authorizationHeader, requiresPassword, videoPassword } = options
-
- let headers: { [ id: string ]: string } = {}
- if (isSameOrigin(serverUrl, segmentsSha256Url)) {
- if (requiresPassword) headers = { 'x-peertube-video-password': videoPassword() }
- else if (requiresUserAuth) headers = { Authorization: authorizationHeader() }
+ destroy () {
+ this.destroyed = true
}
- return fetch(segmentsSha256Url, { headers })
- .then(res => res.json() as Promise)
- .catch(err => {
- logger.error('Cannot get sha256 segments', err)
- return {}
+ private fetchSha256Segments (): Promise {
+ let headers: { [ id: string ]: string } = {}
+
+ if (isSameOrigin(this.options.serverUrl, this.options.segmentsSha256Url)) {
+ if (this.options.requiresPassword) headers = { 'x-peertube-video-password': this.options.videoPassword() }
+ else if (this.options.requiresUserAuth) headers = { Authorization: this.options.authorizationHeader() }
+ }
+
+ return fetch(this.options.segmentsSha256Url, { headers })
+ .then(res => res.json() as Promise)
+ .catch(err => {
+ logger.error('Cannot get sha256 segments', err)
+ return {}
+ })
+ }
+
+ private async sha256Hex (data?: ArrayBuffer) {
+ if (!data) return undefined
+
+ if (window.crypto.subtle) {
+ return window.crypto.subtle.digest('SHA-256', data)
+ .then(data => this.bufferToHex(data))
+ }
+
+ // Fallback for non HTTPS context
+ const shaModule = (await import('sha.js') as any).default
+ // eslint-disable-next-line new-cap
+ return new shaModule.sha256().update(Buffer.from(data)).digest('hex')
+ }
+
+ // Thanks: https://stackoverflow.com/a/53307879
+ private bufferToHex (buffer?: ArrayBuffer) {
+ if (!buffer) return ''
+
+ let s = ''
+ const h = '0123456789abcdef'
+ const o = new Uint8Array(buffer)
+
+ o.forEach((v: any) => {
+ s += h[v >> 4] + h[v & 15]
})
-}
-async function sha256Hex (data?: ArrayBuffer) {
- if (!data) return undefined
-
- if (window.crypto.subtle) {
- return window.crypto.subtle.digest('SHA-256', data)
- .then(data => bufferToHex(data))
+ return s
}
-
- // Fallback for non HTTPS context
- const shaModule = (await import('sha.js') as any).default
- // eslint-disable-next-line new-cap
- return new shaModule.sha256().update(Buffer.from(data)).digest('hex')
-}
-
-// Thanks: https://stackoverflow.com/a/53307879
-function bufferToHex (buffer?: ArrayBuffer) {
- if (!buffer) return ''
-
- let s = ''
- const h = '0123456789abcdef'
- const o = new Uint8Array(buffer)
-
- o.forEach((v: any) => {
- s += h[v >> 4] + h[v & 15]
- })
-
- return s
}
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts
index af2147749..f52ec75f4 100644
--- a/client/src/assets/player/shared/peertube/peertube-plugin.ts
+++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts
@@ -1,7 +1,7 @@
import debug from 'debug'
import videojs from 'video.js'
import { logger } from '@root-helpers/logger'
-import { isMobile } from '@root-helpers/web-browser'
+import { isIOS, isMobile } from '@root-helpers/web-browser'
import { timeToInt } from '@shared/core-utils'
import { VideoView, VideoViewEvent } from '@shared/models/videos'
import {
@@ -13,7 +13,7 @@ import {
saveVideoWatchHistory,
saveVolumeInStore
} from '../../peertube-player-local-storage'
-import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
+import { PeerTubePluginOptions } from '../../types'
import { SettingsButton } from '../settings/settings-menu-button'
const debugLogger = debug('peertube:player:peertube')
@@ -21,43 +21,59 @@ const debugLogger = debug('peertube:player:peertube')
const Plugin = videojs.getPlugin('plugin')
class PeerTubePlugin extends Plugin {
- private readonly videoViewUrl: string
+ private readonly videoViewUrl: () => string
private readonly authorizationHeader: () => string
+ private readonly initialInactivityTimeout: number
- private readonly videoUUID: string
- private readonly startTime: number
+ private readonly hasAutoplay: () => videojs.Autoplay
- private readonly videoViewIntervalMs: number
-
- private videoCaptions: VideoJSCaption[]
- private defaultSubtitle: string
+ private currentSubtitle: string
+ private currentPlaybackRate: number
private videoViewInterval: any
private menuOpened = false
private mouseInControlBar = false
private mouseInSettings = false
- private readonly initialInactivityTimeout: number
- constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
+ private videoViewOnPlayHandler: (...args: any[]) => void
+ private videoViewOnSeekedHandler: (...args: any[]) => void
+ private videoViewOnEndedHandler: (...args: any[]) => void
+
+ private stopTimeHandler: (...args: any[]) => void
+
+ constructor (player: videojs.Player, private readonly options: PeerTubePluginOptions) {
super(player)
this.videoViewUrl = options.videoViewUrl
this.authorizationHeader = options.authorizationHeader
- this.videoUUID = options.videoUUID
- this.startTime = timeToInt(options.startTime)
- this.videoViewIntervalMs = options.videoViewIntervalMs
+ this.hasAutoplay = options.hasAutoplay
- this.videoCaptions = options.videoCaptions
this.initialInactivityTimeout = this.player.options_.inactivityTimeout
- if (options.autoplay !== false) this.player.addClass('vjs-has-autoplay')
+ this.currentSubtitle = this.options.subtitle() || getStoredLastSubtitle()
+
+ this.initializePlayer()
+ this.initOnVideoChange()
+
+ this.deleteLegacyIndexedDB()
this.player.on('autoplay-failure', () => {
+ debugLogger('Autoplay failed')
+
this.player.removeClass('vjs-has-autoplay')
+
+ // Fix a bug on iOS where the big play button is not displayed when autoplay fails
+ if (isIOS()) this.player.hasStarted(false)
})
- this.player.ready(() => {
+ this.player.on('ratechange', () => {
+ this.currentPlaybackRate = this.player.playbackRate()
+
+ this.player.defaultPlaybackRate(this.currentPlaybackRate)
+ })
+
+ this.player.one('canplay', () => {
const playerOptions = this.player.options_
const volume = getStoredVolume()
@@ -65,28 +81,15 @@ class PeerTubePlugin extends Plugin {
const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
if (muted !== undefined) this.player.muted(muted)
+ })
- this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
+ this.player.ready(() => {
this.player.on('volumechange', () => {
saveVolumeInStore(this.player.volume())
saveMuteInStore(this.player.muted())
})
- if (options.stopTime) {
- const stopTime = timeToInt(options.stopTime)
- const self = this
-
- this.player.on('timeupdate', function onTimeUpdate () {
- if (self.player.currentTime() > stopTime) {
- self.player.pause()
- self.player.trigger('stopped')
-
- self.player.off('timeupdate', onTimeUpdate)
- }
- })
- }
-
this.player.textTracks().addEventListener('change', () => {
const showing = this.player.textTracks().tracks_.find(t => {
return t.kind === 'captions' && t.mode === 'showing'
@@ -94,23 +97,24 @@ class PeerTubePlugin extends Plugin {
if (!showing) {
saveLastSubtitle('off')
+ this.currentSubtitle = undefined
return
}
+ this.currentSubtitle = showing.language
saveLastSubtitle(showing.language)
})
- this.player.on('sourcechange', () => this.initCaptions())
-
- this.player.duration(options.videoDuration)
-
- this.initializePlayer()
- this.runUserViewing()
+ this.player.on('video-change', () => {
+ this.initOnVideoChange()
+ })
})
}
dispose () {
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
+
+ super.dispose()
}
onMenuOpened () {
@@ -162,40 +166,70 @@ class PeerTubePlugin extends Plugin {
this.initSmoothProgressBar()
- this.initCaptions()
-
- this.listenControlBarMouse()
+ this.player.ready(() => {
+ this.listenControlBarMouse()
+ })
this.listenFullScreenChange()
}
+ private initOnVideoChange () {
+ if (this.hasAutoplay() !== false) this.player.addClass('vjs-has-autoplay')
+ else this.player.removeClass('vjs-has-autoplay')
+
+ if (this.currentPlaybackRate && this.currentPlaybackRate !== 1) {
+ debugLogger('Setting playback rate to ' + this.currentPlaybackRate)
+
+ this.player.playbackRate(this.currentPlaybackRate)
+ }
+
+ this.player.ready(() => {
+ this.initCaptions()
+ this.updateControlBar()
+ })
+
+ this.handleStartStopTime()
+ this.runUserViewing()
+ }
+
// ---------------------------------------------------------------------------
private runUserViewing () {
- let lastCurrentTime = this.startTime
+ const startTime = timeToInt(this.options.startTime())
+
+ let lastCurrentTime = startTime
let lastViewEvent: VideoViewEvent
- this.player.one('play', () => {
- this.notifyUserIsWatching(this.startTime, lastViewEvent)
- })
+ if (this.videoViewInterval) clearInterval(this.videoViewInterval)
+ if (this.videoViewOnPlayHandler) this.player.off('play', this.videoViewOnPlayHandler)
+ if (this.videoViewOnSeekedHandler) this.player.off('seeked', this.videoViewOnSeekedHandler)
+ if (this.videoViewOnEndedHandler) this.player.off('ended', this.videoViewOnEndedHandler)
- this.player.on('seeked', () => {
+ this.videoViewOnPlayHandler = () => {
+ this.notifyUserIsWatching(startTime, lastViewEvent)
+ }
+
+ this.videoViewOnSeekedHandler = () => {
const diff = Math.floor(this.player.currentTime()) - lastCurrentTime
// Don't take into account small forwards
if (diff > 0 && diff < 3) return
lastViewEvent = 'seek'
- })
+ }
- this.player.one('ended', () => {
+ this.videoViewOnEndedHandler = () => {
const currentTime = Math.floor(this.player.duration())
lastCurrentTime = currentTime
this.notifyUserIsWatching(currentTime, lastViewEvent)
lastViewEvent = undefined
- })
+ }
+
+ this.player.one('play', this.videoViewOnPlayHandler)
+ this.player.on('seeked', this.videoViewOnSeekedHandler)
+ this.player.one('ended', this.videoViewOnEndedHandler)
this.videoViewInterval = setInterval(() => {
const currentTime = Math.floor(this.player.currentTime())
@@ -209,13 +243,13 @@ class PeerTubePlugin extends Plugin {
.catch(err => logger.error('Cannot notify user is watching.', err))
lastViewEvent = undefined
- }, this.videoViewIntervalMs)
+ }, this.options.videoViewIntervalMs)
}
private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
// Server won't save history, so save the video position in local storage
if (!this.authorizationHeader()) {
- saveVideoWatchHistory(this.videoUUID, currentTime)
+ saveVideoWatchHistory(this.options.videoUUID(), currentTime)
}
if (!this.videoViewUrl) return Promise.resolve(true)
@@ -225,7 +259,7 @@ class PeerTubePlugin extends Plugin {
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
- return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
+ return fetch(this.videoViewUrl(), { method: 'POST', body: JSON.stringify(body), headers })
}
// ---------------------------------------------------------------------------
@@ -279,18 +313,89 @@ class PeerTubePlugin extends Plugin {
}
private initCaptions () {
- for (const caption of this.videoCaptions) {
+ debugLogger('Init captions with current subtitle ' + this.currentSubtitle)
+
+ this.player.tech(true).clearTracks('text')
+
+ for (const caption of this.options.videoCaptions()) {
this.player.addRemoteTextTrack({
kind: 'captions',
label: caption.label,
language: caption.language,
id: caption.language,
src: caption.src,
- default: this.defaultSubtitle === caption.language
- }, false)
+ default: this.currentSubtitle === caption.language
+ }, true)
}
- this.player.trigger('captionsChanged')
+ this.player.trigger('captions-changed')
+ }
+
+ private updateControlBar () {
+ debugLogger('Updating control bar')
+
+ if (this.options.isLive()) {
+ this.getPlaybackRateButton().hide()
+
+ this.player.controlBar.getChild('progressControl').hide()
+ this.player.controlBar.getChild('currentTimeDisplay').hide()
+ this.player.controlBar.getChild('timeDivider').hide()
+ this.player.controlBar.getChild('durationDisplay').hide()
+
+ this.player.controlBar.getChild('peerTubeLiveDisplay').show()
+ } else {
+ this.getPlaybackRateButton().show()
+
+ this.player.controlBar.getChild('progressControl').show()
+ this.player.controlBar.getChild('currentTimeDisplay').show()
+ this.player.controlBar.getChild('timeDivider').show()
+ this.player.controlBar.getChild('durationDisplay').show()
+
+ this.player.controlBar.getChild('peerTubeLiveDisplay').hide()
+ }
+
+ if (this.options.videoCaptions().length === 0) {
+ this.getCaptionsButton().hide()
+ } else {
+ this.getCaptionsButton().show()
+ }
+ }
+
+ private handleStartStopTime () {
+ this.player.duration(this.options.videoDuration())
+
+ if (this.stopTimeHandler) {
+ this.player.off('timeupdate', this.stopTimeHandler)
+ this.stopTimeHandler = undefined
+ }
+
+ // Prefer canplaythrough instead of canplay because Chrome has issues with the second one
+ this.player.one('canplaythrough', () => {
+ if (this.options.startTime()) {
+ debugLogger('Start the video at ' + this.options.startTime())
+
+ this.player.currentTime(timeToInt(this.options.startTime()))
+ }
+
+ if (this.options.stopTime()) {
+ const stopTime = timeToInt(this.options.stopTime())
+
+ this.stopTimeHandler = () => {
+ if (this.player.currentTime() <= stopTime) return
+
+ debugLogger('Stopping the video at ' + this.options.stopTime())
+
+ // Time top stop
+ this.player.pause()
+ this.player.trigger('auto-stopped')
+
+ this.player.off('timeupdate', this.stopTimeHandler)
+ this.stopTimeHandler = undefined
+ }
+
+ this.player.on('timeupdate', this.stopTimeHandler)
+ }
+ })
}
// Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
@@ -314,6 +419,37 @@ class PeerTubePlugin extends Plugin {
this.update()
}
}
+
+ private getCaptionsButton () {
+ const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
+
+ return settingsButton.menu.getChild('captionsButton') as videojs.CaptionsButton
+ }
+
+ private getPlaybackRateButton () {
+ const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
+
+ return settingsButton.menu.getChild('playbackRateMenuButton')
+ }
+
+ // We don't use webtorrent anymore, so we can safely remove old chunks from IndexedDB
+ private deleteLegacyIndexedDB () {
+ try {
+ if (typeof window.indexedDB === 'undefined') return
+ if (!window.indexedDB) return
+ if (typeof window.indexedDB.databases !== 'function') return
+
+ window.indexedDB.databases()
+ .then(databases => {
+ for (const db of databases) {
+ window.indexedDB.deleteDatabase(db.name)
+ }
+ })
+ } catch (err) {
+ debugLogger('Cannot delete legacy indexed DB', err)
+ // Nothing to do
+ }
+ }
}
videojs.registerPlugin('peertube', PeerTubePlugin)
diff --git a/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts
new file mode 100644
index 000000000..b467e3637
--- /dev/null
+++ b/client/src/assets/player/shared/player-options-builder/control-bar-options-builder.ts
@@ -0,0 +1,136 @@
+import {
+ NextPreviousVideoButtonOptions,
+ PeerTubeLinkButtonOptions,
+ PeerTubePlayerContructorOptions,
+ PeerTubePlayerLoadOptions,
+ TheaterButtonOptions
+} from '../../types'
+
+type ControlBarOptionsBuilderConstructorOptions =
+ Pick &
+ {
+ videoShortUUID: () => string
+ p2pEnabled: () => boolean
+
+ previousVideo: () => PeerTubePlayerLoadOptions['previousVideo']
+ nextVideo: () => PeerTubePlayerLoadOptions['nextVideo']
+ }
+
+export class ControlBarOptionsBuilder {
+
+ constructor (private options: ControlBarOptionsBuilderConstructorOptions) {
+ }
+
+ getChildrenOptions () {
+ const children = {
+ ...this.getPreviousVideo(),
+
+ playToggle: {},
+
+ ...this.getNextVideo(),
+
+ ...this.getTimeControls(),
+
+ ...this.getProgressControl(),
+
+ p2PInfoButton: {},
+ muteToggle: {},
+ volumeControl: {},
+
+ ...this.getSettingsButton(),
+
+ ...this.getPeerTubeLinkButton(),
+
+ ...this.getTheaterButton(),
+
+ fullscreenToggle: {}
+ }
+
+ return children
+ }
+
+ private getSettingsButton () {
+ const settingEntries: string[] = []
+
+ settingEntries.push('playbackRateMenuButton')
+ settingEntries.push('captionsButton')
+ settingEntries.push('resolutionMenuButton')
+
+ return {
+ settingsButton: {
+ setup: {
+ maxHeightOffset: 40
+ },
+ entries: settingEntries
+ }
+ }
+ }
+
+ private getTimeControls () {
+ return {
+ peerTubeLiveDisplay: {},
+
+ currentTimeDisplay: {},
+ timeDivider: {},
+ durationDisplay: {}
+ }
+ }
+
+ private getProgressControl () {
+ return {
+ progressControl: {
+ children: {
+ seekBar: {
+ children: {
+ loadProgressBar: {},
+ mouseTimeDisplay: {},
+ playProgressBar: {}
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private getPreviousVideo () {
+ const buttonOptions: NextPreviousVideoButtonOptions = {
+ type: 'previous',
+ handler: () => this.options.previousVideo().handler(),
+ isDisabled: () => !this.options.previousVideo().enabled,
+ isDisplayed: () => this.options.previousVideo().displayControlBarButton
+ }
+
+ return { previousVideoButton: buttonOptions }
+ }
+
+ private getNextVideo () {
+ const buttonOptions: NextPreviousVideoButtonOptions = {
+ type: 'next',
+ handler: () => this.options.nextVideo().handler(),
+ isDisabled: () => !this.options.nextVideo().enabled,
+ isDisplayed: () => this.options.nextVideo().displayControlBarButton
+ }
+
+ return { nextVideoButton: buttonOptions }
+ }
+
+ private getPeerTubeLinkButton () {
+ const options: PeerTubeLinkButtonOptions = {
+ isDisplayed: this.options.peertubeLink,
+ shortUUID: this.options.videoShortUUID,
+ instanceName: this.options.instanceName
+ }
+
+ return { peerTubeLinkButton: options }
+ }
+
+ private getTheaterButton () {
+ const options: TheaterButtonOptions = {
+ isDisplayed: () => this.options.theaterButton
+ }
+
+ return {
+ theaterButton: options
+ }
+ }
+}
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts
similarity index 67%
rename from client/src/assets/player/shared/manager-options/hls-options-builder.ts
rename to client/src/assets/player/shared/player-options-builder/hls-options-builder.ts
index 8091110bc..10df2db5d 100644
--- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts
+++ b/client/src/assets/player/shared/player-options-builder/hls-options-builder.ts
@@ -3,49 +3,61 @@ import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
import { logger } from '@root-helpers/logger'
import { LiveVideoLatencyMode } from '@shared/models'
import { getAverageBandwidthInStore } from '../../peertube-player-local-storage'
-import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types'
-import { PeertubePlayerManagerOptions } from '../../types/manager-options'
+import { P2PMediaLoader, P2PMediaLoaderPluginOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions } from '../../types'
import { getRtcConfig, isSameOrigin } from '../common'
import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
-import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator'
+import { SegmentValidator } from '../p2p-media-loader/segment-validator'
+
+type ConstructorOptions =
+ Pick &
+ Pick
export class HLSOptionsBuilder {
constructor (
- private options: PeertubePlayerManagerOptions,
+ private options: ConstructorOptions,
private p2pMediaLoaderModule?: any
) {
}
async getPluginOptions () {
- const commonOptions = this.options.common
-
- const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
+ const redundancyUrlManager = new RedundancyUrlManager(this.options.hls.redundancyBaseUrls)
+ const segmentValidator = new SegmentValidator({
+ segmentsSha256Url: this.options.hls.segmentsSha256Url,
+ authorizationHeader: this.options.authorizationHeader,
+ requiresUserAuth: this.options.requiresUserAuth,
+ serverUrl: this.options.serverUrl,
+ requiresPassword: this.options.requiresPassword,
+ videoPassword: this.options.videoPassword
+ })
const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook(
'filter:internal.player.p2p-media-loader.options.result',
- this.getP2PMediaLoaderOptions(redundancyUrlManager)
+ this.getP2PMediaLoaderOptions({ redundancyUrlManager, segmentValidator })
)
const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
- requiresUserAuth: commonOptions.requiresUserAuth,
- videoFileToken: commonOptions.videoFileToken,
+ requiresUserAuth: this.options.requiresUserAuth,
+ videoFileToken: this.options.videoFileToken,
redundancyUrlManager,
type: 'application/x-mpegURL',
- startTime: commonOptions.startTime,
- src: this.options.p2pMediaLoader.playlistUrl,
+ src: this.options.hls.playlistUrl,
+ segmentValidator,
loader
}
const hlsjs = {
+ hlsjsConfig: this.getHLSJSOptions(loader),
+
levelLabelHandler: (level: { height: number, width: number }) => {
const resolution = Math.min(level.height || 0, level.width || 0)
- const file = this.options.p2pMediaLoader.videoFiles.find(f => f.resolution.id === resolution)
+ const file = this.options.hls.videoFiles.find(f => f.resolution.id === resolution)
// We don't have files for live videos
if (!file) return level.height
@@ -56,26 +68,27 @@ export class HLSOptionsBuilder {
}
}
- const html5 = {
- hlsjsConfig: this.getHLSJSOptions(loader)
- }
-
- return { p2pMediaLoader, hlsjs, html5 }
+ return { p2pMediaLoader, hlsjs }
}
// ---------------------------------------------------------------------------
- private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings {
+ private getP2PMediaLoaderOptions (options: {
+ redundancyUrlManager: RedundancyUrlManager
+ segmentValidator: SegmentValidator
+ }): HlsJsEngineSettings {
+ const { redundancyUrlManager, segmentValidator } = options
+
let consumeOnly = false
if ((navigator as any)?.connection?.type === 'cellular') {
logger.info('We are on a cellular connection: disabling seeding.')
consumeOnly = true
}
- const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce
- .filter(t => t.startsWith('ws'))
+ const trackerAnnounce = this.options.hls.trackerAnnounce
+ .filter(t => t.startsWith('ws'))
- const specificLiveOrVODOptions = this.options.common.isLive
+ const specificLiveOrVODOptions = this.options.isLive
? this.getP2PMediaLoaderLiveOptions()
: this.getP2PMediaLoaderVODOptions()
@@ -88,35 +101,28 @@ export class HLSOptionsBuilder {
httpFailedSegmentTimeout: 1000,
xhrSetup: (xhr, url) => {
- const { requiresUserAuth, requiresPassword } = this.options.common
+ const { requiresUserAuth, requiresPassword } = this.options
if (!(requiresUserAuth || requiresPassword)) return
- if (!isSameOrigin(this.options.common.serverUrl, url)) return
+ if (!isSameOrigin(this.options.serverUrl, url)) return
- if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.common.videoPassword())
+ if (requiresPassword) xhr.setRequestHeader('x-peertube-video-password', this.options.videoPassword())
- else xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader())
+ else xhr.setRequestHeader('Authorization', this.options.authorizationHeader())
},
- segmentValidator: segmentValidatorFactory({
- segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url,
- authorizationHeader: this.options.common.authorizationHeader,
- requiresUserAuth: this.options.common.requiresUserAuth,
- serverUrl: this.options.common.serverUrl,
- requiresPassword: this.options.common.requiresPassword,
- videoPassword: this.options.common.videoPassword
- }),
+ segmentValidator: segmentValidator.validate.bind(segmentValidator),
segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
- useP2P: this.options.common.p2pEnabled,
+ useP2P: this.options.p2pEnabled,
consumeOnly,
...specificLiveOrVODOptions
},
segments: {
- swarmId: this.options.p2pMediaLoader.playlistUrl,
+ swarmId: this.options.hls.playlistUrl,
forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority ?? 20
}
}
@@ -127,7 +133,7 @@ export class HLSOptionsBuilder {
requiredSegmentsPriority: 1
}
- const latencyMode = this.options.common.liveOptions.latencyMode
+ const latencyMode = this.options.liveOptions.latencyMode
switch (latencyMode) {
case LiveVideoLatencyMode.SMALL_LATENCY:
@@ -165,7 +171,7 @@ export class HLSOptionsBuilder {
// ---------------------------------------------------------------------------
private getHLSJSOptions (loader: P2PMediaLoader) {
- const specificLiveOrVODOptions = this.options.common.isLive
+ const specificLiveOrVODOptions = this.options.isLive
? this.getHLSLiveOptions()
: this.getHLSVODOptions()
@@ -193,7 +199,7 @@ export class HLSOptionsBuilder {
}
private getHLSLiveOptions () {
- const latencyMode = this.options.common.liveOptions.latencyMode
+ const latencyMode = this.options.liveOptions.latencyMode
switch (latencyMode) {
case LiveVideoLatencyMode.SMALL_LATENCY:
diff --git a/client/src/assets/player/shared/player-options-builder/index.ts b/client/src/assets/player/shared/player-options-builder/index.ts
new file mode 100644
index 000000000..674754a94
--- /dev/null
+++ b/client/src/assets/player/shared/player-options-builder/index.ts
@@ -0,0 +1,3 @@
+export * from './control-bar-options-builder'
+export * from './hls-options-builder'
+export * from './web-video-options-builder'
diff --git a/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts
new file mode 100644
index 000000000..a3c3c3f27
--- /dev/null
+++ b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts
@@ -0,0 +1,22 @@
+import { PeerTubePlayerLoadOptions, WebVideoPluginOptions } from '../../types'
+
+type ConstructorOptions = Pick
+
+export class WebVideoOptionsBuilder {
+
+ constructor (private options: ConstructorOptions) {
+
+ }
+
+ getPluginOptions (): WebVideoPluginOptions {
+ return {
+ videoFileToken: this.options.videoFileToken,
+
+ videoFiles: this.options.webVideo.videoFiles.length !== 0
+ ? this.options.webVideo.videoFiles
+ : this.options?.hls.videoFiles || [],
+
+ startTime: this.options.startTime
+ }
+ }
+}
diff --git a/client/src/assets/player/shared/playlist/playlist-button.ts b/client/src/assets/player/shared/playlist/playlist-button.ts
index 6cfaf4158..45cbb4899 100644
--- a/client/src/assets/player/shared/playlist/playlist-button.ts
+++ b/client/src/assets/player/shared/playlist/playlist-button.ts
@@ -8,8 +8,15 @@ class PlaylistButton extends ClickableComponent {
private playlistInfoElement: HTMLElement
private wrapper: HTMLElement
- constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) {
- super(player, options as any)
+ options_: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions
+
+ // FIXME: eslint -> it's not a useless constructor, we need to extend constructor options typings
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
+ constructor (
+ player: videojs.Player,
+ options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu } & videojs.ClickableComponentOptions
+ ) {
+ super(player, options)
}
createEl () {
@@ -40,20 +47,15 @@ class PlaylistButton extends ClickableComponent {
}
update () {
- const options = this.options_ as PlaylistPluginOptions
+ this.playlistInfoElement.innerHTML = this.options_.getCurrentPosition() + '/' + this.options_.playlist.videosLength
- this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength
- this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ])
+ this.wrapper.title = this.player().localize('Playlist: {1}', [ this.options_.playlist.displayName ])
}
handleClick () {
- const playlistMenu = this.getPlaylistMenu()
+ const playlistMenu = this.options_.playlistMenu
playlistMenu.open()
}
-
- private getPlaylistMenu () {
- return (this.options_ as any).playlistMenu as PlaylistMenu
- }
}
videojs.registerComponent('PlaylistButton', PlaylistButton)
diff --git a/client/src/assets/player/shared/playlist/playlist-menu-item.ts b/client/src/assets/player/shared/playlist/playlist-menu-item.ts
index 81b5acf30..f9366332d 100644
--- a/client/src/assets/player/shared/playlist/playlist-menu-item.ts
+++ b/client/src/assets/player/shared/playlist/playlist-menu-item.ts
@@ -8,6 +8,11 @@ const Component = videojs.getComponent('Component')
class PlaylistMenuItem extends Component {
private element: VideoPlaylistElement
+ private clickHandler: () => void
+ private keyDownHandler: (event: KeyboardEvent) => void
+
+ options_: videojs.ComponentOptions & PlaylistItemOptions
+
constructor (player: videojs.Player, options?: PlaylistItemOptions) {
super(player, options as any)
@@ -15,19 +20,27 @@ class PlaylistMenuItem extends Component {
this.element = options.element
- this.on([ 'click', 'tap' ], () => this.switchPlaylistItem())
- this.on('keydown', event => this.handleKeyDown(event))
+ this.clickHandler = () => this.switchPlaylistItem()
+ this.keyDownHandler = event => this.handleKeyDown(event)
+
+ this.on([ 'click', 'tap' ], this.clickHandler)
+ this.on('keydown', this.keyDownHandler)
+ }
+
+ dispose () {
+ this.off([ 'click', 'tap' ], this.clickHandler)
+ this.off('keydown', this.keyDownHandler)
+
+ super.dispose()
}
createEl () {
- const options = this.options_ as PlaylistItemOptions
-
const li = super.createEl('li', {
className: 'vjs-playlist-menu-item',
innerHTML: ''
}) as HTMLElement
- if (!options.element.video) {
+ if (!this.options_.element.video) {
li.classList.add('vjs-disabled')
}
@@ -37,14 +50,14 @@ class PlaylistMenuItem extends Component {
const position = super.createEl('div', {
className: 'item-position',
- innerHTML: options.element.position
+ innerHTML: this.options_.element.position
})
positionBlock.appendChild(position)
li.appendChild(positionBlock)
- if (options.element.video) {
- this.buildAvailableVideo(li, positionBlock, options)
+ if (this.options_.element.video) {
+ this.buildAvailableVideo(li, positionBlock, this.options_)
} else {
this.buildUnavailableVideo(li)
}
@@ -125,9 +138,7 @@ class PlaylistMenuItem extends Component {
}
private switchPlaylistItem () {
- const options = this.options_ as PlaylistItemOptions
-
- options.onClicked()
+ this.options_.onClicked()
}
}
diff --git a/client/src/assets/player/shared/playlist/playlist-menu.ts b/client/src/assets/player/shared/playlist/playlist-menu.ts
index 1ec9ac804..53a5a7274 100644
--- a/client/src/assets/player/shared/playlist/playlist-menu.ts
+++ b/client/src/assets/player/shared/playlist/playlist-menu.ts
@@ -6,26 +6,32 @@ import { PlaylistMenuItem } from './playlist-menu-item'
const Component = videojs.getComponent('Component')
class PlaylistMenu extends Component {
- private menuItems: PlaylistMenuItem[]
+ private menuItems: PlaylistMenuItem[] = []
- constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
- super(player, options as any)
+ private readonly userInactiveHandler: () => void
+ private readonly onMouseEnter: () => void
+ private readonly onMouseLeave: () => void
- const self = this
+ private readonly onPlayerCick: (event: Event) => void
- function userInactiveHandler () {
- self.close()
+ options_: PlaylistPluginOptions & videojs.ComponentOptions
+
+ constructor (player: videojs.Player, options?: PlaylistPluginOptions & videojs.ComponentOptions) {
+ super(player, options)
+
+ this.userInactiveHandler = () => {
+ this.close()
}
- this.el().addEventListener('mouseenter', () => {
- this.player().off('userinactive', userInactiveHandler)
- })
+ this.onMouseEnter = () => {
+ this.player().off('userinactive', this.userInactiveHandler)
+ }
- this.el().addEventListener('mouseleave', () => {
- this.player().one('userinactive', userInactiveHandler)
- })
+ this.onMouseLeave = () => {
+ this.player().one('userinactive', this.userInactiveHandler)
+ }
- this.player().on('click', event => {
+ this.onPlayerCick = event => {
let current = event.target as HTMLElement
do {
@@ -40,14 +46,31 @@ class PlaylistMenu extends Component {
} while (current)
this.close()
- })
+ }
+
+ this.el().addEventListener('mouseenter', this.onMouseEnter)
+ this.el().addEventListener('mouseleave', this.onMouseLeave)
+
+ this.player().on('click', this.onPlayerCick)
+ }
+
+ dispose () {
+ this.el().removeEventListener('mouseenter', this.onMouseEnter)
+ this.el().removeEventListener('mouseleave', this.onMouseLeave)
+
+ this.player().off('userinactive', this.userInactiveHandler)
+ this.player().off('click', this.onPlayerCick)
+
+ for (const item of this.menuItems) {
+ item.dispose()
+ }
+
+ super.dispose()
}
createEl () {
this.menuItems = []
- const options = this.getOptions()
-
const menu = super.createEl('div', {
className: 'vjs-playlist-menu',
innerHTML: '',
@@ -61,11 +84,11 @@ class PlaylistMenu extends Component {
const headerLeft = super.createEl('div')
const leftTitle = super.createEl('div', {
- innerHTML: options.playlist.displayName,
+ innerHTML: this.options_.playlist.displayName,
className: 'title'
})
- const playlistChannel = options.playlist.videoChannel
+ const playlistChannel = this.options_.playlist.videoChannel
const leftSubtitle = super.createEl('div', {
innerHTML: playlistChannel
? this.player().localize('By {1}', [ playlistChannel.displayName ])
@@ -86,7 +109,7 @@ class PlaylistMenu extends Component {
const list = super.createEl('ol')
- for (const playlistElement of options.elements) {
+ for (const playlistElement of this.options_.elements) {
const item = new PlaylistMenuItem(this.player(), {
element: playlistElement,
onClicked: () => this.onItemClicked(playlistElement)
@@ -100,13 +123,13 @@ class PlaylistMenu extends Component {
menu.appendChild(header)
menu.appendChild(list)
+ this.update()
+
return menu
}
update () {
- const options = this.getOptions()
-
- this.updateSelected(options.getCurrentPosition())
+ this.updateSelected(this.options_.getCurrentPosition())
}
open () {
@@ -123,12 +146,8 @@ class PlaylistMenu extends Component {
}
}
- private getOptions () {
- return this.options_ as PlaylistPluginOptions
- }
-
private onItemClicked (element: VideoPlaylistElement) {
- this.getOptions().onItemClicked(element)
+ this.options_.onItemClicked(element)
}
}
diff --git a/client/src/assets/player/shared/playlist/playlist-plugin.ts b/client/src/assets/player/shared/playlist/playlist-plugin.ts
index 44de0da5a..c00e45843 100644
--- a/client/src/assets/player/shared/playlist/playlist-plugin.ts
+++ b/client/src/assets/player/shared/playlist/playlist-plugin.ts
@@ -8,17 +8,10 @@ const Plugin = videojs.getPlugin('plugin')
class PlaylistPlugin extends Plugin {
private playlistMenu: PlaylistMenu
private playlistButton: PlaylistButton
- private options: PlaylistPluginOptions
constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
super(player, options)
- this.options = options
-
- this.player.ready(() => {
- player.addClass('vjs-playlist')
- })
-
this.playlistMenu = new PlaylistMenu(player, options)
this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu })
@@ -26,8 +19,16 @@ class PlaylistPlugin extends Plugin {
player.addChild(this.playlistButton, options)
}
- updateSelected () {
- this.playlistMenu.updateSelected(this.options.getCurrentPosition())
+ dispose () {
+ this.player.removeClass('vjs-playlist')
+
+ this.playlistMenu.dispose()
+ this.playlistButton.dispose()
+
+ this.player.removeChild(this.playlistMenu)
+ this.player.removeChild(this.playlistButton)
+
+ super.dispose()
}
}
diff --git a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
index 4fafd27b1..4d6701003 100644
--- a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
+++ b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
@@ -8,7 +8,16 @@ class PeerTubeResolutionsPlugin extends Plugin {
private resolutions: PeerTubeResolution[] = []
private autoResolutionChosenId: number
- private autoResolutionEnabled = true
+
+ constructor (player: videojs.Player) {
+ super(player)
+
+ player.on('video-change', () => {
+ this.resolutions = []
+
+ this.trigger('resolutions-removed')
+ })
+ }
add (resolutions: PeerTubeResolution[]) {
for (const r of resolutions) {
@@ -18,12 +27,12 @@ class PeerTubeResolutionsPlugin extends Plugin {
this.currentSelection = this.getSelected()
this.sort()
- this.trigger('resolutionsAdded')
+ this.trigger('resolutions-added')
}
remove (resolutionIndex: number) {
this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex)
- this.trigger('resolutionRemoved')
+ this.trigger('resolutions-removed')
}
getResolutions () {
@@ -40,10 +49,10 @@ class PeerTubeResolutionsPlugin extends Plugin {
select (options: {
id: number
- byEngine: boolean
+ fireCallback: boolean
autoResolutionChosenId?: number
}) {
- const { id, autoResolutionChosenId, byEngine } = options
+ const { id, autoResolutionChosenId, fireCallback } = options
if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return
@@ -55,25 +64,11 @@ class PeerTubeResolutionsPlugin extends Plugin {
if (r.selected) {
this.currentSelection = r
- if (!byEngine) r.selectCallback()
+ if (fireCallback) r.selectCallback()
}
}
- this.trigger('resolutionChanged')
- }
-
- disableAutoResolution () {
- this.autoResolutionEnabled = false
- this.trigger('autoResolutionEnabledChanged')
- }
-
- enabledAutoResolution () {
- this.autoResolutionEnabled = true
- this.trigger('autoResolutionEnabledChanged')
- }
-
- isAutoResolutionEnabeld () {
- return this.autoResolutionEnabled
+ this.trigger('resolutions-changed')
}
private sort () {
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts
index 672411c11..c39894284 100644
--- a/client/src/assets/player/shared/settings/resolution-menu-button.ts
+++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts
@@ -11,12 +11,12 @@ class ResolutionMenuButton extends MenuButton {
this.controlText('Quality')
- player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
- player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities())
+ player.peertubeResolutions().on('resolutions-added', () => this.update())
+ player.peertubeResolutions().on('resolutions-removed', () => this.update())
// For parent
- player.peertubeResolutions().on('resolutionChanged', () => {
- setTimeout(() => this.trigger('labelUpdated'))
+ player.peertubeResolutions().on('resolutions-changed', () => {
+ setTimeout(() => this.trigger('label-updated'))
})
}
@@ -37,7 +37,34 @@ class ResolutionMenuButton extends MenuButton {
}
createMenu () {
- return new Menu(this.player_)
+ const menu: videojs.Menu = new Menu(this.player_, { menuButton: this })
+ const resolutions = this.player().peertubeResolutions().getResolutions()
+
+ for (const r of resolutions) {
+ const label = r.label === '0p'
+ ? this.player().localize('Audio-only')
+ : r.label
+
+ const component = new ResolutionMenuItem(
+ this.player_,
+ {
+ id: r.id + '',
+ resolutionId: r.id,
+ label,
+ selected: r.selected
+ }
+ )
+
+ menu.addItem(component)
+ }
+
+ return menu
+ }
+
+ update () {
+ super.update()
+
+ this.trigger('menu-changed')
}
buildCSSClass () {
@@ -47,60 +74,6 @@ class ResolutionMenuButton extends MenuButton {
buildWrapperCSSClass () {
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
}
-
- private addClickListener (component: any) {
- component.on('click', () => {
- const children = this.menu.children()
-
- for (const child of children) {
- if (component !== child) {
- (child as videojs.MenuItem).selected(false)
- }
- }
- })
- }
-
- private buildQualities () {
- for (const d of this.player().peertubeResolutions().getResolutions()) {
- const label = d.label === '0p'
- ? this.player().localize('Audio-only')
- : d.label
-
- this.menu.addChild(new ResolutionMenuItem(
- this.player_,
- {
- id: d.id + '',
- resolutionId: d.id,
- label,
- selected: d.selected
- })
- )
- }
-
- for (const m of this.menu.children()) {
- this.addClickListener(m)
- }
-
- this.trigger('menuChanged')
- }
-
- private cleanupQualities () {
- const resolutions = this.player().peertubeResolutions().getResolutions()
-
- this.menu.children().forEach((children: ResolutionMenuItem) => {
- if (children.resolutionId === undefined) {
- return
- }
-
- if (resolutions.find(r => r.id === children.resolutionId)) {
- return
- }
-
- this.menu.removeChild(children)
- })
-
- this.trigger('menuChanged')
- }
}
videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts
index c59b8b891..86387f533 100644
--- a/client/src/assets/player/shared/settings/resolution-menu-item.ts
+++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts
@@ -10,35 +10,32 @@ class ResolutionMenuItem extends MenuItem {
readonly resolutionId: number
private readonly label: string
- private autoResolutionEnabled: boolean
private autoResolutionChosen: string
+ private updateSelectionHandler: () => void
+
constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) {
- options.selectable = true
+ super(player, { ...options, selectable: true })
- super(player, options)
-
- this.autoResolutionEnabled = true
this.autoResolutionChosen = ''
this.resolutionId = options.resolutionId
this.label = options.label
- player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection())
+ this.updateSelectionHandler = () => this.updateSelection()
+ player.peertubeResolutions().on('resolutions-changed', this.updateSelectionHandler)
+ }
- // We only want to disable the "Auto" item
- if (this.resolutionId === -1) {
- player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution())
- }
+ dispose () {
+ this.player().peertubeResolutions().off('resolutions-changed', this.updateSelectionHandler)
+
+ super.dispose()
}
handleClick (event: any) {
- // Auto button disabled?
- if (this.autoResolutionEnabled === false && this.resolutionId === -1) return
-
super.handleClick(event)
- this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false })
+ this.player().peertubeResolutions().select({ id: this.resolutionId, fireCallback: true })
}
updateSelection () {
@@ -51,19 +48,6 @@ class ResolutionMenuItem extends MenuItem {
this.selected(this.resolutionId === selectedResolution.id)
}
- updateAutoResolution () {
- const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld()
-
- // Check if the auto resolution is enabled or not
- if (enabled === false) {
- this.addClass('disabled')
- } else {
- this.removeClass('disabled')
- }
-
- this.autoResolutionEnabled = enabled
- }
-
getLabel () {
if (this.resolutionId === -1) {
return this.label + ' ' + this.autoResolutionChosen + ''
diff --git a/client/src/assets/player/shared/settings/settings-dialog.ts b/client/src/assets/player/shared/settings/settings-dialog.ts
index f5fbbe7ad..ba39d0f45 100644
--- a/client/src/assets/player/shared/settings/settings-dialog.ts
+++ b/client/src/assets/player/shared/settings/settings-dialog.ts
@@ -28,6 +28,18 @@ class SettingsDialog extends Component {
'aria-describedby': dialogDescriptionId
})
}
+
+ show () {
+ this.player().addClass('vjs-settings-dialog-opened')
+
+ super.show()
+ }
+
+ hide () {
+ this.player().removeClass('vjs-settings-dialog-opened')
+
+ super.hide()
+ }
}
Component.registerComponent('SettingsDialog', SettingsDialog)
diff --git a/client/src/assets/player/shared/settings/settings-menu-button.ts b/client/src/assets/player/shared/settings/settings-menu-button.ts
index 4cf29866b..9499a43eb 100644
--- a/client/src/assets/player/shared/settings/settings-menu-button.ts
+++ b/client/src/assets/player/shared/settings/settings-menu-button.ts
@@ -71,7 +71,7 @@ class SettingsButton extends Button {
}
}
- onDisposeSettingsItem (event: any, name: string) {
+ onDisposeSettingsItem (_event: any, name: string) {
if (name === undefined) {
const children = this.menu.children()
@@ -103,6 +103,8 @@ class SettingsButton extends Button {
if (this.isInIframe()) {
window.removeEventListener('blur', this.documentClickHandler)
}
+
+ super.dispose()
}
onAddSettingsItem (event: any, data: any) {
@@ -249,8 +251,8 @@ class SettingsButton extends Button {
}
resetChildren () {
- for (const menuChild of this.menu.children()) {
- (menuChild as SettingsMenuItem).reset()
+ for (const menuChild of this.menu.children() as SettingsMenuItem[]) {
+ menuChild.reset()
}
}
@@ -258,8 +260,8 @@ class SettingsButton extends Button {
* Hide all the sub menus
*/
hideChildren () {
- for (const menuChild of this.menu.children()) {
- (menuChild as SettingsMenuItem).hideSubMenu()
+ for (const menuChild of this.menu.children() as SettingsMenuItem[]) {
+ menuChild.hideSubMenu()
}
}
diff --git a/client/src/assets/player/shared/settings/settings-menu-item.ts b/client/src/assets/player/shared/settings/settings-menu-item.ts
index 288e3b233..9916ae27f 100644
--- a/client/src/assets/player/shared/settings/settings-menu-item.ts
+++ b/client/src/assets/player/shared/settings/settings-menu-item.ts
@@ -70,17 +70,22 @@ class SettingsMenuItem extends MenuItem {
this.build()
// Update on rate change
- player.on('ratechange', this.submenuClickHandler)
+ if (subMenuName === 'PlaybackRateMenuButton') {
+ player.on('ratechange', this.submenuClickHandler)
+ }
if (subMenuName === 'CaptionsButton') {
- // Hack to regenerate captions on HTTP fallback
- player.on('captionsChanged', () => {
+ player.on('captions-changed', () => {
+ // Wait menu component rebuild
setTimeout(() => {
- this.settingsSubMenuEl_.innerHTML = ''
- this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
- this.update()
- this.bindClickEvents()
- }, 0)
+ this.rebuildAfterMenuChange()
+ }, 150)
+ })
+ }
+
+ if (subMenuName === 'ResolutionMenuButton') {
+ this.subMenu.on('menu-changed', () => {
+ this.rebuildAfterMenuChange()
})
}
@@ -89,6 +94,12 @@ class SettingsMenuItem extends MenuItem {
})
}
+ dispose () {
+ this.settingsSubMenuEl_.removeEventListener('transitionend', this.transitionEndHandler)
+
+ super.dispose()
+ }
+
eventHandlers () {
this.submenuClickHandler = this.onSubmenuClick.bind(this)
this.transitionEndHandler = this.onTransitionEnd.bind(this)
@@ -190,27 +201,6 @@ class SettingsMenuItem extends MenuItem {
(button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
}
- /**
- * Add/remove prefixed event listener for CSS Transition
- *
- * @method PrefixedEvent
- */
- PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
- const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
-
- for (let p = 0; p < prefix.length; p++) {
- if (!prefix[p]) {
- type = type.toLowerCase()
- }
-
- if (action === 'addEvent') {
- element.addEventListener(prefix[p] + type, callback, false)
- } else if (action === 'removeEvent') {
- element.removeEventListener(prefix[p] + type, callback, false)
- }
- }
- }
-
onTransitionEnd (event: any) {
if (event.propertyName !== 'margin-right') {
return
@@ -254,12 +244,7 @@ class SettingsMenuItem extends MenuItem {
}
build () {
- this.subMenu.on('labelUpdated', () => {
- this.update()
- })
- this.subMenu.on('menuChanged', () => {
- this.bindClickEvents()
- this.setSize()
+ this.subMenu.on('label-updated', () => {
this.update()
})
@@ -272,25 +257,12 @@ class SettingsMenuItem extends MenuItem {
this.setSize()
this.bindClickEvents()
- // prefixed event listeners for CSS TransitionEnd
- this.PrefixedEvent(
- this.settingsSubMenuEl_,
- 'TransitionEnd',
- this.transitionEndHandler,
- 'addEvent'
- )
+ this.settingsSubMenuEl_.addEventListener('transitionend', this.transitionEndHandler, false)
}
update (event?: any) {
- let target: HTMLElement = null
const subMenu = this.subMenu.name()
- if (event && event.type === 'tap') {
- target = event.target
- } else if (event) {
- target = event.currentTarget
- }
-
// Playback rate menu button doesn't get a vjs-selected class
// or sets options_['selected'] on the selected playback rate.
// Thus we get the submenu value based on the labelEl of playbackRateMenuButton
@@ -321,6 +293,13 @@ class SettingsMenuItem extends MenuItem {
}
}
+ let target: HTMLElement = null
+ if (event && event.type === 'tap') {
+ target = event.target
+ } else if (event) {
+ target = event.currentTarget
+ }
+
if (target && !target.classList.contains('vjs-back-button')) {
this.settingsButton.hideDialog()
}
@@ -369,6 +348,15 @@ class SettingsMenuItem extends MenuItem {
}
}
+ private rebuildAfterMenuChange () {
+ this.settingsSubMenuEl_.innerHTML = ''
+ this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
+ this.update()
+ this.createBackButton()
+ this.setSize()
+ this.bindClickEvents()
+ }
+
}
(SettingsMenuItem as any).prototype.contentElType = 'button'
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts
index 471a5e46c..fad68cec9 100644
--- a/client/src/assets/player/shared/stats/stats-card.ts
+++ b/client/src/assets/player/shared/stats/stats-card.ts
@@ -7,7 +7,7 @@ import { bytes } from '../common'
interface StatsCardOptions extends videojs.ComponentOptions {
videoUUID: string
videoIsLive: boolean
- mode: 'webtorrent' | 'p2p-media-loader'
+ mode: 'web-video' | 'p2p-media-loader'
p2pEnabled: boolean
}
@@ -34,7 +34,7 @@ class StatsCard extends Component {
updateInterval: any
- mode: 'webtorrent' | 'p2p-media-loader'
+ mode: 'web-video' | 'p2p-media-loader'
metadataStore: any = {}
@@ -63,6 +63,9 @@ class StatsCard extends Component {
private liveLatency: InfoElement
+ private onP2PInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void
+ private onHTTPInfoHandler: (_event: any, data: EventPlayerNetworkInfo) => void
+
createEl () {
this.containerEl = videojs.dom.createEl('div', {
className: 'vjs-stats-content'
@@ -86,9 +89,7 @@ class StatsCard extends Component {
this.populateInfoBlocks()
- this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
- if (!data) return // HTTP fallback
-
+ this.onP2PInfoHandler = (_event, data) => {
this.mode = data.source
const p2pStats = data.p2p
@@ -105,11 +106,29 @@ class StatsCard extends Component {
this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
}
- })
+ }
+
+ this.onHTTPInfoHandler = (_event, data) => {
+ this.mode = data.source
+
+ this.playerNetworkInfo.totalDownloaded = bytes(data.http.downloaded).join(' ')
+ }
+
+ this.player().on('p2p-info', this.onP2PInfoHandler)
+ this.player().on('http-info', this.onHTTPInfoHandler)
return this.containerEl
}
+ dispose () {
+ if (this.updateInterval) clearInterval(this.updateInterval)
+
+ this.player().off('p2p-info', this.onP2PInfoHandler)
+ this.player().off('http-info', this.onHTTPInfoHandler)
+
+ super.dispose()
+ }
+
toggle () {
if (this.updateInterval) this.hide()
else this.show()
@@ -122,7 +141,7 @@ class StatsCard extends Component {
try {
const options = this.mode === 'p2p-media-loader'
? this.buildHLSOptions()
- : await this.buildWebTorrentOptions() // Default
+ : await this.buildWebVideoOptions() // Default
this.populateInfoValues(options)
} catch (err) {
@@ -170,8 +189,8 @@ class StatsCard extends Component {
}
}
- private async buildWebTorrentOptions () {
- const videoFile = this.player_.webtorrent().getCurrentVideoFile()
+ private async buildWebVideoOptions () {
+ const videoFile = this.player_.webVideo().getCurrentVideoFile()
if (!this.metadataStore[videoFile.fileUrl]) {
this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
@@ -194,7 +213,7 @@ class StatsCard extends Component {
const resolution = videoFile?.resolution.label + videoFile?.fps
const buffer = this.timeRangesToString(this.player_.buffered())
- const progress = this.player_.webtorrent().getTorrent()?.progress
+ const progress = this.player_.bufferedPercent()
return {
playerNetworkInfo: this.playerNetworkInfo,
@@ -284,8 +303,10 @@ class StatsCard extends Component {
? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
: undefined
- this.setInfoValue(this.playerMode, this.mode || 'HTTP')
- this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))
+ const p2pEnabled = this.options_.p2pEnabled && this.mode === 'p2p-media-loader'
+
+ this.setInfoValue(this.playerMode, this.mode)
+ this.setInfoValue(this.p2p, player.localize(p2pEnabled ? 'enabled' : 'disabled'))
this.setInfoValue(this.uuid, this.options_.videoUUID)
this.setInfoValue(this.viewport, frames)
diff --git a/client/src/assets/player/shared/stats/stats-plugin.ts b/client/src/assets/player/shared/stats/stats-plugin.ts
index 8aad80e8a..86684a78c 100644
--- a/client/src/assets/player/shared/stats/stats-plugin.ts
+++ b/client/src/assets/player/shared/stats/stats-plugin.ts
@@ -7,10 +7,6 @@ class StatsForNerdsPlugin extends Plugin {
private statsCard: StatsCard
constructor (player: videojs.Player, options: StatsCardOptions) {
- const settings = {
- ...options
- }
-
super(player)
this.player.ready(() => {
@@ -19,7 +15,17 @@ class StatsForNerdsPlugin extends Plugin {
this.statsCard = new StatsCard(player, options)
- player.addChild(this.statsCard, settings)
+ // Copy options
+ player.addChild(this.statsCard)
+ }
+
+ dispose () {
+ if (this.statsCard) {
+ this.statsCard.dispose()
+ this.player.removeChild(this.statsCard)
+ }
+
+ super.dispose()
}
show () {
diff --git a/client/src/assets/player/shared/upnext/end-card.ts b/client/src/assets/player/shared/upnext/end-card.ts
index 61668e407..3589e1fd8 100644
--- a/client/src/assets/player/shared/upnext/end-card.ts
+++ b/client/src/assets/player/shared/upnext/end-card.ts
@@ -1,6 +1,7 @@
import videojs from 'video.js'
+import { UpNextPluginOptions } from '../../types'
-function getMainTemplate (options: any) {
+function getMainTemplate (options: EndCardOptions) {
return `
${options.headText}
@@ -23,15 +24,10 @@ function getMainTemplate (options: any) {
`
}
-export interface EndCardOptions extends videojs.ComponentOptions {
- next: () => void
- getTitle: () => string
- timeout: number
+export interface EndCardOptions extends videojs.ComponentOptions, UpNextPluginOptions {
cancelText: string
headText: string
suspendedText: string
- condition: () => boolean
- suspended: () => boolean
}
const Component = videojs.getComponent('Component')
@@ -52,27 +48,43 @@ class EndCard extends Component {
suspendedMessage: HTMLElement
nextButton: HTMLElement
+ private onEndedHandler: () => void
+ private onPlayingHandler: () => void
+
constructor (player: videojs.Player, options: EndCardOptions) {
super(player, options)
this.totalTicks = this.options_.timeout / this.interval
- player.on('ended', (_: any) => {
- if (!this.options_.condition()) return
+ this.onEndedHandler = () => {
+ if (!this.options_.isDisplayed()) return
player.addClass('vjs-upnext--showing')
- this.showCard((canceled: boolean) => {
+
+ this.showCard(canceled => {
player.removeClass('vjs-upnext--showing')
+
this.container.style.display = 'none'
+
if (!canceled) {
this.options_.next()
}
})
- })
+ }
- player.on('playing', () => {
+ this.onPlayingHandler = () => {
this.upNextEvents.trigger('playing')
- })
+ }
+
+ player.on([ 'auto-stopped', 'ended' ], this.onEndedHandler)
+ player.on('playing', this.onPlayingHandler)
+ }
+
+ dispose () {
+ if (this.onEndedHandler) this.player().off([ 'auto-stopped', 'ended' ], this.onEndedHandler)
+ if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler)
+
+ super.dispose()
}
createEl () {
@@ -101,7 +113,7 @@ class EndCard extends Component {
return container
}
- showCard (cb: (value: boolean) => void) {
+ showCard (cb: (canceled: boolean) => void) {
let timeout: any
this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`)
@@ -109,6 +121,10 @@ class EndCard extends Component {
this.title.innerHTML = this.options_.getTitle()
+ if (this.totalTicks === 0) {
+ return cb(false)
+ }
+
this.upNextEvents.one('cancel', () => {
clearTimeout(timeout)
cb(true)
@@ -134,7 +150,7 @@ class EndCard extends Component {
}
const update = () => {
- if (this.options_.suspended()) {
+ if (this.options_.isSuspended()) {
this.suspendedMessage.innerText = this.options_.suspendedText
goToPercent(0)
this.ticks = 0
diff --git a/client/src/assets/player/shared/upnext/upnext-plugin.ts b/client/src/assets/player/shared/upnext/upnext-plugin.ts
index e12e8c503..0badcd68c 100644
--- a/client/src/assets/player/shared/upnext/upnext-plugin.ts
+++ b/client/src/assets/player/shared/upnext/upnext-plugin.ts
@@ -1,26 +1,24 @@
import videojs from 'video.js'
+import { UpNextPluginOptions } from '../../types'
import { EndCardOptions } from './end-card'
const Plugin = videojs.getPlugin('plugin')
class UpNextPlugin extends Plugin {
- constructor (player: videojs.Player, options: Partial
= {}) {
- const settings = {
- next: options.next,
- getTitle: options.getTitle,
- timeout: options.timeout || 5000,
- cancelText: options.cancelText || 'Cancel',
- headText: options.headText || 'Up Next',
- suspendedText: options.suspendedText || 'Autoplay is suspended',
- condition: options.condition,
- suspended: options.suspended
- }
-
+ constructor (player: videojs.Player, options: UpNextPluginOptions) {
super(player)
- // UpNext plugin can be called later, so ensure the player is not disposed
- if (this.player.isDisposed()) return
+ const settings: EndCardOptions = {
+ next: options.next,
+ getTitle: options.getTitle,
+ timeout: options.timeout,
+ cancelText: player.localize('Cancel'),
+ headText: player.localize('Up Next'),
+ suspendedText: player.localize('Autoplay is suspended'),
+ isDisplayed: options.isDisplayed,
+ isSuspended: options.isSuspended
+ }
this.player.ready(() => {
player.addClass('vjs-upnext')
diff --git a/client/src/assets/player/shared/web-video/web-video-plugin.ts b/client/src/assets/player/shared/web-video/web-video-plugin.ts
new file mode 100644
index 000000000..80e56795b
--- /dev/null
+++ b/client/src/assets/player/shared/web-video/web-video-plugin.ts
@@ -0,0 +1,186 @@
+import debug from 'debug'
+import videojs from 'video.js'
+import { logger } from '@root-helpers/logger'
+import { addQueryParams } from '@shared/core-utils'
+import { VideoFile } from '@shared/models'
+import { PeerTubeResolution, PlayerNetworkInfo, WebVideoPluginOptions } from '../../types'
+
+const debugLogger = debug('peertube:player:web-video-plugin')
+
+const Plugin = videojs.getPlugin('plugin')
+
+class WebVideoPlugin extends Plugin {
+ private readonly videoFiles: VideoFile[]
+
+ private currentVideoFile: VideoFile
+ private videoFileToken: () => string
+
+ private networkInfoInterval: any
+
+ private onErrorHandler: () => void
+ private onPlayHandler: () => void
+
+ constructor (player: videojs.Player, options?: WebVideoPluginOptions) {
+ super(player, options)
+
+ this.videoFiles = options.videoFiles
+ this.videoFileToken = options.videoFileToken
+
+ this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false })
+
+ player.ready(() => {
+ this.buildQualities()
+
+ this.setupNetworkInfoInterval()
+
+ if (this.videoFiles.length === 0) {
+ this.player.addClass('disabled')
+ return
+ }
+ })
+ }
+
+ dispose () {
+ clearInterval(this.networkInfoInterval)
+
+ if (this.onErrorHandler) this.player.off('error', this.onErrorHandler)
+ if (this.onPlayHandler) this.player.off('canplay', this.onPlayHandler)
+
+ super.dispose()
+ }
+
+ getCurrentResolutionId () {
+ return this.currentVideoFile.resolution.id
+ }
+
+ updateVideoFile (options: {
+ videoFile: VideoFile
+ isUserResolutionChange: boolean
+ }) {
+ this.currentVideoFile = options.videoFile
+
+ debugLogger('Updating web video file to ' + this.currentVideoFile.fileUrl)
+
+ const paused = this.player.paused()
+ const playbackRate = this.player.playbackRate()
+ const currentTime = this.player.currentTime()
+
+ // Enable error display now this is our last fallback
+ this.onErrorHandler = () => this.player.peertube().displayFatalError()
+ this.player.one('error', this.onErrorHandler)
+
+ let httpUrl = this.currentVideoFile.fileUrl
+
+ if (this.videoFileToken()) {
+ httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
+ }
+
+ const oldAutoplayValue = this.player.autoplay()
+ if (options.isUserResolutionChange) {
+ this.player.autoplay(false)
+ this.player.addClass('vjs-updating-resolution')
+ }
+
+ this.player.src(httpUrl)
+
+ this.onPlayHandler = () => {
+ this.player.playbackRate(playbackRate)
+ this.player.currentTime(currentTime)
+
+ this.adaptPosterForAudioOnly()
+
+ if (options.isUserResolutionChange) {
+ this.player.trigger('user-resolution-change')
+ this.player.trigger('web-video-source-change')
+
+ this.tryToPlay()
+ .then(() => {
+ if (paused) this.player.pause()
+
+ this.player.autoplay(oldAutoplayValue)
+ })
+ }
+ }
+
+ this.player.one('canplay', this.onPlayHandler)
+ }
+
+ getCurrentVideoFile () {
+ return this.currentVideoFile
+ }
+
+ private adaptPosterForAudioOnly () {
+ // Audio-only (resolutionId === 0) gets special treatment
+ if (this.currentVideoFile.resolution.id === 0) {
+ this.player.audioPosterMode(true)
+ } else {
+ this.player.audioPosterMode(false)
+ }
+ }
+
+ private tryToPlay () {
+ debugLogger('Try to play manually the video')
+
+ const playPromise = this.player.play()
+ if (playPromise === undefined) return
+
+ return playPromise
+ .catch((err: Error) => {
+ if (err.message.includes('The play() request was interrupted by a call to pause()')) {
+ return
+ }
+
+ logger.warn(err)
+ this.player.pause()
+ this.player.posterImage.show()
+ this.player.removeClass('vjs-has-autoplay')
+ this.player.removeClass('vjs-playing-audio-only-content')
+ })
+ .finally(() => {
+ this.player.removeClass('vjs-updating-resolution')
+ })
+ }
+
+ private pickAverageVideoFile () {
+ if (this.videoFiles.length === 1) return this.videoFiles[0]
+
+ const files = this.videoFiles.filter(f => f.resolution.id !== 0)
+ return files[Math.floor(files.length / 2)]
+ }
+
+ private buildQualities () {
+ const resolutions: PeerTubeResolution[] = this.videoFiles.map(videoFile => ({
+ id: videoFile.resolution.id,
+ label: this.buildQualityLabel(videoFile),
+ height: videoFile.resolution.id,
+ selected: videoFile.id === this.currentVideoFile.id,
+ selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true })
+ }))
+
+ this.player.peertubeResolutions().add(resolutions)
+ }
+
+ private buildQualityLabel (file: VideoFile) {
+ let label = file.resolution.label
+
+ if (file.fps && file.fps >= 50) {
+ label += file.fps
+ }
+
+ return label
+ }
+
+ private setupNetworkInfoInterval () {
+ this.networkInfoInterval = setInterval(() => {
+ return this.player.trigger('http-info', {
+ source: 'web-video',
+ http: {
+ downloaded: this.player.bufferedPercent() * this.currentVideoFile.size
+ }
+ } as PlayerNetworkInfo)
+ }, 1000)
+ }
+}
+
+videojs.registerPlugin('webVideo', WebVideoPlugin)
+export { WebVideoPlugin }
diff --git a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts b/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts
deleted file mode 100644
index 74ae17704..000000000
--- a/client/src/assets/player/shared/webtorrent/peertube-chunk-store.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-// From https://github.com/MinEduTDF/idb-chunk-store
-// We use temporary IndexDB (all data are removed on destroy) to avoid RAM issues
-// Thanks @santiagogil and @Feross
-
-import Dexie from 'dexie'
-import { EventEmitter } from 'events'
-import { logger } from '@root-helpers/logger'
-
-class ChunkDatabase extends Dexie {
- chunks: Dexie.Table<{ id: number, buf: Buffer }, number>
-
- constructor (dbname: string) {
- super(dbname)
-
- this.version(1).stores({
- chunks: 'id'
- })
- }
-}
-
-class ExpirationDatabase extends Dexie {
- databases: Dexie.Table<{ name: string, expiration: number }, number>
-
- constructor () {
- super('webtorrent-expiration')
-
- this.version(1).stores({
- databases: 'name,expiration'
- })
- }
-}
-
-export class PeertubeChunkStore extends EventEmitter {
- private static readonly BUFFERING_PUT_MS = 1000
- private static readonly CLEANER_INTERVAL_MS = 1000 * 60 // 1 minute
- private static readonly CLEANER_EXPIRATION_MS = 1000 * 60 * 5 // 5 minutes
-
- chunkLength: number
-
- private pendingPut: { id: number, buf: Buffer, cb: (err?: Error) => void }[] = []
- // If the store is full
- private memoryChunks: { [ id: number ]: Buffer | true } = {}
- private databaseName: string
- private putBulkTimeout: any
- private cleanerInterval: any
- private db: ChunkDatabase
- private expirationDB: ExpirationDatabase
- private readonly length: number
- private readonly lastChunkLength: number
- private readonly lastChunkIndex: number
-
- constructor (chunkLength: number, opts: any) {
- super()
-
- this.databaseName = 'webtorrent-chunks-'
-
- if (!opts) opts = {}
- if (opts.torrent?.infoHash) this.databaseName += opts.torrent.infoHash
- else this.databaseName += '-default'
-
- this.setMaxListeners(100)
-
- this.chunkLength = Number(chunkLength)
- if (!this.chunkLength) throw new Error('First argument must be a chunk length')
-
- this.length = Number(opts.length) || Infinity
-
- if (this.length !== Infinity) {
- this.lastChunkLength = (this.length % this.chunkLength) || this.chunkLength
- this.lastChunkIndex = Math.ceil(this.length / this.chunkLength) - 1
- }
-
- this.db = new ChunkDatabase(this.databaseName)
- // Track databases that expired
- this.expirationDB = new ExpirationDatabase()
-
- this.runCleaner()
- }
-
- put (index: number, buf: Buffer, cb: (err?: Error) => void) {
- const isLastChunk = (index === this.lastChunkIndex)
- if (isLastChunk && buf.length !== this.lastChunkLength) {
- return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength))
- }
- if (!isLastChunk && buf.length !== this.chunkLength) {
- return this.nextTick(cb, new Error('Chunk length must be ' + this.chunkLength))
- }
-
- // Specify we have this chunk
- this.memoryChunks[index] = true
-
- // Add it to the pending put
- this.pendingPut.push({ id: index, buf, cb })
- // If it's already planned, return
- if (this.putBulkTimeout) return
-
- // Plan a future bulk insert
- this.putBulkTimeout = setTimeout(async () => {
- const processing = this.pendingPut
- this.pendingPut = []
- this.putBulkTimeout = undefined
-
- try {
- await this.db.transaction('rw', this.db.chunks, () => {
- return this.db.chunks.bulkPut(processing.map(p => ({ id: p.id, buf: p.buf })))
- })
- } catch (err) {
- logger.info('Cannot bulk insert chunks. Store them in memory.', err)
-
- processing.forEach(p => {
- this.memoryChunks[p.id] = p.buf
- })
- } finally {
- processing.forEach(p => p.cb())
- }
- }, PeertubeChunkStore.BUFFERING_PUT_MS)
- }
-
- get (index: number, opts: any, cb: (err?: Error, buf?: Buffer) => void): void {
- if (typeof opts === 'function') return this.get(index, null, opts)
-
- // IndexDB could be slow, use our memory index first
- const memoryChunk = this.memoryChunks[index]
- if (memoryChunk === undefined) {
- const err = new Error('Chunk not found') as any
- err['notFound'] = true
-
- return process.nextTick(() => cb(err))
- }
-
- // Chunk in memory
- if (memoryChunk !== true) return cb(null, memoryChunk)
-
- // Chunk in store
- this.db.transaction('r', this.db.chunks, async () => {
- const result = await this.db.chunks.get({ id: index })
- if (result === undefined) return cb(null, Buffer.alloc(0))
-
- const buf = result.buf
- if (!opts) return this.nextTick(cb, null, buf)
-
- const offset = opts.offset || 0
- const len = opts.length || (buf.length - offset)
- return cb(null, buf.slice(offset, len + offset))
- })
- .catch(err => {
- logger.error(err)
- return cb(err)
- })
- }
-
- close (cb: (err?: Error) => void) {
- return this.destroy(cb)
- }
-
- async destroy (cb: (err?: Error) => void) {
- try {
- if (this.pendingPut) {
- clearTimeout(this.putBulkTimeout)
- this.pendingPut = null
- }
- if (this.cleanerInterval) {
- clearInterval(this.cleanerInterval)
- this.cleanerInterval = null
- }
-
- if (this.db) {
- this.db.close()
-
- await this.dropDatabase(this.databaseName)
- }
-
- if (this.expirationDB) {
- this.expirationDB.close()
- this.expirationDB = null
- }
-
- return cb()
- } catch (err) {
- logger.error('Cannot destroy peertube chunk store.', err)
- return cb(err)
- }
- }
-
- private runCleaner () {
- this.checkExpiration()
-
- this.cleanerInterval = setInterval(() => {
- this.checkExpiration()
- }, PeertubeChunkStore.CLEANER_INTERVAL_MS)
- }
-
- private async checkExpiration () {
- let databasesToDeleteInfo: { name: string }[] = []
-
- try {
- await this.expirationDB.transaction('rw', this.expirationDB.databases, async () => {
- // Update our database expiration since we are alive
- await this.expirationDB.databases.put({
- name: this.databaseName,
- expiration: new Date().getTime() + PeertubeChunkStore.CLEANER_EXPIRATION_MS
- })
-
- const now = new Date().getTime()
- databasesToDeleteInfo = await this.expirationDB.databases.where('expiration').below(now).toArray()
- })
- } catch (err) {
- logger.error('Cannot update expiration of fetch expired databases.', err)
- }
-
- for (const databaseToDeleteInfo of databasesToDeleteInfo) {
- await this.dropDatabase(databaseToDeleteInfo.name)
- }
- }
-
- private async dropDatabase (databaseName: string) {
- const dbToDelete = new ChunkDatabase(databaseName)
- logger.info(`Destroying IndexDB database ${databaseName}`)
-
- try {
- await dbToDelete.delete()
-
- await this.expirationDB.transaction('rw', this.expirationDB.databases, () => {
- return this.expirationDB.databases.where({ name: databaseName }).delete()
- })
- } catch (err) {
- logger.error(`Cannot delete ${databaseName}.`, err)
- }
- }
-
- private nextTick (cb: (err?: Error, val?: T) => void, err: Error, val?: T) {
- process.nextTick(() => cb(err, val), undefined)
- }
-}
diff --git a/client/src/assets/player/shared/webtorrent/video-renderer.ts b/client/src/assets/player/shared/webtorrent/video-renderer.ts
deleted file mode 100644
index a85d7a838..000000000
--- a/client/src/assets/player/shared/webtorrent/video-renderer.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-// Thanks: https://github.com/feross/render-media
-
-const MediaElementWrapper = require('mediasource')
-import { logger } from '@root-helpers/logger'
-import { extname } from 'path'
-const Videostream = require('videostream')
-
-const VIDEOSTREAM_EXTS = [
- '.m4a',
- '.m4v',
- '.mp4'
-]
-
-type RenderMediaOptions = {
- controls: boolean
- autoplay: boolean
-}
-
-function renderVideo (
- file: any,
- elem: HTMLVideoElement,
- opts: RenderMediaOptions,
- callback: (err: Error, renderer: any) => void
-) {
- validateFile(file)
-
- return renderMedia(file, elem, opts, callback)
-}
-
-function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) {
- const extension = extname(file.name).toLowerCase()
- let preparedElem: any
- let currentTime = 0
- let renderer: any
-
- try {
- if (VIDEOSTREAM_EXTS.includes(extension)) {
- renderer = useVideostream()
- } else {
- renderer = useMediaSource()
- }
- } catch (err) {
- return callback(err)
- }
-
- function useVideostream () {
- prepareElem()
- preparedElem.addEventListener('error', function onError (err: Error) {
- preparedElem.removeEventListener('error', onError)
-
- return callback(err)
- })
- preparedElem.addEventListener('loadstart', onLoadStart)
- return new Videostream(file, preparedElem)
- }
-
- function useMediaSource (useVP9 = false) {
- const codecs = getCodec(file.name, useVP9)
-
- prepareElem()
- preparedElem.addEventListener('error', function onError (err: Error) {
- preparedElem.removeEventListener('error', onError)
-
- // Try with vp9 before returning an error
- if (codecs.includes('vp8')) return fallbackToMediaSource(true)
-
- return callback(err)
- })
- preparedElem.addEventListener('loadstart', onLoadStart)
-
- const wrapper = new MediaElementWrapper(preparedElem)
- const writable = wrapper.createWriteStream(codecs)
- file.createReadStream().pipe(writable)
-
- if (currentTime) preparedElem.currentTime = currentTime
-
- return wrapper
- }
-
- function fallbackToMediaSource (useVP9 = false) {
- if (useVP9 === true) logger.info('Falling back to media source with VP9 enabled.')
- else logger.info('Falling back to media source..')
-
- useMediaSource(useVP9)
- }
-
- function prepareElem () {
- if (preparedElem === undefined) {
- preparedElem = elem
-
- preparedElem.addEventListener('progress', function () {
- currentTime = elem.currentTime
- })
- }
- }
-
- function onLoadStart () {
- preparedElem.removeEventListener('loadstart', onLoadStart)
- if (opts.autoplay) preparedElem.play()
-
- callback(null, renderer)
- }
-}
-
-function validateFile (file: any) {
- if (file == null) {
- throw new Error('file cannot be null or undefined')
- }
- if (typeof file.name !== 'string') {
- throw new Error('missing or invalid file.name property')
- }
- if (typeof file.createReadStream !== 'function') {
- throw new Error('missing or invalid file.createReadStream property')
- }
-}
-
-function getCodec (name: string, useVP9 = false) {
- const ext = extname(name).toLowerCase()
- if (ext === '.mp4') {
- return 'video/mp4; codecs="avc1.640029, mp4a.40.5"'
- }
-
- if (ext === '.webm') {
- if (useVP9 === true) return 'video/webm; codecs="vp9, opus"'
-
- return 'video/webm; codecs="vp8, vorbis"'
- }
-
- return undefined
-}
-
-export {
- renderVideo
-}
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
deleted file mode 100644
index e2e220c03..000000000
--- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
+++ /dev/null
@@ -1,663 +0,0 @@
-import videojs from 'video.js'
-import * as WebTorrent from 'webtorrent'
-import { logger } from '@root-helpers/logger'
-import { isIOS } from '@root-helpers/web-browser'
-import { addQueryParams, timeToInt } from '@shared/core-utils'
-import { VideoFile } from '@shared/models'
-import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage'
-import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types'
-import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../common'
-import { PeertubeChunkStore } from './peertube-chunk-store'
-import { renderVideo } from './video-renderer'
-
-const CacheChunkStore = require('cache-chunk-store')
-
-type PlayOptions = {
- forcePlay?: boolean
- seek?: number
- delay?: number
-}
-
-const Plugin = videojs.getPlugin('plugin')
-
-class WebTorrentPlugin extends Plugin {
- readonly videoFiles: VideoFile[]
-
- private readonly playerElement: HTMLVideoElement
-
- private readonly autoplay: boolean | string = false
- private readonly startTime: number = 0
- private readonly savePlayerSrcFunction: videojs.Player['src']
- private readonly videoDuration: number
- private readonly CONSTANTS = {
- INFO_SCHEDULER: 1000, // Don't change this
- AUTO_QUALITY_SCHEDULER: 3000, // Check quality every 3 seconds
- AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
- AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
- AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
- BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
- }
-
- private readonly buildWebSeedUrls: (file: VideoFile) => string[]
-
- private readonly webtorrent = new WebTorrent({
- tracker: {
- rtcConfig: getRtcConfig()
- },
- dht: false
- })
-
- private currentVideoFile: VideoFile
- private torrent: WebTorrent.Torrent
-
- private renderer: any
- private fakeRenderer: any
- private destroyingFakeRenderer = false
-
- private autoResolution = true
- private autoResolutionPossible = true
- private isAutoResolutionObservation = false
- private playerRefusedP2P = false
-
- private requiresUserAuth: boolean
- private videoFileToken: () => string
-
- private torrentInfoInterval: any
- private autoQualityInterval: any
- private addTorrentDelay: any
- private qualityObservationTimer: any
- private runAutoQualitySchedulerTimer: any
-
- private downloadSpeeds: number[] = []
-
- constructor (player: videojs.Player, options?: WebtorrentPluginOptions) {
- super(player)
-
- this.startTime = timeToInt(options.startTime)
-
- // Custom autoplay handled by webtorrent because we lazy play the video
- this.autoplay = options.autoplay
-
- this.playerRefusedP2P = options.playerRefusedP2P
-
- this.videoFiles = options.videoFiles
- this.videoDuration = options.videoDuration
-
- this.savePlayerSrcFunction = this.player.src
- this.playerElement = options.playerElement
-
- this.requiresUserAuth = options.requiresUserAuth
- this.videoFileToken = options.videoFileToken
-
- this.buildWebSeedUrls = options.buildWebSeedUrls
-
- this.player.ready(() => {
- const playerOptions = this.player.options_
-
- const volume = getStoredVolume()
- if (volume !== undefined) this.player.volume(volume)
-
- const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
- if (muted !== undefined) this.player.muted(muted)
-
- this.player.duration(options.videoDuration)
-
- this.initializePlayer()
- this.runTorrentInfoScheduler()
-
- this.player.one('play', () => {
- // Don't run immediately scheduler, wait some seconds the TCP connections are made
- this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
- })
- })
- }
-
- dispose () {
- clearTimeout(this.addTorrentDelay)
- clearTimeout(this.qualityObservationTimer)
- clearTimeout(this.runAutoQualitySchedulerTimer)
-
- clearInterval(this.torrentInfoInterval)
- clearInterval(this.autoQualityInterval)
-
- // Don't need to destroy renderer, video player will be destroyed
- this.flushVideoFile(this.currentVideoFile, false)
-
- this.destroyFakeRenderer()
- }
-
- getCurrentResolutionId () {
- return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1
- }
-
- updateVideoFile (
- videoFile?: VideoFile,
- options: {
- forcePlay?: boolean
- seek?: number
- delay?: number
- } = {},
- done: () => void = () => { /* empty */ }
- ) {
- // Automatically choose the adapted video file
- if (!videoFile) {
- const savedAverageBandwidth = getAverageBandwidthInStore()
- videoFile = savedAverageBandwidth
- ? this.getAppropriateFile(savedAverageBandwidth)
- : this.pickAverageVideoFile()
- }
-
- if (!videoFile) {
- throw Error(`Can't update video file since videoFile is undefined.`)
- }
-
- // Don't add the same video file once again
- if (this.currentVideoFile !== undefined && this.currentVideoFile.magnetUri === videoFile.magnetUri) {
- return
- }
-
- // Do not display error to user because we will have multiple fallback
- this.player.peertube().hideFatalError();
-
- // Hack to "simulate" src link in video.js >= 6
- // Without this, we can't play the video after pausing it
- // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
- (this.player as any).src = () => true
- const oldPlaybackRate = this.player.playbackRate()
-
- const previousVideoFile = this.currentVideoFile
- this.currentVideoFile = videoFile
-
- // Don't try on iOS that does not support MediaSource
- // Or don't use P2P if webtorrent is disabled
- if (isIOS() || this.playerRefusedP2P) {
- return this.fallbackToHttp(options, () => {
- this.player.playbackRate(oldPlaybackRate)
- return done()
- })
- }
-
- this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => {
- this.player.playbackRate(oldPlaybackRate)
- return done()
- })
-
- this.selectAppropriateResolution(true)
- }
-
- updateEngineResolution (resolutionId: number, delay = 0) {
- // Remember player state
- const currentTime = this.player.currentTime()
- const isPaused = this.player.paused()
-
- // Hide bigPlayButton
- if (!isPaused) {
- this.player.bigPlayButton.hide()
- }
-
- // Audio-only (resolutionId === 0) gets special treatment
- if (resolutionId === 0) {
- // Audio-only: show poster, do not auto-hide controls
- this.player.addClass('vjs-playing-audio-only-content')
- this.player.posterImage.show()
- } else {
- // Hide poster to have black background
- this.player.removeClass('vjs-playing-audio-only-content')
- this.player.posterImage.hide()
- }
-
- const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
- const options = {
- forcePlay: false,
- delay,
- seek: currentTime + (delay / 1000)
- }
-
- this.updateVideoFile(newVideoFile, options)
-
- this.player.trigger('engineResolutionChange')
- }
-
- flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
- if (videoFile !== undefined && this.webtorrent.get(videoFile.magnetUri)) {
- if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy()
-
- this.webtorrent.remove(videoFile.magnetUri)
- logger.info(`Removed ${videoFile.magnetUri}`)
- }
- }
-
- disableAutoResolution () {
- this.autoResolution = false
- this.autoResolutionPossible = false
- this.player.peertubeResolutions().disableAutoResolution()
- }
-
- isAutoResolutionPossible () {
- return this.autoResolutionPossible
- }
-
- getTorrent () {
- return this.torrent
- }
-
- getCurrentVideoFile () {
- return this.currentVideoFile
- }
-
- changeQuality (id: number) {
- if (id === -1) {
- if (this.autoResolutionPossible === true) {
- this.autoResolution = true
-
- this.selectAppropriateResolution(false)
- }
-
- return
- }
-
- this.autoResolution = false
- this.updateEngineResolution(id)
- this.selectAppropriateResolution(false)
- }
-
- private addTorrent (
- magnetOrTorrentUrl: string,
- previousVideoFile: VideoFile,
- options: PlayOptions,
- done: (err?: Error) => void
- ) {
- if (!magnetOrTorrentUrl) return this.fallbackToHttp(options, done)
-
- logger.info(`Adding ${magnetOrTorrentUrl}.`)
-
- const oldTorrent = this.torrent
- const torrentOptions = {
- // Don't use arrow function: it breaks webtorrent (that uses `new` keyword)
- store: function (chunkLength: number, storeOpts: any) {
- return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), {
- max: 100
- })
- },
- urlList: this.buildWebSeedUrls(this.currentVideoFile)
- }
-
- this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => {
- logger.info(`Added ${magnetOrTorrentUrl}.`)
-
- if (oldTorrent) {
- // Pause the old torrent
- this.stopTorrent(oldTorrent)
-
- // We use a fake renderer so we download correct pieces of the next file
- if (options.delay) this.renderFileInFakeElement(torrent.files[0], options.delay)
- }
-
- // Render the video in a few seconds? (on resolution change for example, we wait some seconds of the new video resolution)
- this.addTorrentDelay = setTimeout(() => {
- // We don't need the fake renderer anymore
- this.destroyFakeRenderer()
-
- const paused = this.player.paused()
-
- this.flushVideoFile(previousVideoFile)
-
- // Update progress bar (just for the UI), do not wait rendering
- if (options.seek) this.player.currentTime(options.seek)
-
- const renderVideoOptions = { autoplay: false, controls: true }
- renderVideo(torrent.files[0], this.playerElement, renderVideoOptions, (err, renderer) => {
- this.renderer = renderer
-
- if (err) return this.fallbackToHttp(options, done)
-
- return this.tryToPlay(err => {
- if (err) return done(err)
-
- if (options.seek) this.seek(options.seek)
- if (options.forcePlay === false && paused === true) this.player.pause()
-
- return done()
- })
- })
- }, options.delay || 0)
- })
-
- this.torrent.on('error', (err: any) => logger.error(err))
-
- this.torrent.on('warning', (err: any) => {
- // We don't support HTTP tracker but we don't care -> we use the web socket tracker
- if (err.message.indexOf('Unsupported tracker protocol') !== -1) return
-
- // Users don't care about issues with WebRTC, but developers do so log it in the console
- if (err.message.indexOf('Ice connection failed') !== -1) {
- logger.info(err)
- return
- }
-
- // Magnet hash is not up to date with the torrent file, add directly the torrent file
- if (err.message.indexOf('incorrect info hash') !== -1) {
- logger.error('Incorrect info hash detected, falling back to torrent file.')
- const newOptions = { forcePlay: true, seek: options.seek }
- return this.addTorrent((this.torrent as any)['xs'], previousVideoFile, newOptions, done)
- }
-
- // Remote instance is down
- if (err.message.indexOf('from xs param') !== -1) {
- this.handleError(err)
- }
-
- logger.warn(err)
- })
- }
-
- private tryToPlay (done?: (err?: Error) => void) {
- if (!done) done = function () { /* empty */ }
-
- const playPromise = this.player.play()
- if (playPromise !== undefined) {
- return playPromise.then(() => done())
- .catch((err: Error) => {
- if (err.message.includes('The play() request was interrupted by a call to pause()')) {
- return
- }
-
- logger.warn(err)
- this.player.pause()
- this.player.posterImage.show()
- this.player.removeClass('vjs-has-autoplay')
- this.player.removeClass('vjs-has-big-play-button-clicked')
- this.player.removeClass('vjs-playing-audio-only-content')
-
- return done()
- })
- }
-
- return done()
- }
-
- private seek (time: number) {
- this.player.currentTime(time)
- this.player.handleTechSeeked_()
- }
-
- private getAppropriateFile (averageDownloadSpeed?: number): VideoFile {
- if (this.videoFiles === undefined) return undefined
- if (this.videoFiles.length === 1) return this.videoFiles[0]
-
- const files = this.videoFiles.filter(f => f.resolution.id !== 0)
- if (files.length === 0) return undefined
-
- // Don't change the torrent if the player ended
- if (this.torrent && this.torrent.progress === 1 && this.player.ended()) return this.currentVideoFile
-
- if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed()
-
- // Limit resolution according to player height
- const playerHeight = this.playerElement.offsetHeight
-
- // We take the first resolution just above the player height
- // Example: player height is 530px, we want the 720p file instead of 480p
- let maxResolution = files[0].resolution.id
- for (let i = files.length - 1; i >= 0; i--) {
- const resolutionId = files[i].resolution.id
- if (resolutionId !== 0 && resolutionId >= playerHeight) {
- maxResolution = resolutionId
- break
- }
- }
-
- // Filter videos we can play according to our screen resolution and bandwidth
- const filteredFiles = files.filter(f => f.resolution.id <= maxResolution)
- .filter(f => {
- const fileBitrate = (f.size / this.videoDuration)
- let threshold = fileBitrate
-
- // If this is for a higher resolution or an initial load: add a margin
- if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) {
- threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100)
- }
-
- return averageDownloadSpeed > threshold
- })
-
- // If the download speed is too bad, return the lowest resolution we have
- if (filteredFiles.length === 0) return videoFileMinByResolution(files)
-
- return videoFileMaxByResolution(filteredFiles)
- }
-
- private getAndSaveActualDownloadSpeed () {
- const start = Math.max(this.downloadSpeeds.length - this.CONSTANTS.BANDWIDTH_AVERAGE_NUMBER_OF_VALUES, 0)
- const lastDownloadSpeeds = this.downloadSpeeds.slice(start, this.downloadSpeeds.length)
- if (lastDownloadSpeeds.length === 0) return -1
-
- const sum = lastDownloadSpeeds.reduce((a, b) => a + b)
- const averageBandwidth = Math.round(sum / lastDownloadSpeeds.length)
-
- // Save the average bandwidth for future use
- saveAverageBandwidth(averageBandwidth)
-
- return averageBandwidth
- }
-
- private initializePlayer () {
- this.buildQualities()
-
- if (this.videoFiles.length === 0) {
- this.player.addClass('disabled')
- return
- }
-
- if (this.autoplay !== false) {
- this.player.posterImage.hide()
-
- return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
- }
-
- // Proxy first play
- const oldPlay = this.player.play.bind(this.player);
- (this.player as any).play = () => {
- this.player.addClass('vjs-has-big-play-button-clicked')
- this.player.play = oldPlay
-
- this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime })
- }
- }
-
- private runAutoQualityScheduler () {
- this.autoQualityInterval = setInterval(() => {
-
- // Not initialized or in HTTP fallback
- if (this.torrent === undefined || this.torrent === null) return
- if (this.autoResolution === false) return
- if (this.isAutoResolutionObservation === true) return
-
- const file = this.getAppropriateFile()
- let changeResolution = false
- let changeResolutionDelay = 0
-
- // Lower resolution
- if (this.isPlayerWaiting() && file.resolution.id < this.currentVideoFile.resolution.id) {
- logger.info(`Downgrading automatically the resolution to: ${file.resolution.label}`)
- changeResolution = true
- } else if (file.resolution.id > this.currentVideoFile.resolution.id) { // Higher resolution
- logger.info(`Upgrading automatically the resolution to: ${file.resolution.label}`)
- changeResolution = true
- changeResolutionDelay = this.CONSTANTS.AUTO_QUALITY_HIGHER_RESOLUTION_DELAY
- }
-
- if (changeResolution === true) {
- this.updateEngineResolution(file.resolution.id, changeResolutionDelay)
-
- // Wait some seconds in observation of our new resolution
- this.isAutoResolutionObservation = true
-
- this.qualityObservationTimer = setTimeout(() => {
- this.isAutoResolutionObservation = false
- }, this.CONSTANTS.AUTO_QUALITY_OBSERVATION_TIME)
- }
- }, this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
- }
-
- private isPlayerWaiting () {
- return this.player?.hasClass('vjs-waiting')
- }
-
- private runTorrentInfoScheduler () {
- this.torrentInfoInterval = setInterval(() => {
- // Not initialized yet
- if (this.torrent === undefined) return
-
- // Http fallback
- if (this.torrent === null) return this.player.trigger('p2pInfo', false)
-
- // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too
- if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
-
- return this.player.trigger('p2pInfo', {
- source: 'webtorrent',
- http: {
- downloadSpeed: 0,
- downloaded: 0
- },
- p2p: {
- downloadSpeed: this.torrent.downloadSpeed,
- numPeers: this.torrent.numPeers,
- uploadSpeed: this.torrent.uploadSpeed,
- downloaded: this.torrent.downloaded,
- uploaded: this.torrent.uploaded
- },
- bandwidthEstimate: this.webtorrent.downloadSpeed
- } as PlayerNetworkInfo)
- }, this.CONSTANTS.INFO_SCHEDULER)
- }
-
- private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) {
- const paused = this.player.paused()
-
- this.disableAutoResolution()
-
- this.flushVideoFile(this.currentVideoFile, true)
- this.torrent = null
-
- // Enable error display now this is our last fallback
- this.player.one('error', () => this.player.peertube().displayFatalError())
-
- let httpUrl = this.currentVideoFile.fileUrl
-
- if (this.videoFileToken) {
- httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
- }
-
- this.player.src = this.savePlayerSrcFunction
- this.player.src(httpUrl)
-
- this.selectAppropriateResolution(true)
-
- // We changed the source, so reinit captions
- this.player.trigger('sourcechange')
-
- return this.tryToPlay(err => {
- if (err && done) return done(err)
-
- if (options.seek) this.seek(options.seek)
- if (options.forcePlay === false && paused === true) this.player.pause()
-
- if (done) return done()
- })
- }
-
- private handleError (err: Error | string) {
- return this.player.trigger('customError', { err })
- }
-
- private pickAverageVideoFile () {
- if (this.videoFiles.length === 1) return this.videoFiles[0]
-
- const files = this.videoFiles.filter(f => f.resolution.id !== 0)
- return files[Math.floor(files.length / 2)]
- }
-
- private stopTorrent (torrent: WebTorrent.Torrent) {
- torrent.pause()
- // Pause does not remove actual peers (in particular the webseed peer)
- torrent.removePeer((torrent as any)['ws'])
- }
-
- private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) {
- this.destroyingFakeRenderer = false
-
- const fakeVideoElem = document.createElement('video')
- renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => {
- this.fakeRenderer = renderer
-
- // The renderer returns an error when we destroy it, so skip them
- if (this.destroyingFakeRenderer === false && err) {
- logger.error('Cannot render new torrent in fake video element.', err)
- }
-
- // Load the future file at the correct time (in delay MS - 2 seconds)
- fakeVideoElem.currentTime = this.player.currentTime() + (delay - 2000)
- })
- }
-
- private destroyFakeRenderer () {
- if (this.fakeRenderer) {
- this.destroyingFakeRenderer = true
-
- if (this.fakeRenderer.destroy) {
- try {
- this.fakeRenderer.destroy()
- } catch (err) {
- logger.info('Cannot destroy correctly fake renderer.', err)
- }
- }
- this.fakeRenderer = undefined
- }
- }
-
- private buildQualities () {
- const resolutions: PeerTubeResolution[] = this.videoFiles.map(file => ({
- id: file.resolution.id,
- label: this.buildQualityLabel(file),
- height: file.resolution.id,
- selected: false,
- selectCallback: () => this.changeQuality(file.resolution.id)
- }))
-
- resolutions.push({
- id: -1,
- label: this.player.localize('Auto'),
- selected: true,
- selectCallback: () => this.changeQuality(-1)
- })
-
- this.player.peertubeResolutions().add(resolutions)
- }
-
- private buildQualityLabel (file: VideoFile) {
- let label = file.resolution.label
-
- if (file.fps && file.fps >= 50) {
- label += file.fps
- }
-
- return label
- }
-
- private selectAppropriateResolution (byEngine: boolean) {
- const resolution = this.autoResolution
- ? -1
- : this.getCurrentResolutionId()
-
- const autoResolutionChosen = this.autoResolution
- ? this.getCurrentResolutionId()
- : undefined
-
- this.player.peertubeResolutions().select({ id: resolution, autoResolutionChosenId: autoResolutionChosen, byEngine })
- }
-}
-
-videojs.registerPlugin('webtorrent', WebTorrentPlugin)
-export { WebTorrentPlugin }
diff --git a/client/src/assets/player/types/index.ts b/client/src/assets/player/types/index.ts
index b73e0b3cb..4bf49f65c 100644
--- a/client/src/assets/player/types/index.ts
+++ b/client/src/assets/player/types/index.ts
@@ -1,2 +1,2 @@
-export * from './manager-options'
+export * from './peertube-player-options'
export * from './peertube-videojs-typings'
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/peertube-player-options.ts
similarity index 53%
rename from client/src/assets/player/types/manager-options.ts
rename to client/src/assets/player/types/peertube-player-options.ts
index a73341b4c..e1b8c7fab 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/peertube-player-options.ts
@@ -1,101 +1,117 @@
import { PluginsManager } from '@root-helpers/plugins-manager'
import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
+import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings'
-export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
+export type PlayerMode = 'web-video' | 'p2p-media-loader'
-export type WebtorrentOptions = {
- videoFiles: VideoFile[]
-}
+export type PeerTubePlayerContructorOptions = {
+ playerElement: () => HTMLVideoElement
-export type P2PMediaLoaderOptions = {
- playlistUrl: string
- segmentsSha256Url: string
- trackerAnnounce: string[]
- redundancyBaseUrls: string[]
- videoFiles: VideoFile[]
-}
+ controls: boolean
+ controlBar: boolean
-export interface CustomizationOptions {
- startTime: number | string
- stopTime: number | string
+ muted: boolean
+ loop: boolean
- controls?: boolean
- controlBar?: boolean
-
- muted?: boolean
- loop?: boolean
- subtitle?: string
- resume?: string
-
- peertubeLink: boolean
+ peertubeLink: () => boolean
playbackRate?: number | string
-}
-export interface CommonOptions extends CustomizationOptions {
- playerElement: HTMLVideoElement
- onPlayerElementChange: (element: HTMLVideoElement) => void
-
- autoplay: boolean
- forceAutoplay: boolean
-
- p2pEnabled: boolean
-
- nextVideo?: () => void
- hasNextVideo?: () => boolean
-
- previousVideo?: () => void
- hasPreviousVideo?: () => boolean
-
- playlist?: PlaylistPluginOptions
-
- videoDuration: number
enableHotkeys: boolean
inactivityTimeout: number
- poster: string
videoViewIntervalMs: number
instanceName: string
theaterButton: boolean
- captions: boolean
- videoViewUrl: string
- authorizationHeader?: () => string
+ authorizationHeader: () => string
metricsUrl: string
+ serverUrl: string
+
+ errorNotifier: (message: string) => void
+
+ // Current web browser language
+ language: string
+
+ pluginsManager: PluginsManager
+}
+
+export type PeerTubePlayerLoadOptions = {
+ mode: PlayerMode
+
+ startTime?: number | string
+ stopTime?: number | string
+
+ autoplay: boolean
+ forceAutoplay: boolean
+
+ poster: string
+ subtitle?: string
+ videoViewUrl: string
embedUrl: string
embedTitle: string
isLive: boolean
+
liveOptions?: {
latencyMode: LiveVideoLatencyMode
}
- language?: string
-
videoCaptions: VideoJSCaption[]
storyboard: VideoJSStoryboard
videoUUID: string
videoShortUUID: string
- serverUrl: string
+ duration: number
+
requiresUserAuth: boolean
videoFileToken: () => string
requiresPassword: boolean
videoPassword: () => string
- errorNotifier: (message: string) => void
+ nextVideo: {
+ enabled: boolean
+ getVideoTitle: () => string
+ handler?: () => void
+ displayControlBarButton: boolean
+ }
+
+ previousVideo: {
+ enabled: boolean
+ handler?: () => void
+ displayControlBarButton: boolean
+ }
+
+ upnext?: {
+ isEnabled: () => boolean
+ isSuspended: (player: videojs.VideoJsPlayer) => boolean
+ timeout: number
+ }
+
+ dock?: PeerTubeDockPluginOptions
+
+ playlist?: PlaylistPluginOptions
+
+ p2pEnabled: boolean
+
+ hls?: HLSOptions
+ webVideo?: WebVideoOptions
}
-export type PeertubePlayerManagerOptions = {
- common: CommonOptions
- webtorrent: WebtorrentOptions
- p2pMediaLoader?: P2PMediaLoaderOptions
-
- pluginsManager: PluginsManager
+export type WebVideoOptions = {
+ videoFiles: VideoFile[]
+}
+
+export type HLSOptions = {
+ playlistUrl: string
+ segmentsSha256Url: string
+ trackerAnnounce: string[]
+ redundancyBaseUrls: string[]
+ videoFiles: VideoFile[]
}
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index 30d2b287f..f10fc03a8 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -2,8 +2,11 @@ import { HlsConfig, Level } from 'hls.js'
import videojs from 'video.js'
import { Engine } from '@peertube/p2p-media-loader-hlsjs'
import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
-import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
-import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin'
+import { BezelsPlugin } from '../shared/bezels/bezels-plugin'
+import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin'
+import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
+import { HotkeysOptions, PeerTubeHotkeysPlugin } from '../shared/hotkeys/peertube-hotkeys-plugin'
+import { PeerTubeMobilePlugin } from '../shared/mobile/peertube-mobile-plugin'
import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin'
import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin'
import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager'
@@ -12,9 +15,10 @@ import { PlaylistPlugin } from '../shared/playlist/playlist-plugin'
import { PeerTubeResolutionsPlugin } from '../shared/resolutions/peertube-resolutions-plugin'
import { StatsCardOptions } from '../shared/stats/stats-card'
import { StatsForNerdsPlugin } from '../shared/stats/stats-plugin'
-import { EndCardOptions } from '../shared/upnext/end-card'
-import { WebTorrentPlugin } from '../shared/webtorrent/webtorrent-plugin'
-import { PlayerMode } from './manager-options'
+import { UpNextPlugin } from '../shared/upnext/upnext-plugin'
+import { WebVideoPlugin } from '../shared/web-video/web-video-plugin'
+import { PlayerMode } from './peertube-player-options'
+import { SegmentValidator } from '../shared/p2p-media-loader/segment-validator'
declare module 'video.js' {
@@ -31,35 +35,36 @@ declare module 'video.js' {
handleTechSeeked_ (): void
- // Plugins
-
- peertube (): PeerTubePlugin
-
- webtorrent (): WebTorrentPlugin
-
- p2pMediaLoader (): P2pMediaLoaderPlugin
-
- peertubeResolutions (): PeerTubeResolutionsPlugin
-
- contextmenuUI (options: any): any
-
- bezels (): void
- peertubeMobile (): void
- peerTubeHotkeysPlugin (options?: HotkeysOptions): void
-
- stats (options?: StatsCardOptions): StatsForNerdsPlugin
-
- storyboard (options: StoryboardOptions): void
-
textTracks (): TextTrackList & {
tracks_: (TextTrack & { id: string, label: string, src: string })[]
}
- peertubeDock (options: PeerTubeDockPluginOptions): void
+ // Plugins
- upnext (options: Partial): void
+ peertube (): PeerTubePlugin
- playlist (): PlaylistPlugin
+ webVideo (options?: any): WebVideoPlugin
+
+ p2pMediaLoader (options?: any): P2pMediaLoaderPlugin
+ hlsjs (options?: any): any
+
+ peertubeResolutions (): PeerTubeResolutionsPlugin
+
+ contextmenuUI (options?: any): any
+
+ bezels (): BezelsPlugin
+ peertubeMobile (): PeerTubeMobilePlugin
+ peerTubeHotkeysPlugin (options?: HotkeysOptions): PeerTubeHotkeysPlugin
+
+ stats (options?: StatsCardOptions): StatsForNerdsPlugin
+
+ storyboard (options?: StoryboardOptions): StoryboardPlugin
+
+ peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin
+
+ upnext (options?: UpNextPluginOptions): UpNextPlugin
+
+ playlist (options?: PlaylistPluginOptions): PlaylistPlugin
}
}
@@ -99,32 +104,28 @@ type VideoJSStoryboard = {
}
type PeerTubePluginOptions = {
- mode: PlayerMode
+ hasAutoplay: () => videojs.Autoplay
- autoplay: videojs.Autoplay
- videoDuration: number
+ videoViewUrl: () => string
+ videoViewIntervalMs: number
- videoViewUrl: string
authorizationHeader?: () => string
- subtitle?: string
+ videoDuration: () => number
- videoCaptions: VideoJSCaption[]
+ startTime: () => number | string
+ stopTime: () => number | string
- startTime: number | string
- stopTime: number | string
-
- isLive: boolean
-
- videoUUID: string
-
- videoViewIntervalMs: number
+ videoCaptions: () => VideoJSCaption[]
+ isLive: () => boolean
+ videoUUID: () => string
+ subtitle: () => string
}
type MetricsPluginOptions = {
- mode: PlayerMode
- metricsUrl: string
- videoUUID: string
+ mode: () => PlayerMode
+ metricsUrl: () => string
+ videoUUID: () => string
}
type StoryboardOptions = {
@@ -144,37 +145,36 @@ type PlaylistPluginOptions = {
onItemClicked: (element: VideoPlaylistElement) => void
}
+type UpNextPluginOptions = {
+ timeout: number
+
+ next: () => void
+ getTitle: () => string
+ isDisplayed: () => boolean
+ isSuspended: () => boolean
+}
+
type NextPreviousVideoButtonOptions = {
type: 'next' | 'previous'
- handler: () => void
+ handler?: () => void
+ isDisplayed: () => boolean
isDisabled: () => boolean
}
type PeerTubeLinkButtonOptions = {
- shortUUID: string
+ isDisplayed: () => boolean
+ shortUUID: () => string
instanceName: string
}
-type PeerTubeP2PInfoButtonOptions = {
- p2pEnabled: boolean
+type TheaterButtonOptions = {
+ isDisplayed: () => boolean
}
-type WebtorrentPluginOptions = {
- playerElement: HTMLVideoElement
-
- autoplay: videojs.Autoplay
- videoDuration: number
-
+type WebVideoPluginOptions = {
videoFiles: VideoFile[]
-
startTime: number | string
-
- playerRefusedP2P: boolean
-
- requiresUserAuth: boolean
videoFileToken: () => string
-
- buildWebSeedUrls: (file: VideoFile) => string[]
}
type P2PMediaLoaderPluginOptions = {
@@ -182,9 +182,8 @@ type P2PMediaLoaderPluginOptions = {
type: string
src: string
- startTime: number | string
-
loader: P2PMediaLoader
+ segmentValidator: SegmentValidator
requiresUserAuth: boolean
videoFileToken: () => string
@@ -192,6 +191,8 @@ type P2PMediaLoaderPluginOptions = {
export type P2PMediaLoader = {
getEngine(): Engine
+
+ destroy: () => void
}
type VideoJSPluginOptions = {
@@ -200,7 +201,7 @@ type VideoJSPluginOptions = {
peertube: PeerTubePluginOptions
metrics: MetricsPluginOptions
- webtorrent?: WebtorrentPluginOptions
+ webVideo?: WebVideoPluginOptions
p2pMediaLoader?: P2PMediaLoaderPluginOptions
}
@@ -227,14 +228,14 @@ type AutoResolutionUpdateData = {
}
type PlayerNetworkInfo = {
- source: 'webtorrent' | 'p2p-media-loader'
+ source: 'web-video' | 'p2p-media-loader'
http: {
- downloadSpeed: number
+ downloadSpeed?: number
downloaded: number
}
- p2p: {
+ p2p?: {
downloadSpeed: number
uploadSpeed: number
downloaded: number
@@ -243,7 +244,7 @@ type PlayerNetworkInfo = {
}
// In bytes
- bandwidthEstimate: number
+ bandwidthEstimate?: number
}
type PlaylistItemOptions = {
@@ -254,6 +255,7 @@ type PlaylistItemOptions = {
export {
PlayerNetworkInfo,
+ TheaterButtonOptions,
VideoJSStoryboard,
PlaylistItemOptions,
NextPreviousVideoButtonOptions,
@@ -263,12 +265,12 @@ export {
MetricsPluginOptions,
VideoJSCaption,
PeerTubePluginOptions,
- WebtorrentPluginOptions,
+ WebVideoPluginOptions,
P2PMediaLoaderPluginOptions,
PeerTubeResolution,
VideoJSPluginOptions,
+ UpNextPluginOptions,
LoadedQualityData,
StoryboardOptions,
- PeerTubeLinkButtonOptions,
- PeerTubeP2PInfoButtonOptions
+ PeerTubeLinkButtonOptions
}
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss
index 02d5fa169..09a75e2fd 100644
--- a/client/src/sass/player/control-bar.scss
+++ b/client/src/sass/player/control-bar.scss
@@ -3,20 +3,6 @@
@use '_mixins' as *;
@use './_player-variables' as *;
-// Like the time tooltip
-.video-js .vjs-progress-holder .vjs-storyboard-sprite-placeholder {
- display: none;
-}
-
-.video-js .vjs-progress-control:hover .vjs-storyboard-sprite-placeholder,
-.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-storyboard-sprite-placeholder {
- display: block;
-
- // Ensure that we maintain a font-size of ~10px.
- font-size: 0.6em;
- visibility: visible;
-}
-
.video-js.vjs-peertube-skin .vjs-control-bar {
z-index: 100;
@@ -26,11 +12,8 @@
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
transition: visibility 0.3s, opacity 0.3s !important;
- &.control-bar-hidden {
- display: none !important;
- }
-
- > button:first-child {
+ > button:not(.vjs-hidden):first-child,
+ > button.vjs-hidden + button:not(.vjs-hidden) {
@include margin-left($first-control-bar-element-margin-left);
}
@@ -167,7 +150,7 @@
}
}
- .vjs-live-control {
+ .vjs-pt-live-control {
padding: 5px 7px;
border-radius: 3px;
height: fit-content;
@@ -245,6 +228,7 @@
.vjs-next-video,
.vjs-previous-video {
width: $control-bar-button-width - 4px;
+ cursor: pointer;
&.vjs-disabled {
cursor: default;
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss
index 5d0307d95..4bfd67a26 100644
--- a/client/src/sass/player/index.scss
+++ b/client/src/sass/player/index.scss
@@ -10,3 +10,4 @@
@use './playlist';
@use './stats';
@use './offline-notification';
+@use './storyboard.scss';
diff --git a/client/src/sass/player/mobile.scss b/client/src/sass/player/mobile.scss
index d150c54ee..b0019d2c9 100644
--- a/client/src/sass/player/mobile.scss
+++ b/client/src/sass/player/mobile.scss
@@ -170,7 +170,8 @@
}
}
- &.vjs-scrubbing {
+ &.vjs-scrubbing,
+ &.vjs-mobile-sliding {
.vjs-mobile-buttons-overlay {
display: none;
}
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 4df8dbaf0..572ae7050 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -84,7 +84,9 @@ body {
}
// Do not display poster when video is starting
- &.vjs-has-autoplay:not(.vjs-has-started) {
+ // Or if we change resolution manually
+ &.vjs-has-autoplay:not(.vjs-has-started),
+ &.vjs-updating-resolution {
.vjs-poster {
opacity: 0;
visibility: hidden;
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss
index d2346c126..369c827f7 100644
--- a/client/src/sass/player/settings-menu.scss
+++ b/client/src/sass/player/settings-menu.scss
@@ -75,6 +75,7 @@ $setting-transition-easing: ease-out;
> .vjs-menu {
flex: 1;
min-width: 200px;
+ padding: 5px 0;
}
> .vjs-menu,
@@ -90,14 +91,6 @@ $setting-transition-easing: ease-out;
background-color: rgba(255, 255, 255, 0.2);
}
- &:first-child {
- margin-top: 5px;
- }
-
- &:last-child {
- margin-bottom: 5px;
- }
-
&.disabled {
opacity: 0.5;
cursor: default !important;
diff --git a/client/src/sass/player/storyboard.scss b/client/src/sass/player/storyboard.scss
new file mode 100644
index 000000000..c80d1b59d
--- /dev/null
+++ b/client/src/sass/player/storyboard.scss
@@ -0,0 +1,26 @@
+@use 'sass:math';
+@use '_variables' as *;
+@use '_mixins' as *;
+@use './_player-variables' as *;
+
+// Like the time tooltip
+.video-js .vjs-progress-holder .vjs-storyboard-sprite-placeholder {
+ display: none;
+}
+
+.video-js .vjs-progress-control:hover .vjs-storyboard-sprite-placeholder,
+.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-storyboard-sprite-placeholder {
+ display: block;
+
+ // Ensure that we maintain a font-size of ~10px.
+ font-size: 0.6em;
+ visibility: visible;
+}
+
+.video-js.vjs-settings-dialog-opened {
+ .vjs-storyboard-sprite-placeholder,
+ .vjs-time-tooltip,
+ .vjs-mouse-display {
+ display: none !important;
+ }
+}
diff --git a/client/src/shims/http.ts b/client/src/shims/http.ts
deleted file mode 100644
index 1b1767aab..000000000
--- a/client/src/shims/http.ts
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('stream-http')
diff --git a/client/src/shims/https.ts b/client/src/shims/https.ts
deleted file mode 100644
index f5ef70430..000000000
--- a/client/src/shims/https.ts
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('https-browserify')
diff --git a/client/src/shims/stream.ts b/client/src/shims/stream.ts
deleted file mode 100644
index 977fd05a0..000000000
--- a/client/src/shims/stream.ts
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('stream-browserify')
diff --git a/client/src/standalone/player/.npmignore b/client/src/standalone/embed-player-api/.npmignore
similarity index 100%
rename from client/src/standalone/player/.npmignore
rename to client/src/standalone/embed-player-api/.npmignore
diff --git a/client/src/standalone/player/README.md b/client/src/standalone/embed-player-api/README.md
similarity index 100%
rename from client/src/standalone/player/README.md
rename to client/src/standalone/embed-player-api/README.md
diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/embed-player-api/definitions.ts
similarity index 100%
rename from client/src/standalone/player/definitions.ts
rename to client/src/standalone/embed-player-api/definitions.ts
diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/embed-player-api/events.ts
similarity index 100%
rename from client/src/standalone/player/events.ts
rename to client/src/standalone/embed-player-api/events.ts
diff --git a/client/src/standalone/player/package.json b/client/src/standalone/embed-player-api/package.json
similarity index 100%
rename from client/src/standalone/player/package.json
rename to client/src/standalone/embed-player-api/package.json
diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/embed-player-api/player.ts
similarity index 100%
rename from client/src/standalone/player/player.ts
rename to client/src/standalone/embed-player-api/player.ts
diff --git a/client/src/standalone/player/tsconfig.json b/client/src/standalone/embed-player-api/tsconfig.json
similarity index 100%
rename from client/src/standalone/player/tsconfig.json
rename to client/src/standalone/embed-player-api/tsconfig.json
diff --git a/client/src/standalone/player/webpack.config.js b/client/src/standalone/embed-player-api/webpack.config.js
similarity index 100%
rename from client/src/standalone/player/webpack.config.js
rename to client/src/standalone/embed-player-api/webpack.config.js
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts
index a99f1edae..6227c378e 100644
--- a/client/src/standalone/videos/embed-api.ts
+++ b/client/src/standalone/videos/embed-api.ts
@@ -1,7 +1,7 @@
import './embed.scss'
import * as Channel from 'jschannel'
import { logger } from '../../root-helpers'
-import { PeerTubeResolution, PeerTubeTextTrack } from '../player/definitions'
+import { PeerTubeResolution, PeerTubeTextTrack } from '../embed-player-api/definitions'
import { PeerTubeEmbed } from './embed'
/**
@@ -72,15 +72,12 @@ export class PeerTubeEmbedApi {
private setResolution (resolutionId: number) {
logger.info(`Set resolution ${resolutionId}`)
- if (this.isWebtorrent()) {
- if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return
-
- this.embed.player.webtorrent().changeQuality(resolutionId)
-
+ if (this.isWebVideo() && resolutionId === -1) {
+ logger.error('Auto resolution cannot be set in web video player mode')
return
}
- this.embed.player.p2pMediaLoader().getHLSJS().currentLevel = resolutionId
+ this.embed.player.peertubeResolutions().select({ id: resolutionId, fireCallback: true })
}
private getCaptions (): PeerTubeTextTrack[] {
@@ -152,8 +149,8 @@ export class PeerTubeEmbedApi {
// ---------------------------------------------------------------------------
// PeerTube specific capabilities
- this.embed.player.peertubeResolutions().on('resolutionsAdded', () => this.loadResolutions())
- this.embed.player.peertubeResolutions().on('resolutionChanged', () => this.loadResolutions())
+ this.embed.player.peertubeResolutions().on('resolutions-added', () => this.loadResolutions())
+ this.embed.player.peertubeResolutions().on('resolutions-changed', () => this.loadResolutions())
this.loadResolutions()
@@ -193,7 +190,7 @@ export class PeerTubeEmbedApi {
})
}
- private isWebtorrent () {
- return !!this.embed.player.webtorrent
+ private isWebVideo () {
+ return !!this.embed.player.webVideo
}
}
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index a74bb4cee..e2dc02b60 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -44,11 +44,11 @@
-
+
-
+
@@ -60,8 +60,6 @@
-
-