mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Use react-native-video instead of expo-av and add subtitle support
This commit is contained in:
parent
e741b5aa6d
commit
84b4c998a7
62
front/apps/mobile/app.config.js
Normal file
62
front/apps/mobile/app.config.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const IS_DEV = process.env.APP_VARIANT === "development";
|
||||
|
||||
const config = {
|
||||
expo: {
|
||||
name: "kyoo",
|
||||
slug: "kyoo",
|
||||
scheme: "kyoo",
|
||||
version: "1.0.0",
|
||||
orientation: "default",
|
||||
icon: "./assets/icon.png",
|
||||
entryPoint: "./index.ts",
|
||||
userInterfaceStyle: "light",
|
||||
splash: {
|
||||
image: "./assets/splash.png",
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
updates: {
|
||||
fallbackToCacheTimeout: 0,
|
||||
},
|
||||
assetBundlePatterns: ["**/*"],
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
},
|
||||
android: {
|
||||
package: IS_DEV ? "moe.sdg.kyoo.dev" : "moe.sdg.kyoo",
|
||||
adaptiveIcon: {
|
||||
foregroundImage: "./assets/adaptive-icon.png",
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
web: {
|
||||
favicon: "./assets/favicon.png",
|
||||
},
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: "55de6b52-c649-4a15-9a45-569ff5ed036c",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export default config;
|
@ -1,49 +0,0 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "kyoo",
|
||||
"slug": "kyoo",
|
||||
"scheme": "kyoo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"entryPoint": "./index.ts",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"package": "moe.sdg.kyoo",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "55de6b52-c649-4a15-9a45-569ff5ed036c"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"compileSdkVersion": 31
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
@ -6,7 +6,9 @@
|
||||
"dev": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
"web": "expo start --web",
|
||||
"build": "eas build --profile production --platform android",
|
||||
"build:dev": "eas build --profile development --platform android"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
|
@ -6,6 +6,8 @@
|
||||
"web": "yarn workspace web dev",
|
||||
"mobile": "yarn workspace mobile dev",
|
||||
"build:web": "yarn workspace web build",
|
||||
"build:mobile": "yarn workspace mobile build",
|
||||
"build:mobile:dev": "yarn workspace mobile build:dev",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"workspaces": [
|
||||
|
@ -36,12 +36,12 @@ const queryFn = async <Data,>(
|
||||
context: QueryFunctionContext,
|
||||
): Promise<Data> => {
|
||||
const kyooUrl =
|
||||
Platform.OS !== "web"
|
||||
(Platform.OS !== "web"
|
||||
? process.env.PUBLIC_BACK_URL
|
||||
: typeof window === "undefined"
|
||||
? process.env.KYOO_URL ?? "http://localhost:5000"
|
||||
: "/api";
|
||||
console.log(process.env.PUBLIC_BACK_URL)
|
||||
// TODO remove the hardcoded fallback. This is just for testing purposes
|
||||
: "/api") ?? "https://beta.sdg.moe";
|
||||
if (!kyooUrl) console.error("Kyoo's url is not defined.");
|
||||
|
||||
let resp;
|
||||
|
@ -21,10 +21,11 @@
|
||||
import { Platform } from "react-native";
|
||||
import { z } from "zod";
|
||||
|
||||
export const imageFn = (url: string) => {
|
||||
console.log(process.env.PUBLIC_BACK_URL)
|
||||
return Platform.OS === "web" ? `/api/${url}` : process.env.PUBLIC_BACK_URL + url;
|
||||
}
|
||||
export const imageFn = (url: string) =>
|
||||
// TODO remove the hardcodded fallback
|
||||
Platform.OS === "web"
|
||||
? `/api/${url}`
|
||||
: (process.env.PUBLIC_BACK_URL ?? "https://beta.sdg.moe") + url;
|
||||
|
||||
export const ImagesP = z.object({
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import { percent, useYoshiki } from "yoshiki/native";
|
||||
import { StyleList, YoshikiStyle } from "yoshiki/dist/type";
|
||||
import { YoshikiStyle } from "yoshiki/dist/type";
|
||||
import { Skeleton } from "./skeleton";
|
||||
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
|
||||
import { alpha, ContrastArea } from "./themes";
|
||||
|
@ -142,7 +142,7 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
setProgress={setProgress}
|
||||
subtleProgress={buffered}
|
||||
max={duration}
|
||||
markers={chapters?.map((x) => x.startTime * 1000)}
|
||||
markers={chapters?.map((x) => x.startTime)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -132,6 +132,6 @@ const ProgressText = () => {
|
||||
const toTimerString = (timer?: number, duration?: number) => {
|
||||
if (timer === undefined) return "??:??";
|
||||
if (!duration) duration = timer;
|
||||
if (duration >= 3600_000) return new Date(timer).toISOString().substring(11, 19);
|
||||
return new Date(timer).toISOString().substring(14, 19);
|
||||
if (duration >= 3600) return new Date(timer * 1000).toISOString().substring(11, 19);
|
||||
return new Date(timer * 1000).toISOString().substring(14, 19);
|
||||
};
|
||||
|
@ -19,8 +19,9 @@
|
||||
*/
|
||||
|
||||
import { Font, Track } from "@kyoo/models";
|
||||
import { IconButton, tooltip, Menu, ts, A } from "@kyoo/primitives";
|
||||
import { useAtom } from "jotai";
|
||||
import { IconButton, tooltip, Menu, ts } from "@kyoo/primitives";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ClosedCaption from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
||||
@ -30,7 +31,7 @@ import { Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { createParam } from "solito";
|
||||
import { fullscreenAtom, subtitleAtom } from "../state";
|
||||
|
||||
const { useParam } = createParam<{ subtitle?: (string) }>();
|
||||
const { useParam } = createParam<{ subtitle?: string }>();
|
||||
|
||||
export const RightButtons = ({
|
||||
subtitles,
|
||||
@ -47,8 +48,17 @@ export const RightButtons = ({
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
|
||||
const setSubAtom = useSetAtom(subtitleAtom);
|
||||
const [selectedSubtitle, setSubtitle] = useParam("subtitle");
|
||||
|
||||
useEffect(() => {
|
||||
const sub =
|
||||
subtitles?.find(
|
||||
(x) => x.language === selectedSubtitle || x.id.toString() === selectedSubtitle,
|
||||
) ?? null;
|
||||
setSubAtom(sub);
|
||||
}, [subtitles, selectedSubtitle, setSubAtom]);
|
||||
|
||||
const spacing = css({ marginHorizontal: ts(1) });
|
||||
|
||||
return (
|
||||
@ -88,4 +98,3 @@ export const RightButtons = ({
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -187,7 +187,6 @@ export const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
>
|
||||
<Video
|
||||
links={data?.link}
|
||||
videoStyle={{ width: percent(100), height: percent(100) }}
|
||||
setError={setPlaybackError}
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
// onEnded={() => {
|
||||
|
@ -71,6 +71,8 @@ export const [privateFullscreen, fullscreenAtom] = bakedAtom(
|
||||
},
|
||||
);
|
||||
|
||||
export const subtitleAtom = atom<Track | null>(null);
|
||||
|
||||
let hls: Hls | null = null;
|
||||
|
||||
export const Video = ({
|
||||
@ -78,8 +80,6 @@ export const Video = ({
|
||||
setError,
|
||||
...props
|
||||
}: { links?: WatchItem["link"]; setError: (error: string | undefined) => void } & VideoProps) => {
|
||||
// const [playMode, setPlayMode] = useAtom(playModeAtom);
|
||||
|
||||
const ref = useRef<NativeVideo | null>(null);
|
||||
const isPlaying = useAtomValue(playAtom);
|
||||
const setLoad = useSetAtom(loadAtom);
|
||||
@ -92,9 +92,9 @@ export const Video = ({
|
||||
const setPrivateProgress = useSetAtom(privateProgressAtom);
|
||||
const setBuffered = useSetAtom(bufferedAtom);
|
||||
const setDuration = useSetAtom(durationAtom);
|
||||
// useEffect(() => {
|
||||
// ref.current?.setStatusAsync({ positionMillis: publicProgress });
|
||||
// }, [publicProgress]);
|
||||
useEffect(() => {
|
||||
ref.current?.seek(publicProgress);
|
||||
}, [publicProgress]);
|
||||
|
||||
const volume = useAtomValue(volumeAtom);
|
||||
const isMuted = useAtomValue(mutedAtom);
|
||||
@ -109,6 +109,8 @@ export const Video = ({
|
||||
return () => document.removeEventListener("fullscreenchange", handler);
|
||||
});
|
||||
|
||||
const subtitle = useAtomValue(subtitleAtom);
|
||||
|
||||
// useEffect(() => {
|
||||
// setPlayMode(PlayMode.Direct);
|
||||
// }, [links, setPlayMode]);
|
||||
@ -134,139 +136,117 @@ export const Video = ({
|
||||
// }
|
||||
// }, [playMode, links, player]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!player?.current?.duration) return;
|
||||
// setDuration(player.current.duration);
|
||||
// }, [player, setDuration]);
|
||||
//
|
||||
|
||||
if (!links) return null;
|
||||
return (
|
||||
<NativeVideo
|
||||
ref={ref}
|
||||
{...props}
|
||||
source={{ uri: links?.direct }}
|
||||
source={{ uri: links.direct }}
|
||||
paused={!isPlaying}
|
||||
muted={isMuted}
|
||||
volume={volume}
|
||||
// resizeMode={ResizeMode.CONTAIN}
|
||||
// onPlaybackStatusUpdate={(status) => {
|
||||
// if (!status.isLoaded) {
|
||||
// setLoad(true);
|
||||
// if (status.error) setError(status.error);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setLoad(status.isPlaying !== status.shouldPlay);
|
||||
// setPrivateProgress(status.positionMillis);
|
||||
// setBuffered(status.playableDurationMillis ?? 0);
|
||||
// setDuration(status.durationMillis);
|
||||
// }}
|
||||
|
||||
// ref: player,
|
||||
// shouldPlay: isPlaying,
|
||||
// onDoubleClick: () => {
|
||||
// setFullscreen(!document.fullscreenElement);
|
||||
// },
|
||||
// onPlay: () => setPlay(true),
|
||||
// onPause: () => setPlay(false),
|
||||
// onWaiting: () => setLoad(true),
|
||||
// onCanPlay: () => setLoad(false),
|
||||
resizeMode="contain"
|
||||
onBuffer={({ isBuffering }) => setLoad(isBuffering)}
|
||||
onError={(status) => setError(status.error.errorString)}
|
||||
onProgress={(progress) => {
|
||||
setPrivateProgress(progress.currentTime);
|
||||
setBuffered(progress.playableDuration);
|
||||
}}
|
||||
onLoad={(info) => {
|
||||
console.log(info);
|
||||
setDuration(info.duration);
|
||||
}}
|
||||
selectedTextTrack={
|
||||
subtitle
|
||||
? {
|
||||
type: "index",
|
||||
value: subtitle.trackIndex,
|
||||
}
|
||||
: { type: "disabled" }
|
||||
}
|
||||
// TODO: textTracks: external subtitles
|
||||
// onError: () => {
|
||||
// if (player?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
|
||||
// setPlayMode(PlayMode.Transmux);
|
||||
// },
|
||||
// onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
|
||||
// onDurationChange: () => setDuration(player?.current?.duration ?? 0),
|
||||
// onProgress: () =>
|
||||
// setBuffered(
|
||||
// player?.current?.buffered.length
|
||||
// ? player.current.buffered.end(player.current.buffered.length - 1)
|
||||
// : 0,
|
||||
// ),
|
||||
// onVolumeChange: () => {
|
||||
// if (!player.current) return;
|
||||
// setVolume(player.current.volume * 100);
|
||||
// setMuted(player?.current.muted);
|
||||
// },
|
||||
useNativeControls={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const htmlTrackAtom = atom<HTMLTrackElement | null>(null);
|
||||
const suboctoAtom = atom<SubtitleOctopus | null>(null);
|
||||
export const [_subtitleAtom, subtitleAtom] = bakedAtom<
|
||||
Track | null,
|
||||
{ track: Track; fonts: Font[] } | null
|
||||
>(null, (get, set, value, baked) => {
|
||||
const removeHtmlSubtitle = () => {
|
||||
const htmlTrack = get(htmlTrackAtom);
|
||||
if (htmlTrack) htmlTrack.remove();
|
||||
set(htmlTrackAtom, null);
|
||||
};
|
||||
const removeOctoSub = () => {
|
||||
const subocto = get(suboctoAtom);
|
||||
if (subocto) {
|
||||
subocto.freeTrack();
|
||||
subocto.dispose();
|
||||
}
|
||||
set(suboctoAtom, null);
|
||||
};
|
||||
// const htmlTrackAtom = atom<HTMLTrackElement | null>(null);
|
||||
// const suboctoAtom = atom<SubtitleOctopus | null>(null);
|
||||
// export const [_subtitleAtom, subtitleAtom] = bakedAtom<
|
||||
// Track | null,
|
||||
// { track: Track; fonts: Font[] } | null
|
||||
// >(null, (get, set, value, baked) => {
|
||||
// const removeHtmlSubtitle = () => {
|
||||
// const htmlTrack = get(htmlTrackAtom);
|
||||
// if (htmlTrack) htmlTrack.remove();
|
||||
// set(htmlTrackAtom, null);
|
||||
// };
|
||||
// const removeOctoSub = () => {
|
||||
// const subocto = get(suboctoAtom);
|
||||
// if (subocto) {
|
||||
// subocto.freeTrack();
|
||||
// subocto.dispose();
|
||||
// }
|
||||
// set(suboctoAtom, null);
|
||||
// };
|
||||
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
// const player = get(playerAtom);
|
||||
// if (!player?.current) return;
|
||||
|
||||
if (get(baked)?.id === value?.track.id) return;
|
||||
// if (get(baked)?.id === value?.track.id) return;
|
||||
|
||||
set(baked, value?.track ?? null);
|
||||
if (!value) {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
} else if (value.track.codec === "vtt" || value.track.codec === "subrip") {
|
||||
removeOctoSub();
|
||||
if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden";
|
||||
const track: HTMLTrackElement = get(htmlTrackAtom) ?? document.createElement("track");
|
||||
track.kind = "subtitles";
|
||||
track.label = value.track.displayName;
|
||||
if (value.track.language) track.srclang = value.track.language;
|
||||
track.src = value.track.link! + ".vtt";
|
||||
track.className = "subtitle_container";
|
||||
track.default = true;
|
||||
track.onload = () => {
|
||||
if (player.current) player.current.textTracks[0].mode = "showing";
|
||||
};
|
||||
if (!get(htmlTrackAtom)) player.current.appendChild(track);
|
||||
set(htmlTrackAtom, track);
|
||||
} else if (value.track.codec === "ass") {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
set(
|
||||
suboctoAtom,
|
||||
new SubtitleOctopus({
|
||||
video: player.current,
|
||||
subUrl: value.track.link!,
|
||||
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||
fonts: value.fonts?.map((x) => x.link),
|
||||
renderMode: "wasm-blend",
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
// set(baked, value?.track ?? null);
|
||||
// if (!value) {
|
||||
// removeHtmlSubtitle();
|
||||
// removeOctoSub();
|
||||
// } else if (value.track.codec === "vtt" || value.track.codec === "subrip") {
|
||||
// removeOctoSub();
|
||||
// if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden";
|
||||
// const track: HTMLTrackElement = get(htmlTrackAtom) ?? document.createElement("track");
|
||||
// track.kind = "subtitles";
|
||||
// track.label = value.track.displayName;
|
||||
// if (value.track.language) track.srclang = value.track.language;
|
||||
// track.src = value.track.link! + ".vtt";
|
||||
// track.className = "subtitle_container";
|
||||
// track.default = true;
|
||||
// track.onload = () => {
|
||||
// if (player.current) player.current.textTracks[0].mode = "showing";
|
||||
// };
|
||||
// if (!get(htmlTrackAtom)) player.current.appendChild(track);
|
||||
// set(htmlTrackAtom, track);
|
||||
// } else if (value.track.codec === "ass") {
|
||||
// removeHtmlSubtitle();
|
||||
// removeOctoSub();
|
||||
// set(
|
||||
// suboctoAtom,
|
||||
// new SubtitleOctopus({
|
||||
// video: player.current,
|
||||
// subUrl: value.track.link!,
|
||||
// workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||
// legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||
// fonts: value.fonts?.map((x) => x.link),
|
||||
// renderMode: "wasm-blend",
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
const { useParam } = createParam<{ subtitle: string }>();
|
||||
// const { useParam } = createParam<{ subtitle: string }>();
|
||||
|
||||
export const useSubtitleController = (
|
||||
player: RefObject<HTMLVideoElement>,
|
||||
subtitles?: Track[],
|
||||
fonts?: Font[],
|
||||
) => {
|
||||
const [subtitle] = useParam("subtitle");
|
||||
const selectSubtitle = useSetAtom(subtitleAtom);
|
||||
// export const useSubtitleController = (
|
||||
// player: RefObject<HTMLVideoElement>,
|
||||
// subtitles?: Track[],
|
||||
// fonts?: Font[],
|
||||
// ) => {
|
||||
// const [subtitle] = useParam("subtitle");
|
||||
// const selectSubtitle = useSetAtom(subtitleAtom);
|
||||
|
||||
const newSub = subtitles?.find((x) => x.language === subtitle);
|
||||
useEffect(() => {
|
||||
if (newSub === undefined) return;
|
||||
selectSubtitle({ track: newSub, fonts: fonts ?? [] });
|
||||
}, [player.current?.src, newSub, fonts, selectSubtitle]);
|
||||
};
|
||||
// const newSub = subtitles?.find((x) => x.language === subtitle);
|
||||
// useEffect(() => {
|
||||
// if (newSub === undefined) return;
|
||||
// selectSubtitle({ track: newSub, fonts: fonts ?? [] });
|
||||
// }, [player.current?.src, newSub, fonts, selectSubtitle]);
|
||||
// };
|
||||
|
Loading…
x
Reference in New Issue
Block a user