mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-02 13:14:29 -04:00
wip start cast
This commit is contained in:
parent
de3fda6a1a
commit
a7f0bd5a91
@ -21,34 +21,34 @@
|
|||||||
"tsdoc": true
|
"tsdoc": true
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.9.3",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.9.3",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@jellyfin/libass-wasm": "^4.1.1",
|
"@jellyfin/libass-wasm": "^4.1.1",
|
||||||
"@mui/icons-material": "^5.8.4",
|
"@mui/icons-material": "^5.10.9",
|
||||||
"@mui/material": "^5.8.7",
|
"@mui/material": "^5.10.10",
|
||||||
"hls.js": "^1.2.4",
|
"hls.js": "^1.2.4",
|
||||||
"jotai": "^1.8.4",
|
"jotai": "^1.8.6",
|
||||||
"next": "12.2.2",
|
"next": "12.3.1",
|
||||||
"next-translate": "^1.5.0",
|
"next-translate": "^1.6.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-query": "^4.0.0-beta.23",
|
"react-query": "^4.0.0-beta.23",
|
||||||
"superjson": "^1.9.1",
|
"superjson": "^1.10.1",
|
||||||
"zod": "^3.18.0"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chromecast-caf-sender": "^1.0.5",
|
"@types/chromecast-caf-sender": "^1.0.5",
|
||||||
"@types/node": "18.0.3",
|
"@types/node": "18.11.2",
|
||||||
"@types/react": "18.0.15",
|
"@types/react": "18.0.21",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"eslint": "8.19.0",
|
"eslint": "8.25.0",
|
||||||
"eslint-config-next": "12.2.2",
|
"eslint-config-next": "12.3.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-jsdoc": "^0.3.38",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,8 @@ if (typeof window === "undefined") {
|
|||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? {});
|
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? { json: {} });
|
||||||
const getLayout = (Component as QueryPage).getLayout ?? ((page) => page);
|
const getLayout = (Component as QueryPage).getLayout ?? ((page) => page);
|
||||||
const castEnabled = true;
|
|
||||||
|
|
||||||
useMobileHover();
|
useMobileHover();
|
||||||
|
|
||||||
@ -75,7 +74,7 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<ThemeProvider theme={defaultTheme}>
|
<ThemeProvider theme={defaultTheme}>
|
||||||
<Box >
|
<Box >
|
||||||
{getLayout(<Component {...props} />)}
|
{getLayout(<Component {...props} />)}
|
||||||
{castEnabled && <CastProvider />}
|
<CastProvider />
|
||||||
</Box>
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Hydrate>
|
</Hydrate>
|
||||||
|
@ -18,10 +18,18 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const CastController = dynamic(() => import("./state").then((x) => x.CastController), {
|
||||||
|
loading: () => null,
|
||||||
|
});
|
||||||
|
|
||||||
export const CastProvider = () => {
|
export const CastProvider = () => {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.__onGCastApiAvailable = (isAvailable) => {
|
window.__onGCastApiAvailable = (isAvailable) => {
|
||||||
if (!isAvailable) return;
|
if (!isAvailable) return;
|
||||||
@ -34,9 +42,13 @@ export const CastProvider = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Script
|
<>
|
||||||
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
<Script
|
||||||
strategy="lazyOnload"
|
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||||
/>
|
strategy="lazyOnload"
|
||||||
|
onReady={() => setLoaded(true)}
|
||||||
|
/>
|
||||||
|
{loaded && <CastController />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,16 +20,21 @@
|
|||||||
|
|
||||||
import { Pause, PlayArrow, SkipNext, SkipPrevious } from "@mui/icons-material";
|
import { Pause, PlayArrow, SkipNext, SkipPrevious } from "@mui/icons-material";
|
||||||
import { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
|
import { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import useTranslation from "next-translate/useTranslation";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Image } from "~/components/poster";
|
import { Image } from "~/components/poster";
|
||||||
import { ProgressText, VolumeSlider } from "~/player/components/left-buttons";
|
import { ProgressText, VolumeSlider } from "~/player/components/left-buttons";
|
||||||
import { ProgressBar } from "../components/progress-bar";
|
import { ProgressBar } from "../components/progress-bar";
|
||||||
|
import { mediaAtom } from "./state";
|
||||||
|
|
||||||
export const CastMiniPlayer = () => {
|
export const CastMiniPlayer = () => {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [media, setMedia] = useAtom(mediaAtom);
|
||||||
|
console.log(media)
|
||||||
|
|
||||||
const name = "Ansatsu Kyoushitsu";
|
const name = "Ansatsu Kyoushitsu";
|
||||||
const episodeName = "S1:E1 Assassination Time";
|
const episodeName = "S1:E1 Assassination Time";
|
||||||
const thumbnail = "/api/show/ansatsu-kyoushitsu/thumbnail";
|
const thumbnail = "/api/show/ansatsu-kyoushitsu/thumbnail";
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect } from "react";
|
||||||
import { bakedAtom } from "~/utils/jotai-utils";
|
import { bakedAtom } from "~/utils/jotai-utils";
|
||||||
|
import { stopAtom } from "../state";
|
||||||
|
|
||||||
export type Media = {
|
export type Media = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -44,19 +45,35 @@ export const [_playAtom, playAtom] = bakedAtom<boolean, never>(true, (get) => {
|
|||||||
controller.playOrPause();
|
controller.playOrPause();
|
||||||
});
|
});
|
||||||
export const [_durationAtom, durationAtom] = bakedAtom(1, (get, _, value) => {
|
export const [_durationAtom, durationAtom] = bakedAtom(1, (get, _, value) => {
|
||||||
const { controller } = get(playerAtom);
|
const { player, controller } = get(playerAtom);
|
||||||
|
player.currentTime = value;
|
||||||
controller.seek();
|
controller.seek();
|
||||||
});
|
});
|
||||||
|
|
||||||
export const [_mediaAtom, mediaAtom] = bakedAtom<Media | null, string>(null, (get, _, value) => {});
|
export const [_mediaAtom, mediaAtom] = bakedAtom<Media | null, string>(
|
||||||
|
null,
|
||||||
|
async (_, _2, value) => {
|
||||||
|
const session = cast.framework.CastContext.getInstance().getCurrentSession();
|
||||||
|
if (!session) return;
|
||||||
|
const mediaInfo = new chrome.cast.media.MediaInfo(
|
||||||
|
value,
|
||||||
|
process.env.KYOO_URL ?? "http://localhost:5000",
|
||||||
|
);
|
||||||
|
session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const useCastController = () => {
|
export const useCastController = () => {
|
||||||
const { player, controller } = useAtomValue(playerAtom);
|
const { player, controller } = useAtomValue(playerAtom);
|
||||||
const setPlay = useSetAtom(_playAtom);
|
const setPlay = useSetAtom(_playAtom);
|
||||||
const setDuration = useSetAtom(_durationAtom);
|
const setDuration = useSetAtom(_durationAtom);
|
||||||
const setMedia = useSetAtom(_mediaAtom);
|
const setMedia = useSetAtom(_mediaAtom);
|
||||||
|
const stopPlayer = useAtomValue(stopAtom);
|
||||||
|
const media = useAtomValue(mediaAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const context = cast.framework.CastContext.getInstance();
|
||||||
|
|
||||||
const eventListeners: [
|
const eventListeners: [
|
||||||
cast.framework.RemotePlayerEventType,
|
cast.framework.RemotePlayerEventType,
|
||||||
(event: cast.framework.RemotePlayerChangedEvent<any>) => void,
|
(event: cast.framework.RemotePlayerChangedEvent<any>) => void,
|
||||||
@ -69,9 +86,23 @@ export const useCastController = () => {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const sessionStateHandler = (event: cast.framework.SessionStateEventData) => {
|
||||||
|
if (event.sessionState === cast.framework.SessionState.SESSION_STARTED) {
|
||||||
|
stopPlayer[0]();
|
||||||
|
setMedia(media);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler);
|
||||||
for (const [key, handler] of eventListeners) controller.addEventListener(key, handler);
|
for (const [key, handler] of eventListeners) controller.addEventListener(key, handler);
|
||||||
return () => {
|
return () => {
|
||||||
|
context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler);
|
||||||
for (const [key, handler] of eventListeners) controller.removeEventListener(key, handler);
|
for (const [key, handler] of eventListeners) controller.removeEventListener(key, handler);
|
||||||
};
|
};
|
||||||
}, [player, controller, setPlay, setDuration, setMedia]);
|
}, [player, controller, setPlay, setDuration, setMedia, stopPlayer, media]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CastController = (props: any) => {
|
||||||
|
useCastController();
|
||||||
|
return <div></div>;
|
||||||
};
|
};
|
||||||
|
@ -84,7 +84,7 @@ export const LeftButtons = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<VolumeSlider color="white" />
|
<VolumeSlider color="white" />
|
||||||
<ProgressText />
|
<ProgressText sx={{ color: "white" }} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -27,7 +27,13 @@ import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react";
|
|||||||
import { Box, styled } from "@mui/material";
|
import { Box, styled } from "@mui/material";
|
||||||
import { useAtom, useSetAtom } from "jotai";
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import { Hover, LoadingIndicator } from "./components/hover";
|
import { Hover, LoadingIndicator } from "./components/hover";
|
||||||
import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
|
import {
|
||||||
|
fullscreenAtom,
|
||||||
|
playAtom,
|
||||||
|
stopAtom,
|
||||||
|
useSubtitleController,
|
||||||
|
useVideoController,
|
||||||
|
} from "./state";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { makeTitle } from "~/utils/utils";
|
import { makeTitle } from "~/utils/utils";
|
||||||
@ -43,13 +49,15 @@ let mouseCallback: NodeJS.Timeout;
|
|||||||
|
|
||||||
const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
||||||
path: ["watch", slug],
|
path: ["watch", slug],
|
||||||
|
// @ts-ignore
|
||||||
parser: WatchItemP,
|
parser: WatchItemP,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||||
const { data, error } = useFetch(query(slug));
|
const { data, error } = useFetch(query(slug));
|
||||||
const { playerRef, videoProps, onVideoClick } = useVideoController(data?.link);
|
const { playerRef, videoProps, onVideoClick } = useVideoController(slug, data?.link);
|
||||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||||
|
const setStopCallback = useSetAtom(stopAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isPlaying, setPlay] = useAtom(playAtom);
|
const [isPlaying, setPlay] = useAtom(playAtom);
|
||||||
@ -96,6 +104,16 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
|
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
|
||||||
useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
|
useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStopCallback([ () => {
|
||||||
|
console.log("toto")
|
||||||
|
router.push(data ? (data.isMovie ? `/movie/${data.slug}` : `/show/${data.showSlug}`) : "/");
|
||||||
|
}]);
|
||||||
|
return () => {
|
||||||
|
setStopCallback([() => {}]);
|
||||||
|
};
|
||||||
|
}, [setStopCallback, data, router]);
|
||||||
|
|
||||||
if (error) return <ErrorPage {...error} />;
|
if (error) return <ErrorPage {...error} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -84,10 +84,13 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
export const mediaAtom = atom<string | null>(null);
|
||||||
|
// The tuple is only used to prevent jotai from thinking the function is a read func.
|
||||||
|
export const stopAtom = atom<[() => void]>([() => {}]);
|
||||||
|
|
||||||
let hls: Hls | null = null;
|
let hls: Hls | null = null;
|
||||||
|
|
||||||
export const useVideoController = (links?: { direct: string; transmux: string }) => {
|
export const useVideoController = (slug: string, links?: { direct: string; transmux: string }) => {
|
||||||
const player = useRef<HTMLVideoElement>(null);
|
const player = useRef<HTMLVideoElement>(null);
|
||||||
const setPlayer = useSetAtom(playerAtom);
|
const setPlayer = useSetAtom(playerAtom);
|
||||||
const setPlay = useSetAtom(_playAtom);
|
const setPlay = useSetAtom(_playAtom);
|
||||||
@ -99,6 +102,7 @@ export const useVideoController = (links?: { direct: string; transmux: string })
|
|||||||
const setVolume = useSetAtom(_volumeAtom);
|
const setVolume = useSetAtom(_volumeAtom);
|
||||||
const setMuted = useSetAtom(_mutedAtom);
|
const setMuted = useSetAtom(_mutedAtom);
|
||||||
const setFullscreen = useSetAtom(fullscreenAtom);
|
const setFullscreen = useSetAtom(fullscreenAtom);
|
||||||
|
const setMedia = useSetAtom(mediaAtom);
|
||||||
const [playMode, setPlayMode] = useAtom(playModeAtom);
|
const [playMode, setPlayMode] = useAtom(playModeAtom);
|
||||||
|
|
||||||
setPlayer(player);
|
setPlayer(player);
|
||||||
@ -110,7 +114,8 @@ export const useVideoController = (links?: { direct: string; transmux: string })
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPlayMode(PlayMode.Direct);
|
setPlayMode(PlayMode.Direct);
|
||||||
}, [links, setPlayMode]);
|
setMedia(slug);
|
||||||
|
}, [slug, links, setPlayMode, setMedia]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;
|
const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;
|
||||||
|
794
front/yarn.lock
794
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user