add debug info copy button to playback error

This commit is contained in:
Pas 2025-08-03 14:48:17 -06:00
parent a7c5821dd3
commit bd9ff1698f
3 changed files with 210 additions and 3 deletions

View file

@ -749,6 +749,8 @@
"errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.",
"errorNotSupported": "The media or media provider object is not supported."
},
"copyDebugInfo": "Copy debug info",
"debugInfo": "Check console for more details.",
"homeButton": "Go home",
"text": "There was an error trying to play the media 😖. Please try again or try a different source!",
"title": "Failed to play video!"

View file

@ -5,6 +5,10 @@ import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Modal } from "@/components/overlays/Modal";
import { DisplayError } from "@/components/player/display/displayInterface";
import {
formatErrorDebugInfo,
gatherErrorDebugInfo,
} from "@/utils/errorDebugInfo";
export function ErrorCard(props: {
error: DisplayError | string;
@ -25,7 +29,13 @@ export function ErrorCard(props: {
function copyError() {
if (!props.error || !navigator.clipboard) return;
navigator.clipboard.writeText(`\`\`\`${errorMessage}\`\`\``);
const debugInfo = gatherErrorDebugInfo(props.error);
const formattedDebugInfo = formatErrorDebugInfo(debugInfo);
const fullErrorReport = `\`\`\`\n${errorMessage}\n\n${formattedDebugInfo}\n\`\`\``;
navigator.clipboard.writeText(fullErrorReport);
setHasCopied(true);
@ -57,7 +67,7 @@ export function ErrorCard(props: {
<>
<Icon icon={Icons.COPY} className="text-2xl" />
<span className="hidden min-[400px]:inline-block ml-3">
{t("actions.copy")}
{t("player.playbackError.copyDebugInfo")}
</span>
</>
)}
@ -74,7 +84,7 @@ export function ErrorCard(props: {
<div className="pointer-events-auto mt-4 h-60 select-text overflow-y-auto whitespace-pre text-left">
{errorMessage}
</div>
<p className="mt-4 text-sm">Check console for more details</p>
<p className="mt-4 text-sm">{t("player.playbackError.debugInfo")}</p>
</div>
);
}

195
src/utils/errorDebugInfo.ts Normal file
View file

@ -0,0 +1,195 @@
import { detect } from "detect-browser";
import { usePlayerStore } from "@/stores/player/store";
export interface ErrorDebugInfo {
timestamp: string;
error: {
message: string;
type: string;
stackTrace?: string;
};
device: {
userAgent: string;
browser: string;
os: string;
isMobile: boolean;
isTV: boolean;
screenResolution: string;
viewportSize: string;
};
player: {
status: string;
sourceId: string | null;
currentQuality: string | null;
meta: {
title: string;
type: string;
tmdbId: string;
imdbId?: string;
releaseYear: number;
season?: number;
episode?: number;
} | null;
};
network: {
online: boolean;
connectionType?: string;
effectiveType?: string;
downlink?: number;
rtt?: number;
};
performance: {
memory?: {
usedJSHeapSize: number;
totalJSHeapSize: number;
jsHeapSizeLimit: number;
};
timing: {
navigationStart: number;
loadEventEnd: number;
domContentLoadedEventEnd: number;
};
};
}
export function gatherErrorDebugInfo(error: any): ErrorDebugInfo {
const browserInfo = detect();
const isMobile = window.innerWidth <= 768;
const isTV =
/SmartTV|Tizen|WebOS|SamsungBrowser|HbbTV|Viera|NetCast|AppleTV|Android TV|GoogleTV|Roku|PlayStation|Xbox|Opera TV|AquosBrowser|Hisense|SonyBrowser|SharpBrowser|AFT|Chromecast/i.test(
navigator.userAgent,
);
const playerStore = usePlayerStore.getState();
// Get network information
const connection =
(navigator as any).connection ||
(navigator as any).mozConnection ||
(navigator as any).webkitConnection;
// Get performance information
const performanceInfo = performance.getEntriesByType(
"navigation",
)[0] as PerformanceNavigationTiming;
const memory = (performance as any).memory;
return {
timestamp: new Date().toISOString(),
error: {
message: error?.message || error?.key || String(error),
type: error?.type || "unknown",
stackTrace: error?.stackTrace || error?.stack,
},
device: {
userAgent: navigator.userAgent,
browser: browserInfo?.name || "unknown",
os: browserInfo?.os || "unknown",
isMobile,
isTV,
screenResolution: `${window.screen.width}x${window.screen.height}`,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
},
player: {
status: playerStore.status,
sourceId: playerStore.sourceId,
currentQuality: playerStore.currentQuality,
meta: playerStore.meta
? {
title: playerStore.meta.title,
type: playerStore.meta.type,
tmdbId: playerStore.meta.tmdbId,
imdbId: playerStore.meta.imdbId,
releaseYear: playerStore.meta.releaseYear,
season: playerStore.meta.season?.number,
episode: playerStore.meta.episode?.number,
}
: null,
},
network: {
online: navigator.onLine,
connectionType: connection?.type,
effectiveType: connection?.effectiveType,
downlink: connection?.downlink,
rtt: connection?.rtt,
},
performance: {
memory: memory
? {
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
}
: undefined,
timing: {
navigationStart: performanceInfo?.fetchStart || 0,
loadEventEnd: performanceInfo?.loadEventEnd || 0,
domContentLoadedEventEnd:
performanceInfo?.domContentLoadedEventEnd || 0,
},
},
};
}
export function formatErrorDebugInfo(info: ErrorDebugInfo): string {
const sections = [
`=== ERROR DEBUG INFO ===`,
`Timestamp: ${info.timestamp}`,
``,
`=== ERROR DETAILS ===`,
`Type: ${info.error.type}`,
`Message: ${info.error.message}`,
info.error.stackTrace ? `Stack Trace:\n${info.error.stackTrace}` : "",
``,
`=== DEVICE INFO ===`,
`Browser: ${info.device.browser} (${info.device.os})`,
`User Agent: ${info.device.userAgent}`,
`Screen: ${info.device.screenResolution}`,
`Viewport: ${info.device.viewportSize}`,
`Mobile: ${info.device.isMobile}`,
`TV: ${info.device.isTV}`,
``,
`=== PLAYER STATE ===`,
`Status: ${info.player.status}`,
`Source ID: ${info.player.sourceId || "null"}`,
`Quality: ${info.player.currentQuality || "null"}`,
info.player.meta
? [
`Media: ${info.player.meta.title} (${info.player.meta.type})`,
`TMDB ID: ${info.player.meta.tmdbId}`,
info.player.meta.imdbId ? `IMDB ID: ${info.player.meta.imdbId}` : "",
`Year: ${info.player.meta.releaseYear}`,
info.player.meta.season ? `Season: ${info.player.meta.season}` : "",
info.player.meta.episode
? `Episode: ${info.player.meta.episode}`
: "",
]
.filter(Boolean)
.join("\n")
: "No media loaded",
``,
`=== NETWORK INFO ===`,
`Online: ${info.network.online}`,
info.network.connectionType
? `Connection Type: ${info.network.connectionType}`
: "",
info.network.effectiveType
? `Effective Type: ${info.network.effectiveType}`
: "",
info.network.downlink ? `Downlink: ${info.network.downlink} Mbps` : "",
info.network.rtt ? `RTT: ${info.network.rtt} ms` : "",
``,
`=== PERFORMANCE ===`,
info.performance.memory
? [
`Memory Used: ${Math.round(info.performance.memory.usedJSHeapSize / 1024 / 1024)} MB`,
`Memory Total: ${Math.round(info.performance.memory.totalJSHeapSize / 1024 / 1024)} MB`,
`Memory Limit: ${Math.round(info.performance.memory.jsHeapSizeLimit / 1024 / 1024)} MB`,
].join("\n")
: "Memory info not available",
];
return sections.filter(Boolean).join("\n");
}