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
pull/5318/head
kontrollanten 2022-09-28 11:52:23 +02:00 committed by GitHub
parent 43972ee466
commit f2a16d93b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 146 additions and 15 deletions

View 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)
})
})

View 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

View 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)

View 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')
}

View 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
}

View 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)

View 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

View File

@ -9,3 +9,4 @@
@use './bezels';
@use './playlist';
@use './stats';
@use './offline-notification';

View File

@ -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;
}

View 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;
}
}