10 changed files with 175 additions and 6 deletions
@ -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(); |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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; |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
Loading…
Reference in new issue