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