mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add audio menu and rework qualities menu
This commit is contained in:
parent
8ba80e93e3
commit
95133deeb0
@ -141,23 +141,14 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ICollection<Chapter> Chapters { get; set; }
|
public ICollection<Chapter> Chapters { get; set; }
|
||||||
|
|
||||||
string _Type => IsMovie ? "movie" : "episode";
|
[SerializeIgnore]
|
||||||
|
private string _Type => IsMovie ? "movie" : "episode";
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public object Link => new[]
|
public object Link => new
|
||||||
{
|
{
|
||||||
new { Name = "Pristine", Link = $"/video/{_Type}/{Slug}/direct", Type = "direct" },
|
Direct = $"/video/{_Type}/{Slug}/direct",
|
||||||
new { Name = "Original", Link = $"/video/{_Type}/{Slug}/original/index.m3u8", Type = "transmux" },
|
Hls = $"/video/{_Type}/{Slug}/master.m3u8",
|
||||||
new { Name = "Auto", Link = $"/video/{_Type}/{Slug}/master.m3u8", Type = "transcode-auto" },
|
|
||||||
|
|
||||||
new { Name = "8K", Link = $"/video/{_Type}/{Slug}/8k/index.m3u8", Type = "transcode", },
|
|
||||||
new { Name = "4K", Link = $"/video/{_Type}/{Slug}/4k/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "1440p", Link = $"/video/{_Type}/{Slug}/1440p/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "1080p", Link = $"/video/{_Type}/{Slug}/1080p/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "720p", Link = $"/video/{_Type}/{Slug}/720p/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "480p", Link = $"/video/{_Type}/{Slug}/480p/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "360p", Link = $"/video/{_Type}/{Slug}/360p/index.m3u8", Type = "transcode" },
|
|
||||||
new { Name = "240p", Link = $"/video/{_Type}/{Slug}/240p/index.m3u8", Type = "transcode" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -78,7 +78,6 @@ export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [n
|
|||||||
export const getToken = async (cookies?: string): Promise<string | null> =>
|
export const getToken = async (cookies?: string): Promise<string | null> =>
|
||||||
(await getTokenWJ(cookies))[0]
|
(await getTokenWJ(cookies))[0]
|
||||||
|
|
||||||
|
|
||||||
export const logout = async () =>{
|
export const logout = async () =>{
|
||||||
deleteSecureItem("auth")
|
deleteSecureItem("auth")
|
||||||
}
|
}
|
||||||
|
@ -156,13 +156,10 @@ const WatchMovieP = z.preprocess(
|
|||||||
/**
|
/**
|
||||||
* The links to the videos of this watch item.
|
* The links to the videos of this watch item.
|
||||||
*/
|
*/
|
||||||
link: z.array(
|
link: z.object({
|
||||||
z.object({
|
direct: z.string().transform(imageFn),
|
||||||
name: z.string(),
|
hls: z.string().transform(imageFn),
|
||||||
link: z.string().transform(imageFn),
|
}),
|
||||||
type: z.enum(["direct", "transmux", "transcode-auto", "transcode"])
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const Menu = <AsProps,>({
|
|||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
Trigger: ComponentType<AsProps>;
|
Trigger: ComponentType<AsProps>;
|
||||||
children: ReactNode | ReactNode[] | null;
|
children?: ReactNode | ReactNode[] | null;
|
||||||
onMenuOpen?: () => void;
|
onMenuOpen?: () => void;
|
||||||
onMenuClose?: () => void;
|
onMenuClose?: () => void;
|
||||||
} & Omit<AsProps, "onPress">) => {
|
} & Omit<AsProps, "onPress">) => {
|
||||||
|
@ -28,8 +28,10 @@ import ClosedCaption from "@material-symbols/svg-400/rounded/closed_caption-fill
|
|||||||
import Fullscreen from "@material-symbols/svg-400/rounded/fullscreen-fill.svg";
|
import Fullscreen from "@material-symbols/svg-400/rounded/fullscreen-fill.svg";
|
||||||
import FullscreenExit from "@material-symbols/svg-400/rounded/fullscreen_exit-fill.svg";
|
import FullscreenExit from "@material-symbols/svg-400/rounded/fullscreen_exit-fill.svg";
|
||||||
import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg";
|
import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg";
|
||||||
|
import MusicNote from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
||||||
import { Stylable, useYoshiki } from "yoshiki/native";
|
import { Stylable, useYoshiki } from "yoshiki/native";
|
||||||
import { fullscreenAtom, qualityAtom, subtitleAtom } from "../state";
|
import { fullscreenAtom, subtitleAtom } from "../state";
|
||||||
|
import { AudiosMenu, QualitiesMenu } from "../video";
|
||||||
|
|
||||||
export const RightButtons = ({
|
export const RightButtons = ({
|
||||||
subtitles,
|
subtitles,
|
||||||
@ -49,7 +51,6 @@ export const RightButtons = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||||
const setSubAtom = useSetAtom(subtitleAtom);
|
const setSubAtom = useSetAtom(subtitleAtom);
|
||||||
const [quality, setQuality] = useAtom(qualityAtom);
|
|
||||||
const [selectedSubtitle, setSubtitle] = useState<string | undefined>(undefined);
|
const [selectedSubtitle, setSubtitle] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -64,7 +65,7 @@ export const RightButtons = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...css({ flexDirection: "row" }, props)}>
|
<View {...css({ flexDirection: "row" }, props)}>
|
||||||
{subtitles && (
|
{subtitles && subtitles.length && (
|
||||||
<Menu
|
<Menu
|
||||||
Trigger={IconButton}
|
Trigger={IconButton}
|
||||||
icon={ClosedCaption}
|
icon={ClosedCaption}
|
||||||
@ -88,23 +89,22 @@ export const RightButtons = ({
|
|||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
<Menu
|
<AudiosMenu
|
||||||
|
Trigger={IconButton}
|
||||||
|
icon={MusicNote}
|
||||||
|
onMenuOpen={onMenuOpen}
|
||||||
|
onMenuClose={onMenuClose}
|
||||||
|
{...tooltip(t("player.audios"), true)}
|
||||||
|
{...spacing}
|
||||||
|
/>
|
||||||
|
<QualitiesMenu
|
||||||
Trigger={IconButton}
|
Trigger={IconButton}
|
||||||
icon={SettingsIcon}
|
icon={SettingsIcon}
|
||||||
onMenuOpen={onMenuOpen}
|
onMenuOpen={onMenuOpen}
|
||||||
onMenuClose={onMenuClose}
|
onMenuClose={onMenuClose}
|
||||||
{...tooltip(t("player.quality"), true)}
|
{...tooltip(t("player.quality"), true)}
|
||||||
{...spacing}
|
{...spacing}
|
||||||
>
|
/>
|
||||||
{qualities?.map((x) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={x.link}
|
|
||||||
label={x.name}
|
|
||||||
selected={quality === x.name}
|
|
||||||
onSelect={() => setQuality(x.name)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
{Platform.OS === "web" && (
|
{Platform.OS === "web" && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={isFullscreen ? FullscreenExit : Fullscreen}
|
icon={isFullscreen ? FullscreenExit : Fullscreen}
|
||||||
|
@ -26,8 +26,13 @@ import { Platform } from "react-native";
|
|||||||
|
|
||||||
export const playAtom = atom(true);
|
export const playAtom = atom(true);
|
||||||
export const loadAtom = atom(false);
|
export const loadAtom = atom(false);
|
||||||
|
|
||||||
// TODO: Default to auto or pristine depending on the user settings.
|
// TODO: Default to auto or pristine depending on the user settings.
|
||||||
export const qualityAtom = atom<string>("Pristine");
|
export enum PlayMode {
|
||||||
|
Direct,
|
||||||
|
Hls,
|
||||||
|
}
|
||||||
|
export const playModeAtom = atom<PlayMode>(PlayMode.Direct);
|
||||||
|
|
||||||
export const bufferedAtom = atom(0);
|
export const bufferedAtom = atom(0);
|
||||||
export const durationAtom = atom<number | undefined>(undefined);
|
export const durationAtom = atom<number | undefined>(undefined);
|
||||||
@ -78,8 +83,8 @@ export const Video = memo(function _Video({
|
|||||||
const ref = useRef<NativeVideo | null>(null);
|
const ref = useRef<NativeVideo | null>(null);
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||||
const setLoad = useSetAtom(loadAtom);
|
const setLoad = useSetAtom(loadAtom);
|
||||||
const [source, setSource] = useState<WatchItem["link"][0] | null>(null);
|
const [source, setSource] = useState<string | null>(null);
|
||||||
const [quality, setQuality] = useAtom(qualityAtom);
|
const [mode, setPlayMode] = useAtom(playModeAtom);
|
||||||
|
|
||||||
const publicProgress = useAtomValue(publicProgressAtom);
|
const publicProgress = useAtomValue(publicProgressAtom);
|
||||||
const setPrivateProgress = useSetAtom(privateProgressAtom);
|
const setPrivateProgress = useSetAtom(privateProgressAtom);
|
||||||
@ -91,11 +96,11 @@ export const Video = memo(function _Video({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Reset the state when a new video is loaded.
|
// Reset the state when a new video is loaded.
|
||||||
setSource(links?.find(x => x.name == quality) ?? null)
|
setSource((mode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
|
||||||
setLoad(true);
|
setLoad(true);
|
||||||
setPrivateProgress(0);
|
setPrivateProgress(0);
|
||||||
setPlay(true);
|
setPlay(true);
|
||||||
}, [quality, links, setLoad, setPrivateProgress, setPlay]);
|
}, [mode, links, setLoad, setPrivateProgress, setPlay]);
|
||||||
|
|
||||||
const volume = useAtomValue(volumeAtom);
|
const volume = useAtomValue(volumeAtom);
|
||||||
const isMuted = useAtomValue(mutedAtom);
|
const isMuted = useAtomValue(mutedAtom);
|
||||||
@ -112,12 +117,12 @@ export const Video = memo(function _Video({
|
|||||||
|
|
||||||
const subtitle = useAtomValue(subtitleAtom);
|
const subtitle = useAtomValue(subtitleAtom);
|
||||||
|
|
||||||
if (!source) return null;
|
if (!source || !links) return null;
|
||||||
return (
|
return (
|
||||||
<NativeVideo
|
<NativeVideo
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
source={{ uri: source.link, ...source }}
|
source={{ uri: source, ...links }}
|
||||||
paused={!isPlaying}
|
paused={!isPlaying}
|
||||||
muted={isMuted}
|
muted={isMuted}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
@ -142,15 +147,9 @@ export const Video = memo(function _Video({
|
|||||||
}
|
}
|
||||||
fonts={fonts}
|
fonts={fonts}
|
||||||
onMediaUnsupported={() => {
|
onMediaUnsupported={() => {
|
||||||
if (source.type === "direct")
|
if (mode == PlayMode.Direct)
|
||||||
setQuality(links?.find(x => x.type == "transmux")!.name!)
|
setPlayMode(PlayMode.Hls);
|
||||||
|
|
||||||
// TODO: Replace transcode with transcode-auto when supported.
|
|
||||||
if (source.type === "transmux")
|
|
||||||
setQuality(links?.find(x => x.type == "transcode")!.name!)
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// TODO: textTracks: external subtitles
|
// TODO: textTracks: external subtitles
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -25,12 +25,25 @@ declare module "react-native-video" {
|
|||||||
onMediaUnsupported?: () => void;
|
onMediaUnsupported?: () => void;
|
||||||
}
|
}
|
||||||
export type VideoProps = Omit<VideoProperties, "source"> & {
|
export type VideoProps = Omit<VideoProperties, "source"> & {
|
||||||
source: { uri: string } & WatchItem["link"][0];
|
source: { uri: string; hls: string };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from "react-native-video";
|
export * from "react-native-video";
|
||||||
|
|
||||||
import { Font, WatchItem } from "@kyoo/models";
|
import { Font } from "@kyoo/models";
|
||||||
|
import { IconButton, Menu } from "@kyoo/primitives";
|
||||||
|
import { ComponentProps } from "react";
|
||||||
import Video from "react-native-video";
|
import Video from "react-native-video";
|
||||||
export default Video;
|
export default Video;
|
||||||
|
|
||||||
|
// TODO: Implement those for mobile.
|
||||||
|
|
||||||
|
type CustomMenu = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||||
|
export const AudiosMenu = (props: CustomMenu) => {
|
||||||
|
return <Menu {...props}></Menu>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QualitiesMenu = (props: CustomMenu) => {
|
||||||
|
return <Menu {...props}></Menu>;
|
||||||
|
};
|
||||||
|
@ -26,15 +26,19 @@ import {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
|
useReducer,
|
||||||
|
ComponentProps,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { VideoProps } from "react-native-video";
|
import { VideoProps } from "react-native-video";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
||||||
import { useYoshiki } from "yoshiki";
|
import { useYoshiki } from "yoshiki";
|
||||||
import SubtitleOctopus from "libass-wasm";
|
import SubtitleOctopus from "libass-wasm";
|
||||||
import { playAtom, subtitleAtom } from "./state";
|
import { playAtom, PlayMode, playModeAtom, subtitleAtom } from "./state";
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Menu } from "@kyoo/primitives";
|
||||||
|
|
||||||
let hls: Hls | null = null;
|
let hls: Hls = null!;
|
||||||
|
|
||||||
function uuidv4(): string {
|
function uuidv4(): string {
|
||||||
// @ts-ignore I have no clue how this works, thanks https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
|
// @ts-ignore I have no clue how this works, thanks https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
|
||||||
@ -45,6 +49,17 @@ function uuidv4(): string {
|
|||||||
|
|
||||||
let client_id = typeof window === "undefined" ? "ssr" : uuidv4();
|
let client_id = typeof window === "undefined" ? "ssr" : uuidv4();
|
||||||
|
|
||||||
|
const initHls = async () => {
|
||||||
|
if (hls !== null) return;
|
||||||
|
const token = await getToken();
|
||||||
|
hls = new Hls({
|
||||||
|
xhrSetup: (xhr) => {
|
||||||
|
if (token) xhr.setRequestHeader("Authorization", `Bearer: {token}`);
|
||||||
|
xhr.setRequestHeader("X-CLIENT-ID", client_id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function _Video(
|
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function _Video(
|
||||||
{
|
{
|
||||||
source,
|
source,
|
||||||
@ -62,8 +77,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
},
|
},
|
||||||
forwaredRef,
|
forwaredRef,
|
||||||
) {
|
) {
|
||||||
const { uri, type } = source;
|
|
||||||
const ref = useRef<HTMLVideoElement>(null);
|
const ref = useRef<HTMLVideoElement>(null);
|
||||||
|
const oldHls = useRef<string | null>(null);
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
@ -78,7 +93,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (paused) ref.current?.pause();
|
if (paused) ref.current?.pause();
|
||||||
else ref.current?.play().catch(() => {});
|
else ref.current?.play().catch(() => { });
|
||||||
}, [paused]);
|
}, [paused]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !volume) return;
|
if (!ref.current || !volume) return;
|
||||||
@ -89,33 +104,39 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
const subtitle = useAtomValue(subtitleAtom);
|
const subtitle = useAtomValue(subtitleAtom);
|
||||||
useSubtitle(ref, subtitle, fonts);
|
useSubtitle(ref, subtitle, fonts);
|
||||||
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!ref?.current || !uri || !type) return;
|
await initHls();
|
||||||
// TODO: Use hls.js even for safari or handle XHR requests with tokens,auto...
|
// Still load the hls source to list available qualities.
|
||||||
if (type === "direct" || ref.current.canPlayType("application/vnd.apple.mpegurl")) {
|
// Note: This may ask the server to transmux the audio/video by loading the index.m3u8
|
||||||
ref.current.src = uri;
|
hls.loadSource(source.hls);
|
||||||
|
})();
|
||||||
|
}, [source.hls]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!ref?.current || !source.uri) return;
|
||||||
|
await initHls();
|
||||||
|
if (oldHls.current !== source.hls) {
|
||||||
|
// Still load the hls source to list available qualities.
|
||||||
|
// Note: This may ask the server to transmux the audio/video by loading the index.m3u8
|
||||||
|
hls.loadSource(source.hls);
|
||||||
|
oldHls.current = source.hls;
|
||||||
|
}
|
||||||
|
if (!source.uri.endsWith(".m3u8")) {
|
||||||
|
hls.detachMedia();
|
||||||
|
ref.current.src = source.uri;
|
||||||
} else {
|
} else {
|
||||||
if (hls === null) {
|
|
||||||
const token = await getToken();
|
|
||||||
hls = new Hls({
|
|
||||||
xhrSetup: (xhr) => {
|
|
||||||
if (token) xhr.setRequestHeader("Authorization", `Bearer: {token}`);
|
|
||||||
xhr.setRequestHeader("X-CLIENT-ID", client_id);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
hls.loadSource(uri);
|
|
||||||
hls.attachMedia(ref.current);
|
hls.attachMedia(ref.current);
|
||||||
// TODO: Enable custom XHR for tokens
|
// TODO: Enable custom XHR for tokens
|
||||||
hls.on(Hls.Events.MANIFEST_LOADED, async () => {
|
hls.on(Hls.Events.MANIFEST_LOADED, async () => {
|
||||||
try {
|
try {
|
||||||
await ref.current?.play();
|
await ref.current?.play();
|
||||||
} catch {}
|
} catch { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [uri, type]);
|
}, [source.uri, source.hls]);
|
||||||
|
|
||||||
const setPlay = useSetAtom(playAtom);
|
const setPlay = useSetAtom(playAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -221,3 +242,66 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Track | null, f
|
|||||||
}
|
}
|
||||||
}, [player, value, fonts]);
|
}, [player, value, fonts]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AudiosMenu = (props: ComponentProps<typeof Menu>) => {
|
||||||
|
if (!hls || hls.audioTracks.length < 2) return null;
|
||||||
|
return (
|
||||||
|
<Menu {...props}>
|
||||||
|
{hls.audioTracks.map((x, i) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={i.toString()}
|
||||||
|
label={x.name}
|
||||||
|
selected={hls!.audioTrack === i}
|
||||||
|
onSelect={() => (hls!.audioTrack = i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [mode, setPlayMode] = useAtom(playModeAtom);
|
||||||
|
const [_, rerender] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hls) return;
|
||||||
|
hls.on(Hls.Events.LEVEL_SWITCHED, rerender);
|
||||||
|
return () => hls!.off(Hls.Events.LEVEL_SWITCHED, rerender);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu {...props}>
|
||||||
|
<Menu.Item
|
||||||
|
label={t("player.direct")}
|
||||||
|
selected={hls === null || mode == PlayMode.Direct}
|
||||||
|
onSelect={() => setPlayMode(PlayMode.Direct)}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
label={
|
||||||
|
hls != null && hls.autoLevelEnabled && hls.currentLevel >= 0
|
||||||
|
? `${t("player.auto")} (${hls.levels[hls.currentLevel].height}p)`
|
||||||
|
: t("player.auto")
|
||||||
|
}
|
||||||
|
selected={hls?.autoLevelEnabled && mode === PlayMode.Hls}
|
||||||
|
onSelect={() => {
|
||||||
|
setPlayMode(PlayMode.Hls);
|
||||||
|
if (hls) hls.nextLevel = -1;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hls?.levels
|
||||||
|
.map((x, i) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={i.toString()}
|
||||||
|
label={`${x.height}p`}
|
||||||
|
selected={mode === PlayMode.Hls && hls!.currentLevel === i && !hls?.autoLevelEnabled}
|
||||||
|
onSelect={() => {
|
||||||
|
setPlayMode(PlayMode.Hls);
|
||||||
|
hls!.nextLevel = i;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.reverse()}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -44,9 +44,12 @@
|
|||||||
"mute": "Toggle mute",
|
"mute": "Toggle mute",
|
||||||
"volume": "Volume",
|
"volume": "Volume",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
|
"audios": "Audio",
|
||||||
"subtitles": "Subtitles",
|
"subtitles": "Subtitles",
|
||||||
"subtitle-none": "None",
|
"subtitle-none": "None",
|
||||||
"fullscreen": "Fullscreen"
|
"fullscreen": "Fullscreen",
|
||||||
|
"direct": "Pristine",
|
||||||
|
"auto": "Auto"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"empty": "No result found. Try a different query."
|
"empty": "No result found. Try a different query."
|
||||||
|
@ -44,9 +44,12 @@
|
|||||||
"mute": "Muet",
|
"mute": "Muet",
|
||||||
"volume": "Volume",
|
"volume": "Volume",
|
||||||
"quality": "Qualité",
|
"quality": "Qualité",
|
||||||
|
"audios": "Audio",
|
||||||
"subtitles": "Sous titres",
|
"subtitles": "Sous titres",
|
||||||
"subtitle-none": "Aucun",
|
"subtitle-none": "Aucun",
|
||||||
"fullscreen": "Plein-écran"
|
"fullscreen": "Plein-écran",
|
||||||
|
"direct": "Pristine",
|
||||||
|
"auto": "Auto"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"empty": "Aucun résultat trouvé. Essayer avec une autre recherche."
|
"empty": "Aucun résultat trouvé. Essayer avec une autre recherche."
|
||||||
|
Loading…
x
Reference in New Issue
Block a user