aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-14 11:16:54 +0100
committerChocobozzz <me@florianbigard.com>2022-03-14 11:36:38 +0100
commit9597920ee3d4ac99803e7107983ddf98a9dfb3c4 (patch)
tree62c98909b4eb30ece3c0f58c26c8555e6bef1e31
parent9af2accee68082e4e1160a4e4a7036451262be02 (diff)
downloadPeerTube-9597920ee3d4ac99803e7107983ddf98a9dfb3c4.tar.gz
PeerTube-9597920ee3d4ac99803e7107983ddf98a9dfb3c4.tar.zst
PeerTube-9597920ee3d4ac99803e7107983ddf98a9dfb3c4.zip
Reorganize player manager options builder
-rw-r--r--client/src/assets/player/index.ts2
-rw-r--r--client/src/assets/player/manager-options/control-bar-options-builder.ts132
-rw-r--r--client/src/assets/player/manager-options/hls-options-builder.ts192
-rw-r--r--client/src/assets/player/manager-options/manager-options-builder.ts168
-rw-r--r--client/src/assets/player/manager-options/manager-options.model.ts84
-rw-r--r--client/src/assets/player/manager-options/webtorrent-options-builder.ts36
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts2
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts4
-rw-r--r--client/src/assets/player/peertube-player-manager.ts21
-rw-r--r--client/src/assets/player/peertube-player-options-builder.ts577
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts9
11 files changed, 634 insertions, 593 deletions
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 @@
1export * from './peertube-player-manager' 1export * from './peertube-player-manager'
2export * from './peertube-player-options-builder' 2export * 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 @@
1import { NextPreviousVideoButtonOptions, PeerTubeLinkButtonOptions } from '../peertube-videojs-typings'
2import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model'
3
4export class ControlBarOptionsBuilder {
5 private options: CommonOptions
6
7 constructor (
8 globalOptions: PeertubePlayerManagerOptions,
9 private mode: PlayerMode
10 ) {
11 this.options = globalOptions.common
12 }
13
14 getChildrenOptions () {
15 const children = {}
16
17 if (this.options.previousVideo) {
18 Object.assign(children, this.getPreviousVideo())
19 }
20
21 Object.assign(children, { playToggle: {} })
22
23 if (this.options.nextVideo) {
24 Object.assign(children, this.getNextVideo())
25 }
26
27 Object.assign(children, {
28 currentTimeDisplay: {},
29 timeDivider: {},
30 durationDisplay: {},
31 liveDisplay: {},
32
33 flexibleWidthSpacer: {},
34
35 ...this.getProgressControl(),
36
37 p2PInfoButton: {
38 p2pEnabled: this.options.p2pEnabled
39 },
40
41 muteToggle: {},
42 volumeControl: {},
43
44 settingsButton: this.getSettingsButton()
45 })
46
47 if (this.options.peertubeLink === true) {
48 Object.assign(children, {
49 peerTubeLinkButton: { shortUUID: this.options.videoShortUUID } as PeerTubeLinkButtonOptions
50 })
51 }
52
53 if (this.options.theaterButton === true) {
54 Object.assign(children, {
55 theaterButton: {}
56 })
57 }
58
59 Object.assign(children, {
60 fullscreenToggle: {}
61 })
62
63 return children
64 }
65
66 private getSettingsButton () {
67 const settingEntries: string[] = []
68
69 settingEntries.push('playbackRateMenuButton')
70
71 if (this.options.captions === true) settingEntries.push('captionsButton')
72
73 settingEntries.push('resolutionMenuButton')
74
75 return {
76 settingsButton: {
77 setup: {
78 maxHeightOffset: 40
79 },
80 entries: settingEntries
81 }
82 }
83 }
84
85 private getProgressControl () {
86 const loadProgressBar = this.mode === 'webtorrent'
87 ? 'peerTubeLoadProgressBar'
88 : 'loadProgressBar'
89
90 return {
91 progressControl: {
92 children: {
93 seekBar: {
94 children: {
95 [loadProgressBar]: {},
96 mouseTimeDisplay: {},
97 playProgressBar: {}
98 }
99 }
100 }
101 }
102 }
103 }
104
105 private getPreviousVideo () {
106 const buttonOptions: NextPreviousVideoButtonOptions = {
107 type: 'previous',
108 handler: this.options.previousVideo,
109 isDisabled: () => {
110 if (!this.options.hasPreviousVideo) return false
111
112 return !this.options.hasPreviousVideo()
113 }
114 }
115
116 return { previousVideoButton: buttonOptions }
117 }
118
119 private getNextVideo () {
120 const buttonOptions: NextPreviousVideoButtonOptions = {
121 type: 'next',
122 handler: this.options.nextVideo,
123 isDisabled: () => {
124 if (!this.options.hasNextVideo) return false
125
126 return !this.options.hasNextVideo()
127 }
128 }
129
130 return { nextVideoButton: buttonOptions }
131 }
132}
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 @@
1import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
2import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
3import { LiveVideoLatencyMode } from '@shared/models'
4import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
5import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
6import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator'
7import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
8import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../peertube-videojs-typings'
9import { getRtcConfig } from '../utils'
10import { PeertubePlayerManagerOptions } from './manager-options.model'
11
12export class HLSOptionsBuilder {
13
14 constructor (
15 private options: PeertubePlayerManagerOptions,
16 private p2pMediaLoaderModule?: any
17 ) {
18
19 }
20
21 getPluginOptions () {
22 const commonOptions = this.options.common
23
24 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
25
26 const p2pMediaLoaderConfig = this.getP2PMediaLoaderOptions(redundancyUrlManager)
27 const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader
28
29 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
30 redundancyUrlManager,
31 type: 'application/x-mpegURL',
32 startTime: commonOptions.startTime,
33 src: this.options.p2pMediaLoader.playlistUrl,
34 loader
35 }
36
37 const hlsjs = {
38 levelLabelHandler: (level: { height: number, width: number }) => {
39 const resolution = Math.min(level.height || 0, level.width || 0)
40
41 const file = this.options.p2pMediaLoader.videoFiles.find(f => f.resolution.id === resolution)
42 // We don't have files for live videos
43 if (!file) return level.height
44
45 let label = file.resolution.label
46 if (file.fps >= 50) label += file.fps
47
48 return label
49 },
50 html5: {
51 hlsjsConfig: this.getHLSJSOptions(loader)
52 }
53 }
54
55 return { p2pMediaLoader, hlsjs }
56 }
57
58 // ---------------------------------------------------------------------------
59
60 private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): HlsJsEngineSettings {
61 let consumeOnly = false
62 if ((navigator as any)?.connection?.type === 'cellular') {
63 console.log('We are on a cellular connection: disabling seeding.')
64 consumeOnly = true
65 }
66
67 const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce
68 .filter(t => t.startsWith('ws'))
69
70 const specificLiveOrVODOptions = this.options.common.isLive
71 ? this.getP2PMediaLoaderLiveOptions()
72 : this.getP2PMediaLoaderVODOptions()
73
74 return {
75 loader: {
76
77 trackerAnnounce,
78 rtcConfig: getRtcConfig(),
79
80 simultaneousHttpDownloads: 1,
81 httpFailedSegmentTimeout: 1000,
82
83 segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
84 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
85
86 useP2P: this.options.common.p2pEnabled,
87 consumeOnly,
88
89 ...specificLiveOrVODOptions
90 },
91 segments: {
92 swarmId: this.options.p2pMediaLoader.playlistUrl,
93 forwardSegmentCount: specificLiveOrVODOptions.p2pDownloadMaxPriority
94 }
95 }
96 }
97
98 private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
99 const base = {
100 requiredSegmentsPriority: 1
101 }
102
103 const latencyMode = this.options.common.liveOptions.latencyMode
104
105 switch (latencyMode) {
106 case LiveVideoLatencyMode.SMALL_LATENCY:
107 return {
108 ...base,
109
110 useP2P: false,
111 httpDownloadProbability: 1
112 }
113
114 case LiveVideoLatencyMode.HIGH_LATENCY:
115 return base
116
117 default:
118 return base
119 }
120 }
121
122 private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
123 return {
124 requiredSegmentsPriority: 3,
125
126 cachedSegmentExpiration: 86400000,
127 cachedSegmentsCount: 100,
128
129 httpDownloadMaxPriority: 9,
130 httpDownloadProbability: 0.06,
131 httpDownloadProbabilitySkipIfNoPeers: true,
132
133 p2pDownloadMaxPriority: 50
134 }
135 }
136
137 // ---------------------------------------------------------------------------
138
139 private getHLSJSOptions (loader: P2PMediaLoader) {
140 const specificLiveOrVODOptions = this.options.common.isLive
141 ? this.getHLSLiveOptions()
142 : this.getHLSVODOptions()
143
144 const base = {
145 capLevelToPlayerSize: true,
146 autoStartLoad: false,
147
148 loader,
149
150 ...specificLiveOrVODOptions
151 }
152
153 const averageBandwidth = getAverageBandwidthInStore()
154 if (!averageBandwidth) return base
155
156 return {
157 ...base,
158
159 abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
160 startLevel: -1,
161 testBandwidth: false,
162 debug: false
163 }
164 }
165
166 private getHLSLiveOptions () {
167 const latencyMode = this.options.common.liveOptions.latencyMode
168
169 switch (latencyMode) {
170 case LiveVideoLatencyMode.SMALL_LATENCY:
171 return {
172 liveSyncDurationCount: 2
173 }
174
175 case LiveVideoLatencyMode.HIGH_LATENCY:
176 return {
177 liveSyncDurationCount: 10
178 }
179
180 default:
181 return {
182 liveSyncDurationCount: 5
183 }
184 }
185 }
186
187 private getHLSVODOptions () {
188 return {
189 liveSyncDurationCount: 5
190 }
191 }
192}
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 @@
1import videojs from 'video.js'
2import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
3import { isDefaultLocale } from '@shared/core-utils/i18n'
4import { copyToClipboard } from '../../../root-helpers/utils'
5import { VideoJSPluginOptions } from '../peertube-videojs-typings'
6import { buildVideoOrPlaylistEmbed, isIOS, isSafari } from '../utils'
7import { ControlBarOptionsBuilder } from './control-bar-options-builder'
8import { HLSOptionsBuilder } from './hls-options-builder'
9import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model'
10import { WebTorrentOptionsBuilder } from './webtorrent-options-builder'
11
12export class ManagerOptionsBuilder {
13
14 constructor (
15 private mode: PlayerMode,
16 private options: PeertubePlayerManagerOptions,
17 private p2pMediaLoaderModule?: any
18 ) {
19
20 }
21
22 getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
23 const commonOptions = this.options.common
24
25 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
26 const html5 = {
27 preloadTextTracks: false
28 }
29
30 const plugins: VideoJSPluginOptions = {
31 peertube: {
32 mode: this.mode,
33 autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
34 videoViewUrl: commonOptions.videoViewUrl,
35 videoDuration: commonOptions.videoDuration,
36 userWatching: commonOptions.userWatching,
37 subtitle: commonOptions.subtitle,
38 videoCaptions: commonOptions.videoCaptions,
39 stopTime: commonOptions.stopTime,
40 isLive: commonOptions.isLive,
41 videoUUID: commonOptions.videoUUID
42 }
43 }
44
45 if (commonOptions.playlist) {
46 plugins.playlist = commonOptions.playlist
47 }
48
49 if (this.mode === 'p2p-media-loader') {
50 const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule)
51
52 Object.assign(plugins, hlsOptionsBuilder.getPluginOptions())
53 } else if (this.mode === 'webtorrent') {
54 const webtorrentOptionsBuilder = new WebTorrentOptionsBuilder(this.options, this.getAutoPlayValue(autoplay, alreadyPlayed))
55
56 Object.assign(plugins, webtorrentOptionsBuilder.getPluginOptions())
57
58 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
59 autoplay = false
60 }
61
62 const controlBarOptionsBuilder = new ControlBarOptionsBuilder(this.options, this.mode)
63
64 const videojsOptions = {
65 html5,
66
67 // We don't use text track settings for now
68 textTrackSettings: false as any, // FIXME: typings
69 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
70 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
71
72 muted: commonOptions.muted !== undefined
73 ? commonOptions.muted
74 : undefined, // Undefined so the player knows it has to check the local storage
75
76 autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
77
78 poster: commonOptions.poster,
79 inactivityTimeout: commonOptions.inactivityTimeout,
80 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
81
82 plugins,
83
84 controlBar: {
85 children: controlBarOptionsBuilder.getChildrenOptions() as any // FIXME: typings
86 }
87 }
88
89 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
90 Object.assign(videojsOptions, { language: commonOptions.language })
91 }
92
93 return videojsOptions
94 }
95
96 private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
97 if (autoplay !== true) return autoplay
98
99 // On first play, disable autoplay to avoid issues
100 // But if the player already played videos, we can safely autoplay next ones
101 if (isIOS() || isSafari()) {
102 return alreadyPlayed ? 'play' : false
103 }
104
105 return 'play'
106 }
107
108 getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
109 const content = () => {
110 const isLoopEnabled = player.options_['loop']
111
112 const items = [
113 {
114 icon: 'repeat',
115 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
116 listener: function () {
117 player.options_['loop'] = !isLoopEnabled
118 }
119 },
120 {
121 label: player.localize('Copy the video URL'),
122 listener: function () {
123 copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
124 }
125 },
126 {
127 label: player.localize('Copy the video URL at the current time'),
128 listener: function (this: videojs.Player) {
129 const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
130
131 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
132 }
133 },
134 {
135 icon: 'code',
136 label: player.localize('Copy embed code'),
137 listener: () => {
138 copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
139 }
140 }
141 ]
142
143 if (this.mode === 'webtorrent') {
144 items.push({
145 label: player.localize('Copy magnet URI'),
146 listener: function (this: videojs.Player) {
147 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
148 }
149 })
150 }
151
152 items.push({
153 icon: 'info',
154 label: player.localize('Stats for nerds'),
155 listener: () => {
156 player.stats().show()
157 }
158 })
159
160 return items.map(i => ({
161 ...i,
162 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
163 }))
164 }
165
166 return { content }
167 }
168}
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 @@
1import { PluginsManager } from '@root-helpers/plugins-manager'
2import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
3import { PlaylistPluginOptions, UserWatching, VideoJSCaption } from '../peertube-videojs-typings'
4
5export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
6
7export type WebtorrentOptions = {
8 videoFiles: VideoFile[]
9}
10
11export type P2PMediaLoaderOptions = {
12 playlistUrl: string
13 segmentsSha256Url: string
14 trackerAnnounce: string[]
15 redundancyBaseUrls: string[]
16 videoFiles: VideoFile[]
17}
18
19export interface CustomizationOptions {
20 startTime: number | string
21 stopTime: number | string
22
23 controls?: boolean
24 muted?: boolean
25 loop?: boolean
26 subtitle?: string
27 resume?: string
28
29 peertubeLink: boolean
30}
31
32export interface CommonOptions extends CustomizationOptions {
33 playerElement: HTMLVideoElement
34 onPlayerElementChange: (element: HTMLVideoElement) => void
35
36 autoplay: boolean
37 p2pEnabled: boolean
38
39 nextVideo?: () => void
40 hasNextVideo?: () => boolean
41
42 previousVideo?: () => void
43 hasPreviousVideo?: () => boolean
44
45 playlist?: PlaylistPluginOptions
46
47 videoDuration: number
48 enableHotkeys: boolean
49 inactivityTimeout: number
50 poster: string
51
52 theaterButton: boolean
53 captions: boolean
54
55 videoViewUrl: string
56 embedUrl: string
57 embedTitle: string
58
59 isLive: boolean
60 liveOptions?: {
61 latencyMode: LiveVideoLatencyMode
62 }
63
64 language?: string
65
66 videoCaptions: VideoJSCaption[]
67
68 videoUUID: string
69 videoShortUUID: string
70
71 userWatching?: UserWatching
72
73 serverUrl: string
74
75 errorNotifier: (message: string) => void
76}
77
78export type PeertubePlayerManagerOptions = {
79 common: CommonOptions
80 webtorrent: WebtorrentOptions
81 p2pMediaLoader?: P2PMediaLoaderOptions
82
83 pluginsManager: PluginsManager
84}
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 @@
1import { PeertubePlayerManagerOptions } from './manager-options.model'
2
3export class WebTorrentOptionsBuilder {
4
5 constructor (
6 private options: PeertubePlayerManagerOptions,
7 private autoPlayValue: any
8 ) {
9
10 }
11
12 getPluginOptions () {
13 const commonOptions = this.options.common
14 const webtorrentOptions = this.options.webtorrent
15 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
16
17 const autoplay = this.autoPlayValue === 'play'
18
19 const webtorrent = {
20 autoplay,
21
22 playerRefusedP2P: commonOptions.p2pEnabled === false,
23 videoDuration: commonOptions.videoDuration,
24 playerElement: commonOptions.playerElement,
25
26 videoFiles: webtorrentOptions.videoFiles.length !== 0
27 ? webtorrentOptions.videoFiles
28 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
29 : p2pMediaLoaderOptions?.videoFiles || [],
30
31 startTime: commonOptions.startTime
32 }
33
34 return { webtorrent }
35 }
36}
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) {
24 const html5 = vjs.getTech('Html5') 24 const html5 = vjs.getTech('Html5')
25 25
26 if (!html5) { 26 if (!html5) {
27 console.error('Not supported version if video.js') 27 console.error('No Hml5 tech found in videojs')
28 return 28 return
29 } 29 }
30 30
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 {
111 private initializePlugin () { 111 private initializePlugin () {
112 initHlsJsPlayer(this.hlsjs) 112 initHlsJsPlayer(this.hlsjs)
113 113
114 // FIXME: typings 114 this.p2pEngine = this.options.loader.getEngine()
115 const options = (this.player.tech(true).options_ as any)
116 this.p2pEngine = options.hlsjsConfig.loader.getEngine()
117 115
118 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { 116 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
119 console.error('Segment error.', segment, err) 117 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'
10import './control-bar/p2p-info-button' 10import './control-bar/p2p-info-button'
11import './control-bar/peertube-link-button' 11import './control-bar/peertube-link-button'
12import './control-bar/peertube-load-progress-bar' 12import './control-bar/peertube-load-progress-bar'
13import './control-bar/resolution-menu-button'
14import './control-bar/resolution-menu-item'
15import './control-bar/settings-dialog'
16import './control-bar/settings-menu-button'
17import './control-bar/settings-menu-item'
18import './control-bar/settings-panel'
19import './control-bar/settings-panel-child'
20import './control-bar/theater-button' 13import './control-bar/theater-button'
14import './settings/resolution-menu-button'
15import './settings/resolution-menu-item'
16import './settings/settings-dialog'
17import './settings/settings-menu-button'
18import './settings/settings-menu-item'
19import './settings/settings-panel'
20import './settings/settings-panel-child'
21import './playlist/playlist-plugin' 21import './playlist/playlist-plugin'
22import './mobile/peertube-mobile-plugin' 22import './mobile/peertube-mobile-plugin'
23import './mobile/peertube-mobile-buttons' 23import './mobile/peertube-mobile-buttons'
24import './hotkeys/peertube-hotkeys-plugin' 24import './hotkeys/peertube-hotkeys-plugin'
25import videojs from 'video.js' 25import videojs from 'video.js'
26import { PluginsManager } from '@root-helpers/plugins-manager' 26import { PluginsManager } from '@root-helpers/plugins-manager'
27import { ManagerOptionsBuilder } from './manager-options/manager-options-builder'
28import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options/manager-options.model'
27import { saveAverageBandwidth } from './peertube-player-local-storage' 29import { saveAverageBandwidth } from './peertube-player-local-storage'
28import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder'
29import { PlayerNetworkInfo } from './peertube-videojs-typings' 30import { PlayerNetworkInfo } from './peertube-videojs-typings'
30import { TranslationsManager } from './translations-manager' 31import { TranslationsManager } from './translations-manager'
31import { isMobile } from './utils' 32import { isMobile } from './utils'
@@ -75,7 +76,7 @@ export class PeertubePlayerManager {
75 } 76 }
76 77
77 private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> { 78 private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> {
78 const videojsOptionsBuilder = new PeertubePlayerOptionsBuilder(mode, options, this.p2pMediaLoaderModule) 79 const videojsOptionsBuilder = new ManagerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
79 80
80 const videojsOptions = await this.pluginsManager.runHook( 81 const videojsOptions = await this.pluginsManager.runHook(
81 'filter:internal.player.videojs.options.result', 82 'filter:internal.player.videojs.options.result',
@@ -198,7 +199,7 @@ export class PeertubePlayerManager {
198 return newVideoElement 199 return newVideoElement
199 } 200 }
200 201
201 private static addContextMenu (optionsBuilder: PeertubePlayerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) { 202 private static addContextMenu (optionsBuilder: ManagerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
202 const options = optionsBuilder.getContextMenuOptions(player, commonOptions) 203 const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
203 204
204 player.contextmenuUI(options) 205 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 @@
1import videojs from 'video.js'
2import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core'
3import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
4import { PluginsManager } from '@root-helpers/plugins-manager'
5import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
6import { isDefaultLocale } from '@shared/core-utils/i18n'
7import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
8import { copyToClipboard } from '../../root-helpers/utils'
9import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
10import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
11import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
12import { getAverageBandwidthInStore } from './peertube-player-local-storage'
13import {
14 NextPreviousVideoButtonOptions,
15 P2PMediaLoaderPluginOptions,
16 PeerTubeLinkButtonOptions,
17 PlaylistPluginOptions,
18 UserWatching,
19 VideoJSCaption,
20 VideoJSPluginOptions
21} from './peertube-videojs-typings'
22import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
23
24export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
25
26export type WebtorrentOptions = {
27 videoFiles: VideoFile[]
28}
29
30export type P2PMediaLoaderOptions = {
31 playlistUrl: string
32 segmentsSha256Url: string
33 trackerAnnounce: string[]
34 redundancyBaseUrls: string[]
35 videoFiles: VideoFile[]
36}
37
38export interface CustomizationOptions {
39 startTime: number | string
40 stopTime: number | string
41
42 controls?: boolean
43 muted?: boolean
44 loop?: boolean
45 subtitle?: string
46 resume?: string
47
48 peertubeLink: boolean
49}
50
51export interface CommonOptions extends CustomizationOptions {
52 playerElement: HTMLVideoElement
53 onPlayerElementChange: (element: HTMLVideoElement) => void
54
55 autoplay: boolean
56 p2pEnabled: boolean
57
58 nextVideo?: () => void
59 hasNextVideo?: () => boolean
60
61 previousVideo?: () => void
62 hasPreviousVideo?: () => boolean
63
64 playlist?: PlaylistPluginOptions
65
66 videoDuration: number
67 enableHotkeys: boolean
68 inactivityTimeout: number
69 poster: string
70
71 theaterButton: boolean
72 captions: boolean
73
74 videoViewUrl: string
75 embedUrl: string
76 embedTitle: string
77
78 isLive: boolean
79 liveOptions?: {
80 latencyMode: LiveVideoLatencyMode
81 }
82
83 language?: string
84
85 videoCaptions: VideoJSCaption[]
86
87 videoUUID: string
88 videoShortUUID: string
89
90 userWatching?: UserWatching
91
92 serverUrl: string
93
94 errorNotifier: (message: string) => void
95}
96
97export type PeertubePlayerManagerOptions = {
98 common: CommonOptions
99 webtorrent: WebtorrentOptions
100 p2pMediaLoader?: P2PMediaLoaderOptions
101
102 pluginsManager: PluginsManager
103}
104
105export class PeertubePlayerOptionsBuilder {
106
107 constructor (
108 private mode: PlayerMode,
109 private options: PeertubePlayerManagerOptions,
110 private p2pMediaLoaderModule?: any
111 ) {
112
113 }
114
115 getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
116 const commonOptions = this.options.common
117 const isHLS = this.mode === 'p2p-media-loader'
118
119 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
120 const html5 = {
121 preloadTextTracks: false
122 }
123
124 const plugins: VideoJSPluginOptions = {
125 peertube: {
126 mode: this.mode,
127 autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
128 videoViewUrl: commonOptions.videoViewUrl,
129 videoDuration: commonOptions.videoDuration,
130 userWatching: commonOptions.userWatching,
131 subtitle: commonOptions.subtitle,
132 videoCaptions: commonOptions.videoCaptions,
133 stopTime: commonOptions.stopTime,
134 isLive: commonOptions.isLive,
135 videoUUID: commonOptions.videoUUID
136 }
137 }
138
139 if (commonOptions.playlist) {
140 plugins.playlist = commonOptions.playlist
141 }
142
143 if (isHLS) {
144 const { hlsjs } = this.addP2PMediaLoaderOptions(plugins)
145
146 Object.assign(html5, hlsjs.html5)
147 }
148
149 if (this.mode === 'webtorrent') {
150 this.addWebTorrentOptions(plugins, alreadyPlayed)
151
152 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
153 autoplay = false
154 }
155
156 const videojsOptions = {
157 html5,
158
159 // We don't use text track settings for now
160 textTrackSettings: false as any, // FIXME: typings
161 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
162 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
163
164 muted: commonOptions.muted !== undefined
165 ? commonOptions.muted
166 : undefined, // Undefined so the player knows it has to check the local storage
167
168 autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
169
170 poster: commonOptions.poster,
171 inactivityTimeout: commonOptions.inactivityTimeout,
172 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
173
174 plugins,
175
176 controlBar: {
177 children: this.getControlBarChildren(this.mode, {
178 videoShortUUID: commonOptions.videoShortUUID,
179 p2pEnabled: commonOptions.p2pEnabled,
180
181 captions: commonOptions.captions,
182 peertubeLink: commonOptions.peertubeLink,
183 theaterButton: commonOptions.theaterButton,
184
185 nextVideo: commonOptions.nextVideo,
186 hasNextVideo: commonOptions.hasNextVideo,
187
188 previousVideo: commonOptions.previousVideo,
189 hasPreviousVideo: commonOptions.hasPreviousVideo
190 }) as any // FIXME: typings
191 }
192 }
193
194 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
195 Object.assign(videojsOptions, { language: commonOptions.language })
196 }
197
198 return videojsOptions
199 }
200
201 private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) {
202 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
203 const commonOptions = this.options.common
204
205 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
206
207 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
208 redundancyUrlManager,
209 type: 'application/x-mpegURL',
210 startTime: commonOptions.startTime,
211 src: p2pMediaLoaderOptions.playlistUrl
212 }
213
214 const p2pMediaLoaderConfig: HlsJsEngineSettings = {
215 loader: this.getP2PMediaLoaderOptions(redundancyUrlManager),
216 segments: {
217 swarmId: p2pMediaLoaderOptions.playlistUrl
218 }
219 }
220
221 const hlsjs = {
222 levelLabelHandler: (level: { height: number, width: number }) => {
223 const resolution = Math.min(level.height || 0, level.width || 0)
224
225 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
226 // We don't have files for live videos
227 if (!file) return level.height
228
229 let label = file.resolution.label
230 if (file.fps >= 50) label += file.fps
231
232 return label
233 },
234 html5: {
235 hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig)
236 }
237 }
238
239 const toAssign = { p2pMediaLoader, hlsjs }
240 Object.assign(plugins, toAssign)
241
242 return toAssign
243 }
244
245 private getP2PMediaLoaderOptions (redundancyUrlManager: RedundancyUrlManager): Partial<HybridLoaderSettings> {
246 let consumeOnly = false
247 if ((navigator as any)?.connection?.type === 'cellular') {
248 console.log('We are on a cellular connection: disabling seeding.')
249 consumeOnly = true
250 }
251
252 const trackerAnnounce = this.options.p2pMediaLoader.trackerAnnounce
253 .filter(t => t.startsWith('ws'))
254
255 const specificLiveOrVODOptions = this.options.common.isLive
256 ? this.getP2PMediaLoaderLiveOptions()
257 : this.getP2PMediaLoaderVODOptions()
258
259 return {
260 trackerAnnounce,
261 rtcConfig: getRtcConfig(),
262
263 simultaneousHttpDownloads: 1,
264 httpFailedSegmentTimeout: 1000,
265
266 segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
267 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
268
269 useP2P: this.options.common.p2pEnabled,
270 consumeOnly,
271
272 ...specificLiveOrVODOptions
273 }
274 }
275
276 private getP2PMediaLoaderLiveOptions (): Partial<HybridLoaderSettings> {
277 const base = {
278 requiredSegmentsPriority: 1
279 }
280
281 const latencyMode = this.options.common.liveOptions.latencyMode
282
283 switch (latencyMode) {
284 case LiveVideoLatencyMode.SMALL_LATENCY:
285 return {
286 ...base,
287
288 useP2P: false,
289 httpDownloadProbability: 1
290 }
291
292 case LiveVideoLatencyMode.HIGH_LATENCY:
293 return base
294
295 default:
296 return base
297 }
298 }
299
300 private getP2PMediaLoaderVODOptions (): Partial<HybridLoaderSettings> {
301 return {
302 requiredSegmentsPriority: 3,
303
304 cachedSegmentExpiration: 86400000,
305 cachedSegmentsCount: 100,
306
307 httpDownloadMaxPriority: 9,
308 httpDownloadProbability: 0.06,
309 httpDownloadProbabilitySkipIfNoPeers: true,
310
311 p2pDownloadMaxPriority: 50
312 }
313 }
314
315 private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
316 const specificLiveOrVODOptions = this.options.common.isLive
317 ? this.getHLSLiveOptions()
318 : this.getHLSVODOptions()
319
320 const base = {
321 capLevelToPlayerSize: true,
322 autoStartLoad: false,
323
324 loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass(),
325
326 ...specificLiveOrVODOptions
327 }
328
329 const averageBandwidth = getAverageBandwidthInStore()
330 if (!averageBandwidth) return base
331
332 return {
333 ...base,
334
335 abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
336 startLevel: -1,
337 testBandwidth: false,
338 debug: false
339 }
340 }
341
342 private getHLSLiveOptions () {
343 const latencyMode = this.options.common.liveOptions.latencyMode
344
345 switch (latencyMode) {
346 case LiveVideoLatencyMode.SMALL_LATENCY:
347 return {
348 liveSyncDurationCount: 2
349 }
350
351 case LiveVideoLatencyMode.HIGH_LATENCY:
352 return {
353 liveSyncDurationCount: 10
354 }
355
356 default:
357 return {
358 liveSyncDurationCount: 5
359 }
360 }
361 }
362
363 private getHLSVODOptions () {
364 return {
365 liveSyncDurationCount: 5
366 }
367 }
368
369 private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
370 const commonOptions = this.options.common
371 const webtorrentOptions = this.options.webtorrent
372 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
373
374 const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play'
375
376 const webtorrent = {
377 autoplay,
378
379 playerRefusedP2P: commonOptions.p2pEnabled === false,
380 videoDuration: commonOptions.videoDuration,
381 playerElement: commonOptions.playerElement,
382
383 videoFiles: webtorrentOptions.videoFiles.length !== 0
384 ? webtorrentOptions.videoFiles
385 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
386 : p2pMediaLoaderOptions?.videoFiles || [],
387
388 startTime: commonOptions.startTime
389 }
390
391 Object.assign(plugins, { webtorrent })
392 }
393
394 private getControlBarChildren (mode: PlayerMode, options: {
395 p2pEnabled: boolean
396 videoShortUUID: string
397
398 peertubeLink: boolean
399 theaterButton: boolean
400 captions: boolean
401
402 nextVideo?: () => void
403 hasNextVideo?: () => boolean
404
405 previousVideo?: () => void
406 hasPreviousVideo?: () => boolean
407 }) {
408 const settingEntries = []
409 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
410
411 // Keep an order
412 settingEntries.push('playbackRateMenuButton')
413 if (options.captions === true) settingEntries.push('captionsButton')
414 settingEntries.push('resolutionMenuButton')
415
416 const children = {}
417
418 if (options.previousVideo) {
419 const buttonOptions: NextPreviousVideoButtonOptions = {
420 type: 'previous',
421 handler: options.previousVideo,
422 isDisabled: () => {
423 if (!options.hasPreviousVideo) return false
424
425 return !options.hasPreviousVideo()
426 }
427 }
428
429 Object.assign(children, {
430 previousVideoButton: buttonOptions
431 })
432 }
433
434 Object.assign(children, { playToggle: {} })
435
436 if (options.nextVideo) {
437 const buttonOptions: NextPreviousVideoButtonOptions = {
438 type: 'next',
439 handler: options.nextVideo,
440 isDisabled: () => {
441 if (!options.hasNextVideo) return false
442
443 return !options.hasNextVideo()
444 }
445 }
446
447 Object.assign(children, {
448 nextVideoButton: buttonOptions
449 })
450 }
451
452 Object.assign(children, {
453 currentTimeDisplay: {},
454 timeDivider: {},
455 durationDisplay: {},
456 liveDisplay: {},
457
458 flexibleWidthSpacer: {},
459 progressControl: {
460 children: {
461 seekBar: {
462 children: {
463 [loadProgressBar]: {},
464 mouseTimeDisplay: {},
465 playProgressBar: {}
466 }
467 }
468 }
469 },
470
471 p2PInfoButton: {
472 p2pEnabled: options.p2pEnabled
473 },
474
475 muteToggle: {},
476 volumeControl: {},
477
478 settingsButton: {
479 setup: {
480 maxHeightOffset: 40
481 },
482 entries: settingEntries
483 }
484 })
485
486 if (options.peertubeLink === true) {
487 Object.assign(children, {
488 peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
489 })
490 }
491
492 if (options.theaterButton === true) {
493 Object.assign(children, {
494 theaterButton: {}
495 })
496 }
497
498 Object.assign(children, {
499 fullscreenToggle: {}
500 })
501
502 return children
503 }
504
505 private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
506 if (autoplay !== true) return autoplay
507
508 // On first play, disable autoplay to avoid issues
509 // But if the player already played videos, we can safely autoplay next ones
510 if (isIOS() || isSafari()) {
511 return alreadyPlayed ? 'play' : false
512 }
513
514 return 'play'
515 }
516
517 getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
518 const content = () => {
519 const isLoopEnabled = player.options_['loop']
520
521 const items = [
522 {
523 icon: 'repeat',
524 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
525 listener: function () {
526 player.options_['loop'] = !isLoopEnabled
527 }
528 },
529 {
530 label: player.localize('Copy the video URL'),
531 listener: function () {
532 copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
533 }
534 },
535 {
536 label: player.localize('Copy the video URL at the current time'),
537 listener: function (this: videojs.Player) {
538 const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
539
540 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
541 }
542 },
543 {
544 icon: 'code',
545 label: player.localize('Copy embed code'),
546 listener: () => {
547 copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
548 }
549 }
550 ]
551
552 if (this.mode === 'webtorrent') {
553 items.push({
554 label: player.localize('Copy magnet URI'),
555 listener: function (this: videojs.Player) {
556 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
557 }
558 })
559 }
560
561 items.push({
562 icon: 'info',
563 label: player.localize('Stats for nerds'),
564 listener: () => {
565 player.stats().show()
566 }
567 })
568
569 return items.map(i => ({
570 ...i,
571 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
572 }))
573 }
574
575 return { content }
576 }
577}
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 @@
1import { HlsConfig, Level } from 'hls.js' 1import { HlsConfig, Level } from 'hls.js'
2import videojs from 'video.js' 2import videojs from 'video.js'
3import { Engine } from '@peertube/p2p-media-loader-hlsjs'
3import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' 4import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
4import { PeerTubeDockPluginOptions } from './dock/peertube-dock-plugin' 5import { PeerTubeDockPluginOptions } from './dock/peertube-dock-plugin'
6import { PlayerMode } from './manager-options/manager-options.model'
5import { Html5Hlsjs } from './p2p-media-loader/hls-plugin' 7import { Html5Hlsjs } from './p2p-media-loader/hls-plugin'
6import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 8import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
7import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 9import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
8import { PlayerMode } from './peertube-player-options-builder'
9import { PeerTubePlugin } from './peertube-plugin' 10import { PeerTubePlugin } from './peertube-plugin'
10import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' 11import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin'
11import { PlaylistPlugin } from './playlist/playlist-plugin' 12import { PlaylistPlugin } from './playlist/playlist-plugin'
@@ -154,6 +155,12 @@ type P2PMediaLoaderPluginOptions = {
154 src: string 155 src: string
155 156
156 startTime: number | string 157 startTime: number | string
158
159 loader: P2PMediaLoader
160}
161
162export type P2PMediaLoader = {
163 getEngine(): Engine
157} 164}
158 165
159type VideoJSPluginOptions = { 166type VideoJSPluginOptions = {