aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-02 11:16:23 +0100
committerChocobozzz <me@florianbigard.com>2022-02-02 11:26:18 +0100
commitc4207f978e23c77f09c4646b940dfd532281300f (patch)
treeb11f459839dced708a80f6cff5d57e49ecf45917
parentb25fdc73fdf22896093e12c51bb64160c0410879 (diff)
downloadPeerTube-c4207f978e23c77f09c4646b940dfd532281300f.tar.gz
PeerTube-c4207f978e23c77f09c4646b940dfd532281300f.tar.zst
PeerTube-c4207f978e23c77f09c4646b940dfd532281300f.zip
Fast forward on HLS decode error
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts8
-rw-r--r--client/src/assets/player/index.ts2
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts6
-rw-r--r--client/src/assets/player/peertube-player-manager.ts602
-rw-r--r--client/src/assets/player/peertube-player-options-builder.ts489
-rw-r--r--client/src/assets/player/peertube-plugin.ts8
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts2
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts12
-rw-r--r--client/src/standalone/videos/embed.ts9
-rwxr-xr-xscripts/i18n/create-custom-files.ts3
10 files changed, 601 insertions, 540 deletions
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 1ee18f52f..1f45c4d26 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -33,7 +33,6 @@ import {
33 VideoPrivacy, 33 VideoPrivacy,
34 VideoState 34 VideoState
35} from '@shared/models' 35} from '@shared/models'
36import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
37import { 36import {
38 CustomizationOptions, 37 CustomizationOptions,
39 P2PMediaLoaderOptions, 38 P2PMediaLoaderOptions,
@@ -41,7 +40,8 @@ import {
41 PeertubePlayerManagerOptions, 40 PeertubePlayerManagerOptions,
42 PlayerMode, 41 PlayerMode,
43 videojs 42 videojs
44} from '../../../assets/player/peertube-player-manager' 43} from '../../../assets/player'
44import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
45import { environment } from '../../../environments/environment' 45import { environment } from '../../../environments/environment'
46import { VideoWatchPlaylistComponent } from './shared' 46import { VideoWatchPlaylistComponent } from './shared'
47 47
@@ -612,7 +612,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
612 videoCaptions: playerCaptions, 612 videoCaptions: playerCaptions,
613 613
614 videoShortUUID: video.shortUUID, 614 videoShortUUID: video.shortUUID,
615 videoUUID: video.uuid 615 videoUUID: video.uuid,
616
617 errorNotifier: (message: string) => this.notifier.error(message)
616 }, 618 },
617 619
618 webtorrent: { 620 webtorrent: {
diff --git a/client/src/assets/player/index.ts b/client/src/assets/player/index.ts
new file mode 100644
index 000000000..e2a6ccf24
--- /dev/null
+++ b/client/src/assets/player/index.ts
@@ -0,0 +1,2 @@
1export * from './peertube-player-manager'
2export * from './peertube-player-options-builder'
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
index 640858025..ae31bcfe1 100644
--- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -174,6 +174,12 @@ class Html5Hlsjs {
174 dispose () { 174 dispose () {
175 this.videoElement.removeEventListener('play', this.handlers.play) 175 this.videoElement.removeEventListener('play', this.handlers.play)
176 176
177 // FIXME: https://github.com/video-dev/hls.js/issues/4092
178 const untypedHLS = this.hls as any
179 untypedHLS.log = untypedHLS.warn = () => {
180 // empty
181 }
182
177 this.hls.destroy() 183 this.hls.destroy()
178 } 184 }
179 185
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index b9a289aa0..2ef42a961 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -24,28 +24,12 @@ import './mobile/peertube-mobile-plugin'
24import './mobile/peertube-mobile-buttons' 24import './mobile/peertube-mobile-buttons'
25import './hotkeys/peertube-hotkeys-plugin' 25import './hotkeys/peertube-hotkeys-plugin'
26import videojs from 'video.js' 26import videojs from 'video.js'
27import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
28import { PluginsManager } from '@root-helpers/plugins-manager' 27import { PluginsManager } from '@root-helpers/plugins-manager'
29import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' 28import { saveAverageBandwidth } from './peertube-player-local-storage'
30import { isDefaultLocale } from '@shared/core-utils/i18n' 29import { CommonOptions, PeertubePlayerManagerOptions, PeertubePlayerOptionsBuilder, PlayerMode } from './peertube-player-options-builder'
31import { VideoFile } from '@shared/models' 30import { PlayerNetworkInfo } from './peertube-videojs-typings'
32import { copyToClipboard } from '../../root-helpers/utils'
33import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
34import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
35import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
36import { getAverageBandwidthInStore, saveAverageBandwidth } from './peertube-player-local-storage'
37import {
38 NextPreviousVideoButtonOptions,
39 P2PMediaLoaderPluginOptions,
40 PeerTubeLinkButtonOptions,
41 PlayerNetworkInfo,
42 PlaylistPluginOptions,
43 UserWatching,
44 VideoJSCaption,
45 VideoJSPluginOptions
46} from './peertube-videojs-typings'
47import { TranslationsManager } from './translations-manager' 31import { TranslationsManager } from './translations-manager'
48import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isMobile, isSafari } from './utils' 32import { isMobile } from './utils'
49 33
50// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) 34// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
51(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' 35(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
@@ -56,112 +40,49 @@ CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
56// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) 40// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
57CaptionsButton.prototype.label_ = ' ' 41CaptionsButton.prototype.label_ = ' '
58 42
59export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
60
61export type WebtorrentOptions = {
62 videoFiles: VideoFile[]
63}
64
65export type P2PMediaLoaderOptions = {
66 playlistUrl: string
67 segmentsSha256Url: string
68 trackerAnnounce: string[]
69 redundancyBaseUrls: string[]
70 videoFiles: VideoFile[]
71}
72
73export interface CustomizationOptions {
74 startTime: number | string
75 stopTime: number | string
76
77 controls?: boolean
78 muted?: boolean
79 loop?: boolean
80 subtitle?: string
81 resume?: string
82
83 peertubeLink: boolean
84}
85
86export interface CommonOptions extends CustomizationOptions {
87 playerElement: HTMLVideoElement
88 onPlayerElementChange: (element: HTMLVideoElement) => void
89
90 autoplay: boolean
91 p2pEnabled: boolean
92
93 nextVideo?: () => void
94 hasNextVideo?: () => boolean
95
96 previousVideo?: () => void
97 hasPreviousVideo?: () => boolean
98
99 playlist?: PlaylistPluginOptions
100
101 videoDuration: number
102 enableHotkeys: boolean
103 inactivityTimeout: number
104 poster: string
105
106 theaterButton: boolean
107 captions: boolean
108
109 videoViewUrl: string
110 embedUrl: string
111 embedTitle: string
112
113 isLive: boolean
114
115 language?: string
116
117 videoCaptions: VideoJSCaption[]
118
119 videoUUID: string
120 videoShortUUID: string
121
122 userWatching?: UserWatching
123
124 serverUrl: string
125}
126
127export type PeertubePlayerManagerOptions = {
128 common: CommonOptions
129 webtorrent: WebtorrentOptions
130 p2pMediaLoader?: P2PMediaLoaderOptions
131
132 pluginsManager: PluginsManager
133}
134
135export class PeertubePlayerManager { 43export class PeertubePlayerManager {
136 private static playerElementClassName: string 44 private static playerElementClassName: string
137 private static onPlayerChange: (player: videojs.Player) => void 45 private static onPlayerChange: (player: videojs.Player) => void
138 private static alreadyPlayed = false 46 private static alreadyPlayed = false
139 private static pluginsManager: PluginsManager 47 private static pluginsManager: PluginsManager
140 48
49 private static videojsDecodeErrors = 0
50
51 private static p2pMediaLoaderModule: any
52
141 static initState () { 53 static initState () {
142 PeertubePlayerManager.alreadyPlayed = false 54 this.alreadyPlayed = false
143 } 55 }
144 56
145 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) { 57 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
146 this.pluginsManager = options.pluginsManager 58 this.pluginsManager = options.pluginsManager
147 59
148 let p2pMediaLoader: any
149
150 this.onPlayerChange = onPlayerChange 60 this.onPlayerChange = onPlayerChange
151 this.playerElementClassName = options.common.playerElement.className 61 this.playerElementClassName = options.common.playerElement.className
152 62
153 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') 63 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
154 if (mode === 'p2p-media-loader') { 64 if (mode === 'p2p-media-loader') {
155 [ p2pMediaLoader ] = await Promise.all([ 65 const [ p2pMediaLoaderModule ] = await Promise.all([
156 import('@peertube/p2p-media-loader-hlsjs'), 66 import('@peertube/p2p-media-loader-hlsjs'),
157 import('./p2p-media-loader/p2p-media-loader-plugin') 67 import('./p2p-media-loader/p2p-media-loader-plugin')
158 ]) 68 ])
159 }
160 69
161 const videojsOptions = await this.getVideojsOptions(mode, options, p2pMediaLoader) 70 this.p2pMediaLoaderModule = p2pMediaLoaderModule
71 }
162 72
163 await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs) 73 await TranslationsManager.loadLocaleInVideoJS(options.common.serverUrl, options.common.language, videojs)
164 74
75 return this.buildPlayer(mode, options)
76 }
77
78 private static async buildPlayer (mode: PlayerMode, options: PeertubePlayerManagerOptions): Promise<videojs.Player> {
79 const videojsOptionsBuilder = new PeertubePlayerOptionsBuilder(mode, options, this.p2pMediaLoaderModule)
80
81 const videojsOptions = await this.pluginsManager.runHook(
82 'filter:internal.player.videojs.options.result',
83 videojsOptionsBuilder.getVideojsOptions(this.alreadyPlayed)
84 )
85
165 const self = this 86 const self = this
166 return new Promise(res => { 87 return new Promise(res => {
167 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { 88 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
@@ -169,27 +90,24 @@ export class PeertubePlayerManager {
169 90
170 let alreadyFallback = false 91 let alreadyFallback = false
171 92
172 player.tech(true).one('error', () => { 93 const handleError = () => {
173 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) 94 if (alreadyFallback) return
174 alreadyFallback = true 95 alreadyFallback = true
175 })
176 96
177 player.one('error', () => { 97 if (mode === 'p2p-media-loader') {
178 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) 98 self.tryToRecoverHLSError(player.error(), player, options)
179 alreadyFallback = true 99 } else {
180 }) 100 self.maybeFallbackToWebTorrent(mode, player, options)
101 }
102 }
103
104 player.one('error', () => handleError())
181 105
182 player.one('play', () => { 106 player.one('play', () => {
183 PeertubePlayerManager.alreadyPlayed = true 107 self.alreadyPlayed = true
184 }) 108 })
185 109
186 self.addContextMenu({ 110 self.addContextMenu(videojsOptionsBuilder, player, options.common)
187 mode,
188 player,
189 videoShortUUID: options.common.videoShortUUID,
190 videoEmbedUrl: options.common.embedUrl,
191 videoEmbedTitle: options.common.embedTitle
192 })
193 111
194 if (isMobile()) player.peertubeMobile() 112 if (isMobile()) player.peertubeMobile()
195 if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() 113 if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin()
@@ -214,437 +132,77 @@ export class PeertubePlayerManager {
214 }) 132 })
215 } 133 }
216 134
217 private static async maybeFallbackToWebTorrent (currentMode: PlayerMode, player: any, options: PeertubePlayerManagerOptions) { 135 private static async tryToRecoverHLSError (err: any, currentPlayer: videojs.Player, options: PeertubePlayerManagerOptions) {
218 if (currentMode === 'webtorrent') return 136 if (err.code === 3) { // Decode error
219
220 console.log('Fallback to webtorrent.')
221
222 const newVideoElement = document.createElement('video')
223 newVideoElement.className = this.playerElementClassName
224
225 // VideoJS wraps our video element inside a div
226 let currentParentPlayerElement = options.common.playerElement.parentNode
227 // Fix on IOS, don't ask me why
228 if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(options.common.playerElement.id).parentNode
229
230 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
231
232 options.common.playerElement = newVideoElement
233 options.common.onPlayerElementChange(newVideoElement)
234
235 player.dispose()
236
237 await import('./webtorrent/webtorrent-plugin')
238
239 const mode = 'webtorrent'
240 const videojsOptions = await this.getVideojsOptions(mode, options)
241 137
242 const self = this 138 // Display a notification to user
243 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { 139 if (this.videojsDecodeErrors === 0) {
244 const player = this 140 options.common.errorNotifier(currentPlayer.localize('The video failed to play, will try to fast forward.'))
245
246 self.addContextMenu({
247 mode,
248 player,
249 videoShortUUID: options.common.videoShortUUID,
250 videoEmbedUrl: options.common.embedUrl,
251 videoEmbedTitle: options.common.embedTitle
252 })
253
254 PeertubePlayerManager.onPlayerChange(player)
255 })
256 }
257
258 private static async getVideojsOptions (
259 mode: PlayerMode,
260 options: PeertubePlayerManagerOptions,
261 p2pMediaLoaderModule?: any
262 ): Promise<videojs.PlayerOptions> {
263 const commonOptions = options.common
264 const isHLS = mode === 'p2p-media-loader'
265
266 let autoplay = this.getAutoPlayValue(commonOptions.autoplay)
267 const html5 = {
268 preloadTextTracks: false
269 }
270
271 const plugins: VideoJSPluginOptions = {
272 peertube: {
273 mode,
274 autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent
275 videoViewUrl: commonOptions.videoViewUrl,
276 videoDuration: commonOptions.videoDuration,
277 userWatching: commonOptions.userWatching,
278 subtitle: commonOptions.subtitle,
279 videoCaptions: commonOptions.videoCaptions,
280 stopTime: commonOptions.stopTime,
281 isLive: commonOptions.isLive,
282 videoUUID: commonOptions.videoUUID
283 } 141 }
284 }
285
286 if (commonOptions.playlist) {
287 plugins.playlist = commonOptions.playlist
288 }
289
290 if (isHLS) {
291 const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
292
293 Object.assign(html5, hlsjs.html5)
294 }
295
296 if (mode === 'webtorrent') {
297 PeertubePlayerManager.addWebTorrentOptions(plugins, options)
298
299 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
300 autoplay = false
301 }
302
303 const videojsOptions = {
304 html5,
305
306 // We don't use text track settings for now
307 textTrackSettings: false as any, // FIXME: typings
308 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
309 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
310
311 muted: commonOptions.muted !== undefined
312 ? commonOptions.muted
313 : undefined, // Undefined so the player knows it has to check the local storage
314
315 autoplay: this.getAutoPlayValue(autoplay),
316
317 poster: commonOptions.poster,
318 inactivityTimeout: commonOptions.inactivityTimeout,
319 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
320
321 plugins,
322
323 controlBar: {
324 children: this.getControlBarChildren(mode, {
325 videoShortUUID: commonOptions.videoShortUUID,
326 p2pEnabled: commonOptions.p2pEnabled,
327
328 captions: commonOptions.captions,
329 peertubeLink: commonOptions.peertubeLink,
330 theaterButton: commonOptions.theaterButton,
331
332 nextVideo: commonOptions.nextVideo,
333 hasNextVideo: commonOptions.hasNextVideo,
334 142
335 previousVideo: commonOptions.previousVideo, 143 if (this.videojsDecodeErrors === 20) {
336 hasPreviousVideo: commonOptions.hasPreviousVideo 144 this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
337 }) as any // FIXME: typings 145 return
338 } 146 }
339 }
340
341 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
342 Object.assign(videojsOptions, { language: commonOptions.language })
343 }
344
345 return this.pluginsManager.runHook('filter:internal.player.videojs.options.result', videojsOptions)
346 }
347
348 private static addP2PMediaLoaderOptions (
349 plugins: VideoJSPluginOptions,
350 options: PeertubePlayerManagerOptions,
351 p2pMediaLoaderModule: any
352 ) {
353 const p2pMediaLoaderOptions = options.p2pMediaLoader
354 const commonOptions = options.common
355
356 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
357 .filter(t => t.startsWith('ws'))
358
359 const redundancyUrlManager = new RedundancyUrlManager(options.p2pMediaLoader.redundancyBaseUrls)
360
361 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
362 redundancyUrlManager,
363 type: 'application/x-mpegURL',
364 startTime: commonOptions.startTime,
365 src: p2pMediaLoaderOptions.playlistUrl
366 }
367 147
368 let consumeOnly = false 148 console.log('Fast forwarding HLS to recover from an error.')
369 if ((navigator as any)?.connection?.type === 'cellular') {
370 console.log('We are on a cellular connection: disabling seeding.')
371 consumeOnly = true
372 }
373
374 const p2pMediaLoaderConfig: HlsJsEngineSettings = {
375 loader: {
376 trackerAnnounce,
377 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url, options.common.isLive),
378 rtcConfig: getRtcConfig(),
379 requiredSegmentsPriority: 1,
380 simultaneousHttpDownloads: 1,
381 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1),
382 useP2P: commonOptions.p2pEnabled,
383 consumeOnly
384 },
385 segments: {
386 swarmId: p2pMediaLoaderOptions.playlistUrl
387 }
388 }
389
390 const hlsjs = {
391 levelLabelHandler: (level: { height: number, width: number }) => {
392 const resolution = Math.min(level.height || 0, level.width || 0)
393 149
394 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === resolution) 150 this.videojsDecodeErrors++
395 // We don't have files for live videos
396 if (!file) return level.height
397 151
398 let label = file.resolution.label 152 options.common.startTime = currentPlayer.currentTime() + 2
399 if (file.fps >= 50) label += file.fps 153 options.common.autoplay = true
154 this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
400 155
401 return label 156 const newPlayer = await this.buildPlayer('p2p-media-loader', options)
402 }, 157 this.onPlayerChange(newPlayer)
403 html5: { 158 } else {
404 hlsjsConfig: this.getHLSOptions(p2pMediaLoaderModule, p2pMediaLoaderConfig) 159 this.maybeFallbackToWebTorrent('p2p-media-loader', currentPlayer, options)
405 }
406 } 160 }
407
408 const toAssign = { p2pMediaLoader, hlsjs }
409 Object.assign(plugins, toAssign)
410
411 return toAssign
412 } 161 }
413 162
414 private static getHLSOptions (p2pMediaLoaderModule: any, p2pMediaLoaderConfig: HlsJsEngineSettings) { 163 private static async maybeFallbackToWebTorrent (
415 const base = { 164 currentMode: PlayerMode,
416 capLevelToPlayerSize: true, 165 currentPlayer: videojs.Player,
417 autoStartLoad: false, 166 options: PeertubePlayerManagerOptions
418 liveSyncDurationCount: 5, 167 ) {
419 168 if (options.webtorrent.videoFiles.length === 0 || currentMode === 'webtorrent') {
420 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() 169 currentPlayer.peertube().displayFatalError()
421 } 170 return
422
423 const averageBandwidth = getAverageBandwidthInStore()
424 if (!averageBandwidth) return base
425
426 return {
427 ...base,
428
429 abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
430 startLevel: -1,
431 testBandwidth: false,
432 debug: false
433 }
434 }
435
436 private static addWebTorrentOptions (plugins: VideoJSPluginOptions, options: PeertubePlayerManagerOptions) {
437 const commonOptions = options.common
438 const webtorrentOptions = options.webtorrent
439 const p2pMediaLoaderOptions = options.p2pMediaLoader
440
441 const autoplay = this.getAutoPlayValue(commonOptions.autoplay) === 'play'
442
443 const webtorrent = {
444 autoplay,
445 playerRefusedP2P: commonOptions.p2pEnabled === false,
446 videoDuration: commonOptions.videoDuration,
447 playerElement: commonOptions.playerElement,
448 videoFiles: webtorrentOptions.videoFiles.length !== 0
449 ? webtorrentOptions.videoFiles
450 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
451 : p2pMediaLoaderOptions?.videoFiles || [],
452 startTime: commonOptions.startTime
453 }
454
455 Object.assign(plugins, { webtorrent })
456 }
457
458 private static getControlBarChildren (mode: PlayerMode, options: {
459 p2pEnabled: boolean
460 videoShortUUID: string
461
462 peertubeLink: boolean
463 theaterButton: boolean
464 captions: boolean
465
466 nextVideo?: () => void
467 hasNextVideo?: () => boolean
468
469 previousVideo?: () => void
470 hasPreviousVideo?: () => boolean
471 }) {
472 const settingEntries = []
473 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
474
475 // Keep an order
476 settingEntries.push('playbackRateMenuButton')
477 if (options.captions === true) settingEntries.push('captionsButton')
478 settingEntries.push('resolutionMenuButton')
479
480 const children = {}
481
482 if (options.previousVideo) {
483 const buttonOptions: NextPreviousVideoButtonOptions = {
484 type: 'previous',
485 handler: options.previousVideo,
486 isDisabled: () => {
487 if (!options.hasPreviousVideo) return false
488
489 return !options.hasPreviousVideo()
490 }
491 }
492
493 Object.assign(children, {
494 previousVideoButton: buttonOptions
495 })
496 }
497
498 Object.assign(children, { playToggle: {} })
499
500 if (options.nextVideo) {
501 const buttonOptions: NextPreviousVideoButtonOptions = {
502 type: 'next',
503 handler: options.nextVideo,
504 isDisabled: () => {
505 if (!options.hasNextVideo) return false
506
507 return !options.hasNextVideo()
508 }
509 }
510
511 Object.assign(children, {
512 nextVideoButton: buttonOptions
513 })
514 } 171 }
515 172
516 Object.assign(children, { 173 console.log('Fallback to webtorrent.')
517 currentTimeDisplay: {},
518 timeDivider: {},
519 durationDisplay: {},
520 liveDisplay: {},
521
522 flexibleWidthSpacer: {},
523 progressControl: {
524 children: {
525 seekBar: {
526 children: {
527 [loadProgressBar]: {},
528 mouseTimeDisplay: {},
529 playProgressBar: {}
530 }
531 }
532 }
533 },
534
535 p2PInfoButton: {
536 p2pEnabled: options.p2pEnabled
537 },
538
539 muteToggle: {},
540 volumeControl: {},
541
542 settingsButton: {
543 setup: {
544 maxHeightOffset: 40
545 },
546 entries: settingEntries
547 }
548 })
549
550 if (options.peertubeLink === true) {
551 Object.assign(children, {
552 peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
553 })
554 }
555 174
556 if (options.theaterButton === true) { 175 this.rebuildAndUpdateVideoElement(currentPlayer, options.common)
557 Object.assign(children, {
558 theaterButton: {}
559 })
560 }
561 176
562 Object.assign(children, { 177 await import('./webtorrent/webtorrent-plugin')
563 fullscreenToggle: {}
564 })
565 178
566 return children 179 const newPlayer = await this.buildPlayer('webtorrent', options)
180 this.onPlayerChange(newPlayer)
567 } 181 }
568 182
569 private static addContextMenu (options: { 183 private static rebuildAndUpdateVideoElement (player: videojs.Player, commonOptions: CommonOptions) {
570 mode: PlayerMode 184 const newVideoElement = document.createElement('video')
571 player: videojs.Player 185 newVideoElement.className = this.playerElementClassName
572 videoShortUUID: string
573 videoEmbedUrl: string
574 videoEmbedTitle: string
575 }) {
576 const { mode, player, videoEmbedTitle, videoEmbedUrl, videoShortUUID } = options
577
578 const content = () => {
579 const isLoopEnabled = player.options_['loop']
580 const items = [
581 {
582 icon: 'repeat',
583 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
584 listener: function () {
585 player.options_['loop'] = !isLoopEnabled
586 }
587 },
588 {
589 label: player.localize('Copy the video URL'),
590 listener: function () {
591 copyToClipboard(buildVideoLink({ shortUUID: videoShortUUID }))
592 }
593 },
594 {
595 label: player.localize('Copy the video URL at the current time'),
596 listener: function (this: videojs.Player) {
597 const url = buildVideoLink({ shortUUID: videoShortUUID })
598 186
599 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() })) 187 // VideoJS wraps our video element inside a div
600 } 188 let currentParentPlayerElement = commonOptions.playerElement.parentNode
601 }, 189 // Fix on IOS, don't ask me why
602 { 190 if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(commonOptions.playerElement.id).parentNode
603 icon: 'code',
604 label: player.localize('Copy embed code'),
605 listener: () => {
606 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle))
607 }
608 }
609 ]
610 191
611 if (mode === 'webtorrent') { 192 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
612 items.push({
613 label: player.localize('Copy magnet URI'),
614 listener: function (this: videojs.Player) {
615 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
616 }
617 })
618 }
619 193
620 items.push({ 194 commonOptions.playerElement = newVideoElement
621 icon: 'info', 195 commonOptions.onPlayerElementChange(newVideoElement)
622 label: player.localize('Stats for nerds'),
623 listener: () => {
624 player.stats().show()
625 }
626 })
627 196
628 return items.map(i => ({ 197 player.dispose()
629 ...i,
630 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
631 }))
632 }
633 198
634 // adding the menu 199 return newVideoElement
635 player.contextmenuUI({ content })
636 } 200 }
637 201
638 private static getAutoPlayValue (autoplay: any) { 202 private static addContextMenu (optionsBuilder: PeertubePlayerOptionsBuilder, player: videojs.Player, commonOptions: CommonOptions) {
639 if (autoplay !== true) return autoplay 203 const options = optionsBuilder.getContextMenuOptions(player, commonOptions)
640
641 // On first play, disable autoplay to avoid issues
642 // But if the player already played videos, we can safely autoplay next ones
643 if (isIOS() || isSafari()) {
644 return PeertubePlayerManager.alreadyPlayed ? 'play' : false
645 }
646 204
647 return 'play' 205 player.contextmenuUI(options)
648 } 206 }
649} 207}
650 208
diff --git a/client/src/assets/player/peertube-player-options-builder.ts b/client/src/assets/player/peertube-player-options-builder.ts
new file mode 100644
index 000000000..901f6cd3b
--- /dev/null
+++ b/client/src/assets/player/peertube-player-options-builder.ts
@@ -0,0 +1,489 @@
1import videojs from 'video.js'
2import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
3import { PluginsManager } from '@root-helpers/plugins-manager'
4import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
5import { isDefaultLocale } from '@shared/core-utils/i18n'
6import { VideoFile } from '@shared/models'
7import { copyToClipboard } from '../../root-helpers/utils'
8import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
9import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
10import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
11import { getAverageBandwidthInStore } from './peertube-player-local-storage'
12import {
13 NextPreviousVideoButtonOptions,
14 P2PMediaLoaderPluginOptions,
15 PeerTubeLinkButtonOptions,
16 PlaylistPluginOptions,
17 UserWatching,
18 VideoJSCaption,
19 VideoJSPluginOptions
20} from './peertube-videojs-typings'
21import { buildVideoOrPlaylistEmbed, getRtcConfig, isIOS, isSafari } from './utils'
22
23export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
24
25export type WebtorrentOptions = {
26 videoFiles: VideoFile[]
27}
28
29export type P2PMediaLoaderOptions = {
30 playlistUrl: string
31 segmentsSha256Url: string
32 trackerAnnounce: string[]
33 redundancyBaseUrls: string[]
34 videoFiles: VideoFile[]
35}
36
37export interface CustomizationOptions {
38 startTime: number | string
39 stopTime: number | string
40
41 controls?: boolean
42 muted?: boolean
43 loop?: boolean
44 subtitle?: string
45 resume?: string
46
47 peertubeLink: boolean
48}
49
50export interface CommonOptions extends CustomizationOptions {
51 playerElement: HTMLVideoElement
52 onPlayerElementChange: (element: HTMLVideoElement) => void
53
54 autoplay: boolean
55 p2pEnabled: boolean
56
57 nextVideo?: () => void
58 hasNextVideo?: () => boolean
59
60 previousVideo?: () => void
61 hasPreviousVideo?: () => boolean
62
63 playlist?: PlaylistPluginOptions
64
65 videoDuration: number
66 enableHotkeys: boolean
67 inactivityTimeout: number
68 poster: string
69
70 theaterButton: boolean
71 captions: boolean
72
73 videoViewUrl: string
74 embedUrl: string
75 embedTitle: string
76
77 isLive: boolean
78
79 language?: string
80
81 videoCaptions: VideoJSCaption[]
82
83 videoUUID: string
84 videoShortUUID: string
85
86 userWatching?: UserWatching
87
88 serverUrl: string
89
90 errorNotifier: (message: string) => void
91}
92
93export type PeertubePlayerManagerOptions = {
94 common: CommonOptions
95 webtorrent: WebtorrentOptions
96 p2pMediaLoader?: P2PMediaLoaderOptions
97
98 pluginsManager: PluginsManager
99}
100
101export class PeertubePlayerOptionsBuilder {
102
103 constructor (
104 private mode: PlayerMode,
105 private options: PeertubePlayerManagerOptions,
106 private p2pMediaLoaderModule?: any
107 ) {
108
109 }
110
111 getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions {
112 const commonOptions = this.options.common
113 const isHLS = this.mode === 'p2p-media-loader'
114
115 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
116 const html5 = {
117 preloadTextTracks: false
118 }
119
120 const plugins: VideoJSPluginOptions = {
121 peertube: {
122 mode: this.mode,
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
132 }
133 }
134
135 if (commonOptions.playlist) {
136 plugins.playlist = commonOptions.playlist
137 }
138
139 if (isHLS) {
140 const { hlsjs } = this.addP2PMediaLoaderOptions(plugins)
141
142 Object.assign(html5, hlsjs.html5)
143 }
144
145 if (this.mode === 'webtorrent') {
146 this.addWebTorrentOptions(plugins, alreadyPlayed)
147
148 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
149 autoplay = false
150 }
151
152 const videojsOptions = {
153 html5,
154
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,
159
160 muted: commonOptions.muted !== undefined
161 ? commonOptions.muted
162 : undefined, // Undefined so the player knows it has to check the local storage
163
164 autoplay: this.getAutoPlayValue(autoplay, alreadyPlayed),
165
166 poster: commonOptions.poster,
167 inactivityTimeout: commonOptions.inactivityTimeout,
168 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
169
170 plugins,
171
172 controlBar: {
173 children: this.getControlBarChildren(this.mode, {
174 videoShortUUID: commonOptions.videoShortUUID,
175 p2pEnabled: commonOptions.p2pEnabled,
176
177 captions: commonOptions.captions,
178 peertubeLink: commonOptions.peertubeLink,
179 theaterButton: commonOptions.theaterButton,
180
181 nextVideo: commonOptions.nextVideo,
182 hasNextVideo: commonOptions.hasNextVideo,
183
184 previousVideo: commonOptions.previousVideo,
185 hasPreviousVideo: commonOptions.hasPreviousVideo
186 }) as any // FIXME: typings
187 }
188 }
189
190 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
191 Object.assign(videojsOptions, { language: commonOptions.language })
192 }
193
194 return videojsOptions
195 }
196
197 private addP2PMediaLoaderOptions (plugins: VideoJSPluginOptions) {
198 const p2pMediaLoaderOptions = this.options.p2pMediaLoader
199 const commonOptions = this.options.common
200
201 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
202 .filter(t => t.startsWith('ws'))
203
204 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
205
206 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
207 redundancyUrlManager,
208 type: 'application/x-mpegURL',
209 startTime: commonOptions.startTime,
210 src: p2pMediaLoaderOptions.playlistUrl
211 }
212
213 let consumeOnly = false
214 if ((navigator as any)?.connection?.type === 'cellular') {
215 console.log('We are on a cellular connection: disabling seeding.')
216 consumeOnly = true
217 }
218
219 const p2pMediaLoaderConfig: HlsJsEngineSettings = {
220 loader: {
221 trackerAnnounce,
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,
228 consumeOnly
229 },
230 segments: {
231 swarmId: p2pMediaLoaderOptions.playlistUrl
232 }
233 }
234
235 const hlsjs = {
236 levelLabelHandler: (level: { height: number, width: number }) => {
237 const resolution = Math.min(level.height || 0, level.width || 0)
238
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
242
243 let label = file.resolution.label
244 if (file.fps >= 50) label += file.fps
245
246 return label
247 },
248 html5: {
249 hlsjsConfig: this.getHLSOptions(p2pMediaLoaderConfig)
250 }
251 }
252
253 const toAssign = { p2pMediaLoader, hlsjs }
254 Object.assign(plugins, toAssign)
255
256 return toAssign
257 }
258
259 private getHLSOptions (p2pMediaLoaderConfig: HlsJsEngineSettings) {
260 const base = {
261 capLevelToPlayerSize: true,
262 autoStartLoad: false,
263 liveSyncDurationCount: 5,
264
265 loader: new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
266 }
267
268 const averageBandwidth = getAverageBandwidthInStore()
269 if (!averageBandwidth) return base
270
271 return {
272 ...base,
273
274 abrEwmaDefaultEstimate: averageBandwidth * 8, // We want bit/s
275 startLevel: -1,
276 testBandwidth: false,
277 debug: false
278 }
279 }
280
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
285
286 const autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) === 'play'
287
288 const webtorrent = {
289 autoplay,
290
291 playerRefusedP2P: commonOptions.p2pEnabled === false,
292 videoDuration: commonOptions.videoDuration,
293 playerElement: commonOptions.playerElement,
294
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 || [],
299
300 startTime: commonOptions.startTime
301 }
302
303 Object.assign(plugins, { webtorrent })
304 }
305
306 private getControlBarChildren (mode: PlayerMode, options: {
307 p2pEnabled: boolean
308 videoShortUUID: string
309
310 peertubeLink: boolean
311 theaterButton: boolean
312 captions: boolean
313
314 nextVideo?: () => void
315 hasNextVideo?: () => boolean
316
317 previousVideo?: () => void
318 hasPreviousVideo?: () => boolean
319 }) {
320 const settingEntries = []
321 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
322
323 // Keep an order
324 settingEntries.push('playbackRateMenuButton')
325 if (options.captions === true) settingEntries.push('captionsButton')
326 settingEntries.push('resolutionMenuButton')
327
328 const children = {}
329
330 if (options.previousVideo) {
331 const buttonOptions: NextPreviousVideoButtonOptions = {
332 type: 'previous',
333 handler: options.previousVideo,
334 isDisabled: () => {
335 if (!options.hasPreviousVideo) return false
336
337 return !options.hasPreviousVideo()
338 }
339 }
340
341 Object.assign(children, {
342 previousVideoButton: buttonOptions
343 })
344 }
345
346 Object.assign(children, { playToggle: {} })
347
348 if (options.nextVideo) {
349 const buttonOptions: NextPreviousVideoButtonOptions = {
350 type: 'next',
351 handler: options.nextVideo,
352 isDisabled: () => {
353 if (!options.hasNextVideo) return false
354
355 return !options.hasNextVideo()
356 }
357 }
358
359 Object.assign(children, {
360 nextVideoButton: buttonOptions
361 })
362 }
363
364 Object.assign(children, {
365 currentTimeDisplay: {},
366 timeDivider: {},
367 durationDisplay: {},
368 liveDisplay: {},
369
370 flexibleWidthSpacer: {},
371 progressControl: {
372 children: {
373 seekBar: {
374 children: {
375 [loadProgressBar]: {},
376 mouseTimeDisplay: {},
377 playProgressBar: {}
378 }
379 }
380 }
381 },
382
383 p2PInfoButton: {
384 p2pEnabled: options.p2pEnabled
385 },
386
387 muteToggle: {},
388 volumeControl: {},
389
390 settingsButton: {
391 setup: {
392 maxHeightOffset: 40
393 },
394 entries: settingEntries
395 }
396 })
397
398 if (options.peertubeLink === true) {
399 Object.assign(children, {
400 peerTubeLinkButton: { shortUUID: options.videoShortUUID } as PeerTubeLinkButtonOptions
401 })
402 }
403
404 if (options.theaterButton === true) {
405 Object.assign(children, {
406 theaterButton: {}
407 })
408 }
409
410 Object.assign(children, {
411 fullscreenToggle: {}
412 })
413
414 return children
415 }
416
417 private getAutoPlayValue (autoplay: any, alreadyPlayed: boolean) {
418 if (autoplay !== true) return autoplay
419
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
424 }
425
426 return 'play'
427 }
428
429 getContextMenuOptions (player: videojs.Player, commonOptions: CommonOptions) {
430 const content = () => {
431 const isLoopEnabled = player.options_['loop']
432
433 const items = [
434 {
435 icon: 'repeat',
436 label: player.localize('Play in loop') + (isLoopEnabled ? '<span class="vjs-icon-tick-white"></span>' : ''),
437 listener: function () {
438 player.options_['loop'] = !isLoopEnabled
439 }
440 },
441 {
442 label: player.localize('Copy the video URL'),
443 listener: function () {
444 copyToClipboard(buildVideoLink({ shortUUID: commonOptions.videoShortUUID }))
445 }
446 },
447 {
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 })
451
452 copyToClipboard(decorateVideoLink({ url, startTime: this.currentTime() }))
453 }
454 },
455 {
456 icon: 'code',
457 label: player.localize('Copy embed code'),
458 listener: () => {
459 copyToClipboard(buildVideoOrPlaylistEmbed(commonOptions.embedUrl, commonOptions.embedTitle))
460 }
461 }
462 ]
463
464 if (this.mode === 'webtorrent') {
465 items.push({
466 label: player.localize('Copy magnet URI'),
467 listener: function (this: videojs.Player) {
468 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
469 }
470 })
471 }
472
473 items.push({
474 icon: 'info',
475 label: player.localize('Stats for nerds'),
476 listener: () => {
477 player.stats().show()
478 }
479 })
480
481 return items.map(i => ({
482 ...i,
483 label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
484 }))
485 }
486
487 return { content }
488 }
489}
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index fd612dd4f..b5c42d1c5 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -122,6 +122,14 @@ class PeerTubePlugin extends Plugin {
122 this.alterInactivity() 122 this.alterInactivity()
123 } 123 }
124 124
125 displayFatalError () {
126 this.player.addClass('vjs-error-display-enabled')
127 }
128
129 hideFatalError () {
130 this.player.removeClass('vjs-error-display-enabled')
131 }
132
125 private initializePlayer () { 133 private initializePlayer () {
126 if (isMobile()) this.player.addClass('vjs-is-mobile') 134 if (isMobile()) this.player.addClass('vjs-is-mobile')
127 135
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index b20ef7a3b..246f0d390 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -4,7 +4,7 @@ import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
4import { Html5Hlsjs } from './p2p-media-loader/hls-plugin' 4import { Html5Hlsjs } from './p2p-media-loader/hls-plugin'
5import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 5import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
6import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 6import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
7import { PlayerMode } from './peertube-player-manager' 7import { PlayerMode } from './peertube-player-options-builder'
8import { PeerTubePlugin } from './peertube-plugin' 8import { PeerTubePlugin } from './peertube-plugin'
9import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' 9import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin'
10import { PlaylistPlugin } from './playlist/playlist-plugin' 10import { PlaylistPlugin } from './playlist/playlist-plugin'
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 16dc7a244..4bcb2766a 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -145,7 +145,7 @@ class WebTorrentPlugin extends Plugin {
145 } 145 }
146 146
147 // Do not display error to user because we will have multiple fallback 147 // Do not display error to user because we will have multiple fallback
148 this.disableErrorDisplay(); 148 this.player.peertube().hideFatalError();
149 149
150 // Hack to "simulate" src link in video.js >= 6 150 // Hack to "simulate" src link in video.js >= 6
151 // Without this, we can't play the video after pausing it 151 // Without this, we can't play the video after pausing it
@@ -524,7 +524,7 @@ class WebTorrentPlugin extends Plugin {
524 this.torrent = null 524 this.torrent = null
525 525
526 // Enable error display now this is our last fallback 526 // Enable error display now this is our last fallback
527 this.player.one('error', () => this.enableErrorDisplay()) 527 this.player.one('error', () => this.player.peertube().displayFatalError())
528 528
529 const httpUrl = this.currentVideoFile.fileUrl 529 const httpUrl = this.currentVideoFile.fileUrl
530 this.player.src = this.savePlayerSrcFunction 530 this.player.src = this.savePlayerSrcFunction
@@ -549,14 +549,6 @@ class WebTorrentPlugin extends Plugin {
549 return this.player.trigger('customError', { err }) 549 return this.player.trigger('customError', { err })
550 } 550 }
551 551
552 private enableErrorDisplay () {
553 this.player.addClass('vjs-error-display-enabled')
554 }
555
556 private disableErrorDisplay () {
557 this.player.removeClass('vjs-error-display-enabled')
558 }
559
560 private pickAverageVideoFile () { 552 private pickAverageVideoFile () {
561 if (this.videoFiles.length === 1) return this.videoFiles[0] 553 if (this.videoFiles.length === 1) return this.videoFiles[0]
562 554
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index eb8076b98..054f771ab 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -14,7 +14,7 @@ import {
14 VideoPlaylistElement, 14 VideoPlaylistElement,
15 VideoStreamingPlaylistType 15 VideoStreamingPlaylistType
16} from '../../../../shared/models' 16} from '../../../../shared/models'
17import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' 17import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player'
18import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 18import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
19import { TranslationsManager } from '../../assets/player/translations-manager' 19import { TranslationsManager } from '../../assets/player/translations-manager'
20import { isP2PEnabled } from '../../assets/player/utils' 20import { isP2PEnabled } from '../../assets/player/utils'
@@ -558,7 +558,11 @@ export class PeerTubeEmbed {
558 serverUrl: window.location.origin, 558 serverUrl: window.location.origin,
559 language: navigator.language, 559 language: navigator.language,
560 embedUrl: window.location.origin + videoInfo.embedPath, 560 embedUrl: window.location.origin + videoInfo.embedPath,
561 embedTitle: videoInfo.name 561 embedTitle: videoInfo.name,
562
563 errorNotifier: () => {
564 // Empty, we don't have a notifier in the embed
565 }
562 }, 566 },
563 567
564 webtorrent: { 568 webtorrent: {
@@ -664,7 +668,6 @@ export class PeerTubeEmbed {
664 this.player.dispose() 668 this.player.dispose()
665 this.playerElement = null 669 this.playerElement = null
666 this.displayError('This video is not available because the remote instance is not responding.', translations) 670 this.displayError('This video is not available because the remote instance is not responding.', translations)
667
668 } 671 }
669 } 672 }
670 673
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts
index 248a5b038..bf3dfa1c9 100755
--- a/scripts/i18n/create-custom-files.ts
+++ b/scripts/i18n/create-custom-files.ts
@@ -55,7 +55,8 @@ const playerKeys = {
55 'Playlist: {1}': 'Playlist: {1}', 55 'Playlist: {1}': 'Playlist: {1}',
56 'disabled': 'disabled', 56 'disabled': 'disabled',
57 ' off': ' off', 57 ' off': ' off',
58 'Player mode': 'Player mode' 58 'Player mode': 'Player mode',
59 'The video failed to play, will try to fast forward.': 'The video failed to play, will try to fast forward.'
59} 60}
60Object.assign(playerKeys, videojs) 61Object.assign(playerKeys, videojs)
61 62