fix(player): guard cast SDK in Android WebView

Fixes #147
This commit is contained in:
kuekhaoyang
2026-04-09 22:41:15 +08:00
parent fb4f50ee7f
commit 15f6e434c8
@@ -13,7 +13,7 @@ declare global {
interface Window {
chrome: any;
cast: any;
__onGCastApiAvailable: (isAvailable: boolean) => void;
__onGCastApiAvailable?: (isAvailable: boolean) => void;
}
}
@@ -24,69 +24,37 @@ export function useCastControls({
setIsCasting
}: UseCastControlsProps) {
const castContextRef = useRef<any>(null);
const loadMediaRef = useRef<() => void>(() => {});
useEffect(() => {
// Function to initialize Cast
const initializeCastApi = () => {
if (!window.cast || !window.cast.framework) return;
const isCastSdkReady = useCallback(() => {
if (typeof window === 'undefined') return false;
const castContext = window.cast.framework.CastContext.getInstance();
castContextRef.current = castContext;
castContext.setOptions({
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
});
// SDK loaded — show cast button immediately.
// requestSession() will re-scan and show Chrome's native device picker.
setIsCastAvailable(true);
// Monitor session state
castContext.addEventListener(
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(event: any) => {
const sessionState = event.sessionState;
const isSessionActive = sessionState === window.cast.framework.SessionState.SESSION_STARTED ||
sessionState === window.cast.framework.SessionState.SESSION_RESUMED;
setIsCasting(isSessionActive);
if (isSessionActive && videoRef.current) {
videoRef.current.pause();
loadMedia();
}
}
);
};
// If API is already loaded
if (window.cast && window.cast.framework) {
initializeCastApi();
} else {
// Wait for API to be available
window.__onGCastApiAvailable = (isAvailable: boolean) => {
if (isAvailable) {
initializeCastApi();
}
};
}
}, [setIsCastAvailable, setIsCasting, videoRef]);
return Boolean(
window.cast?.framework?.CastContext?.getInstance &&
window.cast?.framework?.CastContextEventType?.SESSION_STATE_CHANGED &&
window.cast?.framework?.SessionState &&
window.chrome?.cast?.media?.DEFAULT_MEDIA_RECEIVER_APP_ID &&
window.chrome?.cast?.media?.MediaInfo &&
window.chrome?.cast?.media?.LoadRequest &&
window.chrome?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED
);
}, []);
const loadMedia = useCallback(() => {
if (!castContextRef.current || !src) return;
const castMedia = window.chrome?.cast?.media;
if (!castContextRef.current || !src || !castMedia?.MediaInfo || !castMedia?.LoadRequest) return;
const castContext = castContextRef.current;
const session = castContext.getCurrentSession();
if (!session) return;
const mediaInfo = new window.chrome.cast.media.MediaInfo(src, 'video/mp4');
const mediaInfo = new castMedia.MediaInfo(src, 'video/mp4');
// Handle HLS specifically if possible, though DEFAULT_MEDIA_RECEIVER supports it
if (src.includes('.m3u8')) {
mediaInfo.contentType = 'application/x-mpegurl';
}
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
const request = new castMedia.LoadRequest(mediaInfo);
// Sync current time
if (videoRef.current) {
@@ -99,11 +67,108 @@ export function useCastControls({
);
}, [src, videoRef]);
const showCastMenu = useCallback(() => {
if (window.cast && window.cast.framework) {
window.cast.framework.CastContext.getInstance().requestSession();
useEffect(() => {
loadMediaRef.current = loadMedia;
}, [loadMedia]);
useEffect(() => {
let sessionStateListener: ((event: any) => void) | null = null;
let onGCastApiAvailable: ((isAvailable: boolean) => void) | null = null;
const markCastUnavailable = () => {
castContextRef.current = null;
setIsCastAvailable(false);
setIsCasting(false);
};
// Function to initialize Cast
const initializeCastApi = () => {
if (!isCastSdkReady()) {
markCastUnavailable();
return;
}
try {
const castContext = window.cast.framework.CastContext.getInstance();
castContextRef.current = castContext;
castContext.setOptions({
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
});
// SDK loaded — show cast button immediately.
// requestSession() will re-scan and show Chrome's native device picker.
setIsCastAvailable(true);
// Monitor session state
sessionStateListener = (event: any) => {
const sessionState = event.sessionState;
const isSessionActive = sessionState === window.cast.framework.SessionState.SESSION_STARTED ||
sessionState === window.cast.framework.SessionState.SESSION_RESUMED;
setIsCasting(isSessionActive);
if (isSessionActive && videoRef.current) {
videoRef.current.pause();
loadMediaRef.current();
}
};
castContext.addEventListener(
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
sessionStateListener
);
} catch (error) {
console.warn('Cast SDK is not usable in this browser context.', error);
markCastUnavailable();
}
};
// If API is already loaded
if (isCastSdkReady()) {
initializeCastApi();
} else {
// Wait for API to be available
onGCastApiAvailable = (isAvailable: boolean) => {
if (isAvailable) {
initializeCastApi();
} else {
markCastUnavailable();
}
};
window.__onGCastApiAvailable = onGCastApiAvailable;
}
}, []);
return () => {
if (onGCastApiAvailable && window.__onGCastApiAvailable === onGCastApiAvailable) {
delete window.__onGCastApiAvailable;
}
const castContext = castContextRef.current;
if (
sessionStateListener &&
castContext?.removeEventListener &&
window.cast?.framework?.CastContextEventType?.SESSION_STATE_CHANGED
) {
castContext.removeEventListener(
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
sessionStateListener
);
}
};
}, [isCastSdkReady, setIsCastAvailable, setIsCasting, videoRef]);
const showCastMenu = useCallback(() => {
if (!isCastSdkReady()) return;
try {
window.cast.framework.CastContext.getInstance().requestSession();
} catch (error) {
console.warn('Cast session request failed.', error);
setIsCastAvailable(false);
}
}, [isCastSdkReady, setIsCastAvailable]);
const castActions = useMemo(() => ({
showCastMenu