10 changed files with 175 additions and 6 deletions
@ -0,0 +1,56 @@
@@ -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 @@
@@ -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 @@
@@ -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