From 9597920ee3d4ac99803e7107983ddf98a9dfb3c4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Mar 2022 11:16:54 +0100 Subject: [PATCH] Reorganize player manager options builder --- client/src/assets/player/index.ts | 2 +- .../control-bar-options-builder.ts | 132 ++++ .../manager-options/hls-options-builder.ts | 192 ++++++ .../manager-options-builder.ts | 168 +++++ .../manager-options/manager-options.model.ts | 84 +++ .../webtorrent-options-builder.ts | 36 ++ .../player/p2p-media-loader/hls-plugin.ts | 2 +- .../p2p-media-loader-plugin.ts | 4 +- .../assets/player/peertube-player-manager.ts | 21 +- .../player/peertube-player-options-builder.ts | 577 ------------------ .../assets/player/peertube-videojs-typings.ts | 9 +- 11 files changed, 634 insertions(+), 593 deletions(-) create mode 100644 client/src/assets/player/manager-options/control-bar-options-builder.ts create mode 100644 client/src/assets/player/manager-options/hls-options-builder.ts create mode 100644 client/src/assets/player/manager-options/manager-options-builder.ts create mode 100644 client/src/assets/player/manager-options/manager-options.model.ts create mode 100644 client/src/assets/player/manager-options/webtorrent-options-builder.ts delete mode 100644 client/src/assets/player/peertube-player-options-builder.ts diff --git a/client/src/assets/player/index.ts b/client/src/assets/player/index.ts index e2a6ccf24..92270476d 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-options-builder' +export * from './manager-options/manager-options.model' diff --git a/client/src/assets/player/manager-options/control-bar-options-builder.ts b/client/src/assets/player/manager-options/control-bar-options-builder.ts new file mode 100644 index 000000000..54e61c5d0 --- /dev/null +++ b/client/src/assets/player/manager-options/control-bar-options-builder.ts @@ -0,0 +1,132 @@ +import { NextPreviousVideoButtonOptions, PeerTubeLinkButtonOptions } from '../peertube-videojs-typings' +import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model' + +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, { + currentTimeDisplay: {}, + timeDivider: {}, + durationDisplay: {}, + liveDisplay: {}, + + flexibleWidthSpacer: {}, + + ...this.getProgressControl(), + + p2PInfoButton: { + p2pEnabled: this.options.p2pEnabled + }, + + muteToggle: {}, + volumeControl: {}, + + settingsButton: this.getSettingsButton() + }) + + if (this.options.peertubeLink === true) { + Object.assign(children, { + peerTubeLinkButton: { shortUUID: this.options.videoShortUUID } as PeerTubeLinkButtonOptions + }) + } + + if (this.options.theaterButton === true) { + Object.assign(children, { + theaterButton: {} + }) + } + + Object.assign(children, { + fullscreenToggle: {} + }) + + return children + } + + private getSettingsButton () { + const settingEntries: string[] = [] + + settingEntries.push('playbackRateMenuButton') + + if (this.options.captions === true) settingEntries.push('captionsButton') + + settingEntries.push('resolutionMenuButton') + + return { + settingsButton: { + setup: { + maxHeightOffset: 40 + }, + entries: settingEntries + } + } + } + + private getProgressControl () { + 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/manager-options/hls-options-builder.ts b/client/src/assets/player/manager-options/hls-options-builder.ts new file mode 100644 index 000000000..9de23561b --- /dev/null +++ b/client/src/assets/player/manager-options/hls-options-builder.ts @@ -0,0 +1,192 @@ +import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' +import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' +import { LiveVideoLatencyMode } from '@shared/models' +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 { getAverageBandwidthInStore } from '../peertube-player-local-storage' +import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../peertube-videojs-typings' +import { getRtcConfig } from '../utils' +import { PeertubePlayerManagerOptions } from './manager-options.model' + +export class HLSOptionsBuilder { + + constructor ( + private options: PeertubePlayerManagerOptions, + private p2pMediaLoaderModule?: any + ) { + + } + + getPluginOptions () { + const commonOptions = this.options.common + + const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls) + + const p2pMediaLoaderConfig = this.getP2PMediaLoaderOptions(redundancyUrlManager) + const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader + + const p2pMediaLoader: P2PMediaLoaderPluginOptions = { + redundancyUrlManager, + type: 'application/x-mpegURL', + startTime: commonOptions.startTime, + src: this.options.p2pMediaLoader.playlistUrl, + loader + } + + const hlsjs = { + 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) + // We don't have files for live videos + if (!file) return level.height + + let label = file.resolution.label + if (file.fps >= 50) label += file.fps + + return label + }, + html5: { + hlsjsConfig: this.getHLSJSOptions(loader) + } + } + + return { p2pMediaLoader, hlsjs } + } + + // --------------------------------------------------------------------------- + + private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings { + let consumeOnly = false + if ((navigator as any)?.connection?.type === 'cellular') { + console.log('We are on a cellular connection: disabling seeding.') + consumeOnly = true + } + + const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce + .filter(t => t.startsWith('ws')) + + const specificLiveOrVODOptions = this.options.common.isLive + ? this.getP2PMediaLoaderLiveOptions() + : this.getP2PMediaLoaderVODOptions() + + return { + loader: { + + trackerAnnounce, + rtcConfig: getRtcConfig(), + + simultaneousHttpDownloads: 1, + httpFailedSegmentTimeout: 1000, + + segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), + segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1), + + useP2P: this.options.common.p2pEnabled, + consumeOnly, + + ...specificLiveOrVODOptions + }, + segments: { + swarmId: this.options.p2pMediaLoader.playlistUrl, + forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority + } + } + } + + private getP2PMediaLoaderLiveOptions (): Partial { + const base = { + requiredSegmentsPriority: 1 + } + + const latencyMode = this.options.common.liveOptions.latencyMode + + switch (latencyMode) { + case LiveVideoLatencyMode.SMALL_LATENCY: + return { + ...base, + + useP2P: false, + httpDownloadProbability: 1 + } + + case LiveVideoLatencyMode.HIGH_LATENCY: + return base + + default: + return base + } + } + + private getP2PMediaLoaderVODOptions (): Partial { + return { + requiredSegmentsPriority: 3, + + cachedSegmentExpiration: 86400000, + cachedSegmentsCount: 100, + + httpDownloadMaxPriority: 9, + httpDownloadProbability: 0.06, + httpDownloadProbabilitySkipIfNoPeers: true, + + p2pDownloadMaxPriority: 50 + } + } + + // --------------------------------------------------------------------------- + + private getHLSJSOptions (loader: P2PMediaLoader) { + const specificLiveOrVODOptions = this.options.common.isLive + ? this.getHLSLiveOptions() + : this.getHLSVODOptions() + + const base = { + capLevelToPlayerSize: true, + autoStartLoad: false, + + loader, + + ...specificLiveOrVODOptions + } + + const averageBandwidth = getAverageBandwidthInStore() + if (!averageBandwidth) return base + + return { + ...base, + + abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s + startLevel: -1, + testBandwidth: false, + debug: false + } + } + + private getHLSLiveOptions () { + const latencyMode = this.options.common.liveOptions.latencyMode + + switch (latencyMode) { + case LiveVideoLatencyMode.SMALL_LATENCY: + return { + liveSyncDurationCount: 2 + } + + case LiveVideoLatencyMode.HIGH_LATENCY: + return { + liveSyncDurationCount: 10 + } + + default: + return { + liveSyncDurationCount: 5 + } + } + } + + private getHLSVODOptions () { + return { + liveSyncDurationCount: 5 + } + } +} diff --git a/client/src/assets/player/manager-options/manager-options-builder.ts b/client/src/assets/player/manager-options/manager-options-builder.ts new file mode 100644 index 000000000..14bdb5d96 --- /dev/null +++ b/client/src/assets/player/manager-options/manager-options-builder.ts @@ -0,0 +1,168 @@ +import videojs from 'video.js' +import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' +import { isDefaultLocale } from '@shared/core-utils/i18n' +import { copyToClipboard } from '../../../root-helpers/utils' +import { VideoJSPluginOptions } from '../peertube-videojs-typings' +import { buildVideoOrPlaylistEmbed, isIOS, isSafari } from '../utils' +import { ControlBarOptionsBuilder } from './control-bar-options-builder' +import { HLSOptionsBuilder } from './hls-options-builder' +import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model' +import { WebTorrentOptionsBuilder } from './webtorrent-options-builder' + +export class ManagerOptionsBuilder { + + constructor ( + private mode: PlayerMode, + private options: PeertubePlayerManagerOptions, + private p2pMediaLoaderModule?: any + ) { + + } + + getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions { + 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 + videoViewUrl: commonOptions.videoViewUrl, + videoDuration: commonOptions.videoDuration, + userWatching: commonOptions.userWatching, + subtitle: commonOptions.subtitle, + videoCaptions: commonOptions.videoCaptions, + stopTime: commonOptions.stopTime, + isLive: commonOptions.isLive, + videoUUID: commonOptions.videoUUID + } + } + + if (commonOptions.playlist) { + plugins.playlist = commonOptions.playlist + } + + if (this.mode === 'p2p-media-loader') { + const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule) + + Object.assign(plugins, hlsOptionsBuilder.getPluginOptions()) + } 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: any, 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 '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(commonOptions.embedUrl, 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/manager-options/manager-options.model.ts b/client/src/assets/player/manager-options/manager-options.model.ts new file mode 100644 index 000000000..0b0f8b435 --- /dev/null +++ b/client/src/assets/player/manager-options/manager-options.model.ts @@ -0,0 +1,84 @@ +import { PluginsManager } from '@root-helpers/plugins-manager' +import { LiveVideoLatencyMode, VideoFile } from '@shared/models' +import { PlaylistPluginOptions, UserWatching, VideoJSCaption } from '../peertube-videojs-typings' + +export type PlayerMode = 'webtorrent' | 'p2p-media-loader' + +export type WebtorrentOptions = { + videoFiles: VideoFile[] +} + +export type P2PMediaLoaderOptions = { + playlistUrl: string + segmentsSha256Url: string + trackerAnnounce: string[] + redundancyBaseUrls: string[] + videoFiles: VideoFile[] +} + +export interface CustomizationOptions { + startTime: number | string + stopTime: number | string + + controls?: boolean + muted?: boolean + loop?: boolean + subtitle?: string + resume?: string + + peertubeLink: boolean +} + +export interface CommonOptions extends CustomizationOptions { + playerElement: HTMLVideoElement + onPlayerElementChange: (element: HTMLVideoElement) => void + + autoplay: boolean + p2pEnabled: boolean + + nextVideo?: () => void + hasNextVideo?: () => boolean + + previousVideo?: () => void + hasPreviousVideo?: () => boolean + + playlist?: PlaylistPluginOptions + + videoDuration: number + enableHotkeys: boolean + inactivityTimeout: number + poster: string + + theaterButton: boolean + captions: boolean + + videoViewUrl: string + embedUrl: string + embedTitle: string + + isLive: boolean + liveOptions?: { + latencyMode: LiveVideoLatencyMode + } + + language?: string + + videoCaptions: VideoJSCaption[] + + videoUUID: string + videoShortUUID: string + + userWatching?: UserWatching + + serverUrl: string + + errorNotifier: (message: string) => void +} + +export type PeertubePlayerManagerOptions = { + common: CommonOptions + webtorrent: WebtorrentOptions + p2pMediaLoader?: P2PMediaLoaderOptions + + pluginsManager: PluginsManager +} diff --git a/client/src/assets/player/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/manager-options/webtorrent-options-builder.ts new file mode 100644 index 000000000..303940b29 --- /dev/null +++ b/client/src/assets/player/manager-options/webtorrent-options-builder.ts @@ -0,0 +1,36 @@ +import { PeertubePlayerManagerOptions } from './manager-options.model' + +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 = { + autoplay, + + playerRefusedP2P: commonOptions.p2pEnabled === false, + videoDuration: commonOptions.videoDuration, + playerElement: commonOptions.playerElement, + + 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/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts index ae31bcfe1..ccee2d90f 100644 --- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts @@ -24,7 +24,7 @@ const registerSourceHandler = function (vjs: typeof videojs) { const html5 = vjs.getTech('Html5') if (!html5) { - console.error('Not supported version if video.js') + console.error('No Hml5 tech found in videojs') return } diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index f8e5e2d6b..1d7a39b4e 100644 --- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts @@ -111,9 +111,7 @@ class P2pMediaLoaderPlugin extends Plugin { private initializePlugin () { initHlsJsPlayer(this.hlsjs) - // FIXME: typings - const options = (this.player.tech(true).options_ as any) - this.p2pEngine = options.hlsjsConfig.loader.getEngine() + this.p2pEngine = this.options.loader.getEngine() this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { console.error('Segment error.', segment, err) diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 81ddb8814..ddb521a52 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -10,22 +10,23 @@ import './control-bar/next-previous-video-button' import './control-bar/p2p-info-button' import './control-bar/peertube-link-button' import './control-bar/peertube-load-progress-bar' -import './control-bar/resolution-menu-button' -import './control-bar/resolution-menu-item' -import './control-bar/settings-dialog' -import './control-bar/settings-menu-button' -import './control-bar/settings-menu-item' -import './control-bar/settings-panel' -import './control-bar/settings-panel-child' import './control-bar/theater-button' +import './settings/resolution-menu-button' +import './settings/resolution-menu-item' +import './settings/settings-dialog' +import './settings/settings-menu-button' +import './settings/settings-menu-item' +import './settings/settings-panel' +import './settings/settings-panel-child' import './playlist/playlist-plugin' import './mobile/peertube-mobile-plugin' import './mobile/peertube-mobile-buttons' import './hotkeys/peertube-hotkeys-plugin' import videojs from 'video.js' import { PluginsManager } from '@root-helpers/plugins-manager' +import { ManagerOptionsBuilder } from './manager-options/manager-options-builder' +import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options/manager-options.model' import { saveAverageBandwidth } from './peertube-player-local-storage' -import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder' import { PlayerNetworkInfo } from './peertube-videojs-typings' import { TranslationsManager } from './translations-manager' import { isMobile } from './utils' @@ -75,7 +76,7 @@ export class PeertubePlayerManager { } private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise { - const videojsOptionsBuilder = new PeertubePlayerOptionsBuilder(mode, options, this.p2pMediaLoaderModule) + const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule) const videojsOptions = await this.pluginsManager.runHook( 'filter:internal.player.videojs.options.result', @@ -198,7 +199,7 @@ export class PeertubePlayerManager { return newVideoElement } - private static addContextMenu (optionsBuilder: PeertubePlayerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) { + private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) { const options = optionsBuilder.getContextMenuOptions(player, commonOptions) player.contextmenuUI(options) diff --git a/client/src/assets/player/peertube-player-options-builder.ts b/client/src/assets/player/peertube-player-options-builder.ts deleted file mode 100644 index c9cbbbf4d..000000000 --- a/client/src/assets/player/peertube-player-options-builder.ts +++ /dev/null @@ -1,577 +0,0 @@ -import videojs from 'video.js' -import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' -import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' -import { PluginsManager } from '@root-helpers/plugins-manager' -import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' -import { isDefaultLocale } from '@shared/core-utils/i18n' -import { LiveVideoLatencyMode, VideoFile } from '@shared/models' -import { copyToClipboard } from '../../root-helpers/utils' -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 { getAverageBandwidthInStore } from './peertube-player-local-storage' -import { - NextPreviousVideoButtonOptions, - P2PMediaLoaderPluginOptions, - PeerTubeLinkButtonOptions, - PlaylistPluginOptions, - UserWatching, - VideoJSCaption, - VideoJSPluginOptions -} from './peertube-videojs-typings' -import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' - -export type PlayerMode = 'webtorrent' | 'p2p-media-loader' - -export type WebtorrentOptions = { - videoFiles: VideoFile[] -} - -export type P2PMediaLoaderOptions = { - playlistUrl: string - segmentsSha256Url: string - trackerAnnounce: string[] - redundancyBaseUrls: string[] - videoFiles: VideoFile[] -} - -export interface CustomizationOptions { - startTime: number | string - stopTime: number | string - - controls?: boolean - muted?: boolean - loop?: boolean - subtitle?: string - resume?: string - - peertubeLink: boolean -} - -export interface CommonOptions extends CustomizationOptions { - playerElement: HTMLVideoElement - onPlayerElementChange: (element: HTMLVideoElement) => void - - autoplay: boolean - p2pEnabled: boolean - - nextVideo?: () => void - hasNextVideo?: () => boolean - - previousVideo?: () => void - hasPreviousVideo?: () => boolean - - playlist?: PlaylistPluginOptions - - videoDuration: number - enableHotkeys: boolean - inactivityTimeout: number - poster: string - - theaterButton: boolean - captions: boolean - - videoViewUrl: string - embedUrl: string - embedTitle: string - - isLive: boolean - liveOptions?: { - latencyMode: LiveVideoLatencyMode - } - - language?: string - - videoCaptions: VideoJSCaption[] - - videoUUID: string - videoShortUUID: string - - userWatching?: UserWatching - - serverUrl: string - - errorNotifier: (message: string) => void -} - -export type PeertubePlayerManagerOptions = { - common: CommonOptions - webtorrent: WebtorrentOptions - p2pMediaLoader?: P2PMediaLoaderOptions - - pluginsManager: PluginsManager -} - -export class PeertubePlayerOptionsBuilder { - - constructor ( - private mode: PlayerMode, - private options: PeertubePlayerManagerOptions, - private p2pMediaLoaderModule?: any - ) { - - } - - getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions { - const commonOptions = this.options.common - const isHLS = this.mode === 'p2p-media-loader' - - 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 - videoViewUrl: commonOptions.videoViewUrl, - videoDuration: commonOptions.videoDuration, - userWatching: commonOptions.userWatching, - subtitle: commonOptions.subtitle, - videoCaptions: commonOptions.videoCaptions, - stopTime: commonOptions.stopTime, - isLive: commonOptions.isLive, - videoUUID: commonOptions.videoUUID - } - } - - if (commonOptions.playlist) { - plugins.playlist = commonOptions.playlist - } - - if (isHLS) { - const { hlsjs } = this.addP2PMediaLoaderOptions(plugins) - - Object.assign(html5, hlsjs.html5) - } - - if (this.mode === 'webtorrent') { - this.addWebTorrentOptions(plugins, alreadyPlayed) - - // WebTorrent plugin handles autoplay, because we do some hackish stuff in there - autoplay = false - } - - 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: this.getControlBarChildren(this.mode, { - videoShortUUID: commonOptions.videoShortUUID, - p2pEnabled: commonOptions.p2pEnabled, - - captions: commonOptions.captions, - peertubeLink: commonOptions.peertubeLink, - theaterButton: commonOptions.theaterButton, - - nextVideo: commonOptions.nextVideo, - hasNextVideo: commonOptions.hasNextVideo, - - previousVideo: commonOptions.previousVideo, - hasPreviousVideo: commonOptions.hasPreviousVideo - }) as any // FIXME: typings - } - } - - if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { - Object.assign(videojsOptions, { language: commonOptions.language }) - } - - return videojsOptions - } - - private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) { - const p2pMediaLoaderOptions = this.options.p2pMediaLoader - const commonOptions = this.options.common - - const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls) - - const p2pMediaLoader: P2PMediaLoaderPluginOptions = { - redundancyUrlManager, - type: 'application/x-mpegURL', - startTime: commonOptions.startTime, - src: p2pMediaLoaderOptions.playlistUrl - } - - const p2pMediaLoaderConfig: HlsJsEngineSettings = { - loader: this.getP2PMediaLoaderOptions(redundancyUrlManager), - segments: { - swarmId: p2pMediaLoaderOptions.playlistUrl - } - } - - const hlsjs = { - levelLabelHandler: (level: { height: number, width: number }) => { - const resolution = Math.min(level.height || 0, level.width || 0) - - const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution) - // We don't have files for live videos - if (!file) return level.height - - let label = file.resolution.label - if (file.fps >= 50) label += file.fps - - return label - }, - html5: { - hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig) - } - } - - const toAssign = { p2pMediaLoader, hlsjs } - Object.assign(plugins, toAssign) - - return toAssign - } - - private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): Partial { - let consumeOnly = false - if ((navigator as any)?.connection?.type === 'cellular') { - console.log('We are on a cellular connection: disabling seeding.') - consumeOnly = true - } - - const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce - .filter(t => t.startsWith('ws')) - - const specificLiveOrVODOptions = this.options.common.isLive - ? this.getP2PMediaLoaderLiveOptions() - : this.getP2PMediaLoaderVODOptions() - - return { - trackerAnnounce, - rtcConfig: getRtcConfig(), - - simultaneousHttpDownloads: 1, - httpFailedSegmentTimeout: 1000, - - segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), - segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1), - - useP2P: this.options.common.p2pEnabled, - consumeOnly, - - ...specificLiveOrVODOptions - } - } - - private getP2PMediaLoaderLiveOptions (): Partial { - const base = { - requiredSegmentsPriority: 1 - } - - const latencyMode = this.options.common.liveOptions.latencyMode - - switch (latencyMode) { - case LiveVideoLatencyMode.SMALL_LATENCY: - return { - ...base, - - useP2P: false, - httpDownloadProbability: 1 - } - - case LiveVideoLatencyMode.HIGH_LATENCY: - return base - - default: - return base - } - } - - private getP2PMediaLoaderVODOptions (): Partial { - return { - requiredSegmentsPriority: 3, - - cachedSegmentExpiration: 86400000, - cachedSegmentsCount: 100, - - httpDownloadMaxPriority: 9, - httpDownloadProbability: 0.06, - httpDownloadProbabilitySkipIfNoPeers: true, - - p2pDownloadMaxPriority: 50 - } - } - - private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) { - const specificLiveOrVODOptions = this.options.common.isLive - ? this.getHLSLiveOptions() - : this.getHLSVODOptions() - - const base = { - capLevelToPlayerSize: true, - autoStartLoad: false, - - loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(), - - ...specificLiveOrVODOptions - } - - const averageBandwidth = getAverageBandwidthInStore() - if (!averageBandwidth) return base - - return { - ...base, - - abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s - startLevel: -1, - testBandwidth: false, - debug: false - } - } - - private getHLSLiveOptions () { - const latencyMode = this.options.common.liveOptions.latencyMode - - switch (latencyMode) { - case LiveVideoLatencyMode.SMALL_LATENCY: - return { - liveSyncDurationCount: 2 - } - - case LiveVideoLatencyMode.HIGH_LATENCY: - return { - liveSyncDurationCount: 10 - } - - default: - return { - liveSyncDurationCount: 5 - } - } - } - - private getHLSVODOptions () { - return { - liveSyncDurationCount: 5 - } - } - - private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) { - const commonOptions = this.options.common - const webtorrentOptions = this.options.webtorrent - const p2pMediaLoaderOptions = this.options.p2pMediaLoader - - const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play' - - const webtorrent = { - autoplay, - - playerRefusedP2P: commonOptions.p2pEnabled === false, - videoDuration: commonOptions.videoDuration, - playerElement: commonOptions.playerElement, - - 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 - } - - Object.assign(plugins, { webtorrent }) - } - - private getControlBarChildren (mode: PlayerMode, options: { - p2pEnabled: boolean - videoShortUUID: string - - peertubeLink: boolean - theaterButton: boolean - captions: boolean - - nextVideo?: () => void - hasNextVideo?: () => boolean - - previousVideo?: () => void - hasPreviousVideo?: () => boolean - }) { - const settingEntries = [] - const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' - - // Keep an order - settingEntries.push('playbackRateMenuButton') - if (options.captions === true) settingEntries.push('captionsButton') - settingEntries.push('resolutionMenuButton') - - const children = {} - - if (options.previousVideo) { - const buttonOptions: NextPreviousVideoButtonOptions = { - type: 'previous', - handler: options.previousVideo, - isDisabled: () => { - if (!options.hasPreviousVideo) return false - - return !options.hasPreviousVideo() - } - } - - Object.assign(children, { - previousVideoButton: buttonOptions - }) - } - - Object.assign(children, { playToggle: {} }) - - if (options.nextVideo) { - const buttonOptions: NextPreviousVideoButtonOptions = { - type: 'next', - handler: options.nextVideo, - isDisabled: () => { - if (!options.hasNextVideo) return false - - return !options.hasNextVideo() - } - } - - Object.assign(children, { - nextVideoButton: buttonOptions - }) - } - - Object.assign(children, { - currentTimeDisplay: {}, - timeDivider: {}, - durationDisplay: {}, - liveDisplay: {}, - - flexibleWidthSpacer: {}, - progressControl: { - children: { - seekBar: { - children: { - [loadProgressBar]: {}, - mouseTimeDisplay: {}, - playProgressBar: {} - } - } - } - }, - - p2PInfoButton: { - p2pEnabled: options.p2pEnabled - }, - - muteToggle: {}, - volumeControl: {}, - - settingsButton: { - setup: { - maxHeightOffset: 40 - }, - entries: settingEntries - } - }) - - if (options.peertubeLink === true) { - Object.assign(children, { - peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions - }) - } - - if (options.theaterButton === true) { - Object.assign(children, { - theaterButton: {} - }) - } - - Object.assign(children, { - fullscreenToggle: {} - }) - - return children - } - - private getAutoPlayValue (autoplay: any, 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 '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(commonOptions.embedUrl, 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/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 09996f75d..fcaa8a9c3 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -1,11 +1,12 @@ 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 './dock/peertube-dock-plugin' +import { PlayerMode } from './manager-options/manager-options.model' import { Html5Hlsjs } from './p2p-media-loader/hls-plugin' import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' -import { PlayerMode } from './peertube-player-options-builder' import { PeerTubePlugin } from './peertube-plugin' import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' import { PlaylistPlugin } from './playlist/playlist-plugin' @@ -154,6 +155,12 @@ type P2PMediaLoaderPluginOptions = { src: string startTime: number | string + + loader: P2PMediaLoader +} + +export type P2PMediaLoader = { + getEngine(): Engine } type VideoJSPluginOptions = {