Merge pull request #1926 from PR0M3TH3AN/codex/add-error-handling-for-media-loading

Improve media failure messaging and avatar fallbacks
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-29 10:40:19 -05:00
commit ad6bd60f89
3 files changed
+217 -9

No files matched your search

+199 -9
View File
@@ -62,6 +62,159 @@ class SimpleEventEmitter {
const HOSTED_URL_SUCCESS_MESSAGE = "✅ Streaming from hosted URL";
const DEFAULT_UNSUPPORTED_BTITH_MESSAGE =
"This magnet link is missing a compatible BitTorrent v1 info hash.";
const AUTH_STATUS_CODES = new Set([401, 403]);
const SSL_ERROR_PATTERN = /(ssl|cert|certificate)/i;
const CORS_ERROR_PATTERN = /cors/i;
const getHostedUrlFailureDetails = (probeResult = {}) => {
const outcome = probeResult?.outcome || "error";
const status = Number.isFinite(probeResult?.status)
? probeResult.status
: null;
const error = probeResult?.error;
const isAuthStatus = status !== null && AUTH_STATUS_CODES.has(status);
if (outcome === "timeout") {
return {
category: "external",
message:
"Hosted URL timed out. We’ll try WebTorrent if available.",
status,
};
}
if (outcome === "bad") {
if (isAuthStatus) {
return {
category: "auth",
message:
"Hosted URL requires authorization or a signed request. Please log in or re-sign.",
status,
};
}
if (status === 404) {
return {
category: "external",
message: "Hosted URL not found (404).",
status,
};
}
if (status === 403) {
return {
category: "external",
message: "Hosted URL blocked (403).",
status,
};
}
if (status && status >= 500) {
return {
category: "external",
message: `Hosted URL unavailable (HTTP ${status}).`,
status,
};
}
if (status) {
return {
category: "external",
message: `Hosted URL failed (HTTP ${status}).`,
status,
};
}
}
if (outcome === "opaque" || outcome === "unknown") {
return {
category: "external",
message:
"Hosted URL blocked by browser security (CORS/SSL). We’ll try WebTorrent if available.",
status,
};
}
if (outcome === "error") {
const message = error?.message || "";
if (SSL_ERROR_PATTERN.test(message)) {
return {
category: "external",
message:
"Hosted URL blocked due to SSL certificate issues.",
status,
};
}
if (CORS_ERROR_PATTERN.test(message)) {
return {
category: "external",
message:
"Hosted URL blocked by CORS. We’ll try WebTorrent if available.",
status,
};
}
return {
category: "external",
message:
"Hosted URL failed to load due to network or security restrictions.",
status,
};
}
return {
category: "external",
message: "Hosted URL failed to load.",
status,
};
};
const getHostedVideoErrorMessage = (videoElement) => {
if (!videoElement?.error) {
return "";
}
const error = videoElement.error;
const code = error.code;
const mediaErrorCodes =
typeof MediaError !== "undefined"
? MediaError
: {
MEDIA_ERR_ABORTED: 1,
MEDIA_ERR_NETWORK: 2,
MEDIA_ERR_DECODE: 3,
MEDIA_ERR_SRC_NOT_SUPPORTED: 4,
};
switch (code) {
case mediaErrorCodes.MEDIA_ERR_ABORTED:
return "Hosted playback was interrupted.";
case mediaErrorCodes.MEDIA_ERR_NETWORK:
return "Hosted playback failed due to network/CORS restrictions.";
case mediaErrorCodes.MEDIA_ERR_DECODE:
return "Hosted playback failed to decode the video.";
case mediaErrorCodes.MEDIA_ERR_SRC_NOT_SUPPORTED:
return "Hosted playback failed: source not supported or blocked.";
default:
return "Hosted playback failed to load.";
}
};
const getTorrentErrorMessage = (error) => {
if (!error) {
return "WebTorrent could not start. Please try again.";
}
const message = error.message || "";
if (message.includes(DEFAULT_UNSUPPORTED_BTITH_MESSAGE)) {
return DEFAULT_UNSUPPORTED_BTITH_MESSAGE;
}
if (/tracker|announce|peer/i.test(message)) {
return "WebTorrent could not reach any peers or trackers.";
}
if (/permission|blocked|denied/i.test(message)) {
return "WebTorrent was blocked by the browser or network.";
}
return "WebTorrent could not start. Please try again.";
};
export class PlaybackService {
constructor({
@@ -602,10 +755,19 @@ class PlaybackSession extends SimpleEventEmitter {
throw new Error("No torrent playback handler provided.");
}
const torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
fallbackMagnet: this.fallbackMagnet,
urlList: webSeedCandidates,
});
let torrentInstance;
try {
torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
fallbackMagnet: this.fallbackMagnet,
urlList: webSeedCandidates,
});
} catch (err) {
const errorMessage = getTorrentErrorMessage(err);
if (errorMessage) {
this.emit("status", { message: errorMessage });
}
throw new Error(errorMessage, { cause: err });
}
this.service.handleAnalyticsEvent("sourcechange", { source: "torrent" });
this.emit("sourcechange", { source: "torrent" });
@@ -726,7 +888,11 @@ class PlaybackSession extends SimpleEventEmitter {
attachWatchdogs({ stallMs: 0 });
return;
}
outcomeResolver({ status: "fallback", reason });
const errorMessage = getHostedVideoErrorMessage(activeVideoEl);
if (errorMessage) {
this.emit("status", { message: errorMessage });
}
outcomeResolver({ status: "fallback", reason, errorMessage });
},
});
};
@@ -792,12 +958,25 @@ class PlaybackSession extends SimpleEventEmitter {
// Fallback triggered
const fallbackReason = playbackOutcome?.reason || "watchdog-triggered";
return { status: "fallback", reason: fallbackReason };
return {
status: "fallback",
reason: fallbackReason,
errorMessage: playbackOutcome?.errorMessage,
};
}
const failureDetails = getHostedUrlFailureDetails(probeResult);
if (failureDetails?.message) {
this.emit("status", { message: failureDetails.message });
}
cleanupHostedUrlStatusListeners();
cleanupDebugListeners();
return { status: "fallback", reason: "probe-failed" };
return {
status: "fallback",
reason: "probe-failed",
errorMessage: failureDetails?.message,
};
};
// --- Timeout Wrapper ---
@@ -829,6 +1008,8 @@ class PlaybackSession extends SimpleEventEmitter {
const effectiveTimeout = forcedSource ? 0 : playbackStartTimeout;
let hostedErrorMessage = "";
if (tryUrlFirst) {
if (httpsUrl) {
// Wrap URL attempt in timeout
@@ -842,6 +1023,10 @@ class PlaybackSession extends SimpleEventEmitter {
return urlResult;
}
if (typeof urlResult?.errorMessage === "string") {
hostedErrorMessage = urlResult.errorMessage;
}
// If timeout or failure, proceed to fallback
if (urlResult?.reason === "timeout") {
// Need to ensure URL probing/playback is effectively cancelled
@@ -905,13 +1090,18 @@ class PlaybackSession extends SimpleEventEmitter {
if (urlResult && urlResult.source === "url") {
return urlResult;
}
if (typeof urlResult?.errorMessage === "string") {
hostedErrorMessage = urlResult.errorMessage;
}
}
}
const message =
this.magnetProvided && !this.magnetForPlayback
hostedErrorMessage ||
(this.magnetProvided && !this.magnetForPlayback
? unsupportedBtihMessage
: "No playable source found.";
: "No playable source found.");
this.emit("status", { message });
this.service.handleAnalyticsEvent("sourcechange", { source: null });
this.emit("sourcechange", { source: null });
+9
View File
@@ -985,6 +985,15 @@ export class SimilarContentCard {
avatarImg.decoding = "async";
avatarImg.loading = "lazy";
avatarImg.alt = "";
if (!avatarImg.dataset.avatarFallbackBound) {
avatarImg.addEventListener("error", () => {
if (avatarImg.getAttribute("src") !== DEFAULT_PROFILE_AVATAR) {
avatarImg.src = DEFAULT_PROFILE_AVATAR;
}
avatarImg.dataset.avatarLoadFailed = "true";
});
avatarImg.dataset.avatarFallbackBound = "true";
}
avatarWrapper.appendChild(avatarImg);
// Append avatar then text
+9
View File
@@ -916,6 +916,15 @@ export class VideoCard {
"rounded-full",
"object-cover"
);
if (!avatar.dataset.avatarFallbackBound) {
avatar.addEventListener("error", () => {
if (avatar.getAttribute("src") !== DEFAULT_PROFILE_AVATAR) {
avatar.src = DEFAULT_PROFILE_AVATAR;
}
avatar.dataset.avatarLoadFailed = "true";
});
avatar.dataset.avatarFallbackBound = "true";
}
if (this.video.pubkey) {
avatar.dataset.pubkey = this.video.pubkey;
}