1 import videojs from 'video.js'
2 import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
3 import { PluginsManager } from '@root-helpers/plugins-manager'
4 import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
5 import { isDefaultLocale } from '@shared/core-utils/i18n'
6 import { VideoFile } from '@shared/models'
7 import { copyToClipboard } from '../../root-helpers/utils'
8 import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
9 import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
10 import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
11 import { getAverageBandwidthInStore } from './peertube-player-local-storage'
13 NextPreviousVideoButtonOptions,
14 P2PMediaLoaderPluginOptions,
15 PeerTubeLinkButtonOptions,
16 PlaylistPluginOptions,
20 } from './peertube-videojs-typings'
21 import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
23 export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
25 export type WebtorrentOptions = {
26 videoFiles: VideoFile[]
29 export type P2PMediaLoaderOptions = {
31 segmentsSha256Url: string
32 trackerAnnounce: string[]
33 redundancyBaseUrls: string[]
34 videoFiles: VideoFile[]
37 export interface CustomizationOptions {
38 startTime: number | string
39 stopTime: number | string
50 export interface CommonOptions extends CustomizationOptions {
51 playerElement: HTMLVideoElement
52 onPlayerElementChange: (element: HTMLVideoElement) => void
57 nextVideo?: () => void
58 hasNextVideo?: () => boolean
60 previousVideo?: () => void
61 hasPreviousVideo?: () => boolean
63 playlist?: PlaylistPluginOptions
66 enableHotkeys: boolean
67 inactivityTimeout: number
70 theaterButton: boolean
81 videoCaptions: VideoJSCaption[]
84 videoShortUUID: string
86 userWatching?: UserWatching
90 errorNotifier: (message: string) => void
93 export type PeertubePlayerManagerOptions = {
95 webtorrent: WebtorrentOptions
96 p2pMediaLoader?: P2PMediaLoaderOptions
98 pluginsManager: PluginsManager
101 export class PeertubePlayerOptionsBuilder {
104 private mode: PlayerMode,
105 private options: PeertubePlayerManagerOptions,
106 private p2pMediaLoaderModule?: any
111 getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
112 const commonOptions = this.options.common
113 const isHLS = this.mode === 'p2p-media-loader'
115 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
117 preloadTextTracks: false
120 const plugins: VideoJSPluginOptions = {
123 autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
124 videoViewUrl: commonOptions.videoViewUrl,
125 videoDuration: commonOptions.videoDuration,
126 userWatching: commonOptions.userWatching,
127 subtitle: commonOptions.subtitle,
128 videoCaptions: commonOptions.videoCaptions,
129 stopTime: commonOptions.stopTime,
130 isLive: commonOptions.isLive,
131 videoUUID: commonOptions.videoUUID
135 if (commonOptions.playlist) {
136 plugins.playlist = commonOptions.playlist
140 const { hlsjs } = this.addP2PMediaLoaderOptions(plugins)
142 Object.assign(html5, hlsjs.html5)
145 if (this.mode === 'webtorrent') {
146 this.addWebTorrentOptions(plugins, alreadyPlayed)
148 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
152 const videojsOptions = {
155 // We don't use text track settings for now
156 textTrackSettings: false as any, // FIXME: typings
157 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
158 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
160 muted: commonOptions.muted !== undefined
161 ? commonOptions.muted
162 : undefined, // Undefined so the player knows it has to check the local storage
164 autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
166 poster: commonOptions.poster,
167 inactivityTimeout: commonOptions.inactivityTimeout,
168 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
173 children: this.getControlBarChildren(this.mode, {
174 videoShortUUID: commonOptions.videoShortUUID,
175 p2pEnabled: commonOptions.p2pEnabled,
177 captions: commonOptions.captions,
178 peertubeLink: commonOptions.peertubeLink,
179 theaterButton: commonOptions.theaterButton,
181 nextVideo: commonOptions.nextVideo,
182 hasNextVideo: commonOptions.hasNextVideo,
184 previousVideo: commonOptions.previousVideo,
185 hasPreviousVideo: commonOptions.hasPreviousVideo
186 }) as any // FIXME: typings
190 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
191 Object.assign(videojsOptions, { language: commonOptions.language })
194 return videojsOptions
197 private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) {
198 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
199 const commonOptions = this.options.common
201 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
202 .filter(t => t.startsWith('ws'))
204 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
206 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
207 redundancyUrlManager,
208 type: 'application/x-mpegURL',
209 startTime: commonOptions.startTime,
210 src: p2pMediaLoaderOptions.playlistUrl
213 let consumeOnly = false
214 if ((navigator as any)?.connection?.type === 'cellular') {
215 console.log('We are on a cellular connection: disabling seeding.')
219 const p2pMediaLoaderConfig: HlsJsEngineSettings = {
222 segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive),
223 rtcConfig: getRtcConfig(),
224 requiredSegmentsPriority: 1,
225 simultaneousHttpDownloads: 1,
226 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
227 useP2P: commonOptions.p2pEnabled,
231 swarmId: p2pMediaLoaderOptions.playlistUrl
236 levelLabelHandler: (level: { height: number, width: number }) => {
237 const resolution = Math.min(level.height || 0, level.width || 0)
239 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution)
240 // We don't have files for live videos
241 if (!file) return level.height
243 let label = file.resolution.label
244 if (file.fps >= 50) label += file.fps
249 hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig)
253 const toAssign = { p2pMediaLoader, hlsjs }
254 Object.assign(plugins, toAssign)
259 private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
261 capLevelToPlayerSize: true,
262 autoStartLoad: false,
263 liveSyncDurationCount: 5,
265 loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
268 const averageBandwidth = getAverageBandwidthInStore()
269 if (!averageBandwidth) return base
274 abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
276 testBandwidth: false,
281 private addWebTorrentOptions (plugins: VideoJSPluginOptions, alreadyPlayed: boolean) {
282 const commonOptions = this.options.common
283 const webtorrentOptions = this.options.webtorrent
284 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
286 const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play'
291 playerRefusedP2P: commonOptions.p2pEnabled === false,
292 videoDuration: commonOptions.videoDuration,
293 playerElement: commonOptions.playerElement,
295 videoFiles: webtorrentOptions.videoFiles.length !== 0
296 ? webtorrentOptions.videoFiles
297 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
298 : p2pMediaLoaderOptions?.videoFiles || [],
300 startTime: commonOptions.startTime
303 Object.assign(plugins, { webtorrent })
306 private getControlBarChildren (mode: PlayerMode, options: {
308 videoShortUUID: string
310 peertubeLink: boolean
311 theaterButton: boolean
314 nextVideo?: () => void
315 hasNextVideo?: () => boolean
317 previousVideo?: () => void
318 hasPreviousVideo?: () => boolean
320 const settingEntries = []
321 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
324 settingEntries.push('playbackRateMenuButton')
325 if (options.captions === true) settingEntries.push('captionsButton')
326 settingEntries.push('resolutionMenuButton')
330 if (options.previousVideo) {
331 const buttonOptions: NextPreviousVideoButtonOptions = {
333 handler: options.previousVideo,
335 if (!options.hasPreviousVideo) return false
337 return !options.hasPreviousVideo()
341 Object.assign(children, {
342 previousVideoButton: buttonOptions
346 Object.assign(children, { playToggle: {} })
348 if (options.nextVideo) {
349 const buttonOptions: NextPreviousVideoButtonOptions = {
351 handler: options.nextVideo,
353 if (!options.hasNextVideo) return false
355 return !options.hasNextVideo()
359 Object.assign(children, {
360 nextVideoButton: buttonOptions
364 Object.assign(children, {
365 currentTimeDisplay: {},
370 flexibleWidthSpacer: {},
375 [loadProgressBar]: {},
376 mouseTimeDisplay: {},
384 p2pEnabled: options.p2pEnabled
394 entries: settingEntries
398 if (options.peertubeLink === true) {
399 Object.assign(children, {
400 peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
404 if (options.theaterButton === true) {
405 Object.assign(children, {
410 Object.assign(children, {
417 private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
418 if (autoplay !== true) return autoplay
420 // On first play, disable autoplay to avoid issues
421 // But if the player already played videos, we can safely autoplay next ones
422 if (isIOS() || isSafari()) {
423 return alreadyPlayed ? 'play' : false
429 getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
430 const content = () => {
431 const isLoopEnabled = player.options_['loop']
436 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
437 listener: function () {
438 player.options_['loop'] = !isLoopEnabled
442 label: player.localize('Copy the video URL'),
443 listener: function () {
444 copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
448 label: player.localize('Copy the video URL at the current time'),
449 listener: function (this: videojs.Player) {
450 const url = buildVideoLink({ shortUUID: commonOptions.videoShortUUID })
452 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
457 label: player.localize('Copy embed code'),
459 copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
464 if (this.mode === 'webtorrent') {
466 label: player.localize('Copy magnet URI'),
467 listener: function (this: videojs.Player) {
468 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
475 label: player.localize('Stats for nerds'),
477 player.stats().show()
481 return items.map(i => ({
483 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label