Fix: Player orientation now correctly resets when exiting video player

This commit is contained in:
tapframe 2025-12-25 12:35:07 +05:30
parent 0a1e008d5f
commit a61c1e6456
9 changed files with 77 additions and 39 deletions

13
package-lock.json generated
View file

@ -51,7 +51,7 @@
"expo-glass-effect": "~0.1.4", "expo-glass-effect": "~0.1.4",
"expo-haptics": "~15.0.7", "expo-haptics": "~15.0.7",
"expo-intent-launcher": "~13.0.7", "expo-intent-launcher": "~13.0.7",
"expo-libvlc-player": "^2.2.3", "expo-keep-awake": "~15.0.8",
"expo-linear-gradient": "~15.0.7", "expo-linear-gradient": "~15.0.7",
"expo-localization": "~17.0.7", "expo-localization": "~17.0.7",
"expo-navigation-bar": "~5.0.10", "expo-navigation-bar": "~5.0.10",
@ -6512,17 +6512,6 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/expo-libvlc-player": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/expo-libvlc-player/-/expo-libvlc-player-2.2.5.tgz",
"integrity": "sha512-Hl0XiRNK5iwPMDRWYouA7+Xzf804GZ/AMVTU87ktUlQMU5bgTUFgmi8QjlOLGEF5LpVp7LDFfQwsDpXP1ggpag==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-linear-gradient": { "node_modules/expo-linear-gradient": {
"version": "15.0.8", "version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.8.tgz", "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.8.tgz",

View file

@ -51,6 +51,7 @@
"expo-glass-effect": "~0.1.4", "expo-glass-effect": "~0.1.4",
"expo-haptics": "~15.0.7", "expo-haptics": "~15.0.7",
"expo-intent-launcher": "~13.0.7", "expo-intent-launcher": "~13.0.7",
"expo-keep-awake": "~15.0.8",
"expo-linear-gradient": "~15.0.7", "expo-linear-gradient": "~15.0.7",
"expo-localization": "~17.0.7", "expo-localization": "~17.0.7",
"expo-navigation-bar": "~5.0.10", "expo-navigation-bar": "~5.0.10",
@ -103,4 +104,4 @@
"xcode": "^3.0.1" "xcode": "^3.0.1"
}, },
"private": true "private": true
} }

View file

@ -10,13 +10,14 @@ import {
usePlayerState, usePlayerState,
usePlayerModals, usePlayerModals,
useSpeedControl, useSpeedControl,
useOpeningAnimation useOpeningAnimation,
useWatchProgress
} from './hooks'; } from './hooks';
// Android-specific hooks // Android-specific hooks
import { usePlayerSetup } from './android/hooks/usePlayerSetup'; import { usePlayerSetup } from './android/hooks/usePlayerSetup';
import { usePlayerTracks } from './android/hooks/usePlayerTracks'; import { usePlayerTracks } from './android/hooks/usePlayerTracks';
import { useWatchProgress } from './android/hooks/useWatchProgress';
import { usePlayerControls } from './android/hooks/usePlayerControls'; import { usePlayerControls } from './android/hooks/usePlayerControls';
import { useNextEpisode } from './android/hooks/useNextEpisode'; import { useNextEpisode } from './android/hooks/useNextEpisode';

View file

