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
This commit is contained in:
parent
43972ee466
commit
f2a16d93b4
10 changed files with 146 additions and 15 deletions
|
@ -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';
|
||||
|
|
22
client/src/sass/player/offline-notification.scss
Normal file
22
client/src/sass/player/offline-notification.scss
Normal 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;
|
||||
}
|
|
@ -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 a new issue