From 9597920ee3d4ac99803e7107983ddf98a9dfb3c4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Mar 2022 11:16:54 +0100 Subject: Reorganize player manager options builder --- .../manager-options/control-bar-options-builder.ts | 132 ++++++++++++++ .../player/manager-options/hls-options-builder.ts | 192 +++++++++++++++++++++ .../manager-options/manager-options-builder.ts | 168 ++++++++++++++++++ .../manager-options/manager-options.model.ts | 84 +++++++++ .../manager-options/webtorrent-options-builder.ts | 36 ++++ 5 files changed, 612 insertions(+) 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 (limited to 'client/src/assets/player/manager-options') 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 } + } +} -- cgit v1.2.3