diff options
Diffstat (limited to 'client/src/standalone/videos/shared/player-options-builder.ts')
-rw-r--r-- | client/src/standalone/videos/shared/player-options-builder.ts | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/client/src/standalone/videos/shared/player-options-builder.ts b/client/src/standalone/videos/shared/player-options-builder.ts new file mode 100644 index 000000000..8a4e32444 --- /dev/null +++ b/client/src/standalone/videos/shared/player-options-builder.ts | |||
@@ -0,0 +1,440 @@ | |||
1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' | ||
2 | import { | ||
3 | HTMLServerConfig, | ||
4 | LiveVideo, | ||
5 | Storyboard, | ||
6 | Video, | ||
7 | VideoCaption, | ||
8 | VideoDetails, | ||
9 | VideoPlaylistElement, | ||
10 | VideoState, | ||
11 | VideoStreamingPlaylistType | ||
12 | } from '../../../../../shared/models' | ||
13 | import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' | ||
14 | import { | ||
15 | getBoolOrDefault, | ||
16 | getParamString, | ||
17 | getParamToggle, | ||
18 | isP2PEnabled, | ||
19 | logger, | ||
20 | peertubeLocalStorage, | ||
21 | UserLocalStorageKeys, | ||
22 | videoRequiresUserAuth | ||
23 | } from '../../../root-helpers' | ||
24 | import { PeerTubePlugin } from './peertube-plugin' | ||
25 | import { PlayerHTML } from './player-html' | ||
26 | import { PlaylistTracker } from './playlist-tracker' | ||
27 | import { Translations } from './translations' | ||
28 | import { VideoFetcher } from './video-fetcher' | ||
29 | |||
30 | export class PlayerOptionsBuilder { | ||
31 | private autoplay: boolean | ||
32 | |||
33 | private controls: boolean | ||
34 | private controlBar: boolean | ||
35 | |||
36 | private muted: boolean | ||
37 | private loop: boolean | ||
38 | private subtitle: string | ||
39 | private enableApi = false | ||
40 | private startTime: number | string = 0 | ||
41 | private stopTime: number | string | ||
42 | private playbackRate: number | string | ||
43 | |||
44 | private title: boolean | ||
45 | private warningTitle: boolean | ||
46 | private peertubeLink: boolean | ||
47 | private p2pEnabled: boolean | ||
48 | private bigPlayBackgroundColor: string | ||
49 | private foregroundColor: string | ||
50 | |||
51 | private mode: PlayerMode | ||
52 | private scope = 'peertube' | ||
53 | |||
54 | constructor ( | ||
55 | private readonly playerHTML: PlayerHTML, | ||
56 | private readonly videoFetcher: VideoFetcher, | ||
57 | private readonly peertubePlugin: PeerTubePlugin | ||
58 | ) {} | ||
59 | |||
60 | hasAPIEnabled () { | ||
61 | return this.enableApi | ||
62 | } | ||
63 | |||
64 | hasAutoplay () { | ||
65 | return this.autoplay | ||
66 | } | ||
67 | |||
68 | hasControls () { | ||
69 | return this.controls | ||
70 | } | ||
71 | |||
72 | hasTitle () { | ||
73 | return this.title | ||
74 | } | ||
75 | |||
76 | hasWarningTitle () { | ||
77 | return this.warningTitle | ||
78 | } | ||
79 | |||
80 | hasP2PEnabled () { | ||
81 | return !!this.p2pEnabled | ||
82 | } | ||
83 | |||
84 | hasBigPlayBackgroundColor () { | ||
85 | return !!this.bigPlayBackgroundColor | ||
86 | } | ||
87 | |||
88 | getBigPlayBackgroundColor () { | ||
89 | return this.bigPlayBackgroundColor | ||
90 | } | ||
91 | |||
92 | hasForegroundColor () { | ||
93 | return !!this.foregroundColor | ||
94 | } | ||
95 | |||
96 | getForegroundColor () { | ||
97 | return this.foregroundColor | ||
98 | } | ||
99 | |||
100 | getMode () { | ||
101 | return this.mode | ||
102 | } | ||
103 | |||
104 | getScope () { | ||
105 | return this.scope | ||
106 | } | ||
107 | |||
108 | // --------------------------------------------------------------------------- | ||
109 | |||
110 | loadParams (config: HTMLServerConfig, video: VideoDetails) { | ||
111 | try { | ||
112 | const params = new URL(window.location.toString()).searchParams | ||
113 | |||
114 | this.autoplay = getParamToggle(params, 'autoplay', false) | ||
115 | // Disable auto play on live videos that are not streamed | ||
116 | if (video.state.id === VideoState.LIVE_ENDED || video.state.id === VideoState.WAITING_FOR_LIVE) { | ||
117 | this.autoplay = false | ||
118 | } | ||
119 | |||
120 | this.controls = getParamToggle(params, 'controls', true) | ||
121 | this.controlBar = getParamToggle(params, 'controlBar', true) | ||
122 | |||
123 | this.muted = getParamToggle(params, 'muted', undefined) | ||
124 | this.loop = getParamToggle(params, 'loop', false) | ||
125 | this.title = getParamToggle(params, 'title', true) | ||
126 | this.enableApi = getParamToggle(params, 'api', this.enableApi) | ||
127 | this.warningTitle = getParamToggle(params, 'warningTitle', true) | ||
128 | this.peertubeLink = getParamToggle(params, 'peertubeLink', true) | ||
129 | this.p2pEnabled = getParamToggle(params, 'p2p', this.isP2PEnabled(config, video)) | ||
130 | |||
131 | this.scope = getParamString(params, 'scope', this.scope) | ||
132 | this.subtitle = getParamString(params, 'subtitle') | ||
133 | this.startTime = getParamString(params, 'start') | ||
134 | this.stopTime = getParamString(params, 'stop') | ||
135 | this.playbackRate = getParamString(params, 'playbackRate') | ||
136 | |||
137 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') | ||
138 | this.foregroundColor = getParamString(params, 'foregroundColor') | ||
139 | |||
140 | const modeParam = getParamString(params, 'mode') | ||
141 | |||
142 | if (modeParam) { | ||
143 | if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' | ||
144 | else this.mode = 'web-video' | ||
145 | } else { | ||
146 | if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' | ||
147 | else this.mode = 'web-video' | ||
148 | } | ||
149 | } catch (err) { | ||
150 | logger.error('Cannot get params from URL.', err) | ||
151 | } | ||
152 | } | ||
153 | |||
154 | // --------------------------------------------------------------------------- | ||
155 | |||
156 | getPlayerConstructorOptions (options: { | ||
157 | serverConfig: HTMLServerConfig | ||
158 | authorizationHeader: () => string | ||
159 | }): PeerTubePlayerContructorOptions { | ||
160 | const { serverConfig, authorizationHeader } = options | ||
161 | |||
162 | return { | ||
163 | controls: this.controls, | ||
164 | controlBar: this.controlBar, | ||
165 | |||
166 | muted: this.muted, | ||
167 | loop: this.loop, | ||
168 | |||
169 | playbackRate: this.playbackRate, | ||
170 | |||
171 | inactivityTimeout: 2500, | ||
172 | videoViewIntervalMs: 5000, | ||
173 | metricsUrl: window.location.origin + '/api/v1/metrics/playback', | ||
174 | |||
175 | authorizationHeader, | ||
176 | |||
177 | playerElement: () => this.playerHTML.getPlayerElement(), | ||
178 | enableHotkeys: true, | ||
179 | |||
180 | peertubeLink: () => this.peertubeLink, | ||
181 | instanceName: serverConfig.instance.name, | ||
182 | |||
183 | theaterButton: false, | ||
184 | |||
185 | serverUrl: window.location.origin, | ||
186 | language: navigator.language, | ||
187 | |||
188 | pluginsManager: this.peertubePlugin.getPluginsManager(), | ||
189 | |||
190 | errorNotifier: () => { | ||
191 | // Empty, we don't have a notifier in the embed | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | async getPlayerLoadOptions (options: { | ||
197 | video: VideoDetails | ||
198 | captionsResponse: Response | ||
199 | |||
200 | storyboardsResponse: Response | ||
201 | |||
202 | live?: LiveVideo | ||
203 | |||
204 | alreadyPlayed: boolean | ||
205 | forceAutoplay: boolean | ||
206 | |||
207 | videoFileToken: () => string | ||
208 | |||
209 | videoPassword: () => string | ||
210 | requiresPassword: boolean | ||
211 | |||
212 | translations: Translations | ||
213 | |||
214 | playlist?: { | ||
215 | playlistTracker: PlaylistTracker | ||
216 | playNext: () => any | ||
217 | playPrevious: () => any | ||
218 | onVideoUpdate: (uuid: string) => any | ||
219 | } | ||
220 | }): Promise<PeerTubePlayerLoadOptions> { | ||
221 | const { | ||
222 | video, | ||
223 | captionsResponse, | ||
224 | videoFileToken, | ||
225 | videoPassword, | ||
226 | requiresPassword, | ||
227 | translations, | ||
228 | alreadyPlayed, | ||
229 | forceAutoplay, | ||
230 | playlist, | ||
231 | live, | ||
232 | storyboardsResponse | ||
233 | } = options | ||
234 | |||
235 | const [ videoCaptions, storyboard ] = await Promise.all([ | ||
236 | this.buildCaptions(captionsResponse, translations), | ||
237 | this.buildStoryboard(storyboardsResponse) | ||
238 | ]) | ||
239 | |||
240 | return { | ||
241 | mode: this.mode, | ||
242 | |||
243 | autoplay: forceAutoplay || alreadyPlayed || this.autoplay, | ||
244 | forceAutoplay, | ||
245 | |||
246 | p2pEnabled: this.p2pEnabled, | ||
247 | |||
248 | subtitle: this.subtitle, | ||
249 | |||
250 | storyboard, | ||
251 | |||
252 | startTime: playlist | ||
253 | ? playlist.playlistTracker.getCurrentElement().startTimestamp | ||
254 | : this.startTime, | ||
255 | stopTime: playlist | ||
256 | ? playlist.playlistTracker.getCurrentElement().stopTimestamp | ||
257 | : this.stopTime, | ||
258 | |||
259 | videoCaptions, | ||
260 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | ||
261 | |||
262 | videoShortUUID: video.shortUUID, | ||
263 | videoUUID: video.uuid, | ||
264 | |||
265 | duration: video.duration, | ||
266 | |||
267 | poster: window.location.origin + video.previewPath, | ||
268 | |||
269 | embedUrl: window.location.origin + video.embedPath, | ||
270 | embedTitle: video.name, | ||
271 | |||
272 | requiresUserAuth: videoRequiresUserAuth(video), | ||
273 | videoFileToken, | ||
274 | |||
275 | requiresPassword, | ||
276 | videoPassword, | ||
277 | |||
278 | ...this.buildLiveOptions(video, live), | ||
279 | |||
280 | ...this.buildPlaylistOptions(playlist), | ||
281 | |||
282 | dock: this.buildDockOptions(video), | ||
283 | |||
284 | webVideo: { | ||
285 | videoFiles: video.files | ||
286 | }, | ||
287 | |||
288 | hls: this.buildHLSOptions(video) | ||
289 | } | ||
290 | } | ||
291 | |||
292 | private buildLiveOptions (video: VideoDetails, live: LiveVideo) { | ||
293 | if (!video.isLive) return { isLive: false } | ||
294 | |||
295 | return { | ||
296 | isLive: true, | ||
297 | liveOptions: { | ||
298 | latencyMode: live.latencyMode | ||
299 | } | ||
300 | } | ||
301 | } | ||
302 | |||
303 | private async buildStoryboard (storyboardsResponse: Response) { | ||
304 | const { storyboards } = await storyboardsResponse.json() as { storyboards: Storyboard[] } | ||
305 | if (!storyboards || storyboards.length === 0) return undefined | ||
306 | |||
307 | return { | ||
308 | url: window.location.origin + storyboards[0].storyboardPath, | ||
309 | height: storyboards[0].spriteHeight, | ||
310 | width: storyboards[0].spriteWidth, | ||
311 | interval: storyboards[0].spriteDuration | ||
312 | } | ||
313 | } | ||
314 | |||
315 | private buildPlaylistOptions (options?: { | ||
316 | playlistTracker: PlaylistTracker | ||
317 | playNext: () => any | ||
318 | playPrevious: () => any | ||
319 | onVideoUpdate: (uuid: string) => any | ||
320 | }) { | ||
321 | if (!options) { | ||
322 | return { | ||
323 | nextVideo: { | ||
324 | enabled: false, | ||
325 | displayControlBarButton: false, | ||
326 | getVideoTitle: () => '' | ||
327 | }, | ||
328 | previousVideo: { | ||
329 | enabled: false, | ||
330 | displayControlBarButton: false | ||
331 | } | ||
332 | } | ||
333 | } | ||
334 | |||
335 | const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options | ||
336 | |||
337 | return { | ||
338 | playlist: { | ||
339 | elements: playlistTracker.getPlaylistElements(), | ||
340 | playlist: playlistTracker.getPlaylist(), | ||
341 | |||
342 | getCurrentPosition: () => playlistTracker.getCurrentPosition(), | ||
343 | |||
344 | onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => { | ||
345 | playlistTracker.setCurrentElement(videoPlaylistElement) | ||
346 | |||
347 | onVideoUpdate(videoPlaylistElement.video.uuid) | ||
348 | } | ||
349 | }, | ||
350 | |||
351 | previousVideo: { | ||
352 | enabled: playlistTracker.hasPreviousPlaylistElement(), | ||
353 | handler: () => playPrevious(), | ||
354 | displayControlBarButton: true | ||
355 | }, | ||
356 | |||
357 | nextVideo: { | ||
358 | enabled: playlistTracker.hasNextPlaylistElement(), | ||
359 | handler: () => playNext(), | ||
360 | getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name, | ||
361 | displayControlBarButton: true | ||
362 | }, | ||
363 | |||
364 | upnext: { | ||
365 | isEnabled: () => true, | ||
366 | isSuspended: () => false, | ||
367 | timeout: 0 | ||
368 | } | ||
369 | } | ||
370 | } | ||
371 | |||
372 | private buildHLSOptions (video: VideoDetails): HLSOptions { | ||
373 | const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
374 | if (!hlsPlaylist) return undefined | ||
375 | |||
376 | return { | ||
377 | playlistUrl: hlsPlaylist.playlistUrl, | ||
378 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
379 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
380 | trackerAnnounce: video.trackerUrls, | ||
381 | videoFiles: hlsPlaylist.files | ||
382 | } | ||
383 | } | ||
384 | |||
385 | // --------------------------------------------------------------------------- | ||
386 | |||
387 | private async buildCaptions (captionsResponse: Response, translations: Translations): Promise<VideoJSCaption[]> { | ||
388 | if (captionsResponse.ok) { | ||
389 | const { data } = await captionsResponse.json() | ||
390 | |||
391 | return data.map((c: VideoCaption) => ({ | ||
392 | label: peertubeTranslate(c.language.label, translations), | ||
393 | language: c.language.id, | ||
394 | src: window.location.origin + c.captionPath | ||
395 | })) | ||
396 | } | ||
397 | |||
398 | return [] | ||
399 | } | ||
400 | |||
401 | // --------------------------------------------------------------------------- | ||
402 | |||
403 | private buildDockOptions (videoInfo: VideoDetails) { | ||
404 | if (!this.hasControls()) return undefined | ||
405 | |||
406 | const title = this.hasTitle() | ||
407 | ? videoInfo.name | ||
408 | : undefined | ||
409 | |||
410 | const description = this.hasWarningTitle() && this.hasP2PEnabled() | ||
411 | ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>' | ||
412 | : undefined | ||
413 | |||
414 | if (!title && !description) return | ||
415 | |||
416 | const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50) | ||
417 | const avatar = availableAvatars.length !== 0 | ||
418 | ? availableAvatars[0] | ||
419 | : undefined | ||
420 | |||
421 | return { | ||
422 | title, | ||
423 | description, | ||
424 | avatarUrl: title && avatar | ||
425 | ? avatar.path | ||
426 | : undefined | ||
427 | } | ||
428 | } | ||
429 | |||
430 | // --------------------------------------------------------------------------- | ||
431 | |||
432 | private isP2PEnabled (config: HTMLServerConfig, video: Video) { | ||
433 | const userP2PEnabled = getBoolOrDefault( | ||
434 | peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), | ||
435 | config.defaults.p2p.embed.enabled | ||
436 | ) | ||
437 | |||
438 | return isP2PEnabled(video, config, userP2PEnabled) | ||
439 | } | ||
440 | } | ||