mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-04-06 17:21:58 -04:00
Handle missing video files in player
This commit is contained in:
parent
066e50f8b5
commit
c8c23a8f65
@ -49,7 +49,7 @@
|
||||
"part": "Part {{number}}",
|
||||
"videos-map": "Edit video mappings",
|
||||
"remap": "Remap",
|
||||
"staff-as":"as {{character}}",
|
||||
"staff-as": "as {{character}}",
|
||||
"staff-kind": {
|
||||
"actor": "Actor",
|
||||
"director": "Director",
|
||||
@ -253,7 +253,9 @@
|
||||
"transmux": "Original",
|
||||
"auto": "Auto",
|
||||
"notInPristine": "Unavailable in pristine",
|
||||
"unsupportedError": "Video codec not supported, transcoding in progress..."
|
||||
"unsupportedError": "Video codec not supported, transcoding in progress...",
|
||||
"not-available": "{{entry}} is not available on kyoo yet, ask your server admins about it",
|
||||
"fatal": "Fatal playback error"
|
||||
},
|
||||
"search": {
|
||||
"empty": "No result found. Try a different query."
|
||||
|
||||
@ -49,8 +49,14 @@ export const FullVideo = Video.extend({
|
||||
playedDate: zdate().nullable(),
|
||||
videoId: z.string().nullable(),
|
||||
}),
|
||||
previous: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||
next: z.object({ video: z.string(), entry: Entry }).nullable().optional(),
|
||||
previous: z
|
||||
.object({ video: z.string().nullable(), entry: Entry })
|
||||
.nullable()
|
||||
.optional(),
|
||||
next: z
|
||||
.object({ video: z.string().nullable(), entry: Entry })
|
||||
.nullable()
|
||||
.optional(),
|
||||
show: Show.optional().nullable(),
|
||||
});
|
||||
export type FullVideo = z.infer<typeof FullVideo>;
|
||||
|
||||
29
front/src/ui/player/controls/error-popup.tsx
Normal file
29
front/src/ui/player/controls/error-popup.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Heading, IconButton, P } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const ErrorPopup = ({
|
||||
message,
|
||||
dismiss,
|
||||
}: {
|
||||
message: string;
|
||||
dismiss: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"absolute inset-x-6 top-1/2 flex-1 -translate-y-1/2 flex-row justify-between",
|
||||
"rounded-xl border border-slate-700 bg-background p-5",
|
||||
)}
|
||||
>
|
||||
<View className="flex-1 flex-wrap">
|
||||
<Heading className="my-2">{t("player.fatal")}</Heading>
|
||||
<P className="mt-2 flex-1">{message}</P>
|
||||
</View>
|
||||
<IconButton icon={Close} onPress={dismiss} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@ -20,6 +20,7 @@ export const Controls = ({
|
||||
chapters,
|
||||
playPrev,
|
||||
playNext,
|
||||
forceShow,
|
||||
}: {
|
||||
player: VideoPlayer;
|
||||
showHref?: string;
|
||||
@ -31,6 +32,7 @@ export const Controls = ({
|
||||
chapters: Chapter[];
|
||||
playPrev: (() => boolean) | null;
|
||||
playNext: (() => boolean) | null;
|
||||
forceShow?: boolean;
|
||||
}) => {
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
@ -56,7 +58,7 @@ export const Controls = ({
|
||||
<View className="absolute inset-0">
|
||||
<TouchControls
|
||||
player={player}
|
||||
forceShow={hover || menuOpened}
|
||||
forceShow={hover || menuOpened || forceShow}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<Back
|
||||
|
||||
@ -2,6 +2,7 @@ import "react-native-get-random-values";
|
||||
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useEvent, useVideoPlayer, VideoView } from "react-native-video";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@ -14,6 +15,7 @@ import { type QueryIdentifier, useFetch } from "~/query";
|
||||
import { Info } from "~/ui/info";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Controls, LoadingIndicator } from "./controls";
|
||||
import { ErrorPopup } from "./controls/error-popup";
|
||||
import { toggleFullscreen } from "./controls/misc";
|
||||
import { PlayModeContext } from "./controls/tracks-menu";
|
||||
import { useKeyboard } from "./keyboard";
|
||||
@ -45,6 +47,7 @@ export const Player = () => {
|
||||
);
|
||||
const playModeState = useState(defaultPlayMode);
|
||||
const [playMode, setPlayMode] = playModeState;
|
||||
const [playbackError, setPlaybackError] = useState<KyooError | undefined>();
|
||||
const player = useVideoPlayer(
|
||||
{
|
||||
uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`,
|
||||
@ -101,18 +104,39 @@ export const Player = () => {
|
||||
}, [player, info?.fonts]);
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const playPrev = useCallback(() => {
|
||||
if (!data?.previous) return false;
|
||||
if (!data.previous.video) {
|
||||
setPlaybackError({
|
||||
status: "not-available",
|
||||
message: t("player.not-available", {
|
||||
entry: `${entryDisplayNumber(data.previous.entry)} ${data.previous.entry.name}`,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
setPlaybackError(undefined);
|
||||
setStart("0");
|
||||
setSlug(data.previous.video);
|
||||
return true;
|
||||
}, [data?.previous, setSlug, setStart]);
|
||||
}, [data?.previous, setSlug, setStart, t]);
|
||||
const playNext = useCallback(() => {
|
||||
if (!data?.next) return false;
|
||||
if (!data.next.video) {
|
||||
setPlaybackError({
|
||||
status: "not-available",
|
||||
message: t("player.not-available", {
|
||||
entry: `${entryDisplayNumber(data.next.entry)} ${data.next.entry.name}`,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
setPlaybackError(undefined);
|
||||
setStart("0");
|
||||
setSlug(data.next.video);
|
||||
return true;
|
||||
}, [data?.next, setSlug, setStart]);
|
||||
}, [data?.next, setSlug, setStart, t]);
|
||||
|
||||
useProgressObserver(
|
||||
player,
|
||||
@ -148,7 +172,6 @@ export const Player = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [playbackError, setPlaybackError] = useState<KyooError | undefined>();
|
||||
useEvent(player, "onError", (error) => {
|
||||
if (
|
||||
error.code === "source/unsupported-content-type" &&
|
||||
@ -157,9 +180,6 @@ export const Player = () => {
|
||||
setPlayMode("hls");
|
||||
else setPlaybackError({ status: error.code, message: error.message });
|
||||
});
|
||||
if (playbackError) {
|
||||
throw playbackError;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-black">
|
||||
@ -201,10 +221,17 @@ export const Player = () => {
|
||||
: data?.path
|
||||
}
|
||||
chapters={info?.chapters ?? []}
|
||||
playPrev={data?.previous?.video ? playPrev : null}
|
||||
playNext={data?.next?.video ? playNext : null}
|
||||
playPrev={data?.previous ? playPrev : null}
|
||||
playNext={data?.next ? playNext : null}
|
||||
forceShow={!!playbackError}
|
||||
/>
|
||||
</PlayModeContext.Provider>
|
||||
{playbackError && (
|
||||
<ErrorPopup
|
||||
message={playbackError.message}
|
||||
dismiss={() => setPlaybackError(undefined)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user