diff options
Diffstat (limited to 'client')
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 @@ | |||
1 | export * from './peertube-player-manager' | 1 | export * from './peertube-player-manager' |
2 | export * from './peertube-player-options-builder' | 2 | 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 @@ | |||
1 | import { NextPreviousVideoButtonOptions, PeerTubeLinkButtonOptions } from '../peertube-videojs-typings' | ||
2 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model' | ||
3 | |||
4 | export 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 @@ | |||
1 | import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' | ||
2 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | ||
3 | import { LiveVideoLatencyMode } from '@shared/models' | ||
4 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' | ||
5 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' | ||
6 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' | ||
7 | import { getAverageBandwidthInStore } from '../peertube-player-local-storage' | ||
8 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../peertube-videojs-typings' | ||
9 | import { getRtcConfig } from '../utils' | ||
10 | import { PeertubePlayerManagerOptions } from './manager-options.model' | ||
11 | |||
12 | export 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | ||
3 | import { isDefaultLocale } from '@shared/core-utils/i18n' | ||
4 | import { copyToClipboard } from '../../../root-helpers/utils' | ||
5 | import { VideoJSPluginOptions } from '../peertube-videojs-typings' | ||
6 | import { buildVideoOrPlaylistEmbed, isIOS, isSafari } from '../utils' | ||
7 | import { ControlBarOptionsBuilder } from './control-bar-options-builder' | ||
8 | import { HLSOptionsBuilder } from './hls-options-builder' | ||
9 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options.model' | ||
10 | import { WebTorrentOptionsBuilder } from './webtorrent-options-builder' | ||
11 | |||
12 | export 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 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | ||
3 | import { PlaylistPluginOptions, UserWatching, VideoJSCaption } from '../peertube-videojs-typings' | ||
4 | |||
5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
6 | |||
7 | export type WebtorrentOptions = { | ||
8 | videoFiles: VideoFile[] | ||
9 | } | ||
10 | |||
11 | export type P2PMediaLoaderOptions = { | ||
12 | playlistUrl: string | ||
13 | segmentsSha256Url: string | ||
14 | trackerAnnounce: string[] | ||
15 | redundancyBaseUrls: string[] | ||
16 | videoFiles: VideoFile[] | ||
17 | } | ||
18 | |||
19 | export 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 | |||
32 | export 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 | |||
78 | export 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 @@ | |||
1 | import { PeertubePlayerManagerOptions } from './manager-options.model' | ||
2 | |||
3 | export 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' | |||
10 | import './control-bar/p2p-info-button' | 10 | import './control-bar/p2p-info-button' |
11 | import './control-bar/peertube-link-button' | 11 | import './control-bar/peertube-link-button' |
12 | import './control-bar/peertube-load-progress-bar' | 12 | import './control-bar/peertube-load-progress-bar' |
13 | import './control-bar/resolution-menu-button' | ||
14 | import './control-bar/resolution-menu-item' | ||
15 | import './control-bar/settings-dialog' | ||
16 | import './control-bar/settings-menu-button' | ||
17 | import './control-bar/settings-menu-item' | ||
18 | import './control-bar/settings-panel' | ||
19 | import './control-bar/settings-panel-child' | ||
20 | import './control-bar/theater-button' | 13 | import './control-bar/theater-button' |
14 | import './settings/resolution-menu-button' | ||
15 | import './settings/resolution-menu-item' | ||
16 | import './settings/settings-dialog' | ||
17 | import './settings/settings-menu-button' | ||
18 | import './settings/settings-menu-item' | ||
19 | import './settings/settings-panel' | ||
20 | import './settings/settings-panel-child' | ||
21 | import './playlist/playlist-plugin' | 21 | import './playlist/playlist-plugin' |
22 | import './mobile/peertube-mobile-plugin' | 22 | import './mobile/peertube-mobile-plugin' |
23 | import './mobile/peertube-mobile-buttons' | 23 | import './mobile/peertube-mobile-buttons' |
24 | import './hotkeys/peertube-hotkeys-plugin' | 24 | import './hotkeys/peertube-hotkeys-plugin' |
25 | import videojs from 'video.js' | 25 | import videojs from 'video.js' |
26 | import { PluginsManager } from '@root-helpers/plugins-manager' | 26 | import { PluginsManager } from '@root-helpers/plugins-manager' |
27 | import { ManagerOptionsBuilder } from './manager-options/manager-options-builder' | ||
28 | import { CommonOptions, PeertubePlayerManagerOptions, PlayerMode } from './manager-options/manager-options.model' | ||
27 | import { saveAverageBandwidth } from './peertube-player-local-storage' | 29 | import { saveAverageBandwidth } from './peertube-player-local-storage' |
28 | import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder' | ||
29 | import { PlayerNetworkInfo } from './peertube-videojs-typings' | 30 | import { PlayerNetworkInfo } from './peertube-videojs-typings' |
30 | import { TranslationsManager } from './translations-manager' | 31 | import { TranslationsManager } from './translations-manager' |
31 | import { isMobile } from './utils' | 32 | import { 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { HybridLoaderSettings } from '@peertube/p2p-media-loader-core' | ||
3 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | ||
4 | import { PluginsManager } from '@root-helpers/plugins-manager' | ||
5 | import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' | ||
6 | import { isDefaultLocale } from '@shared/core-utils/i18n' | ||
7 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | ||
8 | import { copyToClipboard } from '../../root-helpers/utils' | ||
9 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | ||
10 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
11 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
12 | import { getAverageBandwidthInStore } from './peertube-player-local-storage' | ||
13 | import { | ||
14 | NextPreviousVideoButtonOptions, | ||
15 | P2PMediaLoaderPluginOptions, | ||
16 | PeerTubeLinkButtonOptions, | ||
17 | PlaylistPluginOptions, | ||
18 | UserWatching, | ||
19 | VideoJSCaption, | ||
20 | VideoJSPluginOptions | ||
21 | } from './peertube-videojs-typings' | ||
22 | import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils' | ||
23 | |||
24 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
25 | |||
26 | export type WebtorrentOptions = { | ||
27 | videoFiles: VideoFile[] | ||
28 | } | ||
29 | |||
30 | export type P2PMediaLoaderOptions = { | ||
31 | playlistUrl: string | ||
32 | segmentsSha256Url: string | ||
33 | trackerAnnounce: string[] | ||
34 | redundancyBaseUrls: string[] | ||
35 | videoFiles: VideoFile[] | ||
36 | } | ||
37 | |||
38 | export 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 | |||
51 | export 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 | |||
97 | export type PeertubePlayerManagerOptions = { | ||
98 | common: CommonOptions | ||
99 | webtorrent: WebtorrentOptions | ||
100 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
101 | |||
102 | pluginsManager: PluginsManager | ||
103 | } | ||
104 | |||
105 | export 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 @@ | |||
1 | import { HlsConfig, Level } from 'hls.js' | 1 | import { HlsConfig, Level } from 'hls.js' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | ||
3 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
4 | import { PeerTubeDockPluginOptions } from './dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from './dock/peertube-dock-plugin' |
6 | import { PlayerMode } from './manager-options/manager-options.model' | ||
5 | import { Html5Hlsjs } from './p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from './p2p-media-loader/hls-plugin' |
6 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' |
7 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
8 | import { PlayerMode } from './peertube-player-options-builder' | ||
9 | import { PeerTubePlugin } from './peertube-plugin' | 10 | import { PeerTubePlugin } from './peertube-plugin' |
10 | import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' | 11 | import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' |
11 | import { PlaylistPlugin } from './playlist/playlist-plugin' | 12 | import { 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 | |||
162 | export type P2PMediaLoader = { | ||
163 | getEngine(): Engine | ||
157 | } | 164 | } |
158 | 165 | ||
159 | type VideoJSPluginOptions = { | 166 | type VideoJSPluginOptions = { |