mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-30 18:48:30 +01:00
fix: player getting stuck on timer and exit
Fixed a race condition where the upnext countdown started and a user cancelled/stop the current playback that they would exit the player but the timer would still be running and then start playing the next episode and you wouldn't be able to press back or exit out of it Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
@@ -63,6 +63,7 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
|
||||
const typography = useScaledTVTypography();
|
||||
const { t } = useTranslation();
|
||||
const progress = useSharedValue(0);
|
||||
const cancelled = useSharedValue(false);
|
||||
const onFinishRef = useRef(onFinish);
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({
|
||||
@@ -120,13 +121,15 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
cancelled.value = false;
|
||||
|
||||
// Resume from current position
|
||||
const remainingDuration = (1 - progress.value) * 8000;
|
||||
progress.value = withTiming(
|
||||
1,
|
||||
{ duration: remainingDuration, easing: Easing.linear },
|
||||
(finished) => {
|
||||
if (finished) {
|
||||
if (finished && !cancelled.value) {
|
||||
runOnJS(onFinishRef.current)();
|
||||
}
|
||||
},
|
||||
@@ -134,9 +137,10 @@ export const TVNextEpisodeCountdown: FC<TVNextEpisodeCountdownProps> = ({
|
||||
|
||||
// Cancel animation on unmount to prevent onFinish from firing after exit
|
||||
return () => {
|
||||
cancelled.value = true;
|
||||
cancelAnimation(progress);
|
||||
};
|
||||
}, [show, isPlaying, progress]);
|
||||
}, [show, isPlaying, progress, cancelled]);
|
||||
|
||||
const progressStyle = useAnimatedStyle(() => ({
|
||||
width: `${progress.value * 100}%`,
|
||||
|
||||
@@ -517,6 +517,8 @@ export const Controls: FC<Props> = ({
|
||||
const goToNextItemRef = useRef<(opts?: { isAutoPlay?: boolean }) => void>(
|
||||
() => {},
|
||||
);
|
||||
const exitingRef = useRef(false);
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
|
||||
const updateSeekBubbleTime = useCallback((ms: number) => {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
@@ -960,6 +962,16 @@ export const Controls: FC<Props> = ({
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleWillExit = useCallback(() => {
|
||||
exitingRef.current = true;
|
||||
setIsExiting(true);
|
||||
}, []);
|
||||
|
||||
const handleCancelExit = useCallback(() => {
|
||||
exitingRef.current = false;
|
||||
setIsExiting(false);
|
||||
}, []);
|
||||
|
||||
const { isSliding: isRemoteSliding } = useRemoteControl({
|
||||
showControls: showControls,
|
||||
toggleControls,
|
||||
@@ -976,6 +988,8 @@ export const Controls: FC<Props> = ({
|
||||
onVerticalDpad: handleVerticalDpad,
|
||||
onHideControls: hideControls,
|
||||
onBack: handleBack,
|
||||
onWillExit: handleWillExit,
|
||||
onCancelExit: handleCancelExit,
|
||||
videoTitle: item?.Name ?? undefined,
|
||||
});
|
||||
|
||||
@@ -1061,6 +1075,7 @@ export const Controls: FC<Props> = ({
|
||||
goToNextItemRef.current = goToNextItem;
|
||||
|
||||
const handleAutoPlayFinish = useCallback(() => {
|
||||
if (exitingRef.current) return;
|
||||
goToNextItem({ isAutoPlay: true });
|
||||
}, [goToNextItem]);
|
||||
|
||||
@@ -1135,7 +1150,7 @@ export const Controls: FC<Props> = ({
|
||||
nextItem={nextItem}
|
||||
api={api}
|
||||
show={isCountdownActive}
|
||||
isPlaying={isPlaying}
|
||||
isPlaying={isPlaying && !isExiting}
|
||||
onFinish={handleAutoPlayFinish}
|
||||
onPlayNext={handleNextItemButton}
|
||||
controlsVisible={showControls}
|
||||
|
||||
@@ -35,6 +35,10 @@ interface UseRemoteControlProps {
|
||||
onLongSeekStop?: () => void;
|
||||
/** Callback when up/down D-pad pressed (to show controls with play button focused) */
|
||||
onVerticalDpad?: () => void;
|
||||
/** Called before the exit confirmation Alert is shown (e.g., to pause countdown) */
|
||||
onWillExit?: () => void;
|
||||
/** Called when the user cancels the exit confirmation Alert */
|
||||
onCancelExit?: () => void;
|
||||
// Legacy props - kept for backwards compatibility with mobile Controls.tsx
|
||||
// These are ignored in the simplified implementation
|
||||
progress?: SharedValue<number>;
|
||||
@@ -72,6 +76,8 @@ export function useRemoteControl({
|
||||
onLongSeekRightStart,
|
||||
onLongSeekStop,
|
||||
onVerticalDpad,
|
||||
onWillExit,
|
||||
onCancelExit,
|
||||
}: UseRemoteControlProps) {
|
||||
// Keep these for backward compatibility with the component
|
||||
const remoteScrubProgress = useSharedValue<number | null>(null);
|
||||
@@ -85,13 +91,24 @@ export function useRemoteControl({
|
||||
const onHideControlsRef = useRef(onHideControls);
|
||||
const onBackRef = useRef(onBack);
|
||||
const videoTitleRef = useRef(videoTitle);
|
||||
const onWillExitRef = useRef(onWillExit);
|
||||
const onCancelExitRef = useRef(onCancelExit);
|
||||
|
||||
useEffect(() => {
|
||||
showControlsRef.current = showControls;
|
||||
onHideControlsRef.current = onHideControls;
|
||||
onBackRef.current = onBack;
|
||||
videoTitleRef.current = videoTitle;
|
||||
}, [showControls, onHideControls, onBack, videoTitle]);
|
||||
onWillExitRef.current = onWillExit;
|
||||
onCancelExitRef.current = onCancelExit;
|
||||
}, [
|
||||
showControls,
|
||||
onHideControls,
|
||||
onBack,
|
||||
videoTitle,
|
||||
onWillExit,
|
||||
onCancelExit,
|
||||
]);
|
||||
|
||||
// BackHandler owns player exit: Android TV sends hardware back here, and
|
||||
// react-native-tvos maps the Apple TV menu button to the same API.
|
||||
@@ -102,6 +119,9 @@ export function useRemoteControl({
|
||||
return true;
|
||||
}
|
||||
if (onBackRef.current) {
|
||||
// Signal Controls that exit is imminent (pauses countdown, sets guard)
|
||||
onWillExitRef.current?.();
|
||||
|
||||
// Controls are hidden, so confirm before leaving playback.
|
||||
Alert.alert(
|
||||
"Stop Playback",
|
||||
@@ -109,7 +129,11 @@ export function useRemoteControl({
|
||||
? `Stop playing "${videoTitleRef.current}"?`
|
||||
: "Are you sure you want to stop playback?",
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel",
|
||||
onPress: () => onCancelExitRef.current?.(),
|
||||
},
|
||||
{ text: "Stop", style: "destructive", onPress: onBackRef.current },
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user