@ -16,6 +16,7 @@ import EpisodesModal from './modals/EpisodesModal';
import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal'; import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal';
import { ErrorModal } from './modals/ErrorModal'; import { ErrorModal } from './modals/ErrorModal';
import CustomSubtitles from './subtitles/CustomSubtitles'; import CustomSubtitles from './subtitles/CustomSubtitles';
import ResumeOverlay from './modals/ResumeOverlay';
import { SpeedActivatedOverlay, PauseOverlay, GestureControls } from './components'; import { SpeedActivatedOverlay, PauseOverlay, GestureControls } from './components';
// Platform-specific components // Platform-specific components
@ -30,7 +31,8 @@ import {
usePlayerTracks, usePlayerTracks,
useCustomSubtitles, useCustomSubtitles,
usePlayerControls, usePlayerControls,
usePlayerSetup usePlayerSetup,
useWatchProgress
} from './hooks'; } from './hooks';
// Platform-specific hooks // Platform-specific hooks
@ -137,6 +139,15 @@ const KSPlayerCore: React.FC = () => {
isMounted isMounted
}); });
const watchProgress = useWatchProgress(
id, type, episodeId,
currentTime,
duration,
paused,
traktAutosync,
controls.seekToTime
);
// Gestures // Gestures
const fadeAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
@ -157,9 +168,6 @@ const KSPlayerCore: React.FC = () => {
const [brightness, setBrightnessState] = useState(0.5); const [brightness, setBrightnessState] = useState(0.5);
const [isSliderDragging, setIsSliderDragging] = useState(false); const [isSliderDragging, setIsSliderDragging] = useState(false);
// Watch Progress State
const [initialPosition, setInitialPosition] = useState<number | null>(routeInitialPosition || null);
// Shared Gesture Hook // Shared Gesture Hook
const gestureControls = usePlayerGestureControls({ const gestureControls = usePlayerGestureControls({
volume: volume, volume: volume,
@ -173,7 +181,8 @@ const KSPlayerCore: React.FC = () => {
setScreenDimensions, setScreenDimensions,
setVolume: setVolumeState, setVolume: setVolumeState,
setBrightness: setBrightnessState, setBrightness: setBrightnessState,
isOpeningAnimationComplete: openingAnim.isOpeningAnimationComplete isOpeningAnimationComplete: openingAnim.isOpeningAnimationComplete,
paused: paused
}); });
// Refs for Logic // Refs for Logic
@ -328,9 +337,10 @@ const KSPlayerCore: React.FC = () => {
openingAnim.completeOpeningAnimation(); openingAnim.completeOpeningAnimation();
// Initial Seek // Initial Seek
if (initialPosition && initialPosition > 0) { const resumeTarget = routeInitialPosition || watchProgress.initialPosition || watchProgress.initialSeekTargetRef?.current;
if (resumeTarget && resumeTarget > 0 && !watchProgress.showResumeOverlay && data.duration > 0) {
setTimeout(() => { setTimeout(() => {
controls.seekToTime(initialPosition); controls.seekToTime(resumeTarget);
}, 500); }, 500);
} }
@ -602,6 +612,23 @@ const KSPlayerCore: React.FC = () => {
speed={speedControl.holdToSpeedValue} speed={speedControl.holdToSpeedValue}
/> />
<ResumeOverlay
showResumeOverlay={watchProgress.showResumeOverlay}
resumePosition={watchProgress.resumePosition}
duration={watchProgress.savedDuration || duration}
title={title}
season={season}
episode={episode}
handleResume={() => {
watchProgress.setShowResumeOverlay(false);
if (watchProgress.resumePosition) controls.seekToTime(watchProgress.resumePosition);
}}
handleStartFromBeginning={() => {
watchProgress.setShowResumeOverlay(false);
controls.seekToTime(0);
}}
/>
{/* Pause Overlay */} {/* Pause Overlay */}
<PauseOverlay <PauseOverlay
visible={paused && !showControls} visible={paused && !showControls}

View file

@ -3,6 +3,7 @@ import { StatusBar, Platform, Dimensions, AppState } from 'react-native';
import RNImmersiveMode from 'react-native-immersive-mode'; import RNImmersiveMode from 'react-native-immersive-mode';
import * as NavigationBar from 'expo-navigation-bar'; import * as NavigationBar from 'expo-navigation-bar';
import * as Brightness from 'expo-brightness'; import * as Brightness from 'expo-brightness';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { logger } from '../../../../utils/logger'; import { logger } from '../../../../utils/logger';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react'; import { useCallback } from 'react';
@ -19,6 +20,19 @@ export const usePlayerSetup = (
const originalSystemBrightnessModeRef = useRef<number | null>(null); const originalSystemBrightnessModeRef = useRef<number | null>(null);
const isAppBackgrounded = useRef(false); const isAppBackgrounded = useRef(false);
// Prevent screen sleep while playing
// Prevent screen sleep while playing
useEffect(() => {
if (!paused) {
activateKeepAwakeAsync();
} else {
deactivateKeepAwake();
}
return () => {
deactivateKeepAwake();
};
}, [paused]);
const enableImmersiveMode = async () => { const enableImmersiveMode = async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
// Standard immersive mode // Standard immersive mode

View file

@ -19,3 +19,4 @@ export { usePlayerSetup } from './usePlayerSetup';
// Content // Content
export { useNextEpisode } from './useNextEpisode'; export { useNextEpisode } from './useNextEpisode';
export { useWatchProgress } from './useWatchProgress';

View file

@ -46,10 +46,7 @@ export const usePlayerControls = (config: PlayerControlsConfig) => {
isSeeking.current = true; isSeeking.current = true;
// iOS optimization: pause while seeking for smoother experience // iOS optimization: pause while seeking for smoother experience
if (Platform.OS === 'ios') {
iosWasPausedDuringSeekRef.current = paused;
if (!paused) setPaused(true);
}
// Actually perform the seek // Actually perform the seek
playerRef.current.seek(timeInSeconds); playerRef.current.seek(timeInSeconds);
@ -59,10 +56,7 @@ export const usePlayerControls = (config: PlayerControlsConfig) => {
if (isMounted.current && isSeeking.current) { if (isMounted.current && isSeeking.current) {
isSeeking.current = false; isSeeking.current = false;
// Resume if it was playing (iOS specific) // Resume if it was playing (iOS specific)
if (Platform.OS === 'ios' && iosWasPausedDuringSeekRef.current === false) {
setPaused(false);
iosWasPausedDuringSeekRef.current = null;
}
} }
}, 500); }, 500);
} }

View file

@ -1,12 +1,8 @@
/**
* Shared Player Setup Hook
* Used by both Android (VLC) and iOS (KSPlayer) players
* Handles StatusBar, orientation, brightness, and app state
*/
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { StatusBar, Dimensions, AppState, InteractionManager, Platform } from 'react-native'; import { StatusBar, Dimensions, AppState, InteractionManager, Platform } from 'react-native';
import * as Brightness from 'expo-brightness'; import * as Brightness from 'expo-brightness';
import * as ScreenOrientation from 'expo-screen-orientation'; import * as ScreenOrientation from 'expo-screen-orientation';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
@ -15,6 +11,7 @@ interface PlayerSetupConfig {
setVolume: (vol: number) => void; setVolume: (vol: number) => void;
setBrightness: (bri: number) => void; setBrightness: (bri: number) => void;
isOpeningAnimationComplete: boolean; isOpeningAnimationComplete: boolean;
paused: boolean;
} }
export const usePlayerSetup = (config: PlayerSetupConfig) => { export const usePlayerSetup = (config: PlayerSetupConfig) => {
@ -22,9 +19,23 @@ export const usePlayerSetup = (config: PlayerSetupConfig) => {
setScreenDimensions, setScreenDimensions,
setVolume, setVolume,
setBrightness, setBrightness,
isOpeningAnimationComplete isOpeningAnimationComplete,
paused
} = config; } = config;
// Prevent screen sleep while playing
// Prevent screen sleep while playing
useEffect(() => {
if (!paused) {
activateKeepAwakeAsync();
} else {
deactivateKeepAwake();
}
return () => {
deactivateKeepAwake();
};
}, [paused]);
const isAppBackgrounded = useRef(false); const isAppBackgrounded = useRef(false);
const enableImmersiveMode = () => { const enableImmersiveMode = () => {

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { storageService } from '../../../../services/storageService'; import { storageService } from '../../../services/storageService';
import { logger } from '../../../../utils/logger'; import { logger } from '../../../utils/logger';
import { useSettings } from '../../../../hooks/useSettings'; import { useSettings } from '../../../hooks/useSettings';
export const useWatchProgress = ( export const useWatchProgress = (
id: string | undefined, id: string | undefined,