mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 08:21:52 +01:00
chromecasting connectivity
This commit is contained in:
parent
5b145e1707
commit
43d4869f7e
56
src/components/player/atoms/Chromecast.tsx
Normal file
56
src/components/player/atoms/Chromecast.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export interface ChromecastProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Chromecast(props: ChromecastProps) {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const isCasting = usePlayerStore((s) => s.interface.isCasting);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const setButtonVisibility = useCallback(
|
||||
(tag: HTMLElement) => {
|
||||
const isVisible = (tag.getAttribute("style") ?? "").includes("inline");
|
||||
setHidden(!isVisible);
|
||||
},
|
||||
[setHidden]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const tag = ref.current?.querySelector<HTMLElement>("google-cast-launcher");
|
||||
if (!tag) return;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setButtonVisibility(tag);
|
||||
});
|
||||
|
||||
observer.observe(tag, { attributes: true, attributeFilter: ["style"] });
|
||||
setButtonVisibility(tag);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [setButtonVisibility]);
|
||||
|
||||
return (
|
||||
<VideoPlayerButton
|
||||
ref={ref}
|
||||
className={[
|
||||
props.className ?? "",
|
||||
"google-cast-button",
|
||||
isCasting ? "casting" : "",
|
||||
hidden ? "hidden" : "",
|
||||
].join(" ")}
|
||||
icon={Icons.CASTING}
|
||||
onClick={(el) => {
|
||||
const castButton = el.querySelector("google-cast-launcher");
|
||||
if (castButton) (castButton as HTMLDivElement).click();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -13,3 +13,4 @@ export * from "./Episodes";
|
||||
export * from "./Airplay";
|
||||
export * from "./VolumeChangedPopout";
|
||||
export * from "./NextEpisodeButton";
|
||||
export * from "./Chromecast";
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
||||
|
||||
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
||||
import { CastingInternal } from "@/components/player/internals/CastingInternal";
|
||||
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
||||
import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents";
|
||||
import { ProgressSaver } from "@/components/player/internals/ProgressSaver";
|
||||
@ -81,6 +82,7 @@ export function Container(props: PlayerProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<BaseContainer>
|
||||
<CastingInternal />
|
||||
<VideoContainer />
|
||||
<ProgressSaver />
|
||||
<KeyboardEvents />
|
||||
|
@ -1,19 +1,26 @@
|
||||
import classNames from "classnames";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
||||
export function VideoPlayerButton(props: {
|
||||
export interface VideoPlayerButtonProps {
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
onClick?: (el: HTMLButtonElement) => void;
|
||||
icon?: Icons;
|
||||
iconSizeClass?: string;
|
||||
className?: string;
|
||||
activeClass?: string;
|
||||
}) {
|
||||
}
|
||||
|
||||
export const VideoPlayerButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
VideoPlayerButtonProps
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={props.onClick}
|
||||
onClick={(e) => props.onClick?.(e.currentTarget as HTMLButtonElement)}
|
||||
className={classNames([
|
||||
"p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center",
|
||||
props.activeClass ??
|
||||
@ -33,4 +40,4 @@ export function VideoPlayerButton(props: {
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
47
src/components/player/internals/CastingInternal.tsx
Normal file
47
src/components/player/internals/CastingInternal.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useChromecastAvailable } from "@/hooks/useChromecastAvailable";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function CastingInternal() {
|
||||
const setInstance = usePlayerStore((s) => s.casting.setInstance);
|
||||
const setController = usePlayerStore((s) => s.casting.setController);
|
||||
const setPlayer = usePlayerStore((s) => s.casting.setPlayer);
|
||||
const setIsCasting = usePlayerStore((s) => s.casting.setIsCasting);
|
||||
const available = useChromecastAvailable();
|
||||
|
||||
useEffect(() => {
|
||||
if (!available) return;
|
||||
|
||||
const ins = cast.framework.CastContext.getInstance();
|
||||
setInstance(ins);
|
||||
ins.setOptions({
|
||||
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
});
|
||||
|
||||
const player = new cast.framework.RemotePlayer();
|
||||
setPlayer(player);
|
||||
const controller = new cast.framework.RemotePlayerController(player);
|
||||
setController(controller);
|
||||
|
||||
function connectionChanged(e: cast.framework.RemotePlayerChangedEvent) {
|
||||
if (e.field === "isConnected") {
|
||||
setIsCasting(e.value);
|
||||
}
|
||||
}
|
||||
controller.addEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged
|
||||
);
|
||||
|
||||
return () => {
|
||||
controller.removeEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged
|
||||
);
|
||||
};
|
||||
}, [available, setPlayer, setController, setInstance, setIsCasting]);
|
||||
|
||||
return null;
|
||||
}
|
@ -3,6 +3,7 @@ import { ReactNode } from "react";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import { Player } from "@/components/player";
|
||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||
import { useChromecastAvailable } from "@/hooks/useChromecastAvailable";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
@ -59,6 +60,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
</div>
|
||||
<div className="flex sm:hidden items-center justify-end">
|
||||
<Player.Airplay />
|
||||
<Player.Chromecast />
|
||||
</div>
|
||||
</div>
|
||||
</Player.TopControls>
|
||||
@ -79,6 +81,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||
<div className="flex items-center space-x-3">
|
||||
<Player.Episodes />
|
||||
<Player.Airplay />
|
||||
<Player.Chromecast />
|
||||
<Player.Settings />
|
||||
<Player.Fullscreen />
|
||||
</div>
|
||||
|
47
src/stores/player/slices/casting.ts
Normal file
47
src/stores/player/slices/casting.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { MakeSlice } from "@/stores/player/slices/types";
|
||||
|
||||
export interface CastingSlice {
|
||||
casting: {
|
||||
instance: cast.framework.CastContext | null;
|
||||
player: cast.framework.RemotePlayer | null;
|
||||
controller: cast.framework.RemotePlayerController | null;
|
||||
setInstance(instance: cast.framework.CastContext): void;
|
||||
setPlayer(player: cast.framework.RemotePlayer): void;
|
||||
setController(controller: cast.framework.RemotePlayerController): void;
|
||||
setIsCasting(isCasting: boolean): void;
|
||||
clear(): void;
|
||||
};
|
||||
}
|
||||
|
||||
export const createCastingSlice: MakeSlice<CastingSlice> = (set) => ({
|
||||
casting: {
|
||||
instance: null,
|
||||
player: null,
|
||||
controller: null,
|
||||
setInstance(instance) {
|
||||
set((s) => {
|
||||
s.casting.instance = instance;
|
||||
});
|
||||
},
|
||||
setPlayer(player) {
|
||||
set((s) => {
|
||||
s.casting.player = player;
|
||||
});
|
||||
},
|
||||
setController(controller) {
|
||||
set((s) => {
|
||||
s.casting.controller = controller;
|
||||
});
|
||||
},
|
||||
setIsCasting(isCasting) {
|
||||
set((s) => {
|
||||
s.interface.isCasting = isCasting;
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
set((s) => {
|
||||
s.casting.instance = null;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
@ -20,6 +20,7 @@ export interface InterfaceSlice {
|
||||
hovering: PlayerHoverState;
|
||||
lastHoveringState: PlayerHoverState;
|
||||
canAirplay: boolean;
|
||||
isCasting: boolean;
|
||||
hideNextEpisodeBtn: boolean;
|
||||
|
||||
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
||||
@ -39,6 +40,7 @@ export interface InterfaceSlice {
|
||||
|
||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
interface: {
|
||||
isCasting: false,
|
||||
hasOpenOverlay: false,
|
||||
isFullscreen: false,
|
||||
isSeeking: false,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { StateCreator } from "zustand";
|
||||
|
||||
import { CastingSlice } from "@/stores/player/slices/casting";
|
||||
import { DisplaySlice } from "@/stores/player/slices/display";
|
||||
import { InterfaceSlice } from "@/stores/player/slices/interface";
|
||||
import { PlayingSlice } from "@/stores/player/slices/playing";
|
||||
@ -10,7 +11,8 @@ export type AllSlices = InterfaceSlice &
|
||||
PlayingSlice &
|
||||
ProgressSlice &
|
||||
SourceSlice &
|
||||
DisplaySlice;
|
||||
DisplaySlice &
|
||||
CastingSlice;
|
||||
export type MakeSlice<Slice> = StateCreator<
|
||||
AllSlices,
|
||||
[["zustand/immer", never]],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import { createCastingSlice } from "@/stores/player/slices/casting";
|
||||
import { createDisplaySlice } from "@/stores/player/slices/display";
|
||||
import { createInterfaceSlice } from "@/stores/player/slices/interface";
|
||||
import { createPlayingSlice } from "@/stores/player/slices/playing";
|
||||
@ -15,5 +16,6 @@ export const usePlayerStore = create(
|
||||
...createPlayingSlice(...a),
|
||||
...createSourceSlice(...a),
|
||||
...createDisplaySlice(...a),
|
||||
...createCastingSlice(...a),
|
||||
}))
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user