Fix: Improves Chromecast casting experience

Fixes several issues and enhances the Chromecast casting experience:

- Prevents errors when loading media by slimming down the customData payload to avoid exceeding message size limits.
- Improves logic for selecting custom data from media status.
- Fixes an issue with subtitle track selection.
- Recommends stereo audio tracks for better Chromecast compatibility.
- Improves volume control and mute synchronization between the app and the Chromecast device.
- Adds error handling for `loadMedia` in `PlayButton`.
- Fixes image caching issue for season posters in mini player.
- Implements cleanup for scroll retry timeout in episode list.
- Ensures segment skipping functions are asynchronous.
- Resets `hasReportedStartRef` after stopping casting.
- Prevents seeking past the end of Outro segments.
- Reports playback progress more accurately by also taking player state changes into account.
This commit is contained in:
Uruk
2026-02-09 21:43:33 +01:00
parent 7c81c0ff33
commit 2c27186e22
11 changed files with 147 additions and 73 deletions

View File

@@ -32,6 +32,7 @@ export const ChromecastConnectionMenu: React.FC<
// Volume state - use refs to avoid triggering re-renders during sliding
const [displayVolume, setDisplayVolume] = useState(50);
const [isMuted, setIsMuted] = useState(false);
const isMutedRef = useRef(false);
const volumeValue = useSharedValue(50);
const minimumValue = useSharedValue(0);
const maximumValue = useSharedValue(100);
@@ -55,6 +56,7 @@ export const ChromecastConnectionMenu: React.FC<
lastSetVolume.current = percent;
}
const muted = await castSession.isMute();
isMutedRef.current = muted;
setIsMuted(muted);
} catch {
// Ignore errors
@@ -78,7 +80,8 @@ export const ChromecastConnectionMenu: React.FC<
}
}
const muted = await castSession.isMute();
if (muted !== isMuted) {
if (muted !== isMutedRef.current) {
isMutedRef.current = muted;
setIsMuted(muted);
}
} catch {
@@ -87,7 +90,7 @@ export const ChromecastConnectionMenu: React.FC<
}, 1000); // Poll less frequently
return () => clearInterval(interval);
}, [visible, castSession, volumeValue, isMuted]);
}, [visible, castSession, volumeValue]);
// Volume change during sliding - update display only, don't call API
const handleVolumeChange = useCallback((value: number) => {
@@ -120,6 +123,7 @@ export const ChromecastConnectionMenu: React.FC<
try {
const newMute = !isMuted;
await castSession.setMute(newMute);
isMutedRef.current = newMute;
setIsMuted(newMute);
} catch (error) {
console.error("[Connection Menu] Mute error:", error);
@@ -259,6 +263,7 @@ export const ChromecastConnectionMenu: React.FC<
volumeValue.value = value;
handleVolumeChange(value);
if (isMuted) {
isMutedRef.current = false;
setIsMuted(false);
try {
await castSession?.setMute(false);
@@ -267,6 +272,7 @@ export const ChromecastConnectionMenu: React.FC<
"[ChromecastConnectionMenu] Failed to unmute:",
error,
);
isMutedRef.current = true;
setIsMuted(true); // Rollback on failure
}
}

View File

@@ -282,10 +282,10 @@ export const ChromecastDeviceSheet: React.FC<ChromecastDeviceSheetProps> = ({
volumeValue.value = value;
handleVolumeChange(value);
// Unmute when adjusting volume
if (isMuted) {
if (isMuted && castSession) {
setIsMuted(false);
try {
await castSession?.setMute(false);
await castSession.setMute(false);
} catch (error) {
console.error("[Volume] Failed to unmute:", error);
setIsMuted(true); // Rollback on failure

View File

@@ -40,6 +40,17 @@ export const ChromecastEpisodeList: React.FC<ChromecastEpisodeListProps> = ({
const scrollRetryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const MAX_SCROLL_RETRIES = 3;
// Cleanup pending retry timeout on unmount
useEffect(() => {
return () => {
if (scrollRetryTimeoutRef.current) {
clearTimeout(scrollRetryTimeoutRef.current);
scrollRetryTimeoutRef.current = null;
}
scrollRetryCountRef.current = 0;
};
}, []);
// Get unique seasons from episodes
const seasons = useMemo(() => {
const seasonSet = new Set<number>();

View File

@@ -105,27 +105,27 @@ export const useChromecastSegments = (
// Skip functions
const skipIntro = useCallback(
(seekFn: (positionMs: number) => Promise<void>) => {
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (segments.intro) {
return seekFn(segments.intro.end * 1000);
await seekFn(segments.intro.end * 1000);
}
},
[segments.intro],
);
const skipCredits = useCallback(
(seekFn: (positionMs: number) => Promise<void>) => {
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (segments.credits) {
return seekFn(segments.credits.end * 1000);
await seekFn(segments.credits.end * 1000);
}
},
[segments.credits],
);
const skipSegment = useCallback(
(seekFn: (positionMs: number) => Promise<void>) => {
async (seekFn: (positionMs: number) => Promise<void>): Promise<void> => {
if (currentSegment?.segment) {
return seekFn(currentSegment.segment.end * 1000);
await seekFn(currentSegment.segment.end * 1000);
}
},
[currentSegment],