Whoops, it broke
{props.children ? (
- props.children
+
{props.children}
) : (
The app encountered an error and wasn't able to recover, please
diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx
index e1582d5c..f5a471ee 100644
--- a/src/components/video/DecoratedVideoPlayer.tsx
+++ b/src/components/video/DecoratedVideoPlayer.tsx
@@ -8,6 +8,7 @@ import { PauseControl } from "./controls/PauseControl";
import { ProgressControl } from "./controls/ProgressControl";
import { TimeControl } from "./controls/TimeControl";
import { VolumeControl } from "./controls/VolumeControl";
+import { VideoPlayerError } from "./parts/VideoPlayerError";
import { VideoPlayerHeader } from "./parts/VideoPlayerHeader";
import { useVideoPlayerState } from "./VideoContext";
import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer";
@@ -56,60 +57,62 @@ export function DecoratedVideoPlayer(
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
-
-
-
- {props.children}
+
+
+
+
+
+ {props.children}
+
);
}
diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx
index 02d71440..d00b917a 100644
--- a/src/components/video/VideoPlayer.tsx
+++ b/src/components/video/VideoPlayer.tsx
@@ -1,4 +1,6 @@
+import { useGoBack } from "@/hooks/useGoBack";
import { forwardRef, useContext, useEffect, useRef } from "react";
+import { VideoErrorBoundary } from "./parts/VideoErrorBoundary";
import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext";
export interface VideoPlayerProps {
@@ -35,6 +37,9 @@ const VideoPlayerInternals = forwardRef<
export function VideoPlayer(props: VideoPlayerProps) {
const playerRef = useRef
(null);
const playerWrapperRef = useRef(null);
+ const goBack = useGoBack();
+
+ // TODO move error boundary to only decorated, shouldn't have styling
return (
@@ -42,11 +47,13 @@ export function VideoPlayer(props: VideoPlayerProps) {
className="relative h-full w-full select-none overflow-hidden bg-black"
ref={playerWrapperRef}
>
-
- {props.children}
+
+
+ {props.children}
+
);
diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts
index 50e85fde..d9e8f87b 100644
--- a/src/components/video/hooks/controlVideo.ts
+++ b/src/components/video/hooks/controlVideo.ts
@@ -108,19 +108,36 @@ export function populateControls(
initPlayer(sourceUrl: string, sourceType: MWStreamType) {
this.setVolume(getStoredVolume());
+ // TODO test HLS errors
if (sourceType === MWStreamType.HLS) {
if (player.canPlayType("application/vnd.apple.mpegurl")) {
player.src = sourceUrl;
} else {
// HLS support
- if (!Hls.isSupported()) throw new Error("HLS not supported"); // TODO handle errors
+ if (!Hls.isSupported()) {
+ update((s) => ({
+ ...s,
+ error: {
+ name: `Not supported`,
+ description: "Your browser does not support HLS video",
+ },
+ }));
+ return;
+ }
const hls = new Hls();
hls.on(Hls.Events.ERROR, (event, data) => {
- // eslint-disable-next-line no-alert
- if (data.fatal) alert("HLS fatal error");
- console.error("HLS error", data); // TODO handle errors
+ if (data.fatal) {
+ update((s) => ({
+ ...s,
+ error: {
+ name: `error ${data.details}`,
+ description: data.error?.message ?? "Something went wrong",
+ },
+ }));
+ }
+ console.error("HLS error", data);
});
hls.attachMedia(player);
diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts
index 4589c07e..937af32b 100644
--- a/src/components/video/hooks/useVideoPlayer.ts
+++ b/src/components/video/hooks/useVideoPlayer.ts
@@ -23,6 +23,10 @@ export type PlayerState = {
hasInitialized: boolean;
leftControlHovering: boolean;
hasPlayedOnce: boolean;
+ error: null | {
+ name: string;
+ description: string;
+ };
};
export type PlayerContext = PlayerState & PlayerControls;
@@ -42,6 +46,7 @@ export const initialPlayerState: PlayerContext = {
hasInitialized: false,
leftControlHovering: false,
hasPlayedOnce: false,
+ error: null,
...initialControls,
};
@@ -61,6 +66,7 @@ function readState(player: HTMLVideoElement, update: SetPlayer) {
state.buffered = handleBuffered(player.currentTime, player.buffered);
state.isLoading = false;
state.hasInitialized = true;
+ state.error = null;
update((s) => ({
...state,
@@ -131,6 +137,19 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
isFirstLoading: false,
}));
};
+ const error = () => {
+ console.error("Native video player threw error", player.error);
+ // TODO check if these errors are actually fatal
+ update((s) => ({
+ ...s,
+ error: player.error
+ ? {
+ description: player.error.message,
+ name: `Error ${player.error.code}`,
+ }
+ : null,
+ }));
+ };
player.addEventListener("pause", pause);
player.addEventListener("playing", playing);
@@ -143,6 +162,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
player.addEventListener("progress", progress);
player.addEventListener("waiting", waiting);
player.addEventListener("canplay", canplay);
+ player.addEventListener("error", error);
return () => {
player.removeEventListener("pause", pause);
@@ -156,6 +176,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
player.removeEventListener("progress", progress);
player.removeEventListener("waiting", waiting);
player.removeEventListener("canplay", canplay);
+ player.removeEventListener("error", error);
};
}
diff --git a/src/components/video/parts/VideoErrorBoundary.tsx b/src/components/video/parts/VideoErrorBoundary.tsx
new file mode 100644
index 00000000..c05bbd5a
--- /dev/null
+++ b/src/components/video/parts/VideoErrorBoundary.tsx
@@ -0,0 +1,82 @@
+import { ErrorMessage } from "@/components/layout/ErrorBoundary";
+import { Link } from "@/components/text/Link";
+import { conf } from "@/setup/config";
+import { Component, ReactNode } from "react";
+import { VideoPlayerHeader } from "./VideoPlayerHeader";
+
+interface ErrorBoundaryState {
+ hasError: boolean;
+ error?: {
+ name: string;
+ description: string;
+ path: string;
+ };
+}
+
+interface VideoErrorBoundaryProps {
+ children?: ReactNode;
+ title?: string;
+ onGoBack?: () => void;
+}
+
+export class VideoErrorBoundary extends Component<
+ VideoErrorBoundaryProps,
+ ErrorBoundaryState
+> {
+ constructor(props: VideoErrorBoundaryProps) {
+ super(props);
+ this.state = {
+ hasError: false,
+ };
+ }
+
+ static getDerivedStateFromError() {
+ return {
+ hasError: true,
+ };
+ }
+
+ componentDidCatch(error: any, errorInfo: any) {
+ console.error("Render error caught", error, errorInfo);
+ if (error instanceof Error) {
+ const realError: Error = error as Error;
+ this.setState((s) => ({
+ ...s,
+ hasError: true,
+ error: {
+ name: realError.name,
+ description: realError.message,
+ path: errorInfo.componentStack.split("\n")[1],
+ },
+ }));
+ }
+ }
+
+ render() {
+ if (!this.state.hasError) return this.props.children;
+
+ // TODO make responsive, needs to work in tiny player
+
+ return (
+
+
+
+
+
+ The video player encounted a fatal error, please report it to the{" "}
+
+ Discord server
+ {" "}
+ or on{" "}
+
+ GitHub
+
+ .
+
+
+ );
+ }
+}
diff --git a/src/components/video/parts/VideoPlayerError.tsx b/src/components/video/parts/VideoPlayerError.tsx
new file mode 100644
index 00000000..26ae3dee
--- /dev/null
+++ b/src/components/video/parts/VideoPlayerError.tsx
@@ -0,0 +1,35 @@
+import { IconPatch } from "@/components/buttons/IconPatch";
+import { Icons } from "@/components/Icon";
+import { Title } from "@/components/text/Title";
+import { ReactNode } from "react";
+import { useVideoPlayerState } from "../VideoContext";
+import { VideoPlayerHeader } from "./VideoPlayerHeader";
+
+interface VideoPlayerErrorProps {
+ title?: string;
+ onGoBack?: () => void;
+ children?: ReactNode;
+}
+
+export function VideoPlayerError(props: VideoPlayerErrorProps) {
+ const { videoState } = useVideoPlayerState();
+
+ const err = videoState.error;
+
+ if (!err) return props.children as any;
+
+ return (
+
+
+
+
Failed to load media
+
+ {err.name}: {err.description}
+
+
+
+
+
+
+ );
+}
diff --git a/src/index.tsx b/src/index.tsx
index fce8fd42..008b958c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -17,7 +17,6 @@ if (key) {
}
// TODO video todos:
-// - error handling
// - captions
// - mobile UI
// - safari fullscreen will make video overlap player controls
@@ -35,6 +34,7 @@ if (key) {
// TODO general todos:
// - localize everything
+// - add titles to pages
ReactDOM.render(
diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx
index f0224953..b019e688 100644
--- a/src/views/media/MediaView.tsx
+++ b/src/views/media/MediaView.tsx
@@ -116,7 +116,6 @@ export function MediaView() {
}, [exec, params.media]);
// TODO watched store
- // TODO error page with video header
if (loading) return ;
if (error) return ;