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)
|
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)
|
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) {
|
private _handleMediaError (error: any) {
|
||||||
if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) {
|
if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) {
|
||||||
logger.info('trying to recover media error')
|
logger.info('trying to recover media error')
|
||||||
|
@ -226,14 +248,13 @@ class Html5Hlsjs {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) {
|
if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) {
|
||||||
logger.info('bubbling media error up to VIDEOJS')
|
this._handleUnrecovarableError(error)
|
||||||
this.hls.destroy()
|
|
||||||
this.tech.error = () => error
|
|
||||||
this.tech.trigger('error')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleNetworkError (error: any) {
|
private _handleNetworkError (error: any) {
|
||||||
|
if (navigator.onLine === false) return
|
||||||
|
|
||||||
if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) {
|
if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) {
|
||||||
logger.info('trying to recover network error')
|
logger.info('trying to recover network error')
|
||||||
|
|
||||||
|
@ -248,10 +269,7 @@ class Html5Hlsjs {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('bubbling network error up to VIDEOJS')
|
this._handleUnrecovarableError(error)
|
||||||
this.hls.destroy()
|
|
||||||
this.tech.error = () => error
|
|
||||||
this.tech.trigger('error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onError (_event: any, data: ErrorData) {
|
private _onError (_event: any, data: ErrorData) {
|
||||||
|
@ -273,10 +291,7 @@ class Html5Hlsjs {
|
||||||
error.code = 3
|
error.code = 3
|
||||||
this._handleMediaError(error)
|
this._handleMediaError(error)
|
||||||
} else if (data.fatal) {
|
} else if (data.fatal) {
|
||||||
this.hls.destroy()
|
this._handleUnrecovarableError(error)
|
||||||
logger.info('bubbling error up to VIDEOJS')
|
|
||||||
this.tech.error = () => error as any
|
|
||||||
this.tech.trigger('error')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,6 +307,12 @@ class Html5Hlsjs {
|
||||||
return '0'
|
return '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _removeQuality (index: number) {
|
||||||
|
this.hls.removeLevel(index)
|
||||||
|
this.player.peertubeResolutions().remove(index)
|
||||||
|
this.hls.currentLevel = -1
|
||||||
|
}
|
||||||
|
|
||||||
private _notifyVideoQualities () {
|
private _notifyVideoQualities () {
|
||||||
if (!this.metadata) return
|
if (!this.metadata) return
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,8 @@ class P2pMediaLoaderPlugin extends Plugin {
|
||||||
this.p2pEngine = this.options.loader.getEngine()
|
this.p2pEngine = this.options.loader.getEngine()
|
||||||
|
|
||||||
this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
|
this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
|
||||||
|
if (navigator.onLine === false) return
|
||||||
|
|
||||||
logger.error(`Segment ${segment.id} error.`, err)
|
logger.error(`Segment ${segment.id} error.`, err)
|
||||||
|
|
||||||
this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
|
this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
|
||||||
|
|
|
@ -125,6 +125,32 @@ class PeerTubePlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
displayFatalError () {
|
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')
|
this.player.addClass('vjs-error-display-enabled')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,11 @@ class PeerTubeResolutionsPlugin extends Plugin {
|
||||||
this.trigger('resolutionsAdded')
|
this.trigger('resolutionsAdded')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove (resolutionIndex: number) {
|
||||||
|
this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex)
|
||||||
|
this.trigger('resolutionRemoved')
|
||||||
|
}
|
||||||
|
|
||||||
getResolutions () {
|
getResolutions () {
|
||||||
return this.resolutions
|
return this.resolutions
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
this.controlText('Quality')
|
this.controlText('Quality')
|
||||||
|
|
||||||
player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
|
player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
|
||||||
|
player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities())
|
||||||
|
|
||||||
// For parent
|
// For parent
|
||||||
player.peertubeResolutions().on('resolutionChanged', () => {
|
player.peertubeResolutions().on('resolutionChanged', () => {
|
||||||
|
@ -82,6 +83,24 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
|
|
||||||
this.trigger('menuChanged')
|
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)
|
videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
|
||||||
|
|
|
@ -7,7 +7,7 @@ export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResolutionMenuItem extends MenuItem {
|
class ResolutionMenuItem extends MenuItem {
|
||||||
private readonly resolutionId: number
|
readonly resolutionId: number
|
||||||
private readonly label: string
|
private readonly label: string
|
||||||
|
|
||||||
private autoResolutionEnabled: boolean
|
private autoResolutionEnabled: boolean
|
||||||
|
|
|
@ -9,3 +9,4 @@
|
||||||
@use './bezels';
|
@use './bezels';
|
||||||
@use './playlist';
|
@use './playlist';
|
||||||
@use './stats';
|
@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
|
// Error display disabled
|
||||||
.vjs-error:not(.vjs-error-display-enabled) {
|
.vjs-error:not(.vjs-error-display-enabled) {
|
||||||
.vjs-error-display {
|
.vjs-custom-error-display {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +215,7 @@ body {
|
||||||
|
|
||||||
// Error display enabled
|
// Error display enabled
|
||||||
.vjs-error.vjs-error-display-enabled {
|
.vjs-error.vjs-error-display-enabled {
|
||||||
.vjs-error-display {
|
.vjs-custom-error-display {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue