]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Handle network issues in video player (#5138)
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>
Wed, 28 Sep 2022 09:52:23 +0000 (11:52 +0200)
committerGitHub <noreply@github.com>
Wed, 28 Sep 2022 09:52:23 +0000 (11:52 +0200)
* feat(client/player): handle network offline

* feat(client/player): human friendly err msg

* feat(client/player): handle broken resolutions

When an error occurs for a resolution, remove the resolution and try
with another resolution.

* fix(client/player): prevent err handl when offline

* fix(client/player): localize offline text

client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/shared/peertube/peertube-plugin.ts
client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
client/src/assets/player/shared/settings/resolution-menu-button.ts
client/src/assets/player/shared/settings/resolution-menu-item.ts
client/src/sass/player/index.scss
client/src/sass/player/offline-notification.scss [new file with mode: 0644]
client/src/sass/player/peertube-skin.scss

index 0d4acc3d91e576054f45cfed29baca455e61f088..533ee1bb8f8de2a11ddd8b192bc7ff0a2a176398 100644 (file)
@@ -129,6 +129,28 @@ export class PeertubePlayerManager {
           saveAverageBandwidth(data.bandwidthEstimate)
         })
 
+        const offlineNotificationElem = document.createElement('div')
+        offlineNotificationElem.classList.add('vjs-peertube-offline-notification')
+        offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work')
+
+        const handleOnline = () => {
+          player.el().removeChild(offlineNotificationElem)
+          logger.info('The browser is online')
+        }
+
+        const handleOffline = () => {
+          player.el().appendChild(offlineNotificationElem)
+          logger.info('The browser is offline')
+        }
+
+        window.addEventListener('online', handleOnline)
+        window.addEventListener('offline', handleOffline)
+
+        player.on('dispose', () => {
+          window.removeEventListener('online', handleOnline)
+          window.removeEventListener('offline', handleOffline)
+        })
+
         return res(player)
       })
     })
index e49e5c6943348aa4841e045b87b1f82c6401635e..a14beb347ae290c74fd215e25dcf294478e982c6 100644 (file)
@@ -211,6 +211,28 @@ class Html5Hlsjs {
     }
   }
 
+  private _getHumanErrorMsg (error: { message: string, code?: number }) {
+    switch (error.code) {
+      default:
+        return error.message
+    }
+  }
+
+  private _handleUnrecovarableError (error: any) {
+    if (this.hls.levels.filter(l => l.id > -1).length > 1) {
+      this._removeQuality(this.hls.loadLevel)
+      return
+    }
+
+    this.hls.destroy()
+    logger.info('bubbling error up to VIDEOJS')
+    this.tech.error = () => ({
+      ...error,
+      message: this._getHumanErrorMsg(error)
+    })
+    this.tech.trigger('error')
+  }
+
   private _handleMediaError (error: any) {
     if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) {
       logger.info('trying to recover media error')
@@ -226,14 +248,13 @@ class Html5Hlsjs {
     }
 
     if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) {
-      logger.info('bubbling media error up to VIDEOJS')
-      this.hls.destroy()
-      this.tech.error = () => error
-      this.tech.trigger('error')
+      this._handleUnrecovarableError(error)
     }
   }
 
   private _handleNetworkError (error: any) {
+    if (navigator.onLine === false) return
+
     if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) {
       logger.info('trying to recover network error')
 
@@ -248,10 +269,7 @@ class Html5Hlsjs {
       return
     }
 
-    logger.info('bubbling network error up to VIDEOJS')
-    this.hls.destroy()
-    this.tech.error = () => error
-    this.tech.trigger('error')
+    this._handleUnrecovarableError(error)
   }
 
   private _onError (_event: any, data: ErrorData) {
@@ -273,10 +291,7 @@ class Html5Hlsjs {
       error.code = 3
       this._handleMediaError(error)
     } else if (data.fatal) {
-      this.hls.destroy()
-      logger.info('bubbling error up to VIDEOJS')
-      this.tech.error = () => error as any
-      this.tech.trigger('error')
+      this._handleUnrecovarableError(error)
     }
   }
 
@@ -292,6 +307,12 @@ class Html5Hlsjs {
     return '0'
   }
 
