From 2a3c504c6711b8681794df2a53c860aeaaa5340b Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 12 Sep 2025 23:45:42 +0530 Subject: [PATCH] test --- package-lock.json | 25 ++ package.json | 3 + src/components/player/AndroidVideoPlayer.tsx | 307 +++++++++++++++++- src/components/player/VideoPlayer.tsx | 312 ++++++++++++++++++- 4 files changed, 643 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4afb90..5ff2de7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "nuvio", "version": "0.6.0-beta.6", "dependencies": { + "@adrianso/react-native-device-brightness": "^1.2.7", "@backpackapp-io/react-native-toast": "^0.14.0", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "~14.0.4", @@ -63,11 +64,13 @@ "react-native-paper": "^5.13.1", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", + "react-native-screen-brightness": "^2.0.0-alpha", "react-native-screens": "~4.4.0", "react-native-svg": "15.8.0", "react-native-url-polyfill": "^2.0.0", "react-native-video": "^6.12.0", "react-native-vlc-media-player": "^1.0.87", + "react-native-volume-manager": "^2.0.8", "react-native-web": "~0.19.13", "react-native-wheel-color-picker": "^1.3.1" }, @@ -94,6 +97,12 @@ } } }, + "node_modules/@adrianso/react-native-device-brightness": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@adrianso/react-native-device-brightness/-/react-native-device-brightness-1.2.7.tgz", + "integrity": "sha512-q3OTFGohAh04R8cBka7/eNXaeSD8bAZocMsifLIR3oDF5N9cNH2UVEjzJaFXW8DHFEsODpljpT+eyGWIhKlkow==", + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -12329,6 +12338,12 @@ "react-native": "*" } }, + "node_modules/react-native-screen-brightness": { + "version": "2.0.0-alpha", + "resolved": "https://registry.npmjs.org/react-native-screen-brightness/-/react-native-screen-brightness-2.0.0-alpha.tgz", + "integrity": "sha512-NdJPptcWiFwG9aypm0awYv9d/aoZSJetXPFkx6w/UuNX9SRr5YTKXMSND/2lwuSOw6uGU9mQ0f4dBuGZSNgzow==", + "license": "MIT" + }, "node_modules/react-native-screens": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz", @@ -12608,6 +12623,16 @@ "react-native-vector-icons": "^9.2.0" } }, + "node_modules/react-native-volume-manager": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/react-native-volume-manager/-/react-native-volume-manager-2.0.8.tgz", + "integrity": "sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.19.13", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", diff --git a/package.json b/package.json index 30ba791..0709452 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "ios": "expo run:ios" }, "dependencies": { + "@adrianso/react-native-device-brightness": "^1.2.7", "@backpackapp-io/react-native-toast": "^0.14.0", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "~14.0.4", @@ -63,11 +64,13 @@ "react-native-paper": "^5.13.1", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", + "react-native-screen-brightness": "^2.0.0-alpha", "react-native-screens": "~4.4.0", "react-native-svg": "15.8.0", "react-native-url-polyfill": "^2.0.0", "react-native-video": "^6.12.0", "react-native-vlc-media-player": "^1.0.87", + "react-native-volume-manager": "^2.0.8", "react-native-web": "~0.19.13", "react-native-wheel-color-picker": "^1.3.1" }, diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 165cd58..d528145 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -4,7 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; -import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { PinchGestureHandler, PanGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import RNImmersiveMode from 'react-native-immersive-mode'; import * as ScreenOrientation from 'expo-screen-orientation'; import { storageService } from '../../services/storageService'; @@ -38,6 +38,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles'; import { SourcesModal } from './modals/SourcesModal'; import { stremioService } from '../../services/stremioService'; import axios from 'axios'; +import DeviceBrightness from '@adrianso/react-native-device-brightness'; +import { VolumeManager } from 'react-native-volume-manager'; // Map VLC resize modes to react-native-video resize modes const getVideoResizeMode = (resizeMode: ResizeModeType) => { @@ -214,6 +216,18 @@ const AndroidVideoPlayer: React.FC = () => { const [errorDetails, setErrorDetails] = useState(''); const errorTimeoutRef = useRef(null); + // Volume and brightness controls + const [volume, setVolume] = useState(1.0); + const [brightness, setBrightness] = useState(1.0); + const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); + const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); + const volumeOverlayOpacity = useRef(new Animated.Value(0)).current; + const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current; + const volumeOverlayTimeout = useRef(null); + const brightnessOverlayTimeout = useRef(null); + const lastVolumeChange = useRef(0); + const lastBrightnessChange = useRef(0); + // iOS startup timing diagnostics const loadStartAtRef = useRef(null); const firstFrameAtRef = useRef(null); @@ -357,6 +371,114 @@ const AndroidVideoPlayer: React.FC = () => { } }; + // Volume gesture handler (right side of screen) + const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + const screenHeight = screenDimensions.height; + const sensitivity = 0.003; // Adjust sensitivity + + if (state === State.ACTIVE) { + const deltaY = -translationY; // Invert for natural feel (up = increase) + const volumeChange = deltaY * sensitivity; + const newVolume = Math.max(0, Math.min(1, volume + volumeChange)); + + if (Math.abs(newVolume - volume) > 0.01) { // Only update if significant change + setVolume(newVolume); + lastVolumeChange.current = Date.now(); + + // Set device volume using VolumeManager + try { + await VolumeManager.setVolume(newVolume); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Device volume set to: ${newVolume}`); + } + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error setting device volume:', error); + } + + // Show overlay + if (!showVolumeOverlay) { + setShowVolumeOverlay(true); + Animated.timing(volumeOverlayOpacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }).start(); + } + + // Clear existing timeout + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + + // Hide overlay after 2 seconds + volumeOverlayTimeout.current = setTimeout(() => { + Animated.timing(volumeOverlayOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => { + setShowVolumeOverlay(false); + }); + }, 2000); + } + } + }; + + // Brightness gesture handler (left side of screen) + const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + const screenHeight = screenDimensions.height; + const sensitivity = 0.003; // Adjust sensitivity + + if (state === State.ACTIVE) { + const deltaY = -translationY; // Invert for natural feel (up = increase) + const brightnessChange = deltaY * sensitivity; + const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange)); + + if (Math.abs(newBrightness - brightness) > 0.01) { // Only update if significant change + setBrightness(newBrightness); + lastBrightnessChange.current = Date.now(); + + // Set device brightness using DeviceBrightness + try { + await DeviceBrightness.setBrightnessLevel(newBrightness); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Device brightness set to: ${newBrightness}`); + } + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error); + } + + // Show overlay + if (!showBrightnessOverlay) { + setShowBrightnessOverlay(true); + Animated.timing(brightnessOverlayOpacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }).start(); + } + + // Clear existing timeout + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } + + // Hide overlay after 2 seconds + brightnessOverlayTimeout.current = setTimeout(() => { + Animated.timing(brightnessOverlayOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => { + setShowBrightnessOverlay(false); + }); + }, 2000); + } + } + }; + const resetZoom = () => { const targetZoom = is16by9Content ? 1.1 : 1; setZoomScale(targetZoom); @@ -385,10 +507,31 @@ const AndroidVideoPlayer: React.FC = () => { const subscription = Dimensions.addEventListener('change', ({ screen }) => { setScreenDimensions(screen); }); - const initializePlayer = () => { + const initializePlayer = async () => { StatusBar.setHidden(true, 'none'); enableImmersiveMode(); startOpeningAnimation(); + + // Initialize current volume and brightness levels + try { + const currentVolume = await VolumeManager.getVolume(); + setVolume(currentVolume.volume); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Initial volume: ${currentVolume.volume}`); + } + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error getting initial volume:', error); + } + + try { + const currentBrightness = await DeviceBrightness.getBrightnessLevel(); + setBrightness(currentBrightness); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Initial brightness: ${currentBrightness}`); + } + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error getting initial brightness:', error); + } }; initializePlayer(); return () => { @@ -1767,6 +1910,12 @@ const AndroidVideoPlayer: React.FC = () => { if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current); } + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } }; }, []); @@ -2139,6 +2288,40 @@ const AndroidVideoPlayer: React.FC = () => { onPress={toggleControls} activeOpacity={1} > + {/* Left side brightness gesture handler */} + + + + + {/* Right side volume gesture handler */} + + + + { selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined} selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)} rate={1.0} - volume={1.0} + volume={volume} muted={false} repeat={false} playInBackground={false} @@ -2634,6 +2817,124 @@ const AndroidVideoPlayer: React.FC = () => { controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 120 : 100} /> + {/* Volume Overlay */} + {showVolumeOverlay && ( + + + + + + + + {Math.round(volume * 100)}% + + + + )} + + {/* Brightness Overlay */} + {showBrightnessOverlay && ( + + + + + + + + {Math.round(brightness * 100)}% + + + + )} + {/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */} diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 80413cc..ca06932 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -4,7 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { VLCPlayer } from 'react-native-vlc-media-player'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList, RootStackNavigationProp } from '../../navigation/AppNavigator'; -import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { PinchGestureHandler, PanGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import RNImmersiveMode from 'react-native-immersive-mode'; import * as ScreenOrientation from 'expo-screen-orientation'; import { storageService } from '../../services/storageService'; @@ -40,6 +40,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles'; import { SourcesModal } from './modals/SourcesModal'; import axios from 'axios'; import { stremioService } from '../../services/stremioService'; +import DeviceBrightness from '@adrianso/react-native-device-brightness'; +import { VolumeManager } from 'react-native-volume-manager'; const VideoPlayer: React.FC = () => { const insets = useSafeAreaInsets(); @@ -236,6 +238,18 @@ const VideoPlayer: React.FC = () => { const castDetailsOpacity = useRef(new Animated.Value(0)).current; const castDetailsScale = useRef(new Animated.Value(0.95)).current; + // Volume and brightness controls + const [volume, setVolume] = useState(100); // VLC uses 0-100 range + const [brightness, setBrightness] = useState(1.0); + const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); + const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); + const volumeOverlayOpacity = useRef(new Animated.Value(0)).current; + const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current; + const volumeOverlayTimeout = useRef(null); + const brightnessOverlayTimeout = useRef(null); + const lastVolumeChange = useRef(0); + const lastBrightnessChange = useRef(0); + // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ @@ -363,6 +377,123 @@ const VideoPlayer: React.FC = () => { } }; + // Volume gesture handler (right side of screen) + const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + const screenHeight = screenDimensions.height; + const sensitivity = 0.3; // Adjust sensitivity for VLC (0-100 range) + + if (state === State.ACTIVE) { + const deltaY = -translationY; // Invert for natural feel (up = increase) + const volumeChange = deltaY * sensitivity; + const newVolume = Math.max(0, Math.min(100, volume + volumeChange)); + + if (Math.abs(newVolume - volume) > 1) { // Only update if significant change + setVolume(newVolume); + lastVolumeChange.current = Date.now(); + + // Set device volume using VolumeManager + try { + await VolumeManager.setVolume(newVolume / 100); // Convert to 0-1 range + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Device volume set to: ${newVolume / 100}`); + } + } catch (error) { + logger.warn('[VideoPlayer] Error setting device volume:', error); + } + + // Set VLC volume as well + if (vlcRef.current) { + try { + vlcRef.current.setVolume(newVolume); + } catch (error) { + logger.warn('[VideoPlayer] Error setting VLC volume:', error); + } + } + + // Show overlay + if (!showVolumeOverlay) { + setShowVolumeOverlay(true); + Animated.timing(volumeOverlayOpacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }).start(); + } + + // Clear existing timeout + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + + // Hide overlay after 2 seconds + volumeOverlayTimeout.current = setTimeout(() => { + Animated.timing(volumeOverlayOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => { + setShowVolumeOverlay(false); + }); + }, 2000); + } + } + }; + + // Brightness gesture handler (left side of screen) + const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + const screenHeight = screenDimensions.height; + const sensitivity = 0.003; // Adjust sensitivity + + if (state === State.ACTIVE) { + const deltaY = -translationY; // Invert for natural feel (up = increase) + const brightnessChange = deltaY * sensitivity; + const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange)); + + if (Math.abs(newBrightness - brightness) > 0.01) { // Only update if significant change + setBrightness(newBrightness); + lastBrightnessChange.current = Date.now(); + + // Set device brightness using DeviceBrightness + try { + await DeviceBrightness.setBrightnessLevel(newBrightness); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Device brightness set to: ${newBrightness}`); + } + } catch (error) { + logger.warn('[VideoPlayer] Error setting device brightness:', error); + } + + // Show overlay + if (!showBrightnessOverlay) { + setShowBrightnessOverlay(true); + Animated.timing(brightnessOverlayOpacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }).start(); + } + + // Clear existing timeout + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } + + // Hide overlay after 2 seconds + brightnessOverlayTimeout.current = setTimeout(() => { + Animated.timing(brightnessOverlayOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => { + setShowBrightnessOverlay(false); + }); + }, 2000); + } + } + }; + useEffect(() => { if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) { const styles = calculateVideoStyles( @@ -405,6 +536,27 @@ const VideoPlayer: React.FC = () => { StatusBar.setHidden(true, 'none'); enableImmersiveMode(); startOpeningAnimation(); + + // Initialize current volume and brightness levels + try { + const currentVolume = await VolumeManager.getVolume(); + setVolume(currentVolume.volume * 100); // Convert to 0-100 range for VLC + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Initial volume: ${currentVolume.volume * 100}`); + } + } catch (error) { + logger.warn('[VideoPlayer] Error getting initial volume:', error); + } + + try { + const currentBrightness = await DeviceBrightness.getBrightnessLevel(); + setBrightness(currentBrightness); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Initial brightness: ${currentBrightness}`); + } + } catch (error) { + logger.warn('[VideoPlayer] Error getting initial brightness:', error); + } }; initializePlayer(); return () => { @@ -1556,6 +1708,12 @@ const VideoPlayer: React.FC = () => { if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current); } + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } }; }, []); @@ -1942,6 +2100,40 @@ const VideoPlayer: React.FC = () => { onPress={toggleControls} activeOpacity={1} > + {/* Left side brightness gesture handler */} + + + + + {/* Right side volume gesture handler */} + + + + { controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 126 : 106} /> + {/* Volume Overlay */} + {showVolumeOverlay && ( + + + + + + + + {volume}% + + + + )} + + {/* Brightness Overlay */} + {showBrightnessOverlay && ( + + + + + + + + {Math.round(brightness * 100)}% + + + + )} + {/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}