mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-07 11:33:37 -05:00
Add keyboard bindings for player
This commit is contained in:
parent
70ff2285d5
commit
dfdeca35f3
@ -1,6 +1,9 @@
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useEvent, useVideoPlayer, VideoView } from "react-native-video";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { entryDisplayNumber } from "~/components/entries";
|
||||
import { FullVideo, type KyooError, VideoInfo } from "~/models";
|
||||
import { ContrastArea, Head } from "~/primitives";
|
||||
@ -8,14 +11,12 @@ import { useToken } from "~/providers/account-context";
|
||||
import { useLocalSetting } from "~/providers/settings";
|
||||
import { type QueryIdentifier, useFetch } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { Controls, LoadingIndicator } from "./controls";
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { toggleFullscreen } from "./controls/misc";
|
||||
import { Back } from "./controls/back";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ErrorView } from "../errors";
|
||||
import { Controls, LoadingIndicator } from "./controls";
|
||||
import { Back } from "./controls/back";
|
||||
import { toggleFullscreen } from "./controls/misc";
|
||||
import { PlayModeContext } from "./controls/tracks-menu";
|
||||
import { useKeyboard } from "./keyboard";
|
||||
|
||||
const clientId = uuidv4();
|
||||
|
||||
@ -83,44 +84,38 @@ export const Player = () => {
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const playPrev = useCallback(() => {
|
||||
if (!data?.previous) return false;
|
||||
setStart(0);
|
||||
setSlug(data.previous.video);
|
||||
return true;
|
||||
}, [data?.previous, setSlug, setStart]);
|
||||
const playNext = useCallback(() => {
|
||||
if (!data?.next) return false;
|
||||
setStart(0);
|
||||
setSlug(data.next.video);
|
||||
return true;
|
||||
}, [data?.next, setSlug, setStart]);
|
||||
|
||||
useEvent(player, "onEnd", () => {
|
||||
if (!data) return;
|
||||
if (data.next) {
|
||||
setStart(0);
|
||||
setSlug(data.next.video);
|
||||
} else {
|
||||
router.navigate(data.show!.href);
|
||||
}
|
||||
const hasNext = playNext();
|
||||
if (!hasNext && data?.show) router.navigate(data.show.href);
|
||||
});
|
||||
|
||||
// TODO: add the equivalent of this for android
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const prev = data?.previous?.video;
|
||||
window.navigator.mediaSession.setActionHandler(
|
||||
"previoustrack",
|
||||
prev
|
||||
? () => {
|
||||
setStart(0);
|
||||
setSlug(prev);
|
||||
}
|
||||
: null,
|
||||
data?.previous?.video ? playPrev : null,
|
||||
);
|
||||
const next = data?.next?.video;
|
||||
window.navigator.mediaSession.setActionHandler(
|
||||
"nexttrack",
|
||||
next
|
||||
? () => {
|
||||
setStart(0);
|
||||
setSlug(next);
|
||||
}
|
||||
: null,
|
||||
data?.next?.video ? playNext : null,
|
||||
);
|
||||
}, [data?.next?.video, data?.previous?.video, setSlug, setStart]);
|
||||
}, [data?.next?.video, data?.previous?.video, playNext, playPrev]);
|
||||
|
||||
// useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
|
||||
|
||||
// const startTime = startTimeP ?? data?.watchStatus?.watchedTime;
|
||||
useKeyboard(player, playPrev, playNext);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
|
||||
164
front/src/ui/player/keyboard.tsx
Normal file
164
front/src/ui/player/keyboard.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { useEffect } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import type { VideoPlayer } from "react-native-video";
|
||||
import type { Subtitle } from "~/models";
|
||||
import { toggleFullscreen } from "./controls/misc";
|
||||
|
||||
type Action =
|
||||
| { type: "play" }
|
||||
| { type: "mute" }
|
||||
| { type: "fullscreen" }
|
||||
| { type: "seek"; value: number }
|
||||
| { type: "seekTo"; value: number }
|
||||
| { type: "seekPercent"; value: number }
|
||||
| { type: "volume"; value: number }
|
||||
| { type: "subtitle"; subtitles: Subtitle[]; fonts: string[] };
|
||||
|
||||
const reducer = (player: VideoPlayer, action: Action) => {
|
||||
switch (action.type) {
|
||||
case "play":
|
||||
if (player.isPlaying) player.pause();
|
||||
else player.play();
|
||||
break;
|
||||
case "mute":
|
||||
player.muted = !player.muted;
|
||||
break;
|
||||
case "fullscreen":
|
||||
toggleFullscreen();
|
||||
break;
|
||||
case "seek":
|
||||
player.seekBy(action.value);
|
||||
break;
|
||||
case "seekTo":
|
||||
player.seekTo(action.value);
|
||||
break;
|
||||
case "seekPercent":
|
||||
player.seekTo((player.duration * action.value) / 100);
|
||||
break;
|
||||
case "volume":
|
||||
player.volume = Math.max(0, Math.min(player.volume + action.value, 100));
|
||||
break;
|
||||
// case "subtitle": {
|
||||
// const subtitle = get(subtitleAtom);
|
||||
// const index = subtitle
|
||||
// ? action.subtitles.findIndex((x) => x.index === subtitle.index)
|
||||
// : -1;
|
||||
// set(
|
||||
// subtitleAtom,
|
||||
// index === -1
|
||||
// ? null
|
||||
// : action.subtitles[(index + 1) % action.subtitles.length],
|
||||
// );
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
export const useKeyboard = (
|
||||
player: VideoPlayer,
|
||||
playPrev: () => void,
|
||||
playNext: () => void,
|
||||
// subtitles?: Subtitle[],
|
||||
// fonts?: string[],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case " ":
|
||||
case "k":
|
||||
case "MediaPlay":
|
||||
case "MediaPause":
|
||||
case "MediaPlayPause":
|
||||
reducer(player, { type: "play" });
|
||||
break;
|
||||
|
||||
case "m":
|
||||
reducer(player, { type: "mute" });
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
reducer(player, { type: "seek", value: -5 });
|
||||
break;
|
||||
case "ArrowRight":
|
||||
reducer(player, { type: "seek", value: +5 });
|
||||
break;
|
||||
|
||||
case "j":
|
||||
reducer(player, { type: "seek", value: -10 });
|
||||
break;
|
||||
case "l":
|
||||
reducer(player, { type: "seek", value: +10 });
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
reducer(player, { type: "volume", value: +.05 });
|
||||
break;
|
||||
case "ArrowDown":
|
||||
reducer(player, { type: "volume", value: -.05 });
|
||||
break;
|
||||
|
||||
case "f":
|
||||
reducer(player, { type: "fullscreen" });
|
||||
break;
|
||||
|
||||
// case "v":
|
||||
// case "c":
|
||||
// if (!subtitles || !fonts) return;
|
||||
// reducer(player, { type: "subtitle", subtitles, fonts });
|
||||
// break;
|
||||
|
||||
case "n":
|
||||
case "N":
|
||||
playNext();
|
||||
break;
|
||||
|
||||
case "p":
|
||||
case "P":
|
||||
playPrev();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (event.code) {
|
||||
case "Digit0":
|
||||
reducer(player, { type: "seekPercent", value: 0 });
|
||||
break;
|
||||
case "Digit1":
|
||||
reducer(player, { type: "seekPercent", value: 10 });
|
||||
break;
|
||||
case "Digit2":
|
||||
reducer(player, { type: "seekPercent", value: 20 });
|
||||
break;
|
||||
case "Digit3":
|
||||
reducer(player, { type: "seekPercent", value: 30 });
|
||||
break;
|
||||
case "Digit4":
|
||||
reducer(player, { type: "seekPercent", value: 40 });
|
||||
break;
|
||||
case "Digit5":
|
||||
reducer(player, { type: "seekPercent", value: 50 });
|
||||
break;
|
||||
case "Digit6":
|
||||
reducer(player, { type: "seekPercent", value: 60 });
|
||||
break;
|
||||
case "Digit7":
|
||||
reducer(player, { type: "seekPercent", value: 70 });
|
||||
break;
|
||||
case "Digit8":
|
||||
reducer(player, { type: "seekPercent", value: 80 });
|
||||
break;
|
||||
case "Digit9":
|
||||
reducer(player, { type: "seekPercent", value: 90 });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keyup", handler);
|
||||
return () => document.removeEventListener("keyup", handler);
|
||||
}, [player, playPrev, playNext]);
|
||||
};
|
||||
@ -1,191 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Subtitle } from "@kyoo/models";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { useRouter } from "solito/router";
|
||||
import {
|
||||
durationAtom,
|
||||
fullscreenAtom,
|
||||
mutedAtom,
|
||||
playAtom,
|
||||
progressAtom,
|
||||
subtitleAtom,
|
||||
volumeAtom,
|
||||
} from "./old/statee";
|
||||
|
||||
type Action =
|
||||
| { type: "play" }
|
||||
| { type: "mute" }
|
||||
| { type: "fullscreen" }
|
||||
| { type: "seek"; value: number }
|
||||
| { type: "seekTo"; value: number }
|
||||
| { type: "seekPercent"; value: number }
|
||||
| { type: "volume"; value: number }
|
||||
| { type: "subtitle"; subtitles: Subtitle[]; fonts: string[] };
|
||||
|
||||
export const reducerAtom = atom(null, (get, set, action: Action) => {
|
||||
const duration = get(durationAtom);
|
||||
switch (action.type) {
|
||||
case "play":
|
||||
set(playAtom, !get(playAtom));
|
||||
break;
|
||||
case "mute":
|
||||
set(mutedAtom, !get(mutedAtom));
|
||||
break;
|
||||
case "fullscreen":
|
||||
set(fullscreenAtom, !get(fullscreenAtom));
|
||||
break;
|
||||
case "seek":
|
||||
if (duration)
|
||||
set(progressAtom, Math.max(0, Math.min(get(progressAtom) + action.value, duration)));
|
||||
break;
|
||||
case "seekTo":
|
||||
set(progressAtom, action.value);
|
||||
break;
|
||||
case "seekPercent":
|
||||
if (duration) set(progressAtom, (duration * action.value) / 100);
|
||||
break;
|
||||
case "volume":
|
||||
set(volumeAtom, Math.max(0, Math.min(get(volumeAtom) + action.value, 100)));
|
||||
break;
|
||||
case "subtitle": {
|
||||
const subtitle = get(subtitleAtom);
|
||||
const index = subtitle ? action.subtitles.findIndex((x) => x.index === subtitle.index) : -1;
|
||||
set(
|
||||
subtitleAtom,
|
||||
index === -1 ? null : action.subtitles[(index + 1) % action.subtitles.length],
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const useVideoKeyboard = (
|
||||
subtitles?: Subtitle[],
|
||||
fonts?: string[],
|
||||
previousEpisode?: string,
|
||||
nextEpisode?: string,
|
||||
) => {
|
||||
const reducer = useSetAtom(reducerAtom);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
||||
|
||||
switch (event.key) {
|
||||
case " ":
|
||||
case "k":
|
||||
case "MediaPlay":
|
||||
case "MediaPause":
|
||||
case "MediaPlayPause":
|
||||
reducer({ type: "play" });
|
||||
break;
|
||||
|
||||
case "m":
|
||||
reducer({ type: "mute" });
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
reducer({ type: "seek", value: -5 });
|
||||
break;
|
||||
case "ArrowRight":
|
||||
reducer({ type: "seek", value: +5 });
|
||||
break;
|
||||
|
||||
case "j":
|
||||
reducer({ type: "seek", value: -10 });
|
||||
break;
|
||||
case "l":
|
||||
reducer({ type: "seek", value: +10 });
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
reducer({ type: "volume", value: +5 });
|
||||
break;
|
||||
case "ArrowDown":
|
||||
reducer({ type: "volume", value: -5 });
|
||||
break;
|
||||
|
||||
case "f":
|
||||
reducer({ type: "fullscreen" });
|
||||
break;
|
||||
|
||||
case "v":
|
||||
case "c":
|
||||
if (!subtitles || !fonts) return;
|
||||
reducer({ type: "subtitle", subtitles, fonts });
|
||||
break;
|
||||
|
||||
case "n":
|
||||
case "N":
|
||||
if (nextEpisode) router.push(nextEpisode);
|
||||
break;
|
||||
|
||||
case "p":
|
||||
case "P":
|
||||
if (previousEpisode) router.push(previousEpisode);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (event.code) {
|
||||
case "Digit0":
|
||||
reducer({ type: "seekPercent", value: 0 });
|
||||
break;
|
||||
case "Digit1":
|
||||
reducer({ type: "seekPercent", value: 10 });
|
||||
break;
|
||||
case "Digit2":
|
||||
reducer({ type: "seekPercent", value: 20 });
|
||||
break;
|
||||
case "Digit3":
|
||||
reducer({ type: "seekPercent", value: 30 });
|
||||
break;
|
||||
case "Digit4":
|
||||
reducer({ type: "seekPercent", value: 40 });
|
||||
break;
|
||||
case "Digit5":
|
||||
reducer({ type: "seekPercent", value: 50 });
|
||||
break;
|
||||
case "Digit6":
|
||||
reducer({ type: "seekPercent", value: 60 });
|
||||
break;
|
||||
case "Digit7":
|
||||
reducer({ type: "seekPercent", value: 70 });
|
||||
break;
|
||||
case "Digit8":
|
||||
reducer({ type: "seekPercent", value: 80 });
|
||||
break;
|
||||
case "Digit9":
|
||||
reducer({ type: "seekPercent", value: 90 });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keyup", handler);
|
||||
return () => document.removeEventListener("keyup", handler);
|
||||
}, [subtitles, fonts, nextEpisode, previousEpisode, router, reducer]);
|
||||
};
|
||||
@ -1,88 +0,0 @@
|
||||
import {
|
||||
IconButton,
|
||||
Link,
|
||||
noTouch,
|
||||
tooltip,
|
||||
touchOnly,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { px, type Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { HoverTouch, hoverAtom } from ".";
|
||||
import { playAtom } from "../old/state";
|
||||
|
||||
export const TouchControls = ({
|
||||
previousSlug,
|
||||
nextSlug,
|
||||
...props
|
||||
}: {
|
||||
previousSlug?: string | null;
|
||||
nextSlug?: string | null;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||
const hover = useAtomValue(hoverAtom);
|
||||
|
||||
const common = css(
|
||||
[
|
||||
{
|
||||
backgroundColor: (theme) => theme.darkOverlay,
|
||||
marginHorizontal: ts(3),
|
||||
},
|
||||
],
|
||||
touchOnly,
|
||||
);
|
||||
|
||||
return (
|
||||
<HoverTouch
|
||||
{...css(
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{hover && (
|
||||
<>
|
||||
<IconButton
|
||||
icon={SkipPrevious}
|
||||
as={Link}
|
||||
href={previousSlug!}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...css(
|
||||
[!previousSlug && { opacity: 0, pointerEvents: "none" }],
|
||||
common,
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
icon={isPlaying ? Pause : PlayArrow}
|
||||
onPress={() => setPlay(!isPlaying)}
|
||||
size={ts(8)}
|
||||
{...common}
|
||||
/>
|
||||
<IconButton
|
||||
icon={SkipNext}
|
||||
as={Link}
|
||||
href={nextSlug!}
|
||||
replace
|
||||
size={ts(4)}
|
||||
{...css(
|
||||
[!nextSlug && { opacity: 0, pointerEvents: "none" }],
|
||||
common,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</HoverTouch>
|
||||
);
|
||||
};
|
||||
@ -1,199 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "react-native-video";
|
||||
import type { ReactVideoSourceProperties } from "react-native-video";
|
||||
|
||||
declare module "react-native-video" {
|
||||
interface ReactVideoProps {
|
||||
fonts?: string[];
|
||||
subtitles?: Subtitle[];
|
||||
onMediaUnsupported?: () => void;
|
||||
}
|
||||
export type VideoProps = Omit<ReactVideoProps, "source"> & {
|
||||
source: ReactVideoSourceProperties & { hls: string | null };
|
||||
};
|
||||
}
|
||||
|
||||
export * from "react-native-video";
|
||||
|
||||
import { type Audio, type Subtitle, useToken } from "@kyoo/models";
|
||||
import { type IconButton, Menu } from "@kyoo/primitives";
|
||||
import "@kyoo/primitives/src/types.d.ts";
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { type ComponentProps, forwardRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import uuid from "react-native-uuid";
|
||||
import NativeVideo, {
|
||||
type VideoRef,
|
||||
type OnLoadData,
|
||||
type VideoProps,
|
||||
SelectedTrackType,
|
||||
SelectedVideoTrackType,
|
||||
} from "react-native-video";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { useDisplayName } from "../../../../packages/ui/src/utils";
|
||||
import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./old/statee";
|
||||
|
||||
const MimeTypes: Map<string, string> = new Map([
|
||||
["subrip", "application/x-subrip"],
|
||||
["ass", "text/x-ssa"],
|
||||
["vtt", "text/vtt"],
|
||||
]);
|
||||
|
||||
const infoAtom = atom<OnLoadData | null>(null);
|
||||
const videoAtom = atom(0);
|
||||
|
||||
const clientId = uuid.v4() as string;
|
||||
|
||||
const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
||||
{ onLoad, onBuffer, onError, onMediaUnsupported, source, subtitles, ...props },
|
||||
ref,
|
||||
) {
|
||||
const { css } = useYoshiki();
|
||||
const token = useToken();
|
||||
const setInfo = useSetAtom(infoAtom);
|
||||
const [video, setVideo] = useAtom(videoAtom);
|
||||
const audio = useAtomValue(audioAtom);
|
||||
const subtitle = useAtomValue(subtitleAtom);
|
||||
const mode = useAtomValue(playModeAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === PlayMode.Hls) setVideo(-1);
|
||||
}, [mode, setVideo]);
|
||||
|
||||
return (
|
||||
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
|
||||
<NativeVideo
|
||||
ref={ref}
|
||||
source={{
|
||||
...source,
|
||||
headers: {
|
||||
...(token ? { Authorization: token } : {}),
|
||||
"X-CLIENT-ID": clientId,
|
||||
},
|
||||
}}
|
||||
onLoad={(info) => {
|
||||
onBuffer?.({ isBuffering: false });
|
||||
setInfo(info);
|
||||
onLoad?.(info);
|
||||
}}
|
||||
onBuffer={onBuffer}
|
||||
onError={(error) => {
|
||||
console.error(error);
|
||||
if (mode === PlayMode.Direct) onMediaUnsupported?.();
|
||||
else onError?.(error);
|
||||
}}
|
||||
selectedVideoTrack={
|
||||
video === -1
|
||||
? { type: SelectedVideoTrackType.AUTO }
|
||||
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
|
||||
}
|
||||
// when video file is invalid, audio is undefined
|
||||
selectedAudioTrack={{ type: SelectedTrackType.INDEX, value: audio?.index ?? 0 }}
|
||||
textTracks={subtitles
|
||||
?.filter((x) => !!x.link)
|
||||
.map((x) => ({
|
||||
type: MimeTypes.get(x.codec) as any,
|
||||
uri: x.link!,
|
||||
title: x.title ?? "Unknown",
|
||||
language: x.language ?? ("Unknown" as any),
|
||||
}))}
|
||||
selectedTextTrack={
|
||||
subtitle
|
||||
? {
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: subtitles?.indexOf(subtitle),
|
||||
}
|
||||
: { type: SelectedTrackType.DISABLED, value: "" }
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export default Video;
|
||||
|
||||
// mobile should be able to play everything
|
||||
export const canPlay = (_codec: string) => true;
|
||||
|
||||
type CustomMenu = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||
export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => {
|
||||
const info = useAtomValue(infoAtom);
|
||||
const [audio, setAudio] = useAtom(audioAtom);
|
||||
const getDisplayName = useDisplayName();
|
||||
|
||||
if (!info || info.audioTracks.length < 2) return null;
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
{info.audioTracks.map((x) => (
|
||||
<Menu.Item
|
||||
key={x.index}
|
||||
label={audios ? getDisplayName(audios[x.index]) : (x.title ?? x.language ?? "Unknown")}
|
||||
selected={audio!.index === x.index}
|
||||
onSelect={() => setAudio(x as any)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const QualitiesMenu = (props: CustomMenu) => {
|
||||
const { t } = useTranslation();
|
||||
const info = useAtomValue(infoAtom);
|
||||
const [mode, setPlayMode] = useAtom(playModeAtom);
|
||||
const [video, setVideo] = useAtom(videoAtom);
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
<Menu.Item
|
||||
label={t("player.direct")}
|
||||
selected={mode === PlayMode.Direct}
|
||||
onSelect={() => setPlayMode(PlayMode.Direct)}
|
||||
/>
|
||||
<Menu.Item
|
||||
// TODO: Display the currently selected quality (impossible with rn-video right now)
|
||||
label={t("player.auto")}
|
||||
selected={video === -1 && mode === PlayMode.Hls}
|
||||
onSelect={() => {
|
||||
setPlayMode(PlayMode.Hls);
|
||||
setVideo(-1);
|
||||
}}
|
||||
/>
|
||||
{/* TODO: Support video tracks when the play mode is not hls. */}
|
||||
{info?.videoTracks
|
||||
.sort((a: any, b: any) => b.height - a.height)
|
||||
.map((x: any, i: number) => (
|
||||
<Menu.Item
|
||||
key={i}
|
||||
label={`${x.height}p`}
|
||||
selected={video === x.height}
|
||||
onSelect={() => {
|
||||
setPlayMode(PlayMode.Hls);
|
||||
setVideo(x.height);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user