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": {
"home": "Home",
"login": "Login"
},
"cast": {
"start": "Cast to device"
}
}

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import { defaultTheme } from "~/utils/themes/default-theme";
import superjson from "superjson";
import Head from "next/head";
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)
if (typeof window === "undefined") {
@ -39,6 +40,7 @@ const App = ({ Component, pageProps }: AppProps) => {
const [queryClient] = useState(() => createQueryClient());
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? {});
const getLayout = (Component as QueryPage).getLayout ?? ((page) => page);
const castEnabled = true;
useMobileHover();
@ -70,7 +72,10 @@ const App = ({ Component, pageProps }: AppProps) => {
</Head>
<QueryClientProvider client={queryClient}>
<Hydrate state={queryState}>
<ThemeProvider theme={defaultTheme}>{getLayout(<Component {...props} />)}</ThemeProvider>
<ThemeProvider theme={defaultTheme}>
{getLayout(<Component {...props} />)}
{castEnabled && <CastProvider />}
</ThemeProvider>
</Hydrate>
</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 { Font, Track } from "~/models/resources/watch-item";
import { Link } from "~/utils/link";
import { CastButton } from "../cast/cast-button";
import { fullscreenAtom, subtitleAtom } from "../state";
export const RightButtons = ({
@ -40,6 +41,7 @@ export const RightButtons = ({
onMenuClose: () => void;
}) => {
const { t } = useTranslation("player");
const { t: tc } = useTranslation("common");
const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null);
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
@ -71,6 +73,16 @@ export const RightButtons = ({
</IconButton>
</Tooltip>
)}
<Tooltip title={tc("cast.start")}>
<CastButton
sx={{
width: "24px",
height: "24px",
"--connected-color": "white",
"--disconnected-color": "white",
}}
/>
</Tooltip>
<Tooltip title={t("fullscreen")}>
<IconButton
onClick={() => setFullscreen(!isFullscreen)}

View File

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

View File

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

View File

@ -402,6 +402,21 @@
dependencies:
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":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -409,6 +424,23 @@
dependencies:
"@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":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"