1 import 'videojs-hotkeys/videojs.hotkeys'
3 import 'videojs-contextmenu-ui'
4 import 'videojs-contrib-quality-levels'
5 import './upnext/end-card'
6 import './upnext/upnext-plugin'
7 import './bezels/bezels-plugin'
8 import './peertube-plugin'
9 import './videojs-components/next-video-button'
10 import './videojs-components/p2p-info-button'
11 import './videojs-components/peertube-link-button'
12 import './videojs-components/peertube-load-progress-bar'
13 import './videojs-components/resolution-menu-button'
14 import './videojs-components/resolution-menu-item'
15 import './videojs-components/settings-dialog'
16 import './videojs-components/settings-menu-button'
17 import './videojs-components/settings-menu-item'
18 import './videojs-components/settings-panel'
19 import './videojs-components/settings-panel-child'
20 import './videojs-components/theater-button'
21 import './playlist/playlist-plugin'
22 import videojs from 'video.js'
23 import { isDefaultLocale } from '@shared/core-utils/i18n'
24 import { VideoFile } from '@shared/models'
25 import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
26 import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
27 import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
28 import { getStoredP2PEnabled } from './peertube-player-local-storage'
30 P2PMediaLoaderPluginOptions,
31 PlaylistPluginOptions,
35 } from './peertube-videojs-typings'
36 import { TranslationsManager } from './translations-manager'
37 import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils'
39 // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
40 (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
42 const CaptionsButton = videojs.getComponent('CaptionsButton') as any
43 // Change Captions to Subtitles/CC
44 CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
45 // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
46 CaptionsButton.prototype.label_ = ' '
48 export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
50 export type WebtorrentOptions = {
51 videoFiles: VideoFile[]
54 export type P2PMediaLoaderOptions = {
56 segmentsSha256Url: string
57 trackerAnnounce: string[]
58 redundancyBaseUrls: string[]
59 videoFiles: VideoFile[]
62 export interface CustomizationOptions {
63 startTime: number | string
64 stopTime: number | string
75 export interface CommonOptions extends CustomizationOptions {
76 playerElement: HTMLVideoElement
77 onPlayerElementChange: (element: HTMLVideoElement) => void
82 playlist?: PlaylistPluginOptions
85 enableHotkeys: boolean
86 inactivityTimeout: number
89 theaterButton: boolean
97 videoCaptions: VideoJSCaption[]
99 userWatching?: UserWatching
104 export type PeertubePlayerManagerOptions = {
105 common: CommonOptions,
106 webtorrent: WebtorrentOptions,
107 p2pMediaLoader?: P2PMediaLoaderOptions
110 export class PeertubePlayerManager {
111 private static playerElementClassName: string
112 private static onPlayerChange: (player: videojs.Player) => void
114 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
115 let p2pMediaLoader: any
117 this.onPlayerChange = onPlayerChange
118 this.playerElementClassName = options.common.playerElement.className
120 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
121 if (mode === 'p2p-media-loader') {
122 [ p2pMediaLoader ] = await Promise.all([
123 import('p2p-media-loader-hlsjs'),
124 import('./p2p-media-loader/p2p-media-loader-plugin')
128 const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader)
130 await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
133 return new Promise(res => {
134 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
137 let alreadyFallback = false
139 player.tech(true).one('error', () => {
140 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
141 alreadyFallback = true
144 player.one('error', () => {
145 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
146 alreadyFallback = true
149 self.addContextMenu(mode, player, options.common.embedUrl)
158 private static async maybeFallbackToWebTorrent (currentMode: PlayerMode, player: any, options: PeertubePlayerManagerOptions) {
159 if (currentMode === 'webtorrent') return
161 console.log('Fallback to webtorrent.')
163 const newVideoElement = document.createElement('video')
164 newVideoElement.className = this.playerElementClassName
166 // VideoJS wraps our video element inside a div
167 let currentParentPlayerElement = options.common.playerElement.parentNode
168 // Fix on IOS, don't ask me why
169 if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(options.common.playerElement.id).parentNode
171 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
173 options.common.playerElement = newVideoElement
174 options.common.onPlayerElementChange(newVideoElement)
178 await import('./webtorrent/webtorrent-plugin')
180 const mode = 'webtorrent'
181 const videojsOptions = this.getVideojsOptions(mode, options)
184 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
187 self.addContextMenu(mode, player, options.common.embedUrl)
189 PeertubePlayerManager.onPlayerChange(player)
193 private static getVideojsOptions (
195 options: PeertubePlayerManagerOptions,
196 p2pMediaLoaderModule?: any
197 ): videojs.PlayerOptions {
198 const commonOptions = options.common
200 let autoplay = this.getAutoPlayValue(commonOptions.autoplay)
203 const plugins: VideoJSPluginOptions = {
206 autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
207 videoViewUrl: commonOptions.videoViewUrl,
208 videoDuration: commonOptions.videoDuration,
209 userWatching: commonOptions.userWatching,
210 subtitle: commonOptions.subtitle,
211 videoCaptions: commonOptions.videoCaptions,
212 stopTime: commonOptions.stopTime
216 if (commonOptions.playlist) {
217 plugins.playlist = commonOptions.playlist
220 if (commonOptions.enableHotkeys === true) {
221 PeertubePlayerManager.addHotkeysOptions(plugins)
224 if (mode === 'p2p-media-loader') {
225 const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
230 if (mode === 'webtorrent') {
231 PeertubePlayerManager.addWebTorrentOptions(plugins, options)
233 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
237 const videojsOptions = {
240 // We don't use text track settings for now
241 textTrackSettings: false as any, // FIXME: typings
242 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
243 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
245 muted: commonOptions.muted !== undefined
246 ? commonOptions.muted
247 : undefined, // Undefined so the player knows it has to check the local storage
249 autoplay: this.getAutoPlayValue(autoplay),
251 poster: commonOptions.poster,
252 inactivityTimeout: commonOptions.inactivityTimeout,
253 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
258 children: this.getControlBarChildren(mode, {
259 captions: commonOptions.captions,
260 peertubeLink: commonOptions.peertubeLink,
261 theaterButton: commonOptions.theaterButton,
262 nextVideo: commonOptions.nextVideo
263 }) as any // FIXME: typings
267 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
268 Object.assign(videojsOptions, { language: commonOptions.language })
271 return videojsOptions
274 private static addP2PMediaLoaderOptions (
275 plugins: VideoJSPluginOptions,
276 options: PeertubePlayerManagerOptions,
277 p2pMediaLoaderModule: any
279 const p2pMediaLoaderOptions = options.p2pMediaLoader
280 const commonOptions = options.common
282 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
283 .filter(t => t.startsWith('ws'))
285 const redundancyUrlManager = new RedundancyUrlManager(options.p2pMediaLoader.redundancyBaseUrls)
287 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
288 redundancyUrlManager,
289 type: 'application/x-mpegURL',
290 startTime: commonOptions.startTime,
291 src: p2pMediaLoaderOptions.playlistUrl
294 let consumeOnly = false
296 if (navigator && (navigator as any).connection && (navigator as any).connection.type === 'cellular') {
297 console.log('We are on a cellular connection: disabling seeding.')
301 const p2pMediaLoaderConfig = {
304 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url),
305 rtcConfig: getRtcConfig(),
306 requiredSegmentsPriority: 5,
307 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
308 useP2P: getStoredP2PEnabled(),
312 swarmId: p2pMediaLoaderOptions.playlistUrl
316 levelLabelHandler: (level: { height: number, width: number }) => {
317 const resolution = Math.min(level.height || 0, level.width || 0)
319 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
321 console.error('Cannot find video file for level %d.', level.height)
325 let label = file.resolution.label
326 if (file.fps >= 50) label += file.fps
332 capLevelToPlayerSize: true,
333 autoStartLoad: false,
334 liveSyncDurationCount: 7,
335 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
340 const toAssign = { p2pMediaLoader, hlsjs }
341 Object.assign(plugins, toAssign)
346 private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) {
347 const commonOptions = options.common
348 const webtorrentOptions = options.webtorrent
351 autoplay: commonOptions.autoplay,
352 videoDuration: commonOptions.videoDuration,
353 playerElement: commonOptions.playerElement,
354 videoFiles: webtorrentOptions.videoFiles,
355 startTime: commonOptions.startTime
358 Object.assign(plugins, { webtorrent })
361 private static getControlBarChildren (mode: PlayerMode, options: {
362 peertubeLink: boolean
363 theaterButton: boolean,
367 const settingEntries = []
368 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
371 settingEntries.push('playbackRateMenuButton')
372 if (options.captions === true) settingEntries.push('captionsButton')
373 settingEntries.push('resolutionMenuButton')
379 if (options.nextVideo) {
380 Object.assign(children, {
382 handler: options.nextVideo
387 Object.assign(children, {
388 'currentTimeDisplay': {},
390 'durationDisplay': {},
393 'flexibleWidthSpacer': {},
398 [loadProgressBar]: {},
399 'mouseTimeDisplay': {},
400 'playProgressBar': {}
415 entries: settingEntries
419 if (options.peertubeLink === true) {
420 Object.assign(children, {
421 'peerTubeLinkButton': {}
425 if (options.theaterButton === true) {
426 Object.assign(children, {
431 Object.assign(children, {
432 'fullscreenToggle': {}
438 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string) {
441 label: player.localize('Copy the video URL'),
442 listener: function () {
443 copyToClipboard(buildVideoLink())
447 label: player.localize('Copy the video URL at the current time'),
448 listener: function (this: videojs.Player) {
449 copyToClipboard(buildVideoLink({ startTime: this.currentTime() }))
453 label: player.localize('Copy embed code'),
455 copyToClipboard(buildVideoEmbed(videoEmbedUrl))
460 if (mode === 'webtorrent') {
462 label: player.localize('Copy magnet URI'),
463 listener: function (this: videojs.Player) {
464 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
469 player.contextmenuUI({ content })
472 private static addHotkeysOptions (plugins: VideoJSPluginOptions) {
473 Object.assign(plugins, {
475 skipInitialFocus: true,
476 enableInactiveFocus: false,
477 captureDocumentHotkeys: true,
478 documentHotkeysFocusElementFilter: (e: HTMLElement) => {
479 const tagName = e.tagName.toLowerCase()
480 return e.id === 'content' || tagName === 'body' || tagName === 'video'
483 enableVolumeScroll: false,
484 enableModifiersForNumbers: false,
486 fullscreenKey: function (event: KeyboardEvent) {
487 // fullscreen with the f key or Ctrl+Enter
488 return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
491 seekStep: function (event: KeyboardEvent) {
492 // mimic VLC seek behavior, and default to 5 (original value is 5).
493 if (event.ctrlKey && event.altKey) {
495 } else if (event.ctrlKey) {
497 } else if (event.altKey) {
505 increasePlaybackRateKey: {
506 key: function (event: KeyboardEvent) {
507 return event.key === '>'
509 handler: function (player: videojs.Player) {
510 const newValue = Math.min(player.playbackRate() + 0.1, 5)
511 player.playbackRate(parseFloat(newValue.toFixed(2)))
514 decreasePlaybackRateKey: {
515 key: function (event: KeyboardEvent) {
516 return event.key === '<'
518 handler: function (player: videojs.Player) {
519 const newValue = Math.max(player.playbackRate() - 0.1, 0.10)
520 player.playbackRate(parseFloat(newValue.toFixed(2)))
524 key: function (event: KeyboardEvent) {
525 return event.key === '.'
527 handler: function (player: videojs.Player) {
529 // Calculate movement distance (assuming 30 fps)
531 player.currentTime(player.currentTime() + dist)
539 private static getAutoPlayValue (autoplay: any) {
540 if (autoplay !== true) return autoplay
542 // Giving up with iOS
543 if (isIOS()) return false
545 // We have issues with autoplay and Safari.
546 // any that tries to play using auto mute seems to work
547 if (isSafari()) return 'any'
553 // ############################################################################