diff options
author | kontrollanten <6680299+kontrollanten@users.noreply.github.com> | 2022-09-28 11:52:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-28 11:52:23 +0200 |
commit | f2a16d93b476aff16d5353e4d44350298ec7e01c (patch) | |
tree | 36c43eb3299c4a1137ca38dd1a564701a5a27236 /client/src | |
parent | 43972ee466740e91b16c08fe106551657969e669 (diff) | |
download | PeerTube-f2a16d93b476aff16d5353e4d44350298ec7e01c.tar.gz PeerTube-f2a16d93b476aff16d5353e4d44350298ec7e01c.tar.zst PeerTube-f2a16d93b476aff16d5353e4d44350298ec7e01c.zip |
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
Diffstat (limited to 'client/src')
10 files changed, 146 insertions, 15 deletions
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 { | |||
129 | saveAverageBandwidth(data.bandwidthEstimate) | 129 | saveAverageBandwidth(data.bandwidthEstimate) |
130 | }) | 130 | }) |
131 | 131 | ||
132 | const offlineNotificationElem = document.createElement('div') | ||
133 | offlineNotificationElem.classList.add('vjs-peertube-offline-notification') | ||
134 | offlineNotificationElem.innerText = player.localize('You seem to be offline and the video may not work') | ||
135 | |||
136 | const handleOnline = () => { | ||
137 | player.el().removeChild(offlineNotificationElem) | ||
138 | logger.info('The browser is online') | ||
139 | } | ||
140 | |||
141 | const handleOffline = () => { | ||
142 | player.el().appendChild(offlineNotificationElem) | ||
143 | logger.info('The browser is offline') | ||
144 | } | ||
145 | |||
146 | window.addEventListener('online', handleOnline) | ||
147 | window.addEventListener('offline', handleOffline) | ||
148 | |||
149 | player.on('dispose', () => { | ||
150 | window.removeEventListener('online', handleOnline) | ||
151 | window.removeEventListener('offline', handleOffline) | ||
152 | }) | ||
153 | |||
132 | return res(player) | 154 | return res(player) |
133 | }) | 155 | }) |
134 | }) | 156 | }) |
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 { | |||
211 | } | 211 | } |
212 | } | 212 | } |
213 | 213 | ||
214 | private _getHumanErrorMsg (error: { message: string, code?: number }) { | ||
215 | switch (error.code) { | ||
216 | default: | ||
217 | return error.message | ||
218 | } | ||
219 | } | ||
220 | |||
221 | private _handleUnrecovarableError (error: any) { | ||
222 | if (this.hls.levels.filter(l => l.id > -1).length > 1) { | ||
223 | this._removeQuality(this.hls.loadLevel) | ||
224 | return | ||
225 | } | ||
226 | |||
227 | this.hls.destroy() | ||
228 | logger.info('bubbling error up to VIDEOJS') | ||
229 | this.tech.error = () => ({ | ||
230 | ...error, | ||
231 | message: this._getHumanErrorMsg(error) | ||
232 | }) | ||
233 | this.tech.trigger('error') | ||
234 | } | ||
235 | |||
214 | private _handleMediaError (error: any) { | 236 | private _handleMediaError (error: any) { |
215 | if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) { | 237 | if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) { |
216 | logger.info('trying to recover media error') | 238 | logger.info('trying to recover media error') |
@@ -226,14 +248,13 @@ class Html5Hlsjs { | |||
226 | } | 248 | } |
227 | 249 | ||
228 | if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) { | 250 | if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) { |
229 | logger.info('bubbling media error up to VIDEOJS') | 251 | this._handleUnrecovarableError(error) |
230 | this.hls.destroy() | ||
231 | this.tech.error = () => error | ||
232 | this.tech.trigger('error') | ||
233 | } | 252 | } |
234 | } | 253 | } |
235 | 254 | ||
236 | private _handleNetworkError (error: any) { | 255 | private _handleNetworkError (error: any) { |
256 | if (navigator.onLine === false) return | ||
257 | |||
237 | if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) { | 258 | if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) { |
238 | logger.info('trying to recover network error') | 259 | logger.info('trying to recover network error') |
239 | 260 | ||
@@ -248,10 +269,7 @@ class Html5Hlsjs { | |||
248 | return | 269 | return |
249 | } | 270 | } |
250 | 271 | ||
251 | logger.info('bubbling network error up to VIDEOJS') | 272 | this._handleUnrecovarableError(error) |
252 | this.hls.destroy() | ||
253 | this.tech.error = () => error | ||
254 | this.tech.trigger('error') | ||
255 | } | 273 | } |
256 | 274 | ||
257 | private _onError (_event: any, data: ErrorData) { | 275 | private _onError (_event: any, data: ErrorData) { |
@@ -273,10 +291,7 @@ class Html5Hlsjs { | |||
273 | error.code = 3 | 291 | error.code = 3 |
274 | this._handleMediaError(error) | 292 | this._handleMediaError(error) |
275 | } else if (data.fatal) { | 293 | } else if (data.fatal) { |
276 | this.hls.destroy() | 294 | this._handleUnrecovarableError(error) |
277 | logger.info('bubbling error up to VIDEOJS') | ||
278 | this.tech.error = () => error as any | ||
279 | this.tech.trigger('error') | ||
280 | } | 295 | } |
281 | } | 296 | } |
282 | 297 | ||
@@ -292,6 +307,12 @@ class Html5Hlsjs { | |||
292 | return '0' | 307 | return '0' |
293 | } | 308 | } |
294 | 309 | ||
310 | private _removeQuality (index: number) { | ||
311 | this.hls.removeLevel(index) | ||
312 | this.player.peertubeResolutions().remove(index) | ||
313 | this.hls.currentLevel = -1 | ||
314 | } | ||
315 | |||
295 | private _notifyVideoQualities () { | 316 | private _notifyVideoQualities () { |
296 | if (!this.metadata) return | 317 | if (!this.metadata) return |
297 | 318 | ||
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 { | |||
115 | this.p2pEngine = this.options.loader.getEngine() | 115 | this.p2pEngine = this.options.loader.getEngine() |
116 | 116 | ||
117 | this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { | 117 | this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { |
118 | if (navigator.onLine === false) return | ||
119 | |||
118 | logger.error(`Segment ${segment.id} error.`, err) | 120 | logger.error(`Segment ${segment.id} error.`, err) |
119 | 121 | ||
120 | this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl) | 122 | 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 { | |||
125 | } | 125 | } |
126 | 126 | ||
127 | displayFatalError () { | 127 | displayFatalError () { |
128 | this.player.loadingSpinner.hide() | ||
129 | |||
130 | const buildModal = (error: MediaError) => { | ||
131 | const localize = this.player.localize.bind(this.player) | ||
132 | |||
133 | const wrapper = document.createElement('div') | ||
134 | const header = document.createElement('h1') | ||
135 | header.innerText = localize('Failed to play video') | ||
136 | wrapper.appendChild(header) | ||
137 | const desc = document.createElement('div') | ||
138 | desc.innerText = localize('The video failed to play due to technical issues.') | ||
139 | wrapper.appendChild(desc) | ||
140 | const details = document.createElement('p') | ||
141 | details.classList.add('error-details') | ||
142 | details.innerText = error.message | ||
143 | wrapper.appendChild(details) | ||
144 | |||
145 | return wrapper | ||
146 | } | ||
147 | |||
148 | const modal = this.player.createModal(buildModal(this.player.error()), { | ||
149 | temporary: false, | ||
150 | uncloseable: true | ||
151 | }) | ||
152 | modal.addClass('vjs-custom-error-display') | ||
153 | |||
128 | this.player.addClass('vjs-error-display-enabled') | 154 | this.player.addClass('vjs-error-display-enabled') |
129 | } | 155 | } |
130 | 156 | ||
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 { | |||
21 | this.trigger('resolutionsAdded') | 21 | this.trigger('resolutionsAdded') |
22 | } | 22 | } |
23 | 23 | ||
24 | remove (resolutionIndex: number) { | ||
25 | this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex) | ||
26 | this.trigger('resolutionRemoved') | ||
27 | } | ||
28 | |||
24 | getResolutions () { | 29 | getResolutions () { |
25 | return this.resolutions | 30 | return this.resolutions |
26 | } | 31 | } |
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 { | |||
12 | this.controlText('Quality') | 12 | this.controlText('Quality') |
13 | 13 | ||
14 | player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) | 14 | player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) |
15 | player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities()) | ||
15 | 16 | ||
16 | // For parent | 17 | // For parent |
17 | player.peertubeResolutions().on('resolutionChanged', () => { | 18 | player.peertubeResolutions().on('resolutionChanged', () => { |
@@ -82,6 +83,24 @@ class ResolutionMenuButton extends MenuButton { | |||
82 | 83 | ||
83 | this.trigger('menuChanged') | 84 | this.trigger('menuChanged') |
84 | } | 85 | } |
86 | |||
87 | private cleanupQualities () { | ||
88 | const resolutions = this.player().peertubeResolutions().getResolutions() | ||
89 | |||
90 | this.menu.children().forEach((children: ResolutionMenuItem) => { | ||
91 | if (children.resolutionId === undefined) { | ||
92 | return | ||
93 | } | ||
94 | |||
95 | if (resolutions.find(r => r.id === children.resolutionId)) { | ||
96 | return | ||
97 | } | ||
98 | |||
99 | this.menu.removeChild(children) | ||
100 | }) | ||
101 | |||
102 | this.trigger('menuChanged') | ||
103 | } | ||
85 | } | 104 | } |
86 | 105 | ||
87 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 106 | 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 { | |||
7 | } | 7 | } |
8 | 8 | ||
9 | class ResolutionMenuItem extends MenuItem { | 9 | class ResolutionMenuItem extends MenuItem { |
10 | private readonly resolutionId: number | 10 | readonly resolutionId: number |
11 | private readonly label: string | 11 | private readonly label: string |
12 | 12 | ||
13 | private autoResolutionEnabled: boolean | 13 | 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 @@ | |||
9 | @use './bezels'; | 9 | @use './bezels'; |
10 | @use './playlist'; | 10 | @use './playlist'; |
11 | @use './stats'; | 11 | @use './stats'; |
12 | @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 @@ | |||
1 | $height: 40px; | ||
2 | |||
3 | .vjs-peertube-offline-notification { | ||
4 | position: absolute; | ||
5 | top: 0; | ||
6 | left: 0; | ||
7 | right: 0; | ||
8 | height: $height; | ||
9 | color: #000; | ||
10 | background-color: var(--mainColorLightest); | ||
11 | text-align: center; | ||
12 | z-index: 1; | ||
13 | display: flex; | ||
14 | justify-content: center; | ||
15 | align-items: center; | ||
16 | } | ||
17 | |||
18 | .vjs-modal-dialog | ||
19 | .vjs-modal-dialog-content, | ||
20 | .video-js .vjs-modal-dialog { | ||
21 | top: $height; | ||
22 | } | ||
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 { | |||
189 | } | 189 | } |
190 | } | 190 | } |
191 | 191 | ||
192 | .vjs-error-display { | ||
193 | display: none; | ||
194 | } | ||
195 | |||
196 | .vjs-custom-error-display { | ||
197 | font-family: $main-fonts; | ||
198 | |||
199 | .error-details { | ||
200 | margin-top: 40px; | ||
201 | font-size: 80%; | ||
202 | } | ||
203 | } | ||
204 | |||
192 | // Error display disabled | 205 | // Error display disabled |
193 | .vjs-error:not(.vjs-error-display-enabled) { | 206 | .vjs-error:not(.vjs-error-display-enabled) { |
194 | .vjs-error-display { | 207 | .vjs-custom-error-display { |
195 | display: none; | 208 | display: none; |
196 | } | 209 | } |
197 | 210 | ||
@@ -202,7 +215,7 @@ body { | |||
202 | 215 | ||
203 | // Error display enabled | 216 | // Error display enabled |
204 | .vjs-error.vjs-error-display-enabled { | 217 | .vjs-error.vjs-error-display-enabled { |
205 | .vjs-error-display { | 218 | .vjs-custom-error-display { |
206 | display: block; | 219 | display: block; |
207 | } | 220 | } |
208 | } | 221 | } |