feat(plex): surface playback errors + media-mount status in the UI

PlexPlayer: when the stream session can't start, show why instead of an eternal
spinner — 404 (physical file unreachable: missing media mount / wrong path map),
501 (format needs transcoding), or a generic failure; also catch fatal hls.js
errors. ConfigPanel: render the Test-connection media_check (mount OK, or a
warning naming the unreadable local path). New plex/config i18n in en/hu/de.
This commit is contained in:
npeter83 2026-07-05 06:58:34 +02:00
parent 2138f3c357
commit fdd39ef3e4
9 changed files with 59 additions and 7 deletions

View file

@ -56,6 +56,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
const [muted, setMuted] = useState(false);
const [fs, setFs] = useState(false);
const [ready, setReady] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [uiVisible, setUiVisible] = useState(true);
const hideTimer = useRef<number | null>(null);
// Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off).
@ -83,10 +84,21 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
const video = videoRef.current;
if (!video) return;
setReady(false);
setLoadError(null);
let sess;
try {
sess = await api.plexSession(id, startAt, audioRef.current, subRef.current);
} catch {
} catch (e: any) {
// Surface WHY playback can't start instead of an eternal spinner. 404 = the physical file
// isn't reachable (missing media bind-mount or a wrong plex_path_map); 501 = the codec
// needs full transcoding (a later phase).
setLoadError(
e?.status === 501
? t("plex.player.errTranscode")
: e?.status === 404
? t("plex.player.errNotFound")
: t("plex.player.errGeneric"),
);
return;
}
sessionStartRef.current = sess.start_s;
@ -112,6 +124,11 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
setReady(true);
video.play().catch(() => {});
});
// A fatal HLS error (e.g. the remux session died / the file became unreadable) would
// otherwise leave the spinner up forever — surface it instead.
hls.on(Hls.Events.ERROR, (_e, data) => {
if (data.fatal) setLoadError(t("plex.player.errGeneric"));
});
} else {
// direct file (or Safari native HLS)
video.src = sess.url;
@ -125,7 +142,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
video.addEventListener("loadedmetadata", onMeta);
}
},
[id],
[id, t],
);
// Start playback when the detail arrives (resume from the saved position unless already watched).
@ -407,8 +424,12 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
/>
{!ready && (
<div className="absolute inset-0 grid place-items-center text-white/70 text-sm pointer-events-none">
{t("plex.player.loading")}
<div className="absolute inset-0 grid place-items-center px-6 text-center text-sm pointer-events-none">
{loadError ? (
<span className="max-w-md text-red-300">{loadError}</span>
) : (
<span className="text-white/70">{t("plex.player.loading")}</span>
)}
</div>
)}