imporved behaviour of videoplayer and added a nice loading screen

This commit is contained in:
tapframe 2025-07-07 13:36:32 +05:30
parent ab7134bd3a
commit 379bcc7507
8 changed files with 257 additions and 32 deletions

View file

@ -95,9 +95,6 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
signingConfigs {
debug {
@ -129,16 +126,6 @@ android {
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
// --- ABI SPLITS: generate separate APKs/AABs for each CPU architecture (plus a universal build) ---
splits {
abi {
enable true // turn on ABI splits
reset() // start from a clean slate
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk true // also generate a universal APK that contains all ABIs
}
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text } from 'react-native';
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet } from 'react-native';
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -10,8 +10,11 @@ import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings';
import {
DEFAULT_SUBTITLE_SIZE,
@ -62,7 +65,8 @@ const AndroidVideoPlayer: React.FC = () => {
type,
episodeId,
imdbId,
availableStreams: passedAvailableStreams
availableStreams: passedAvailableStreams,
backdrop
} = route.params;
// Initialize Trakt autosync
@ -157,7 +161,25 @@ const AndroidVideoPlayer: React.FC = () => {
const isMounted = useRef(true);
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
// Offset in seconds to avoid seeking to the exact end, which fires onEnd and resets.
// Get metadata to access logo (only if we have a valid id)
const shouldLoadMetadata = Boolean(id && type);
const metadataResult = useMetadata({
id: id || 'placeholder',
type: type || 'movie'
});
const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false };
const { settings } = useSettings();
// Logo animation values
const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
const logoOpacityAnim = useRef(new Animated.Value(0)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
// Check if we have a logo to show
const hasLogo = metadata && metadata.logo && !metadataLoading;
// Small offset (in seconds) used to avoid seeking to the *exact* end of the
// file which triggers the `onEnd` callback and causes playback to restart.
const END_EPSILON = 0.3;
const hideControls = () => {
@ -241,7 +263,51 @@ const AndroidVideoPlayer: React.FC = () => {
}, []);
const startOpeningAnimation = () => {
// Animation logic here
// Logo entrance animation
Animated.parallel([
Animated.timing(logoOpacityAnim, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}),
Animated.spring(logoScaleAnim, {
toValue: 1,
tension: 50,
friction: 8,
useNativeDriver: true,
}),
]).start();
// Continuous pulse animation for the logo
const createPulseAnimation = () => {
return Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.05,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]);
};
const loopPulse = () => {
createPulseAnimation().start(() => {
if (!isOpeningAnimationComplete) {
loopPulse();
}
});
};
// Start pulsing after a short delay
setTimeout(() => {
if (!isOpeningAnimationComplete) {
loopPulse();
}
}, 800);
};
const completeOpeningAnimation = () => {
@ -951,6 +1017,25 @@ const AndroidVideoPlayer: React.FC = () => {
]}
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
>
{backdrop && (
<Image
source={{ uri: backdrop }}
style={[StyleSheet.absoluteFill, { width: screenDimensions.width, height: screenDimensions.height }]}
resizeMode="cover"
blurRadius={0}
/>
)}
<LinearGradient
colors={[
'rgba(0,0,0,0.3)',
'rgba(0,0,0,0.6)',
'rgba(0,0,0,0.8)',
'rgba(0,0,0,0.9)'
]}
locations={[0, 0.3, 0.7, 1]}
style={StyleSheet.absoluteFill}
/>
<TouchableOpacity
style={styles.loadingCloseButton}
onPress={handleClose}
@ -960,8 +1045,28 @@ const AndroidVideoPlayer: React.FC = () => {
</TouchableOpacity>
<View style={styles.openingContent}>
{hasLogo ? (
<Animated.View style={{
transform: [
{ scale: Animated.multiply(logoScaleAnim, pulseAnim) }
],
opacity: logoOpacityAnim,
alignItems: 'center',
}}>
<Image
source={{ uri: metadata.logo }}
style={{
width: 300,
height: 180,
resizeMode: 'contain',
}}
/>
</Animated.View>
) : (
<>
<ActivityIndicator size="large" color="#E50914" />
<Text style={styles.openingText}>Loading video...</Text>
</>
)}
</View>
</Animated.View>

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text } from 'react-native';
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet } from 'react-native';
import { VLCPlayer } from 'react-native-vlc-media-player';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -10,16 +10,19 @@ import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import AndroidVideoPlayer from './AndroidVideoPlayer';
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings';
import {
DEFAULT_SUBTITLE_SIZE,
AudioTrack,
TextTrack,
ResizeModeType,
WyzieSubtitle,
WyzieSubtitle,
SubtitleCue,
RESUME_PREF_KEY,
RESUME_PREF,
@ -57,7 +60,8 @@ const VideoPlayer: React.FC = () => {
type,
episodeId,
imdbId,
availableStreams: passedAvailableStreams
availableStreams: passedAvailableStreams,
backdrop
} = route.params;
// Initialize Trakt autosync
@ -152,6 +156,24 @@ const VideoPlayer: React.FC = () => {
const isMounted = useRef(true);
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
// Get metadata to access logo (only if we have a valid id)
const shouldLoadMetadata = Boolean(id && type);
const metadataResult = useMetadata({
id: id || 'placeholder',
type: type || 'movie'
});
const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false };
const { settings } = useSettings();
// Logo animation values
const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
const logoOpacityAnim = useRef(new Animated.Value(0)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
// Check if we have a logo to show
const hasLogo = metadata && metadata.logo && !metadataLoading;
// Small offset (in seconds) used to avoid seeking to the *exact* end of the
// file which triggers the `onEnd` callback and causes playback to restart.
const END_EPSILON = 0.3;
@ -237,7 +259,51 @@ const VideoPlayer: React.FC = () => {
}, []);
const startOpeningAnimation = () => {
// Animation logic here
// Logo entrance animation
Animated.parallel([
Animated.timing(logoOpacityAnim, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}),
Animated.spring(logoScaleAnim, {
toValue: 1,
tension: 50,
friction: 8,
useNativeDriver: true,
}),
]).start();
// Continuous pulse animation for the logo
const createPulseAnimation = () => {
return Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.05,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]);
};
const loopPulse = () => {
createPulseAnimation().start(() => {
if (!isOpeningAnimationComplete) {
loopPulse();
}
});
};
// Start pulsing after a short delay
setTimeout(() => {
if (!isOpeningAnimationComplete) {
loopPulse();
}
}, 800);
};
const completeOpeningAnimation = () => {
@ -389,7 +455,6 @@ const VideoPlayer: React.FC = () => {
// Calculate position as fraction
const position = timeInSeconds / duration;
vlcRef.current.seek(position);
// Clear seek state after Android seek
setTimeout(() => {
if (isMounted.current) {
@ -400,12 +465,18 @@ const VideoPlayer: React.FC = () => {
}
}, 500);
} else {
// iOS fallback - use seek prop
const position = timeInSeconds / duration;
setSeekPosition(position);
// iOS (and other platforms) prefer direct seek on the ref to avoid re-mounts caused by the `seek` prop
const position = timeInSeconds / duration; // VLC expects a 0-1 fraction
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
vlcRef.current.seek(position);
} else {
// Fallback to legacy behaviour only if direct seek is unavailable
setSeekPosition(position);
}
setTimeout(() => {
if (isMounted.current) {
// Reset temporary seek state
setSeekPosition(null);
isSeeking.current = false;
if (DEBUG_MODE) {
@ -967,6 +1038,25 @@ const VideoPlayer: React.FC = () => {
]}
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
>
{backdrop && (
<Image
source={{ uri: backdrop }}
style={[StyleSheet.absoluteFill, { width: screenDimensions.width, height: screenDimensions.height }]}
resizeMode="cover"
blurRadius={0}
/>
)}
<LinearGradient
colors={[
'rgba(0,0,0,0.3)',
'rgba(0,0,0,0.6)',
'rgba(0,0,0,0.8)',
'rgba(0,0,0,0.9)'
]}
locations={[0, 0.3, 0.7, 1]}
style={StyleSheet.absoluteFill}
/>
<TouchableOpacity
style={styles.loadingCloseButton}
onPress={handleClose}
@ -976,8 +1066,28 @@ const VideoPlayer: React.FC = () => {
</TouchableOpacity>
<View style={styles.openingContent}>
<ActivityIndicator size="large" color="#E50914" />
<Text style={styles.openingText}>Loading video...</Text>
{hasLogo ? (
<Animated.View style={{
transform: [
{ scale: Animated.multiply(logoScaleAnim, pulseAnim) }
],
opacity: logoOpacityAnim,
alignItems: 'center',
}}>
<Image
source={{ uri: metadata.logo }}
style={{
width: 300,
height: 180,
resizeMode: 'contain',
}}
/>
</Animated.View>
) : (
<>
<ActivityIndicator size="large" color="#E50914" />
</>
)}
</View>
</Animated.View>
@ -1055,7 +1165,6 @@ const VideoPlayer: React.FC = () => {
resizeMode={resizeMode as any}
audioTrack={selectedAudioTrack ?? undefined}
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)}
seek={Platform.OS === 'ios' ? (seekPosition ?? undefined) : undefined}
autoAspectRatio
/>
</TouchableOpacity>

View file

@ -474,7 +474,6 @@ export const styles = StyleSheet.create({
},
openingContent: {
padding: 20,
backgroundColor: 'rgba(0,0,0,0.85)',
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',

View file

@ -66,6 +66,7 @@ export type RootStackParamList = {
type: string;
stream: Stream;
episodeId?: string;
backdrop?: string;
};
Player: {
uri: string;
@ -82,6 +83,7 @@ export type RootStackParamList = {
episodeId?: string;
imdbId?: string;
availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } };
backdrop?: string;
};
Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string };
Credits: { mediaId: string; mediaType: string };

View file

@ -24,6 +24,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import { Image } from 'expo-image';
import { RootStackParamList, RootStackNavigationProp } from '../navigation/AppNavigator';
import { useMetadata } from '../hooks/useMetadata';
import { useMetadataAssets } from '../hooks/useMetadataAssets';
import { useTheme } from '../contexts/ThemeContext';
import { Stream } from '../types/metadata';
import { tmdbService } from '../services/tmdbService';
@ -274,6 +275,11 @@ export const StreamsScreen = () => {
imdbId,
} = useMetadata({ id, type });
// Get backdrop from metadata assets
const setMetadataStub = useCallback(() => {}, []);
const memoizedSettings = useMemo(() => settings, [settings.logoSourcePreference, settings.tmdbLanguagePreference]);
const { bannerImage } = useMetadataAssets(metadata, id, type, imdbId, memoizedSettings, setMetadataStub);
// Create styles using current theme colors
const styles = React.useMemo(() => createStyles(colors), [colors]);
@ -578,6 +584,7 @@ export const StreamsScreen = () => {
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
imdbId: imdbId || undefined,
availableStreams: streamsToPass,
backdrop: bannerImage || undefined,
});
} catch (error) {
logger.error('[StreamsScreen] Error locking orientation before navigation:', error);
@ -598,9 +605,10 @@ export const StreamsScreen = () => {
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
imdbId: imdbId || undefined,
availableStreams: streamsToPass,
backdrop: bannerImage || undefined,
});
}
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams]);
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]);
// Update handleStreamPress
const handleStreamPress = useCallback(async (stream: Stream) => {