wip start cast

This commit is contained in:
Zoe Roux 2022-10-20 22:04:53 +09:00
parent de3fda6a1a
commit a7f0bd5a91
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
9 changed files with 537 additions and 391 deletions

View File

@ -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"
} }
} }

View File

@ -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>

View File

@ -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 />}
</>
); );
}; };

View File

@ -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";

View File

@ -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>;
}; };

View File

@ -84,7 +84,7 @@ export const LeftButtons = ({
</Tooltip> </Tooltip>
)} )}
<VolumeSlider color="white" /> <VolumeSlider color="white" />
<ProgressText /> <ProgressText sx={{ color: "white" }} />
</Box> </Box>
); );
}; };

View File

@ -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 (

View File

@ -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;

File diff suppressed because it is too large Load Diff