mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
Fix casting (#49)
* Tbh i tried adding it and it works but i cant fix the dual casting * clean up chromecasting * Apply Chromecast fixes * Update Icon.tsx --------- Co-authored-by: Pas <74743263+Pasithea0@users.noreply.github.com>
This commit is contained in:
parent
c45004dc11
commit
47653c2906
9 changed files with 105 additions and 135 deletions
|
|
@ -594,6 +594,8 @@
|
|||
"short": "Back"
|
||||
},
|
||||
"casting": {
|
||||
"to": "Casting to {{device}} 📺",
|
||||
"device": "device",
|
||||
"enabled": "Casting to device 🎬"
|
||||
},
|
||||
"menus": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from "classnames";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
export enum Icons {
|
||||
SEARCH = "search",
|
||||
|
|
@ -128,7 +128,7 @@ const iconList: Record<Icons, string> = {
|
|||
captions: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 25 20"><path transform="translate(-3 -6)" d="M25.5,6H5.5A2.507,2.507,0,0,0,3,8.5v15A2.507,2.507,0,0,0,5.5,26h20A2.507,2.507,0,0,0,28,23.5V8.5A2.507,2.507,0,0,0,25.5,6ZM5.5,16h5v2.5h-5ZM18,23.5H5.5V21H18Zm7.5,0h-5V21h5Zm0-5H13V16H25.5Z" fill="currentColor"/></svg>`,
|
||||
link: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
||||
circle_exclamation: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
|
||||
casting: "",
|
||||
casting: "", // leave blank because Chrome imports it's own icon
|
||||
download: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
||||
gear: `<svg fill="currentColor" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" viewBox="0 0 48.4 48.4" xml:space="preserve"><g><path d="M48.4,24.2c0-1.8-1.297-3.719-2.896-4.285s-3.149-1.952-3.6-3.045c-0.451-1.093-0.334-3.173,0.396-4.705 c0.729-1.532,0.287-3.807-0.986-5.08c-1.272-1.273-3.547-1.714-5.08-0.985c-1.532,0.729-3.609,0.848-4.699,0.397 s-2.477-2.003-3.045-3.602C27.921,1.296,26,0,24.2,0c-1.8,0-3.721,1.296-4.29,2.895c-0.569,1.599-1.955,3.151-3.045,3.602 c-1.09,0.451-3.168,0.332-4.7-0.397c-1.532-0.729-3.807-0.288-5.08,0.985c-1.273,1.273-1.714,3.547-0.985,5.08 c0.729,1.533,0.845,3.611,0.392,4.703c-0.453,1.092-1.998,2.481-3.597,3.047S0,22.4,0,24.2s1.296,3.721,2.895,4.29 c1.599,0.568,3.146,1.957,3.599,3.047c0.453,1.089,0.335,3.166-0.394,4.698s-0.288,3.807,0.985,5.08 c1.273,1.272,3.547,1.714,5.08,0.985c1.533-0.729,3.61-0.847,4.7-0.395c1.091,0.452,2.476,2.008,3.045,3.604 c0.569,1.596,2.49,2.891,4.29,2.891c1.8,0,3.721-1.295,4.29-2.891c0.568-1.596,1.953-3.15,3.043-3.604 c1.09-0.453,3.17-0.334,4.701,0.396c1.533,0.729,3.808,0.287,5.08-0.985c1.273-1.273,1.715-3.548,0.986-5.08 c-0.729-1.533-0.849-3.61-0.398-4.7c0.451-1.09,2.004-2.477,3.603-3.045C47.104,27.921,48.4,26,48.4,24.2z M24.2,33.08 c-4.91,0-8.88-3.97-8.88-8.87c0-4.91,3.97-8.88,8.88-8.88c4.899,0,8.87,3.97,8.87,8.88C33.07,29.11,29.1,33.08,24.2,33.08z"/></g></svg>`,
|
||||
watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`,
|
||||
|
|
@ -183,23 +183,7 @@ const iconList: Record<Icons, string> = {
|
|||
repeat: `<svg viewBox="0 0 24 24" width="1em" height="1em" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const tag = document.createElement("google-cast-launcher");
|
||||
tag.setAttribute("id", "castbutton");
|
||||
ref.current?.appendChild(tag);
|
||||
}, []);
|
||||
|
||||
return <div ref={ref} className="h-6" />;
|
||||
}
|
||||
|
||||
export const Icon = memo((props: IconProps) => {
|
||||
if (props.icon === Icons.CASTING) {
|
||||
return <ChromeCastButton />;
|
||||
}
|
||||
|
||||
const flipClass =
|
||||
props.icon === Icons.ARROW_LEFT ||
|
||||
props.icon === Icons.ARROW_RIGHT ||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ export * from "./base/SubtitleView";
|
|||
export * from "./internals/BookmarkButton";
|
||||
export * from "./internals/InfoButton";
|
||||
export * from "./internals/SkipEpisodeButton";
|
||||
export * from "./atoms/Chromecast";
|
||||
|
|
|
|||
|
|
@ -8,15 +8,23 @@ export function CastingNotification() {
|
|||
const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading);
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
const isCasting = display?.getType() === "casting";
|
||||
const remotePlayer = usePlayerStore((s) => s.casting.player);
|
||||
|
||||
if (isLoading || !isCasting) return null;
|
||||
|
||||
let deviceName = remotePlayer?.displayName || t("player.casting.device");
|
||||
if (deviceName === "Default Media Receiver") {
|
||||
deviceName = t("player.casting.device"); // e.g., "your TV"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<div className="rounded-full bg-opacity-10 bg-video-buttonBackground p-3 brightness-100 grayscale">
|
||||
<Icon icon={Icons.CASTING} />
|
||||
</div>
|
||||
<p className="text-center">{t("player.casting.enabled")}</p>
|
||||
<p className="text-center">
|
||||
{t("player.casting.to", { device: deviceName })}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
// Allow the custom element in TSX without adding a global d.ts file
|
||||
// Allow the custom element in TSX
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
declare global {
|
||||
namespace JSX {
|
||||
|
|
@ -19,89 +20,55 @@ export interface ChromecastProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export function Chromecast(props: ChromecastProps) {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
export function Chromecast({ className }: ChromecastProps) {
|
||||
const [castHidden, setCastHidden] = 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],
|
||||
);
|
||||
const launcherRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const tag = ref.current?.querySelector<HTMLElement>("google-cast-launcher");
|
||||
if (!tag) return;
|
||||
const w = window as unknown as { cast?: typeof cast };
|
||||
const castFramework = w.cast?.framework;
|
||||
if (!castFramework) return;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setButtonVisibility(tag);
|
||||
});
|
||||
|
||||
observer.observe(tag, { attributes: true, attributeFilter: ["style"] });
|
||||
setButtonVisibility(tag);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [setButtonVisibility]);
|
||||
|
||||
// Hide the button when there are no cast devices available according to CAF
|
||||
useEffect(() => {
|
||||
const w = window as any;
|
||||
const cast = w?.cast;
|
||||
if (!cast?.framework) return;
|
||||
|
||||
const context = cast.framework.CastContext.getInstance();
|
||||
const update = () => {
|
||||
const context = castFramework.CastContext.getInstance();
|
||||
const updateVisibility = () => {
|
||||
const state = context.getCastState();
|
||||
setCastHidden(state === cast.framework.CastState.NO_DEVICES_AVAILABLE);
|
||||
setCastHidden(state === castFramework.CastState.NO_DEVICES_AVAILABLE);
|
||||
};
|
||||
const handler = () => update();
|
||||
|
||||
const handler = () => updateVisibility();
|
||||
context.addEventListener(
|
||||
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
||||
castFramework.CastContextEventType.CAST_STATE_CHANGED,
|
||||
handler,
|
||||
);
|
||||
update();
|
||||
updateVisibility();
|
||||
|
||||
return () => {
|
||||
context.removeEventListener(
|
||||
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
||||
castFramework.CastContextEventType.CAST_STATE_CHANGED,
|
||||
handler,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!launcherRef.current || launcherRef.current.children.length > 0) return;
|
||||
|
||||
const launcher = document.createElement("google-cast-launcher");
|
||||
launcherRef.current.appendChild(launcher);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<VideoPlayerButton
|
||||
ref={ref}
|
||||
className={[
|
||||
props.className ?? "",
|
||||
className ?? "",
|
||||
"google-cast-button",
|
||||
"cast-button-container",
|
||||
isCasting ? "casting" : "",
|
||||
hidden || castHidden ? "hidden" : "",
|
||||
castHidden ? "hidden" : "",
|
||||
].join(" ")}
|
||||
icon={Icons.CASTING}
|
||||
onClick={(el) => {
|
||||
const castButton = el.querySelector("google-cast-launcher");
|
||||
if (castButton) (castButton as HTMLDivElement).click();
|
||||
}}
|
||||
>
|
||||
{/* Render a hidden launcher so programmatic click always works */}
|
||||
<google-cast-launcher
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div ref={launcherRef} />
|
||||
</VideoPlayerButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,16 +90,18 @@ export function CastingInternal() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!available || !window.cast || !window.chrome || !window.chrome.cast)
|
||||
return;
|
||||
|
||||
if (!chrome.cast || !chrome.cast.media) {
|
||||
console.error(
|
||||
"Chrome Cast API not fully initialized: chrome.cast.media is undefined",
|
||||
);
|
||||
if (
|
||||
!available ||
|
||||
!window.cast?.framework ||
|
||||
!window.chrome?.cast?.media?.DEFAULT_MEDIA_RECEIVER_APP_ID ||
|
||||
!window.chrome.cast.AutoJoinPolicy
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newPlayer: cast.framework.RemotePlayer | null = null;
|
||||
let newController: cast.framework.RemotePlayerController | null = null;
|
||||
|
||||
try {
|
||||
const ins = cast.framework.CastContext.getInstance();
|
||||
setInstance(ins);
|
||||
|
|
@ -110,37 +112,33 @@ export function CastingInternal() {
|
|||
ins.setOptions({
|
||||
receiverApplicationId: receiverAppId,
|
||||
autoJoinPolicy,
|
||||
androidReceiverCompatible: false,
|
||||
resumeSavedSession: false,
|
||||
});
|
||||
|
||||
const newPlayer = new cast.framework.RemotePlayer();
|
||||
newPlayer = new cast.framework.RemotePlayer();
|
||||
newController = new cast.framework.RemotePlayerController(newPlayer);
|
||||
setPlayer(newPlayer);
|
||||
const newControlller = new cast.framework.RemotePlayerController(
|
||||
newPlayer,
|
||||
);
|
||||
setController(newControlller);
|
||||
setController(newController);
|
||||
|
||||
newControlller.addEventListener(
|
||||
newController.addEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error initializing Chromecast:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
newControlller.removeEventListener(
|
||||
return () => {
|
||||
if (newController) {
|
||||
newController.removeEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged,
|
||||
);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error initializing Chromecast:", error);
|
||||
}
|
||||
}, [
|
||||
available,
|
||||
setPlayer,
|
||||
setController,
|
||||
setInstance,
|
||||
setIsCasting,
|
||||
connectionChanged,
|
||||
]);
|
||||
}
|
||||
};
|
||||
}, [available, setPlayer, setController, setInstance, connectionChanged]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/// <reference types="chromecast-caf-sender"/>
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
|
@ -8,7 +8,17 @@ export function useChromecastAvailable() {
|
|||
const [available, setAvailable] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
isChromecastAvailable((bool) => setAvailable(bool));
|
||||
let isMounted = true;
|
||||
|
||||
isChromecastAvailable((bool) => {
|
||||
if (isMounted) {
|
||||
setAvailable(bool);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return available;
|
||||
|
|
|
|||
|
|
@ -85,15 +85,11 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||
<Player.SubtitleView controlsShown={showTargets} />
|
||||
|
||||
{status === playerStatus.PLAYING ? (
|
||||
<>
|
||||
<Player.CenterControls>
|
||||
<Player.LoadingSpinner />
|
||||
<Player.AutoPlayStart />
|
||||
</Player.CenterControls>
|
||||
<Player.CenterControls>
|
||||
<Player.CastingNotification />
|
||||
</Player.CenterControls>
|
||||
</>
|
||||
<Player.CenterControls>
|
||||
<Player.LoadingSpinner />
|
||||
<Player.AutoPlayStart />
|
||||
<Player.CastingNotification />
|
||||
</Player.CenterControls>
|
||||
) : null}
|
||||
|
||||
<Player.CenterMobileControls
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/// <reference types="chromecast-caf-sender" />
|
||||
|
||||
const CHROMECAST_SENDER_SDK =
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
||||
|
||||
|
|
@ -8,12 +10,14 @@ let _initialized = false;
|
|||
function init(available: boolean) {
|
||||
_available = available;
|
||||
callbacks.forEach((cb) => cb(available));
|
||||
// Clear callbacks after first resolution to avoid leaks/repeated calls
|
||||
callbacks.length = 0;
|
||||
}
|
||||
|
||||
export function isChromecastAvailable(cb: (available: boolean) => void) {
|
||||
if (_available !== null) return cb(_available);
|
||||
if (_available !== null) {
|
||||
setTimeout(() => cb(_available!), 0);
|
||||
return;
|
||||
}
|
||||
callbacks.push(cb);
|
||||
}
|
||||
|
||||
|
|
@ -21,33 +25,33 @@ export function initializeChromecast() {
|
|||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
const w = window as any;
|
||||
// Only set the global callback if not already present
|
||||
if (!w.__onGCastApiAvailable) {
|
||||
w.__onGCastApiAvailable = (isAvailable: boolean) => {
|
||||
if (!(window as any).__onGCastApiAvailable) {
|
||||
(window as any).__onGCastApiAvailable = (isAvailable: boolean) => {
|
||||
try {
|
||||
if (isAvailable && w.cast?.framework) {
|
||||
const context = w.cast.framework.CastContext.getInstance();
|
||||
if (isAvailable && (window as any).cast?.framework) {
|
||||
const context = (
|
||||
window as any
|
||||
).cast.framework.CastContext.getInstance();
|
||||
context.setOptions({
|
||||
receiverApplicationId:
|
||||
w.chrome?.cast?.media?.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: w.cast.framework.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
receiverApplicationId: (window as any).chrome?.cast?.media
|
||||
?.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: (window as any).cast.framework.AutoJoinPolicy
|
||||
.ORIGIN_SCOPED,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Swallow errors; availability will still be reported below
|
||||
} catch (e) {
|
||||
console.warn("Chromecast initialization error:", e);
|
||||
} finally {
|
||||
init(!!isAvailable);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// add script if doesnt exist yet
|
||||
const exists = !!document.getElementById("chromecast-script");
|
||||
if (!exists) {
|
||||
if (!document.getElementById("chromecast-script")) {
|
||||
const script = document.createElement("script");
|
||||
script.setAttribute("src", CHROMECAST_SENDER_SDK);
|
||||
script.setAttribute("id", "chromecast-script");
|
||||
script.src = CHROMECAST_SENDER_SDK;
|
||||
script.id = "chromecast-script";
|
||||
script.onerror = () => console.warn("Failed to load Chromecast SDK");
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue