Add cast button

This commit is contained in:
Zoe Roux 2022-10-12 17:07:04 +09:00
parent 3488aeb03b
commit c985a972f0
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
10 changed files with 145 additions and 8 deletions

View File

@ -2,5 +2,8 @@
"navbar": { "navbar": {
"home": "Home", "home": "Home",
"login": "Login" "login": "Login"
},
"cast": {
"start": "Cast to device"
} }
} }

View File

@ -2,5 +2,8 @@
"navbar": { "navbar": {
"home": "Accueil", "home": "Accueil",
"login": "Connexion" "login": "Connexion"
},
"cast": {
"start": "Caster sur un appareil"
} }
} }

View File

@ -38,6 +38,7 @@
"zod": "^3.18.0" "zod": "^3.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chromecast-caf-sender": "^1.0.5",
"@types/node": "18.0.3", "@types/node": "18.0.3",
"@types/react": "18.0.15", "@types/react": "18.0.15",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",

View File

@ -29,6 +29,7 @@ import { defaultTheme } from "~/utils/themes/default-theme";
import superjson from "superjson"; import superjson from "superjson";
import Head from "next/head"; import Head from "next/head";
import { useMobileHover } from "~/utils/utils"; import { useMobileHover } from "~/utils/utils";
import { CastProvider } from "~/player/cast/cast-provider";
// Simply silence a SSR warning (see https://github.com/facebook/react/issues/14927 for more details) // Simply silence a SSR warning (see https://github.com/facebook/react/issues/14927 for more details)
if (typeof window === "undefined") { if (typeof window === "undefined") {
@ -39,6 +40,7 @@ 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 ?? {});
const getLayout = (Component as QueryPage).getLayout ?? ((page) => page); const getLayout = (Component as QueryPage).getLayout ?? ((page) => page);
const castEnabled = true;
useMobileHover(); useMobileHover();
@ -70,7 +72,10 @@ const App = ({ Component, pageProps }: AppProps) => {
</Head> </Head>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Hydrate state={queryState}> <Hydrate state={queryState}>
<ThemeProvider theme={defaultTheme}>{getLayout(<Component {...props} />)}</ThemeProvider> <ThemeProvider theme={defaultTheme}>
{getLayout(<Component {...props} />)}
{castEnabled && <CastProvider />}
</ThemeProvider>
</Hydrate> </Hydrate>
</QueryClientProvider> </QueryClientProvider>
</> </>

View File

@ -0,0 +1,39 @@
/*
* 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/>.
*/
import { Box, styled } from "@mui/material";
import { BoxProps } from "@mui/system";
import { ComponentProps, forwardRef } from "react";
type CastProps = { class?: string };
declare global {
namespace JSX {
interface IntrinsicElements {
"google-cast-launcher": CastProps;
}
}
}
export const _CastButton = forwardRef<HTMLDivElement, ComponentProps<"div">>(function Cast({className, ...props}, ref) {
return <google-cast-launcher ref={ref} class={className} {...props} />;
});
export const CastButton = styled(_CastButton)({});

View File

@ -0,0 +1,42 @@
/*
* 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/>.
*/
import Script from "next/script";
import { useEffect } from "react";
export const CastProvider = () => {
useEffect(() => {
window.__onGCastApiAvailable = (isAvailable) => {
if (!isAvailable) return;
cast.framework.CastContext.getInstance().setOptions({
receiverApplicationId:
process.env.APPLICATION_ID ?? chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
});
};
}, []);
return (
<Script
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
strategy="lazyOnload"
/>
);
};

View File

@ -26,6 +26,7 @@ import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { Font, Track } from "~/models/resources/watch-item"; import { Font, Track } from "~/models/resources/watch-item";
import { Link } from "~/utils/link"; import { Link } from "~/utils/link";
import { CastButton } from "../cast/cast-button";
import { fullscreenAtom, subtitleAtom } from "../state"; import { fullscreenAtom, subtitleAtom } from "../state";
export const RightButtons = ({ export const RightButtons = ({
@ -40,6 +41,7 @@ export const RightButtons = ({
onMenuClose: () => void; onMenuClose: () => void;
}) => { }) => {
const { t } = useTranslation("player"); const { t } = useTranslation("player");
const { t: tc } = useTranslation("common");
const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null); const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null);
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom); const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
@ -71,6 +73,16 @@ export const RightButtons = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
<Tooltip title={tc("cast.start")}>
<CastButton
sx={{
width: "24px",
height: "24px",
"--connected-color": "white",
"--disconnected-color": "white",
}}
/>
</Tooltip>
<Tooltip title={t("fullscreen")}> <Tooltip title={t("fullscreen")}>
<IconButton <IconButton
onClick={() => setFullscreen(!isFullscreen)} onClick={() => setFullscreen(!isFullscreen)}

View File

@ -24,8 +24,8 @@ import { WatchItem, WatchItemP } from "~/models/resources/watch-item";
import { useFetch } from "~/utils/query"; import { useFetch } from "~/utils/query";
import { ErrorPage } from "~/components/errors"; import { ErrorPage } from "~/components/errors";
import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react"; import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react";
import { Box } from "@mui/material"; import { Box, styled } from "@mui/material";
import { useAtom, useAtomValue, 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, useSubtitleController, useVideoController } from "./state";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -35,6 +35,8 @@ import { episodeDisplayNumber } from "~/components/episode";
import { useVideoKeyboard } from "./keyboard"; import { useVideoKeyboard } from "./keyboard";
import { MediaSessionManager } from "./media-session"; import { MediaSessionManager } from "./media-session";
const Video = styled("video")({});
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout // Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move) // if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
let mouseCallback: NodeJS.Timeout; let mouseCallback: NodeJS.Timeout;
@ -132,8 +134,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
onMouseLeave={() => setMouseMoved(false)} onMouseLeave={() => setMouseMoved(false)}
sx={{ cursor: displayControls ? "unset" : "none" }} sx={{ cursor: displayControls ? "unset" : "none" }}
> >
<Box <Video
component="video"
{...videoProps} {...videoProps}
onPointerDown={(e: ReactPointerEvent<HTMLVideoElement>) => { onPointerDown={(e: ReactPointerEvent<HTMLVideoElement>) => {
if (e.pointerType === "mouse") { if (e.pointerType === "mouse") {

View File

@ -18,10 +18,9 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { BoxProps } from "@mui/material";
import { atom, useAtom, useSetAtom } from "jotai"; import { atom, useAtom, useSetAtom } from "jotai";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { RefObject, useEffect, useRef } from "react"; import { ComponentProps, RefObject, useEffect, useRef } from "react";
import { Font, Track } from "~/models/resources/watch-item"; import { Font, Track } from "~/models/resources/watch-item";
import { bakedAtom } from "~/utils/jotai-utils"; import { bakedAtom } from "~/utils/jotai-utils";
// @ts-ignore // @ts-ignore
@ -139,7 +138,7 @@ export const useVideoController = (links?: { direct: string; transmux: string })
setDuration(player.current.duration); setDuration(player.current.duration);
}, [player, setDuration]); }, [player, setDuration]);
const videoProps: BoxProps<"video"> = { const videoProps: ComponentProps<"video"> = {
ref: player, ref: player,
onDoubleClick: () => { onDoubleClick: () => {
setFullscreen(!document.fullscreenElement); setFullscreen(!document.fullscreenElement);

View File

@ -402,6 +402,21 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@types/chrome@*":
version "0.0.197"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.197.tgz#c1b50cdb72ee40f9bc1411506031a9f8a925ab35"
integrity sha512-m1NfS5bOjaypyqQfaX6CxmJodZVcvj5+Mt/K94EBHkflYjPNmXHAzbxfifdLMa0YM3PDyOxohoTS5ug/e6p5jA==
dependencies:
"@types/filesystem" "*"
"@types/har-format" "*"
"@types/chromecast-caf-sender@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.5.tgz#197bfae77efb7399818ceaee5d2c303a4b72d51f"
integrity sha512-8d6RRCOYYiKzDyFJKAYKOp7Eo0kUfj9imnLQj0uuh/QGSz8euL9OOeKmh8XizqTcKW5tXva6li0mRYtnvzVIcA==
dependencies:
"@types/chrome" "*"
"@types/debug@^4.0.0": "@types/debug@^4.0.0":
version "4.1.7" version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -409,6 +424,23 @@
dependencies: dependencies:
"@types/ms" "*" "@types/ms" "*"
"@types/filesystem@*":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf"
integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==
dependencies:
"@types/filewriter" "*"
"@types/filewriter@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee"
integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==
"@types/har-format@*":
version "1.2.9"
resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.9.tgz#b9b3a9bfc33a078e7d898a00b09662910577f4a4"
integrity sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg==
"@types/json-schema@^7.0.9": "@types/json-schema@^7.0.9":
version "7.0.11" version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"