+  private _removeQuality (index: number) {
+    this.hls.removeLevel(index)
+    this.player.peertubeResolutions().remove(index)
+    this.hls.currentLevel = -1
+  }
+
   private _notifyVideoQualities () {
     if (!this.metadata) return
 
index 56068e34015596e7003b5e53e62a837adeb2edfa..3c4482f2e5281c023136ceb3a14d391ec9ca2d6a 100644 (file)
@@ -115,6 +115,8 @@ class P2pMediaLoaderPlugin extends Plugin {
     this.p2pEngine = this.options.loader.getEngine()
 
     this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
+      if (navigator.onLine === false) return
+
       logger.error(`Segment ${segment.id} error.`, err)
 
       this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
index 83c32415e833f4c8bd1b8bd6c5422badb395b6c5..a5d712d70dbd9e1340526e047b58b859afe2fe93 100644 (file)
@@ -125,6 +125,32 @@ class PeerTubePlugin extends Plugin {
   }
 
   displayFatalError () {
+    this.player.loadingSpinner.hide()
+
+    const buildModal = (error: MediaError) => {
+      const localize = this.player.localize.bind(this.player)
+
+      const wrapper = document.createElement('div')
+      const header = document.createElement('h1')
+      header.innerText = localize('Failed to play video')
+      wrapper.appendChild(header)
+      const desc = document.createElement('div')
+      desc.innerText = localize('The video failed to play due to technical issues.')
+      wrapper.appendChild(desc)
+      const details = document.createElement('p')
+      details.classList.add('error-details')
+      details.innerText = error.message
+      wrapper.appendChild(details)
+
+      return wrapper
+    }
+
+    const modal = this.player.createModal(buildModal(this.player.error()), {
+      temporary: false,
+      uncloseable: true
+    })
+    modal.addClass('vjs-custom-error-display')
+
     this.player.addClass('vjs-error-display-enabled')
   }
 
index e7899ac71a0459c337e80c2d572604524ca4e1b1..4fafd27b1ba21dcb08f94527477960cfb9fe0e56 100644 (file)
@@ -21,6 +21,11 @@ class PeerTubeResolutionsPlugin extends Plugin {
     this.trigger('resolutionsAdded')
   }
 
+  remove (resolutionIndex: number) {
+    this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex)
+    this.trigger('resolutionRemoved')
+  }
+
   getResolutions () {
     return this.resolutions
   }
index a0b349f67a557cae3e7e242df12d5784b860d23d..672411c11cee383accf0b74199a4d7bac5cdb892 100644 (file)
@@ -12,6 +12,7 @@ class ResolutionMenuButton extends MenuButton {
     this.controlText('Quality')
 
     player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
+    player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities())
 
     // For parent
     player.peertubeResolutions().on('resolutionChanged', () => {
@@ -82,6 +83,24 @@ class ResolutionMenuButton extends MenuButton {
 
     this.trigger('menuChanged')
   }
+
+  private cleanupQualities () {
+    const resolutions = this.player().peertubeResolutions().getResolutions()
+
+    this.menu.children().forEach((children: ResolutionMenuItem) => {
+      if (children.resolutionId === undefined) {
+        return
+      }
+
+      if (resolutions.find(r => r.id === children.resolutionId)) {
+        return
+      }
+
+      this.menu.removeChild(children)
+    })
+
+    this.trigger('menuChanged')
+  }
 }
 
 videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
index 678eb368bb720edf8de53d43733e4aef26dc1461..c59b8b8914da39b32e6ebd31878d4741440e2acf 100644 (file)
@@ -7,7 +7,7 @@ export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
 }
 
 class ResolutionMenuItem extends MenuItem {
-  private readonly resolutionId: number
+  readonly resolutionId: number
   private readonly label: string
 
   private autoResolutionEnabled: boolean
index 7420460e7ce1554d04e83eeaebc08f6bd82d524f..5d0307d95852f59e0fea59368ea3995f0d1d9c4e 100644 (file)
@@ -9,3 +9,4 @@
 @use './bezels';
 @use './playlist';
 @use './stats';
+@use './offline-notification';
diff --git a/client/src/sass/player/offline-notification.scss b/client/src/sass/player/offline-notification.scss
new file mode 100644 (file)
index 0000000..2108c2e
--- /dev/null
@@ -0,0 +1,22 @@
+$height: 40px;
+
+.vjs-peertube-offline-notification {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: $height;
+  color: #000;
+  background-color: var(--mainColorLightest);
+  text-align: center;
+  z-index: 1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.vjs-modal-dialog
+.vjs-modal-dialog-content,
+.video-js .vjs-modal-dialog {
+  top: $height;
+}
index 43c1446242f3ffc6e3b30897ea1001d6c365eb2d..d4c43ff68341b36857d6ecb11f2861fee441b0b4 100644 (file)
@@ -189,9 +189,22 @@ body {
   }
 }
 
+.vjs-error-display {
+  display: none;
+}
+
+.vjs-custom-error-display {
+  font-family: $main-fonts;
+
+  .error-details {
+    margin-top: 40px;
+    font-size: 80%;
+  }
+}
+
 // Error display disabled
 .vjs-error:not(.vjs-error-display-enabled) {
-  .vjs-error-display {
+  .vjs-custom-error-display {
     display: none;
   }
 
@@ -202,7 +215,7 @@ body {
 
 // Error display enabled
 .vjs-error.vjs-error-display-enabled {
-  .vjs-error-display {
+  .vjs-custom-error-display {
     display: block;
   }
 }