From f2a16d93b476aff16d5353e4d44350298ec7e01c Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Wed, 28 Sep 2022 11:52:23 +0200 Subject: 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 text --- .../src/assets/player/peertube-player-manager.ts | 22 +++++++++++ .../player/shared/p2p-media-loader/hls-plugin.ts | 45 ++++++++++++++++------ .../p2p-media-loader/p2p-media-loader-plugin.ts | 2 + .../player/shared/peertube/peertube-plugin.ts | 26 +++++++++++++ .../resolutions/peertube-resolutions-plugin.ts | 5 +++ .../shared/settings/resolution-menu-button.ts | 19 +++++++++ .../player/shared/settings/resolution-menu-item.ts | 2 +- client/src/sass/player/index.scss | 1 + client/src/sass/player/offline-notification.scss | 22 +++++++++++ client/src/sass/player/peertube-skin.scss | 17 +++++++- 10 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 client/src/sass/player/offline-notification.scss (limited to 'client') diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 0d4acc3d9..533ee1bb8 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -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) }) }) diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index e49e5c694..a14beb347 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts @@ -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 diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts index 56068e340..3c4482f2e 100644 --- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts @@ -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) diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index 83c32415e..a5d712d70 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts @@ -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') } diff --git a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts index e7899ac71..4fafd27b1 100644 --- a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts +++ b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts @@ -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 } diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts index a0b349f67..672411c11 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-button.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts @@ -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) diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts index 678eb368b..c59b8b891 100644 --- a/client/src/assets/player/shared/settings/resolution-menu-item.ts +++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts @@ -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 diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss index 7420460e7..5d0307d95 100644 --- a/client/src/sass/player/index.scss +++ b/client/src/sass/player/index.scss @@ -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 index 000000000..2108c2e30 --- /dev/null +++ b/client/src/sass/player/offline-notification.scss @@ -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; +} diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index 43c144624..d4c43ff68 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss @@ -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; } } -- cgit v1.2.3