mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			Handle network issues in video player (#5138)
* 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 textpull/5318/head
							parent
							
								
									43972ee466
								
							
						
					
					
						commit
						f2a16d93b4
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,3 +9,4 @@
 | 
			
		|||
@use './bezels';
 | 
			
		||||
@use './playlist';
 | 
			
		||||
@use './stats';
 | 
			
		||||
@use './offline-notification';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue