mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 07:51:48 +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 "./Airplay";
|
||||||
export * from "./VolumeChangedPopout";
|
export * from "./VolumeChangedPopout";
|
||||||
export * from "./NextEpisodeButton";
|
export * from "./NextEpisodeButton";
|
||||||
|
export * from "./Chromecast";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
import { ReactNode, RefObject, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
||||||
|
import { CastingInternal } from "@/components/player/internals/CastingInternal";
|
||||||
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
||||||
import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents";
|
import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents";
|
||||||
import { ProgressSaver } from "@/components/player/internals/ProgressSaver";
|
import { ProgressSaver } from "@/components/player/internals/ProgressSaver";
|
||||||
@ -81,6 +82,7 @@ export function Container(props: PlayerProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<BaseContainer>
|
<BaseContainer>
|
||||||
|
<CastingInternal />
|
||||||
<VideoContainer />
|
<VideoContainer />
|
||||||
<ProgressSaver />
|
<ProgressSaver />
|
||||||
<KeyboardEvents />
|
<KeyboardEvents />
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export function VideoPlayerButton(props: {
|
export interface VideoPlayerButtonProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: (el: HTMLButtonElement) => void;
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
iconSizeClass?: string;
|
iconSizeClass?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
activeClass?: string;
|
activeClass?: string;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export const VideoPlayerButton = forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
VideoPlayerButtonProps
|
||||||
|
>((props, ref) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={ref}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={props.onClick}
|
onClick={(e) => props.onClick?.(e.currentTarget as HTMLButtonElement)}
|
||||||
className={classNames([
|
className={classNames([
|
||||||
"p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center",
|
"p-2 rounded-full hover:bg-video-buttonBackground hover:bg-opacity-50 transition-transform duration-100 flex items-center",
|
||||||
props.activeClass ??
|
props.activeClass ??
|
||||||
@ -33,4 +40,4 @@ export function VideoPlayerButton(props: {
|
|||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</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 { BrandPill } from "@/components/layout/BrandPill";
|
||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||||
|
import { useChromecastAvailable } from "@/hooks/useChromecastAvailable";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
@ -59,6 +60,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex sm:hidden items-center justify-end">
|
<div className="flex sm:hidden items-center justify-end">
|
||||||
<Player.Airplay />
|
<Player.Airplay />
|
||||||
|
<Player.Chromecast />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Player.TopControls>
|
</Player.TopControls>
|
||||||
@ -79,6 +81,7 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Player.Episodes />
|
<Player.Episodes />
|
||||||
<Player.Airplay />
|
<Player.Airplay />
|
||||||
|
<Player.Chromecast />
|
||||||
<Player.Settings />
|
<Player.Settings />
|
||||||
<Player.Fullscreen />
|
<Player.Fullscreen />
|
||||||
</div>
|
</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;
|
hovering: PlayerHoverState;
|
||||||
lastHoveringState: PlayerHoverState;
|
lastHoveringState: PlayerHoverState;
|
||||||
canAirplay: boolean;
|
canAirplay: boolean;
|
||||||
|
isCasting: boolean;
|
||||||
hideNextEpisodeBtn: boolean;
|
hideNextEpisodeBtn: boolean;
|
||||||
|
|
||||||
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
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) => ({
|
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||||
interface: {
|
interface: {
|
||||||
|
isCasting: false,
|
||||||
hasOpenOverlay: false,
|
hasOpenOverlay: false,
|
||||||
isFullscreen: false,
|
isFullscreen: false,
|
||||||
isSeeking: false,
|
isSeeking: false,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { StateCreator } from "zustand";
|
import { StateCreator } from "zustand";
|
||||||
|
|
||||||
|
import { CastingSlice } from "@/stores/player/slices/casting";
|
||||||
import { DisplaySlice } from "@/stores/player/slices/display";
|
import { DisplaySlice } from "@/stores/player/slices/display";
|
||||||
import { InterfaceSlice } from "@/stores/player/slices/interface";
|
import { InterfaceSlice } from "@/stores/player/slices/interface";
|
||||||
import { PlayingSlice } from "@/stores/player/slices/playing";
|
import { PlayingSlice } from "@/stores/player/slices/playing";
|
||||||
@ -10,7 +11,8 @@ export type AllSlices = InterfaceSlice &
|
|||||||
PlayingSlice &
|
PlayingSlice &
|
||||||
ProgressSlice &
|
ProgressSlice &
|
||||||
SourceSlice &
|
SourceSlice &
|
||||||
DisplaySlice;
|
DisplaySlice &
|
||||||
|
CastingSlice;
|
||||||
export type MakeSlice<Slice> = StateCreator<
|
export type MakeSlice<Slice> = StateCreator<
|
||||||
AllSlices,
|
AllSlices,
|
||||||
[["zustand/immer", never]],
|
[["zustand/immer", never]],
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
import { createCastingSlice } from "@/stores/player/slices/casting";
|
||||||
import { createDisplaySlice } from "@/stores/player/slices/display";
|
import { createDisplaySlice } from "@/stores/player/slices/display";
|
||||||
import { createInterfaceSlice } from "@/stores/player/slices/interface";
|
import { createInterfaceSlice } from "@/stores/player/slices/interface";
|
||||||
import { createPlayingSlice } from "@/stores/player/slices/playing";
|
import { createPlayingSlice } from "@/stores/player/slices/playing";
|
||||||
@ -15,5 +16,6 @@ export const usePlayerStore = create(
|
|||||||
...createPlayingSlice(...a),
|
...createPlayingSlice(...a),
|
||||||
...createSourceSlice(...a),
|
...createSourceSlice(...a),
|
||||||
...createDisplaySlice(...a),
|
...createDisplaySlice(...a),
|
||||||
|
...createCastingSlice(...a),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user