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
},
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@jellyfin/libass-wasm": "^4.1.1",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"@mui/icons-material": "^5.10.9",
"@mui/material": "^5.10.10",
"hls.js": "^1.2.4",
"jotai": "^1.8.4",
"next": "12.2.2",
"next-translate": "^1.5.0",
"jotai": "^1.8.6",
"next": "12.3.1",
"next-translate": "^1.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-infinite-scroll-component": "^6.1.0",
"react-query": "^4.0.0-beta.23",
"superjson": "^1.9.1",
"zod": "^3.18.0"
"superjson": "^1.10.1",
"zod": "^3.19.1"
},
"devDependencies": {
"@types/chromecast-caf-sender": "^1.0.5",
"@types/node": "18.0.3",
"@types/react": "18.0.15",
"@types/node": "18.11.2",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
"copy-webpack-plugin": "^11.0.0",
"eslint": "8.19.0",
"eslint-config-next": "12.2.2",
"eslint": "8.25.0",
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-header": "^3.1.1",
"prettier": "^2.7.1",
"prettier-plugin-jsdoc": "^0.3.38",
"typescript": "4.7.4"
"prettier-plugin-jsdoc": "^0.4.2",
"typescript": "4.8.4"
}
}

View File

@ -38,9 +38,8 @@ if (typeof window === "undefined") {
const App = ({ Component, pageProps }: AppProps) => {
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 castEnabled = true;
useMobileHover();
@ -75,7 +74,7 @@ const App = ({ Component, pageProps }: AppProps) => {
<ThemeProvider theme={defaultTheme}>
<Box >
{getLayout(<Component {...props} />)}
{castEnabled && <CastProvider />}
<CastProvider />
</Box>
</ThemeProvider>
</Hydrate>

View File

@ -18,10 +18,18 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import dynamic from "next/dynamic";
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 = () => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
window.__onGCastApiAvailable = (isAvailable) => {
if (!isAvailable) return;
@ -34,9 +42,13 @@ export const CastProvider = () => {
}, []);
return (
<Script
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
strategy="lazyOnload"
/>
<>
<Script
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 { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
import { useAtom } from "jotai";
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { Image } from "~/components/poster";
import { ProgressText, VolumeSlider } from "~/player/components/left-buttons";
import { ProgressBar } from "../components/progress-bar";
import { mediaAtom } from "./state";
export const CastMiniPlayer = () => {
const { t } = useTranslation("player");
const router = useRouter();
const [media, setMedia] = useAtom(mediaAtom);
console.log(media)
const name = "Ansatsu Kyoushitsu";
const episodeName = "S1:E1 Assassination Time";
const thumbnail = "/api/show/ansatsu-kyoushitsu/thumbnail";

View File

@ -19,8 +19,9 @@
*/
import { atom, useAtomValue, useSetAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";
import { bakedAtom } from "~/utils/jotai-utils";
import { stopAtom } from "../state";
export type Media = {
name: string;
@ -44,19 +45,35 @@ export const [_playAtom, playAtom] = bakedAtom<boolean, never>(true, (get) => {
controller.playOrPause();
});
export const [_durationAtom, durationAtom] = bakedAtom(1, (get, _, value) => {
const { controller } = get(playerAtom);
const { player, controller } = get(playerAtom);
player.currentTime = value;
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 = () => {
const { player, controller } = useAtomValue(playerAtom);
const setPlay = useSetAtom(_playAtom);
const setDuration = useSetAtom(_durationAtom);
const setMedia = useSetAtom(_mediaAtom);
const stopPlayer = useAtomValue(stopAtom);
const media = useAtomValue(mediaAtom);
useEffect(() => {
const context = cast.framework.CastContext.getInstance();
const eventListeners: [
cast.framework.RemotePlayerEventType,
(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);
return () => {
context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler);
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>
)}
<VolumeSlider color="white" />
<ProgressText />
<ProgressText sx={{ color: "white" }} />
</Box>
);
};

View File

@ -27,7 +27,13 @@ import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react";
import { Box, styled } from "@mui/material";
import { useAtom, useSetAtom } from "jotai";
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 Head from "next/head";
import { makeTitle } from "~/utils/utils";
@ -43,13 +49,15 @@ let mouseCallback: NodeJS.Timeout;
const query = (slug: string): QueryIdentifier<WatchItem> => ({
path: ["watch", slug],
// @ts-ignore
parser: WatchItemP,
});
const Player: QueryPage<{ slug: string }> = ({ 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 setStopCallback = useSetAtom(stopAtom);
const router = useRouter();
const [isPlaying, setPlay] = useAtom(playAtom);
@ -96,6 +104,16 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
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} />;
return (

View File

@ -84,10 +84,13 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
}
} 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;
export const useVideoController = (links?: { direct: string; transmux: string }) => {
export const useVideoController = (slug: string, links?: { direct: string; transmux: string }) => {
const player = useRef<HTMLVideoElement>(null);
const setPlayer = useSetAtom(playerAtom);
const setPlay = useSetAtom(_playAtom);
@ -99,6 +102,7 @@ export const useVideoController = (links?: { direct: string; transmux: string })
const setVolume = useSetAtom(_volumeAtom);
const setMuted = useSetAtom(_mutedAtom);
const setFullscreen = useSetAtom(fullscreenAtom);
const setMedia = useSetAtom(mediaAtom);
const [playMode, setPlayMode] = useAtom(playModeAtom);
setPlayer(player);
@ -110,7 +114,8 @@ export const useVideoController = (links?: { direct: string; transmux: string })
useEffect(() => {
setPlayMode(PlayMode.Direct);
}, [links, setPlayMode]);
setMedia(slug);
}, [slug, links, setPlayMode, setMedia]);
useEffect(() => {
const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;

File diff suppressed because it is too large Load Diff