]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/peertube-player-manager.ts
Merge branch 'release/3.3.0' into develop
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / peertube-player-manager.ts
CommitLineData
512decf3 1import 'videojs-hotkeys/videojs.hotkeys'
2adfc7ea 2import 'videojs-dock'
a472cf03 3import 'videojs-contextmenu-pt'
2adfc7ea 4import 'videojs-contrib-quality-levels'
f5fcd9f7 5import './upnext/end-card'
3bcb4fd7 6import './upnext/upnext-plugin'
ff563914
RK
7import './stats/stats-card'
8import './stats/stats-plugin'
62ab565d 9import './bezels/bezels-plugin'
2adfc7ea 10import './peertube-plugin'
a950e4c8 11import './videojs-components/next-previous-video-button'
f5fcd9f7 12import './videojs-components/p2p-info-button'
2adfc7ea 13import './videojs-components/peertube-link-button'
f5fcd9f7 14import './videojs-components/peertube-load-progress-bar'
2adfc7ea 15import './videojs-components/resolution-menu-button'
f5fcd9f7
C
16import './videojs-components/resolution-menu-item'
17import './videojs-components/settings-dialog'
2adfc7ea 18import './videojs-components/settings-menu-button'
f5fcd9f7
C
19import './videojs-components/settings-menu-item'
20import './videojs-components/settings-panel'
21import './videojs-components/settings-panel-child'
2adfc7ea 22import './videojs-components/theater-button'
4572c3d0 23import './playlist/playlist-plugin'
3e2bc4ea 24import videojs from 'video.js'
72f611ca 25import { PluginsManager } from '@root-helpers/plugins-manager'
15a7eafb 26import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
bd45d503 27import { isDefaultLocale } from '@shared/core-utils/i18n'
4572c3d0 28import { VideoFile } from '@shared/models'
72f611ca 29import { copyToClipboard } from '../../root-helpers/utils'
da332417 30import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
3e2bc4ea
C
31import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
32import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
43c66a91 33import { getStoredP2PEnabled } from './peertube-player-local-storage'
4572c3d0 34import {
a950e4c8 35 NextPreviousVideoButtonOptions,
4572c3d0 36 P2PMediaLoaderPluginOptions,
9162fdd3 37 PeerTubeLinkButtonOptions,
4572c3d0
C
38 PlaylistPluginOptions,
39 UserWatching,
40 VideoJSCaption,
41 VideoJSPluginOptions
42} from './peertube-videojs-typings'
3f9c4955 43import { TranslationsManager } from './translations-manager'
15a7eafb 44import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
2adfc7ea
C
45
46// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
f5fcd9f7
C
47(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
48
49const CaptionsButton = videojs.getComponent('CaptionsButton') as any
2adfc7ea 50// Change Captions to Subtitles/CC
f5fcd9f7 51CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
2adfc7ea 52// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
f5fcd9f7 53CaptionsButton.prototype.label_ = ' '
2adfc7ea 54
3b6f205c 55export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
2adfc7ea 56
3b6f205c 57export type WebtorrentOptions = {
2adfc7ea
C
58 videoFiles: VideoFile[]
59}
60
3b6f205c 61export type P2PMediaLoaderOptions = {
2adfc7ea 62 playlistUrl: string
09209296 63 segmentsSha256Url: string
4348a27d 64 trackerAnnounce: string[]
09209296
C
65 redundancyBaseUrls: string[]
66 videoFiles: VideoFile[]
2adfc7ea
C
67}
68
5efab546
C
69export interface CustomizationOptions {
70 startTime: number | string
71 stopTime: number | string
72
73 controls?: boolean
74 muted?: boolean
75 loop?: boolean
76 subtitle?: string
96f6278f 77 resume?: string
5efab546
C
78
79 peertubeLink: boolean
80}
81
82export interface CommonOptions extends CustomizationOptions {
2adfc7ea 83 playerElement: HTMLVideoElement
6ec0b75b 84 onPlayerElementChange: (element: HTMLVideoElement) => void
2adfc7ea
C
85
86 autoplay: boolean
a950e4c8
C
87
88 nextVideo?: () => void
89 hasNextVideo?: () => boolean
90
91 previousVideo?: () => void
92 hasPreviousVideo?: () => boolean
4572c3d0
C
93
94 playlist?: PlaylistPluginOptions
95
2adfc7ea
C
96 videoDuration: number
97 enableHotkeys: boolean
98 inactivityTimeout: number
99 poster: string
2adfc7ea 100
3d9a63d3 101 theaterButton: boolean
2adfc7ea 102 captions: boolean
2adfc7ea
C
103
104 videoViewUrl: string
105 embedUrl: string
4097c6d6 106 embedTitle: string
2adfc7ea 107
25b7c847
C
108 isLive: boolean
109
2adfc7ea 110 language?: string
2adfc7ea
C
111
112 videoCaptions: VideoJSCaption[]
113
58b9ce30 114 videoUUID: string
9162fdd3 115 videoShortUUID: string
58b9ce30 116
2adfc7ea
C
117 userWatching?: UserWatching
118
119 serverUrl: string
120}
121
122export type PeertubePlayerManagerOptions = {
72f611ca 123 common: CommonOptions
124 webtorrent: WebtorrentOptions
2adfc7ea 125 p2pMediaLoader?: P2PMediaLoaderOptions
72f611ca 126
127 pluginsManager: PluginsManager
2adfc7ea
C
128}
129
130export class PeertubePlayerManager {
6ec0b75b 131 private static playerElementClassName: string
7e37e111 132 private static onPlayerChange: (player: videojs.Player) => void
9eccae74 133 private static alreadyPlayed = false
72f611ca 134 private static pluginsManager: PluginsManager
9eccae74 135
1a568b6f
C
136 static initState () {
137 PeertubePlayerManager.alreadyPlayed = false
138 }
139
7e37e111 140 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
72f611ca 141 this.pluginsManager = options.pluginsManager
142
4348a27d
C
143 let p2pMediaLoader: any
144
bfbd9128 145 this.onPlayerChange = onPlayerChange
6ec0b75b
C
146 this.playerElementClassName = options.common.playerElement.className
147
09209296 148 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
4348a27d
C
149 if (mode === 'p2p-media-loader') {
150 [ p2pMediaLoader ] = await Promise.all([
151 import('p2p-media-loader-hlsjs'),
09209296 152 import('./p2p-media-loader/p2p-media-loader-plugin')
4348a27d
C
153 ])
154 }
2adfc7ea 155
72f611ca 156 const videojsOptions = await this.getVideojsOptions(mode, options, p2pMediaLoader)
2adfc7ea 157
3f9c4955 158 await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
2adfc7ea
C
159
160 const self = this
161 return new Promise(res => {
7e37e111 162 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
2adfc7ea
C
163 const player = this
164
536598cf
C
165 let alreadyFallback = false
166
f5fcd9f7 167 player.tech(true).one('error', () => {
536598cf
C
168 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
169 alreadyFallback = true
170 })
171
172 player.one('error', () => {
173 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
174 alreadyFallback = true
175 })
6ec0b75b 176
9eccae74
C
177 player.one('play', () => {
178 PeertubePlayerManager.alreadyPlayed = true
179 })
180
9162fdd3
C
181 self.addContextMenu({
182 mode,
183 player,
184 videoShortUUID: options.common.videoShortUUID,
185 videoEmbedUrl: options.common.embedUrl,
186 videoEmbedTitle: options.common.embedTitle
187 })
2adfc7ea 188
10475dea 189 player.bezels()
ff563914
RK
190 player.stats({
191 videoUUID: options.common.videoUUID,
192 videoIsLive: options.common.isLive,
193 mode
194 })
10475dea 195
2adfc7ea
C
196 return res(player)
197 })
198 })
199 }
200
96cb4527
C
201 private static async maybeFallbackToWebTorrent (currentMode: PlayerMode, player: any, options: PeertubePlayerManagerOptions) {
202 if (currentMode === 'webtorrent') return
203
204 console.log('Fallback to webtorrent.')
205
6ec0b75b
C
206 const newVideoElement = document.createElement('video')
207 newVideoElement.className = this.playerElementClassName
208
209 // VideoJS wraps our video element inside a div
96cb4527
C
210 let currentParentPlayerElement = options.common.playerElement.parentNode
211 // Fix on IOS, don't ask me why
212 if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(options.common.playerElement.id).parentNode
213
6ec0b75b
C
214 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
215
216 options.common.playerElement = newVideoElement
217 options.common.onPlayerElementChange(newVideoElement)
218
219 player.dispose()
220
221 await import('./webtorrent/webtorrent-plugin')
222
223 const mode = 'webtorrent'
72f611ca 224 const videojsOptions = await this.getVideojsOptions(mode, options)
6ec0b75b
C
225
226 const self = this
7e37e111 227 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
6ec0b75b
C
228 const player = this
229
9162fdd3
C
230 self.addContextMenu({
231 mode,
232 player,
233 videoShortUUID: options.common.videoShortUUID,
234 videoEmbedUrl: options.common.embedUrl,
235 videoEmbedTitle: options.common.embedTitle
236 })
bfbd9128
C
237
238 PeertubePlayerManager.onPlayerChange(player)
6ec0b75b
C
239 })
240 }
241
72f611ca 242 private static async getVideojsOptions (
f5fcd9f7
C
243 mode: PlayerMode,
244 options: PeertubePlayerManagerOptions,
245 p2pMediaLoaderModule?: any
72f611ca 246 ): Promise<videojs.PlayerOptions> {
2adfc7ea 247 const commonOptions = options.common
9eccae74 248 const isHLS = mode === 'p2p-media-loader'
09209296 249
72efdda5 250 let autoplay = this.getAutoPlayValue(commonOptions.autoplay)
72f611ca 251 const html5 = {
93f30abf
C
252 preloadTextTracks: false
253 }
2adfc7ea
C
254
255 const plugins: VideoJSPluginOptions = {
256 peertube: {
09209296 257 mode,
cb5c2abc 258 autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
2adfc7ea
C
259 videoViewUrl: commonOptions.videoViewUrl,
260 videoDuration: commonOptions.videoDuration,
2adfc7ea
C
261 userWatching: commonOptions.userWatching,
262 subtitle: commonOptions.subtitle,
f0a39880 263 videoCaptions: commonOptions.videoCaptions,
10f26f42 264 stopTime: commonOptions.stopTime,
58b9ce30 265 isLive: commonOptions.isLive,
266 videoUUID: commonOptions.videoUUID
2adfc7ea
C
267 }
268 }
269
3e0e8d4a 270 if (commonOptions.playlist) {
4572c3d0
C
271 plugins.playlist = commonOptions.playlist
272 }
273
39aad8cc
C
274 if (commonOptions.enableHotkeys === true) {
275 PeertubePlayerManager.addHotkeysOptions(plugins)
276 }
09209296 277
9eccae74 278 if (isHLS) {
83fcadac 279 const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
2adfc7ea 280
93f30abf 281 Object.assign(html5, hlsjs.html5)
2adfc7ea
C
282 }
283
6ec0b75b 284 if (mode === 'webtorrent') {
39aad8cc 285 PeertubePlayerManager.addWebTorrentOptions(plugins, options)
09209296
C
286
287 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
288 autoplay = false
2adfc7ea
C
289 }
290
291 const videojsOptions = {
3b6f205c
C
292 html5,
293
2adfc7ea 294 // We don't use text track settings for now
f5fcd9f7 295 textTrackSettings: false as any, // FIXME: typings
2adfc7ea
C
296 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
297 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
298
299 muted: commonOptions.muted !== undefined
300 ? commonOptions.muted
301 : undefined, // Undefined so the player knows it has to check the local storage
302
72efdda5 303 autoplay: this.getAutoPlayValue(autoplay),
39aad8cc 304
2adfc7ea 305 poster: commonOptions.poster,
2adfc7ea 306 inactivityTimeout: commonOptions.inactivityTimeout,
08844775 307 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
39aad8cc 308
2adfc7ea 309 plugins,
39aad8cc 310
2adfc7ea
C
311 controlBar: {
312 children: this.getControlBarChildren(mode, {
9162fdd3
C
313 videoShortUUID: commonOptions.videoShortUUID,
314
2adfc7ea
C
315 captions: commonOptions.captions,
316 peertubeLink: commonOptions.peertubeLink,
1dc240a9 317 theaterButton: commonOptions.theaterButton,
a950e4c8
C
318
319 nextVideo: commonOptions.nextVideo,
320 hasNextVideo: commonOptions.hasNextVideo,
321
322 previousVideo: commonOptions.previousVideo,
323 hasPreviousVideo: commonOptions.hasPreviousVideo
f5fcd9f7 324 }) as any // FIXME: typings
2adfc7ea
C
325 }
326 }
327
39aad8cc
C
328 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
329 Object.assign(videojsOptions, { language: commonOptions.language })
330 }
2adfc7ea 331
72f611ca 332 return this.pluginsManager.runHook('filter:internal.player.videojs.options.result', videojsOptions)
39aad8cc 333 }
2adfc7ea 334
39aad8cc
C
335 private static addP2PMediaLoaderOptions (
336 plugins: VideoJSPluginOptions,
337 options: PeertubePlayerManagerOptions,
338 p2pMediaLoaderModule: any
339 ) {
340 const p2pMediaLoaderOptions = options.p2pMediaLoader
341 const commonOptions = options.common
342
343 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
344 .filter(t => t.startsWith('ws'))
345
346 const redundancyUrlManager = new RedundancyUrlManager(options.p2pMediaLoader.redundancyBaseUrls)
347
348 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
349 redundancyUrlManager,
350 type: 'application/x-mpegURL',
351 startTime: commonOptions.startTime,
352 src: p2pMediaLoaderOptions.playlistUrl
353 }
354
355 let consumeOnly = false
356 // FIXME: typings
fe9d0531 357 if (navigator && (navigator as any).connection && (navigator as any).connection.type === 'cellular') {
39aad8cc
C
358 console.log('We are on a cellular connection: disabling seeding.')
359 consumeOnly = true
360 }
361
362 const p2pMediaLoaderConfig = {
363 loader: {
364 trackerAnnounce,
25b7c847 365 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url, options.common.isLive),
39aad8cc 366 rtcConfig: getRtcConfig(),
c6c0fa6c 367 requiredSegmentsPriority: 1,
39aad8cc
C
368 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
369 useP2P: getStoredP2PEnabled(),
370 consumeOnly
371 },
372 segments: {
373 swarmId: p2pMediaLoaderOptions.playlistUrl
374 }
375 }
83fcadac 376 const hlsjs = {
39aad8cc 377 levelLabelHandler: (level: { height: number, width: number }) => {
dca0fe12
C
378 const resolution = Math.min(level.height || 0, level.width || 0)
379
380 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
053aed43
C
381 // We don't have files for live videos
382 if (!file) return level.height
39aad8cc
C
383
384 let label = file.resolution.label
385 if (file.fps >= 50) label += file.fps
386
387 return label
388 },
389 html5: {
390 hlsjsConfig: {
391 capLevelToPlayerSize: true,
392 autoStartLoad: false,
e14de000 393 liveSyncDurationCount: 5,
39aad8cc 394 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
2adfc7ea 395 }
39aad8cc 396 }
2adfc7ea
C
397 }
398
83fcadac 399 const toAssign = { p2pMediaLoader, hlsjs }
39aad8cc
C
400 Object.assign(plugins, toAssign)
401
402 return toAssign
403 }
404
405 private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) {
406 const commonOptions = options.common
407 const webtorrentOptions = options.webtorrent
3e9cf564 408 const p2pMediaLoaderOptions = options.p2pMediaLoader
39aad8cc 409
ebc8dd52
C
410 const autoplay = this.getAutoPlayValue(commonOptions.autoplay) === 'play'
411 ? true
412 : false
413
39aad8cc 414 const webtorrent = {
ebc8dd52 415 autoplay,
39aad8cc
C
416 videoDuration: commonOptions.videoDuration,
417 playerElement: commonOptions.playerElement,
3e9cf564
C
418 videoFiles: webtorrentOptions.videoFiles.length !== 0
419 ? webtorrentOptions.videoFiles
420 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
05287a2e 421 : p2pMediaLoaderOptions?.videoFiles || [],
39aad8cc 422 startTime: commonOptions.startTime
2adfc7ea
C
423 }
424
39aad8cc 425 Object.assign(plugins, { webtorrent })
2adfc7ea
C
426 }
427
428 private static getControlBarChildren (mode: PlayerMode, options: {
9162fdd3
C
429 videoShortUUID: string
430
2adfc7ea 431 peertubeLink: boolean
a950e4c8
C
432 theaterButton: boolean
433 captions: boolean
434
1dc240a9 435 nextVideo?: Function
a950e4c8
C
436 hasNextVideo?: () => boolean
437
438 previousVideo?: Function
439 hasPreviousVideo?: () => boolean
2adfc7ea
C
440 }) {
441 const settingEntries = []
442 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
443
444 // Keep an order
445 settingEntries.push('playbackRateMenuButton')
446 if (options.captions === true) settingEntries.push('captionsButton')
447 settingEntries.push('resolutionMenuButton')
448
a950e4c8
C
449 const children = {}
450
451 if (options.previousVideo) {
452 const buttonOptions: NextPreviousVideoButtonOptions = {
453 type: 'previous',
454 handler: options.previousVideo,
455 isDisabled: () => {
456 if (!options.hasPreviousVideo) return false
457
458 return !options.hasPreviousVideo()
459 }
460 }
461
462 Object.assign(children, {
463 'previousVideoButton': buttonOptions
464 })
1dc240a9
RK
465 }
466
a950e4c8
C
467 Object.assign(children, { playToggle: {} })
468
1dc240a9 469 if (options.nextVideo) {
a950e4c8
C
470 const buttonOptions: NextPreviousVideoButtonOptions = {
471 type: 'next',
472 handler: options.nextVideo,
473 isDisabled: () => {
474 if (!options.hasNextVideo) return false
475
476 return !options.hasNextVideo()
1dc240a9 477 }
a950e4c8
C
478 }
479
480 Object.assign(children, {
481 'nextVideoButton': buttonOptions
1dc240a9
RK
482 })
483 }
484
485 Object.assign(children, {
2adfc7ea
C
486 'currentTimeDisplay': {},
487 'timeDivider': {},
488 'durationDisplay': {},
489 'liveDisplay': {},
490
491 'flexibleWidthSpacer': {},
492 'progressControl': {
493 children: {
494 'seekBar': {
495 children: {
496 [loadProgressBar]: {},
497 'mouseTimeDisplay': {},
498 'playProgressBar': {}
499 }
500 }
501 }
502 },
503
504 'p2PInfoButton': {},
505
506 'muteToggle': {},
507 'volumeControl': {},
508
509 'settingsButton': {
510 setup: {
511 maxHeightOffset: 40
512 },
513 entries: settingEntries
514 }
1dc240a9 515 })
2adfc7ea
C
516
517 if (options.peertubeLink === true) {
518 Object.assign(children, {
9162fdd3 519 'peerTubeLinkButton': { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
2adfc7ea
C
520 })
521 }
522
3d9a63d3 523 if (options.theaterButton === true) {
2adfc7ea
C
524 Object.assign(children, {
525 'theaterButton': {}
526 })
527 }
528
529 Object.assign(children, {
530 'fullscreenToggle': {}
531 })
532
533 return children
534 }
535
9162fdd3
C
536 private static addContextMenu (options: {
537 mode: PlayerMode
538 player: videojs.Player
539 videoShortUUID: string
540 videoEmbedUrl: string
541 videoEmbedTitle: string
542 }) {
543 const { mode, player, videoEmbedTitle, videoEmbedUrl, videoShortUUID } = options
544
a472cf03 545 const content = () => {
3e0e8d4a
C
546 const isLoopEnabled = player.options_['loop']
547 const items = [
548 {
549 icon: 'repeat',
550 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
551 listener: function () {
552 player.options_['loop'] = !isLoopEnabled
a472cf03 553 }
3e0e8d4a
C
554 },
555 {
556 label: player.localize('Copy the video URL'),
557 listener: function () {
9162fdd3 558 copyToClipboard(buildVideoLink({ shortUUID: videoShortUUID }))
3e0e8d4a
C
559 }
560 },
561 {
562 label: player.localize('Copy the video URL at the current time'),
563 listener: function (this: videojs.Player) {
9162fdd3
C
564 const url = buildVideoLink({ shortUUID: videoShortUUID })
565
566 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
3e0e8d4a
C
567 }
568 },
569 {
570 icon: 'code',
571 label: player.localize('Copy embed code'),
572 listener: () => {
573 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle))
a472cf03 574 }
2adfc7ea 575 }
3e0e8d4a 576 ]
a472cf03
RK
577
578 if (mode === 'webtorrent') {
579 items.push({
580 label: player.localize('Copy magnet URI'),
581 listener: function (this: videojs.Player) {
582 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
583 }
584 })
2adfc7ea 585 }
2adfc7ea 586
ff563914
RK
587 items.push({
588 icon: 'info',
589 label: player.localize('Stats for nerds'),
590 listener: () => {
591 player.stats().show()
592 }
593 })
594
83ff5481
RK
595 return items.map(i => ({
596 ...i,
597 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
598 }))
2adfc7ea
C
599 }
600
a472cf03 601 // adding the menu
2adfc7ea
C
602 player.contextmenuUI({ content })
603 }
604
39aad8cc 605 private static addHotkeysOptions (plugins: VideoJSPluginOptions) {
e0b59721 606 const isNaked = (event: KeyboardEvent, key: string) =>
607 (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key)
608
39aad8cc
C
609 Object.assign(plugins, {
610 hotkeys: {
7ede74ad
C
611 skipInitialFocus: true,
612 enableInactiveFocus: false,
613 captureDocumentHotkeys: true,
614 documentHotkeysFocusElementFilter: (e: HTMLElement) => {
e85bfe96
C
615 const tagName = e.tagName.toLowerCase()
616 return e.id === 'content' || tagName === 'body' || tagName === 'video'
7ede74ad
C
617 },
618
39aad8cc
C
619 enableVolumeScroll: false,
620 enableModifiersForNumbers: false,
621
684cdacb 622 rewindKey: function (event: KeyboardEvent) {
623 return isNaked(event, 'ArrowLeft')
624 },
625
626 forwardKey: function (event: KeyboardEvent) {
627 return isNaked(event, 'ArrowRight')
628 },
629
39aad8cc
C
630 fullscreenKey: function (event: KeyboardEvent) {
631 // fullscreen with the f key or Ctrl+Enter
e0b59721 632 return isNaked(event, 'f') || (!event.altKey && event.ctrlKey && event.key === 'Enter')
39aad8cc
C
633 },
634
39aad8cc
C
635 customKeys: {
636 increasePlaybackRateKey: {
637 key: function (event: KeyboardEvent) {
e0b59721 638 return isNaked(event, '>')
39aad8cc
C
639 },
640 handler: function (player: videojs.Player) {
f5fcd9f7
C
641 const newValue = Math.min(player.playbackRate() + 0.1, 5)
642 player.playbackRate(parseFloat(newValue.toFixed(2)))
39aad8cc
C
643 }
644 },
645 decreasePlaybackRateKey: {
646 key: function (event: KeyboardEvent) {
e0b59721 647 return isNaked(event, '<')
39aad8cc
C
648 },
649 handler: function (player: videojs.Player) {
f5fcd9f7
C
650 const newValue = Math.max(player.playbackRate() - 0.1, 0.10)
651 player.playbackRate(parseFloat(newValue.toFixed(2)))
39aad8cc
C
652 }
653 },
654 frameByFrame: {
655 key: function (event: KeyboardEvent) {
e0b59721 656 return isNaked(event, '.')
39aad8cc
C
657 },
658 handler: function (player: videojs.Player) {
659 player.pause()
660 // Calculate movement distance (assuming 30 fps)
661 const dist = 1 / 30
662 player.currentTime(player.currentTime() + dist)
663 }
664 }
665 }
666 }
667 })
668 }
64228474 669
72efdda5
C
670 private static getAutoPlayValue (autoplay: any) {
671 if (autoplay !== true) return autoplay
672
ebc8dd52
C
673 // On first play, disable autoplay to avoid issues
674 // But if the player already played videos, we can safely autoplay next ones
675 if (isIOS() || isSafari()) {
9eccae74
C
676 return PeertubePlayerManager.alreadyPlayed ? 'play' : false
677 }
64228474
C
678
679 return 'play'
680 }
2adfc7ea
C
681}
682
683// ############################################################################
684
685export {
686 videojs
687}