mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #5274 from thornbill/media-errors
Add more media playback error messages
This commit is contained in:
commit
2956de9095
8 changed files with 97 additions and 39 deletions
|
@ -1,6 +1,7 @@
|
|||
import appSettings from '../scripts/settings/appSettings' ;
|
||||
import browser from '../scripts/browser';
|
||||
import Events from '../utils/events.ts';
|
||||
import { MediaError } from 'types/mediaError';
|
||||
|
||||
export function getSavedVolume() {
|
||||
return appSettings.get('volume') || 1;
|
||||
|
@ -87,7 +88,7 @@ export function handleHlsJsMediaError(instance, reject) {
|
|||
if (reject) {
|
||||
reject();
|
||||
} else {
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
onErrorInternal(instance, MediaError.FATAL_HLS_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,11 +99,7 @@ export function onErrorInternal(instance, type) {
|
|||
instance.destroyCustomTrack(instance._mediaElement);
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'error', [
|
||||
{
|
||||
type: type
|
||||
}
|
||||
]);
|
||||
Events.trigger(instance, 'error', [{ type }]);
|
||||
}
|
||||
|
||||
export function isValidDuration(duration) {
|
||||
|
@ -193,7 +190,7 @@ export function playWithPromise(elem, onErrorFn) {
|
|||
// swallow this error because the user can still click the play button on the video element
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
return Promise.reject(e);
|
||||
})
|
||||
.then(() => {
|
||||
onSuccessfulPlay(elem, onErrorFn);
|
||||
|
@ -269,10 +266,10 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
|||
hls.destroy();
|
||||
|
||||
if (reject) {
|
||||
reject('servererror');
|
||||
reject(MediaError.SERVER_ERROR);
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'servererror');
|
||||
onErrorInternal(instance, MediaError.SERVER_ERROR);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -291,10 +288,10 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
|||
hls.destroy();
|
||||
|
||||
if (reject) {
|
||||
reject('network');
|
||||
reject(MediaError.NETWORK_ERROR);
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'network');
|
||||
onErrorInternal(instance, MediaError.NETWORK_ERROR);
|
||||
}
|
||||
} else {
|
||||
console.debug('fatal network error encountered, try to recover');
|
||||
|
@ -318,7 +315,7 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r
|
|||
reject();
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
onErrorInternal(instance, MediaError.FATAL_HLS_ERROR);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { PlaybackErrorCode } from '@jellyfin/sdk/lib/generated-client/models/playback-error-code.js';
|
||||
import merge from 'lodash-es/merge';
|
||||
import Screenfull from 'screenfull';
|
||||
|
||||
import Events from '../../utils/events.ts';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import appSettings from '../../scripts/settings/appSettings';
|
||||
|
@ -8,14 +12,15 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
|||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../loading/loading';
|
||||
import { appHost } from '../apphost';
|
||||
import Screenfull from 'screenfull';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import { includesAny } from '../../utils/container.ts';
|
||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage';
|
||||
import merge from 'lodash-es/merge';
|
||||
|
||||
import { MediaError } from 'types/mediaError';
|
||||
import { getMediaError } from 'utils/mediaError';
|
||||
|
||||
const UNLIMITED_ITEMS = -1;
|
||||
|
||||
|
@ -588,9 +593,18 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PlaybackManager} instance
|
||||
* @param {import('@jellyfin/sdk/lib/generated-client/index.js').PlaybackInfoResponse} result
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function validatePlaybackInfoResult(instance, result) {
|
||||
if (result.ErrorCode) {
|
||||
showPlaybackInfoErrorMessage(instance, 'PlaybackError' + result.ErrorCode);
|
||||
// NOTE: To avoid needing to retranslate the "NoCompatibleStream" message,
|
||||
// we need to keep the key in the same format.
|
||||
const errMessage = result.ErrorCode === PlaybackErrorCode.NoCompatibleStream ?
|
||||
'PlaybackErrorNoCompatibleStream' : `PlaybackError.${result.ErrorCode}`;
|
||||
showPlaybackInfoErrorMessage(instance, errMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1720,7 +1734,8 @@ class PlaybackManager {
|
|||
streamInfo.resetSubtitleOffset = false;
|
||||
|
||||
if (!streamInfo.url) {
|
||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
||||
cancelPlayback();
|
||||
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1768,8 +1783,8 @@ class PlaybackManager {
|
|||
playerData.isChangingStream = false;
|
||||
|
||||
onPlaybackError.call(player, e, {
|
||||
type: 'mediadecodeerror',
|
||||
streamInfo: streamInfo
|
||||
type: getMediaError(e),
|
||||
streamInfo
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -2179,7 +2194,7 @@ class PlaybackManager {
|
|||
|
||||
// If it's still null then there's nothing to play
|
||||
if (!firstItem) {
|
||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
||||
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
|
@ -2551,8 +2566,8 @@ class PlaybackManager {
|
|||
onPlaybackStarted(player, playOptions, streamInfo, mediaSource);
|
||||
setTimeout(function () {
|
||||
onPlaybackError.call(player, err, {
|
||||
type: 'mediadecodeerror',
|
||||
streamInfo: streamInfo
|
||||
type: getMediaError(err),
|
||||
streamInfo
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
@ -2785,7 +2800,7 @@ class PlaybackManager {
|
|||
return mediaSource;
|
||||
}
|
||||
} else {
|
||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
||||
showPlaybackInfoErrorMessage(self, `PlaybackError.${MediaError.NO_MEDIA_ERROR}`);
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
|
@ -3194,22 +3209,32 @@ class PlaybackManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} streamInfo
|
||||
* @param {MediaError} errorType
|
||||
* @param {boolean} currentlyPreventsVideoStreamCopy
|
||||
* @param {boolean} currentlyPreventsAudioStreamCopy
|
||||
* @returns {boolean} Returns true if the stream should be retried by transcoding.
|
||||
*/
|
||||
function enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy) {
|
||||
// mediadecodeerror, medianotsupported, network, servererror
|
||||
return streamInfo.mediaSource.SupportsTranscoding
|
||||
&& (!currentlyPreventsVideoStreamCopy || !currentlyPreventsAudioStreamCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback error handler.
|
||||
* @param {Error} e
|
||||
* @param {object} error
|
||||
* @param {object} error.streamInfo
|
||||
* @param {MediaError} error.type
|
||||
*/
|
||||
function onPlaybackError(e, error) {
|
||||
const player = this;
|
||||
error = error || {};
|
||||
|
||||
// network
|
||||
// mediadecodeerror
|
||||
// medianotsupported
|
||||
const errorType = error.type;
|
||||
|
||||
console.debug('playbackmanager playback error type: ' + (errorType || ''));
|
||||
console.warn('[playbackmanager] onPlaybackError:', e, error);
|
||||
|
||||
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
|
||||
|
||||
|
@ -3235,8 +3260,7 @@ class PlaybackManager {
|
|||
|
||||
Events.trigger(self, 'playbackerror', [errorType]);
|
||||
|
||||
const displayErrorCode = 'NoCompatibleStream';
|
||||
onPlaybackStopped.call(player, e, displayErrorCode);
|
||||
onPlaybackStopped.call(player, e, `.${errorType}`);
|
||||
}
|
||||
|
||||
function onPlaybackStopped(e, displayErrorCode) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import profileBuilder from '../../scripts/browserDeviceProfile';
|
|||
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import Events from '../../utils/events.ts';
|
||||
import { MediaError } from 'types/mediaError';
|
||||
|
||||
function getDefaultProfile() {
|
||||
return profileBuilder({});
|
||||
|
@ -343,7 +344,7 @@ class HtmlAudioPlayer {
|
|||
return;
|
||||
case 2:
|
||||
// MEDIA_ERR_NETWORK
|
||||
type = 'network';
|
||||
type = MediaError.NETWORK_ERROR;
|
||||
break;
|
||||
case 3:
|
||||
// MEDIA_ERR_DECODE
|
||||
|
@ -351,12 +352,12 @@ class HtmlAudioPlayer {
|
|||
htmlMediaHelper.handleHlsJsMediaError(self);
|
||||
return;
|
||||
} else {
|
||||
type = 'mediadecodeerror';
|
||||
type = MediaError.MEDIA_DECODE_ERROR;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||
type = 'medianotsupported';
|
||||
type = MediaError.MEDIA_NOT_SUPPORTED;
|
||||
break;
|
||||
default:
|
||||
// seeing cases where Edge is firing error events with no error code
|
||||
|
|
|
@ -37,6 +37,7 @@ import Events from '../../utils/events.ts';
|
|||
import { includesAny } from '../../utils/container.ts';
|
||||
import { isHls } from '../../utils/mediaSource.ts';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { MediaError } from 'types/mediaError';
|
||||
|
||||
/**
|
||||
* Returns resolved URL.
|
||||
|
@ -520,7 +521,7 @@ export class HtmlVideoPlayer {
|
|||
|
||||
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && isHls(options.mediaSource)) {
|
||||
return this.setSrcWithHlsJs(elem, options, val);
|
||||
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') {
|
||||
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container?.toUpperCase() === 'FLV') {
|
||||
return this.setSrcWithFlvJs(elem, options, val);
|
||||
} else {
|
||||
elem.autoplay = true;
|
||||
|
@ -1021,7 +1022,7 @@ export class HtmlVideoPlayer {
|
|||
// Only trigger this if there is media info
|
||||
// Avoid triggering in situations where it might not actually have a video stream (audio only live tv channel)
|
||||
if (!mediaSource || mediaSource.RunTimeTicks) {
|
||||
onErrorInternal(this, 'mediadecodeerror');
|
||||
onErrorInternal(this, MediaError.NO_MEDIA_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1073,7 +1074,7 @@ export class HtmlVideoPlayer {
|
|||
return;
|
||||
case 2:
|
||||
// MEDIA_ERR_NETWORK
|
||||
type = 'network';
|
||||
type = MediaError.NETWORK_ERROR;
|
||||
break;
|
||||
case 3:
|
||||
// MEDIA_ERR_DECODE
|
||||
|
@ -1081,12 +1082,12 @@ export class HtmlVideoPlayer {
|
|||
handleHlsJsMediaError(this);
|
||||
return;
|
||||
} else {
|
||||
type = 'mediadecodeerror';
|
||||
type = MediaError.MEDIA_DECODE_ERROR;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||
type = 'medianotsupported';
|
||||
type = MediaError.MEDIA_NOT_SUPPORTED;
|
||||
break;
|
||||
default:
|
||||
// seeing cases where Edge is firing error events with no error code
|
||||
|
@ -1276,7 +1277,7 @@ export class HtmlVideoPlayer {
|
|||
|
||||
// HACK: Give JavascriptSubtitlesOctopus time to dispose itself
|
||||
setTimeout(() => {
|
||||
onErrorInternal(htmlVideoPlayer, 'mediadecodeerror');
|
||||
onErrorInternal(this, MediaError.ASS_RENDER_ERROR);
|
||||
}, 0);
|
||||
},
|
||||
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000,
|
||||
|
|
|
@ -1228,6 +1228,16 @@
|
|||
"Play": "Play",
|
||||
"PlayAllFromHere": "Play all from here",
|
||||
"PlaybackData": "Playback Info",
|
||||
"PlaybackError.ASS_RENDER_ERROR": "An error was encountered in the ASS/SSA subtitle renderer.",
|
||||
"PlaybackError.FATAL_HLS_ERROR": "A fatal error was encountered in the HLS stream.",
|
||||
"PlaybackError.MEDIA_DECODE_ERROR": "Playback failed due to an error decoding the media.",
|
||||
"PlaybackError.MEDIA_NOT_SUPPORTED": "Playback failed because the media is not supported by this client.",
|
||||
"PlaybackError.NETWORK_ERROR": "Playback failed due to a network error.",
|
||||
"PlaybackError.NO_MEDIA_ERROR": "Unable to find a valid media source to play.",
|
||||
"PlaybackError.PLAYER_ERROR": "Playback failed due to a fatal player error.",
|
||||
"PlaybackError.SERVER_ERROR": "Playback failed due to a server error.",
|
||||
"PlaybackError.NotAllowed": "Playback of this media is not allowed.",
|
||||
"PlaybackError.RateLimitExceeded": "This media cannot be played at this time due to rate limits.",
|
||||
"PlaybackErrorNoCompatibleStream": "This client isn't compatible with the media and the server isn't sending a compatible media format.",
|
||||
"PlaybackErrorPlaceHolder": "This is a placeholder for physical media that Jellyfin cannot play. Please insert the disc to play.",
|
||||
"PlaybackRate": "Playback Speed",
|
||||
|
|
13
src/types/mediaError.ts
Normal file
13
src/types/mediaError.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Error types used for reporting media playback errors.
|
||||
*/
|
||||
export enum MediaError {
|
||||
ASS_RENDER_ERROR = 'ASS_RENDER_ERROR',
|
||||
FATAL_HLS_ERROR = 'FATAL_HLS_ERROR',
|
||||
MEDIA_DECODE_ERROR = 'MEDIA_DECODE_ERROR',
|
||||
MEDIA_NOT_SUPPORTED = 'MEDIA_NOT_SUPPORTED',
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
NO_MEDIA_ERROR = 'NO_MEDIA_ERROR',
|
||||
PLAYER_ERROR = 'PLAYER_ERROR',
|
||||
SERVER_ERROR = 'SERVER_ERROR'
|
||||
}
|
11
src/utils/mediaError.ts
Normal file
11
src/utils/mediaError.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { MediaError } from 'types/mediaError';
|
||||
|
||||
/**
|
||||
* Maps a DOMException name to an equivalent {@link MediaError}.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException#error_names
|
||||
*/
|
||||
export function getMediaError(e?: DOMException): MediaError {
|
||||
if (e?.name === 'NotSupportedError') return MediaError.MEDIA_NOT_SUPPORTED;
|
||||
return MediaError.PLAYER_ERROR;
|
||||
}
|
|
@ -6,5 +6,6 @@ import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client';
|
|||
* @returns _true_ if the media source is an HLS stream, _false_ otherwise.
|
||||
*/
|
||||
export function isHls(mediaSource: MediaSourceInfo|null|undefined): boolean {
|
||||
return (mediaSource?.TranscodingSubProtocol || mediaSource?.Container) === 'hls';
|
||||
const protocol = mediaSource?.TranscodingSubProtocol || mediaSource?.Container;
|
||||
return protocol?.toUpperCase() === 'HLS';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue