mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
Improved trakt.
This commit is contained in:
parent
df89c16246
commit
6405fd2c71
6 changed files with 185 additions and 225 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -42,4 +42,5 @@ CHANGELOG.md
|
||||||
android/
|
android/
|
||||||
HEATING_OPTIMIZATIONS.md
|
HEATING_OPTIMIZATIONS.md
|
||||||
ios
|
ios
|
||||||
android
|
android
|
||||||
|
sliderreadme.md
|
||||||
|
|
|
||||||
|
|
@ -264,16 +264,16 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
const startOpeningAnimation = () => {
|
||||||
// Logo entrance animation
|
// Logo entrance animation - optimized for faster appearance
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
Animated.timing(logoOpacityAnim, {
|
Animated.timing(logoOpacityAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 300, // Reduced from 600ms to 300ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.spring(logoScaleAnim, {
|
Animated.spring(logoScaleAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
tension: 50,
|
tension: 80, // Increased tension for faster spring
|
||||||
friction: 8,
|
friction: 8,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
|
|
@ -284,12 +284,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return Animated.sequence([
|
return Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.05,
|
toValue: 1.05,
|
||||||
duration: 1000,
|
duration: 800, // Reduced from 1000ms to 800ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1000,
|
duration: 800, // Reduced from 1000ms to 800ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
@ -303,38 +303,34 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start pulsing after a short delay
|
// Start pulsing immediately without delay
|
||||||
setTimeout(() => {
|
// Removed the 800ms delay
|
||||||
if (!isOpeningAnimationComplete) {
|
loopPulse();
|
||||||
loopPulse();
|
|
||||||
}
|
|
||||||
}, 800);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeOpeningAnimation = () => {
|
const completeOpeningAnimation = () => {
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
Animated.timing(openingFadeAnim, {
|
Animated.timing(openingFadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 300, // Reduced from 600ms to 300ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(openingScaleAnim, {
|
Animated.timing(openingScaleAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 700,
|
duration: 350, // Reduced from 700ms to 350ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(backgroundFadeAnim, {
|
Animated.timing(backgroundFadeAnim, {
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
duration: 800,
|
duration: 400, // Reduced from 800ms to 400ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start(() => {
|
]).start(() => {
|
||||||
openingScaleAnim.setValue(1);
|
openingScaleAnim.setValue(1);
|
||||||
openingFadeAnim.setValue(1);
|
openingFadeAnim.setValue(1);
|
||||||
setIsOpeningAnimationComplete(true);
|
setIsOpeningAnimationComplete(true);
|
||||||
setTimeout(() => {
|
// Removed the 100ms delay
|
||||||
backgroundFadeAnim.setValue(0);
|
backgroundFadeAnim.setValue(0);
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -396,15 +392,14 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
clearInterval(progressSaveInterval);
|
clearInterval(progressSaveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the user's configured sync frequency with increased minimum to reduce heating
|
// IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync
|
||||||
// Minimum interval increased from 5s to 30s to reduce CPU usage
|
const syncInterval = 5000; // 5 seconds for immediate sync
|
||||||
const syncInterval = Math.max(30000, traktSettings.syncFrequency);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
saveWatchProgress();
|
saveWatchProgress();
|
||||||
}, syncInterval);
|
}, syncInterval);
|
||||||
|
|
||||||
logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms`);
|
logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`);
|
||||||
|
|
||||||
setProgressSaveInterval(interval);
|
setProgressSaveInterval(interval);
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -412,7 +407,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
setProgressSaveInterval(null);
|
setProgressSaveInterval(null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]);
|
}, [id, type, paused, currentTime, duration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -583,8 +578,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complete opening animation immediately before seeking
|
||||||
|
completeOpeningAnimation();
|
||||||
|
|
||||||
if (initialPosition && !isInitialSeekComplete) {
|
if (initialPosition && !isInitialSeekComplete) {
|
||||||
logger.log(`[AndroidVideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
logger.log(`[AndroidVideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
||||||
|
// Reduced timeout from 1000ms to 500ms
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (videoRef.current && videoDuration > 0 && isMounted.current) {
|
if (videoRef.current && videoDuration > 0 && isMounted.current) {
|
||||||
seekToTime(initialPosition);
|
seekToTime(initialPosition);
|
||||||
|
|
@ -593,9 +592,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
} else {
|
} else {
|
||||||
logger.error(`[AndroidVideoPlayer] Initial seek failed: videoRef=${!!videoRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
|
logger.error(`[AndroidVideoPlayer] Initial seek failed: videoRef=${!!videoRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 500);
|
||||||
}
|
}
|
||||||
completeOpeningAnimation();
|
|
||||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[AndroidVideoPlayer] Error in onLoad:', error);
|
logger.error('[AndroidVideoPlayer] Error in onLoad:', error);
|
||||||
|
|
@ -651,9 +650,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
logger.log('[AndroidVideoPlayer] Close button pressed - syncing to Trakt before closing');
|
// Prevent multiple close attempts
|
||||||
|
if (isSyncingBeforeClose) {
|
||||||
// Set syncing state to prevent multiple close attempts
|
logger.log('[AndroidVideoPlayer] Close already in progress, ignoring duplicate call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('[AndroidVideoPlayer] Close button pressed - closing immediately and syncing to Trakt in background');
|
||||||
setIsSyncingBeforeClose(true);
|
setIsSyncingBeforeClose(true);
|
||||||
|
|
||||||
// Make sure we have the most accurate current time
|
// Make sure we have the most accurate current time
|
||||||
|
|
@ -662,44 +665,34 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
||||||
|
|
||||||
try {
|
// Navigate immediately without delay
|
||||||
// Force one last progress update (scrobble/pause) with the exact time
|
ScreenOrientation.unlockAsync().then(() => {
|
||||||
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
|
||||||
|
|
||||||
// Sync progress to Trakt before closing
|
|
||||||
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
|
||||||
|
|
||||||
// Start exit animation
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(fadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(openingFadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
|
|
||||||
// Longer delay to ensure Trakt sync completes
|
|
||||||
setTimeout(() => {
|
|
||||||
ScreenOrientation.unlockAsync().then(() => {
|
|
||||||
disableImmersiveMode();
|
|
||||||
navigation.goBack();
|
|
||||||
}).catch(() => {
|
|
||||||
// Fallback: navigate even if orientation unlock fails
|
|
||||||
disableImmersiveMode();
|
|
||||||
navigation.goBack();
|
|
||||||
});
|
|
||||||
}, 500); // Increased from 100ms to 500ms
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[AndroidVideoPlayer] Error syncing to Trakt before closing:', error);
|
|
||||||
// Navigate anyway even if sync fails
|
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
}
|
}).catch(() => {
|
||||||
|
// Fallback: navigate even if orientation unlock fails
|
||||||
|
disableImmersiveMode();
|
||||||
|
navigation.goBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send Trakt sync in background (don't await)
|
||||||
|
const backgroundSync = async () => {
|
||||||
|
try {
|
||||||
|
logger.log('[AndroidVideoPlayer] Starting background Trakt sync');
|
||||||
|
// Force one last progress update (scrobble/pause) with the exact time
|
||||||
|
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||||
|
|
||||||
|
// Sync progress to Trakt
|
||||||
|
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||||
|
|
||||||
|
logger.log('[AndroidVideoPlayer] Background Trakt sync completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[AndroidVideoPlayer] Error in background Trakt sync:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start background sync without blocking UI
|
||||||
|
backgroundSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResume = async () => {
|
const handleResume = async () => {
|
||||||
|
|
@ -752,10 +745,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
logger.log('[AndroidVideoPlayer] Video ended naturally, sending final progress update with 100%');
|
logger.log('[AndroidVideoPlayer] Video ended naturally, sending final progress update with 100%');
|
||||||
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
||||||
|
|
||||||
// Small delay to ensure the progress update is processed
|
// IMMEDIATE SYNC: Remove delay for instant sync
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
// Now send the stop call immediately
|
||||||
|
|
||||||
// Now send the stop call
|
|
||||||
logger.log('[AndroidVideoPlayer] Sending final stop call after natural end');
|
logger.log('[AndroidVideoPlayer] Sending final stop call after natural end');
|
||||||
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
||||||
|
|
||||||
|
|
@ -1280,4 +1271,4 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AndroidVideoPlayer;
|
export default AndroidVideoPlayer;
|
||||||
|
|
@ -286,16 +286,16 @@ const VideoPlayer: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
const startOpeningAnimation = () => {
|
||||||
// Logo entrance animation
|
// Logo entrance animation - optimized for faster appearance
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
Animated.timing(logoOpacityAnim, {
|
Animated.timing(logoOpacityAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 300, // Reduced from 600ms to 300ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.spring(logoScaleAnim, {
|
Animated.spring(logoScaleAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
tension: 50,
|
tension: 80, // Increased tension for faster spring
|
||||||
friction: 8,
|
friction: 8,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
|
|
@ -306,12 +306,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
return Animated.sequence([
|
return Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.05,
|
toValue: 1.05,
|
||||||
duration: 1000,
|
duration: 800, // Reduced from 1000ms to 800ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1000,
|
duration: 800, // Reduced from 1000ms to 800ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
@ -325,29 +325,26 @@ const VideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start pulsing after a short delay
|
// Start pulsing immediately without delay
|
||||||
setTimeout(() => {
|
// Removed the 800ms delay
|
||||||
if (!isOpeningAnimationComplete) {
|
loopPulse();
|
||||||
loopPulse();
|
|
||||||
}
|
|
||||||
}, 800);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeOpeningAnimation = () => {
|
const completeOpeningAnimation = () => {
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
Animated.timing(openingFadeAnim, {
|
Animated.timing(openingFadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 600,
|
duration: 300, // Reduced from 600ms to 300ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(openingScaleAnim, {
|
Animated.timing(openingScaleAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 700,
|
duration: 350, // Reduced from 700ms to 350ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(backgroundFadeAnim, {
|
Animated.timing(backgroundFadeAnim, {
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
duration: 800,
|
duration: 400, // Reduced from 800ms to 400ms
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start(() => {
|
]).start(() => {
|
||||||
|
|
@ -418,15 +415,14 @@ const VideoPlayer: React.FC = () => {
|
||||||
clearInterval(progressSaveInterval);
|
clearInterval(progressSaveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the user's configured sync frequency with increased minimum to reduce heating
|
// IMMEDIATE SYNC: Reduce sync interval to 5 seconds for near real-time sync
|
||||||
// Minimum interval increased from 5s to 30s to reduce CPU usage
|
const syncInterval = 5000; // 5 seconds for immediate sync
|
||||||
const syncInterval = Math.max(30000, traktSettings.syncFrequency);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
saveWatchProgress();
|
saveWatchProgress();
|
||||||
}, syncInterval);
|
}, syncInterval);
|
||||||
|
|
||||||
logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms`);
|
logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`);
|
||||||
|
|
||||||
setProgressSaveInterval(interval);
|
setProgressSaveInterval(interval);
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -434,7 +430,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
setProgressSaveInterval(null);
|
setProgressSaveInterval(null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [id, type, paused, currentTime, duration, traktSettings.syncFrequency]);
|
}, [id, type, paused, currentTime, duration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -629,8 +625,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complete opening animation immediately before seeking
|
||||||
|
completeOpeningAnimation();
|
||||||
|
|
||||||
if (initialPosition && !isInitialSeekComplete) {
|
if (initialPosition && !isInitialSeekComplete) {
|
||||||
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
||||||
|
// Reduced timeout from 1000ms to 500ms
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (vlcRef.current && videoDuration > 0 && isMounted.current) {
|
if (vlcRef.current && videoDuration > 0 && isMounted.current) {
|
||||||
seekToTime(initialPosition);
|
seekToTime(initialPosition);
|
||||||
|
|
@ -639,9 +639,9 @@ const VideoPlayer: React.FC = () => {
|
||||||
} else {
|
} else {
|
||||||
logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
|
logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 500);
|
||||||
}
|
}
|
||||||
completeOpeningAnimation();
|
|
||||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VideoPlayer] Error in onLoad:', error);
|
logger.error('[VideoPlayer] Error in onLoad:', error);
|
||||||
|
|
@ -704,9 +704,13 @@ const VideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing');
|
// Prevent multiple close attempts
|
||||||
|
if (isSyncingBeforeClose) {
|
||||||
|
logger.log('[VideoPlayer] Close already in progress, ignoring duplicate call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set syncing state to prevent multiple close attempts
|
logger.log('[VideoPlayer] Close button pressed - closing immediately and syncing to Trakt in background');
|
||||||
setIsSyncingBeforeClose(true);
|
setIsSyncingBeforeClose(true);
|
||||||
|
|
||||||
// Make sure we have the most accurate current time
|
// Make sure we have the most accurate current time
|
||||||
|
|
@ -715,75 +719,56 @@ const VideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
||||||
|
|
||||||
try {
|
// Cleanup and navigate back immediately without delay
|
||||||
// Force one last progress update (scrobble/pause) with the exact time
|
const cleanup = async () => {
|
||||||
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
try {
|
||||||
|
// Unlock orientation first
|
||||||
|
await ScreenOrientation.unlockAsync();
|
||||||
|
logger.log('[VideoPlayer] Orientation unlocked');
|
||||||
|
} catch (orientationError) {
|
||||||
|
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync progress to Trakt before closing
|
// Disable immersive mode
|
||||||
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
disableImmersiveMode();
|
||||||
|
|
||||||
// Start exit animation
|
// Navigate back with proper handling for fullscreen modal
|
||||||
Animated.parallel([
|
try {
|
||||||
Animated.timing(fadeAnim, {
|
if (navigation.canGoBack()) {
|
||||||
toValue: 0,
|
navigation.goBack();
|
||||||
duration: 150,
|
} else {
|
||||||
useNativeDriver: true,
|
// Fallback: navigate to main tabs if can't go back
|
||||||
}),
|
|
||||||
Animated.timing(openingFadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
|
|
||||||
// Cleanup and navigate back
|
|
||||||
const cleanup = async () => {
|
|
||||||
try {
|
|
||||||
// Unlock orientation first
|
|
||||||
await ScreenOrientation.unlockAsync();
|
|
||||||
logger.log('[VideoPlayer] Orientation unlocked');
|
|
||||||
} catch (orientationError) {
|
|
||||||
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable immersive mode
|
|
||||||
disableImmersiveMode();
|
|
||||||
|
|
||||||
// Navigate back with proper handling for fullscreen modal
|
|
||||||
try {
|
|
||||||
if (navigation.canGoBack()) {
|
|
||||||
navigation.goBack();
|
|
||||||
} else {
|
|
||||||
// Fallback: navigate to main tabs if can't go back
|
|
||||||
navigation.navigate('MainTabs');
|
|
||||||
}
|
|
||||||
logger.log('[VideoPlayer] Navigation completed');
|
|
||||||
} catch (navError) {
|
|
||||||
logger.error('[VideoPlayer] Navigation error:', navError);
|
|
||||||
// Last resort: try to navigate to home
|
|
||||||
navigation.navigate('MainTabs');
|
navigation.navigate('MainTabs');
|
||||||
}
|
}
|
||||||
};
|
logger.log('[VideoPlayer] Navigation completed');
|
||||||
|
} catch (navError) {
|
||||||
// Delay to ensure Trakt sync completes and animations finish
|
logger.error('[VideoPlayer] Navigation error:', navError);
|
||||||
setTimeout(cleanup, 500);
|
// Last resort: try to navigate to home
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[VideoPlayer] Error syncing to Trakt before closing:', error);
|
|
||||||
// Navigate anyway even if sync fails
|
|
||||||
disableImmersiveMode();
|
|
||||||
try {
|
|
||||||
await ScreenOrientation.unlockAsync();
|
|
||||||
} catch (orientationError) {
|
|
||||||
// Ignore orientation unlock errors
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigation.canGoBack()) {
|
|
||||||
navigation.goBack();
|
|
||||||
} else {
|
|
||||||
navigation.navigate('MainTabs');
|
navigation.navigate('MainTabs');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Navigate immediately
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
// Send Trakt sync in background (don't await)
|
||||||
|
const backgroundSync = async () => {
|
||||||
|
try {
|
||||||
|
logger.log('[VideoPlayer] Starting background Trakt sync');
|
||||||
|
// Force one last progress update (scrobble/pause) with the exact time
|
||||||
|
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||||
|
|
||||||
|
// Sync progress to Trakt
|
||||||
|
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||||
|
|
||||||
|
logger.log('[VideoPlayer] Background Trakt sync completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VideoPlayer] Error in background Trakt sync:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start background sync without blocking UI
|
||||||
|
backgroundSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResume = async () => {
|
const handleResume = async () => {
|
||||||
|
|
@ -836,10 +821,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%');
|
logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%');
|
||||||
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
||||||
|
|
||||||
// Small delay to ensure the progress update is processed
|
// IMMEDIATE SYNC: Remove delay for instant sync
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
// Now send the stop call immediately
|
||||||
|
|
||||||
// Now send the stop call
|
|
||||||
logger.log('[VideoPlayer] Sending final stop call after natural end');
|
logger.log('[VideoPlayer] Sending final stop call after natural end');
|
||||||
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,11 +165,11 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
const progressPercent = (currentTime / duration) * 100;
|
const progressPercent = (currentTime / duration) * 100;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Use the user's configured sync frequency
|
// IMMEDIATE SYNC: Remove all debouncing and frequency checks for instant sync
|
||||||
const timeSinceLastSync = now - lastSyncTime.current;
|
|
||||||
const progressDiff = Math.abs(progressPercent - lastSyncProgress.current);
|
const progressDiff = Math.abs(progressPercent - lastSyncProgress.current);
|
||||||
|
|
||||||
if (!force && timeSinceLastSync < autosyncSettings.syncFrequency && progressDiff < 5) {
|
// Only skip if not forced and progress difference is minimal (< 1%)
|
||||||
|
if (!force && progressDiff < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,7 +195,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[TraktAutosync] Error syncing progress:', error);
|
logger.error('[TraktAutosync] Error syncing progress:', error);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, autosyncSettings.enabled, autosyncSettings.syncFrequency, updateProgress, buildContentData, options]);
|
}, [isAuthenticated, autosyncSettings.enabled, updateProgress, buildContentData, options]);
|
||||||
|
|
||||||
// Handle playback end/pause
|
// Handle playback end/pause
|
||||||
const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => {
|
const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => {
|
||||||
|
|
@ -232,10 +232,10 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ENHANCED DEDUPLICATION: Prevent rapid successive calls (within 5 seconds)
|
// IMMEDIATE SYNC: Remove debouncing for instant sync when closing
|
||||||
// Bypass for significant updates
|
// Only prevent truly duplicate calls (within 1 second)
|
||||||
if (!isSignificantUpdate && now - lastStopCall.current < 5000) {
|
if (!isSignificantUpdate && now - lastStopCall.current < 1000) {
|
||||||
logger.log(`[TraktAutosync] Ignoring rapid successive stop call within 5 seconds (reason: ${reason})`);
|
logger.log(`[TraktAutosync] Ignoring duplicate stop call within 1 second (reason: ${reason})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,4 +377,4 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
handlePlaybackEnd,
|
handlePlaybackEnd,
|
||||||
resetState
|
resetState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -645,60 +645,45 @@ export const StreamsScreen = () => {
|
||||||
}, [selectedEpisode, groupedEpisodes, id]);
|
}, [selectedEpisode, groupedEpisodes, id]);
|
||||||
|
|
||||||
const navigateToPlayer = useCallback(async (stream: Stream) => {
|
const navigateToPlayer = useCallback(async (stream: Stream) => {
|
||||||
|
// Prepare available streams for the change source feature
|
||||||
|
const streamsToPass = type === 'series' ? episodeStreams : groupedStreams;
|
||||||
|
|
||||||
|
// Determine the stream name using the same logic as StreamCard
|
||||||
|
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||||
|
|
||||||
|
// Navigate to player immediately without waiting for orientation lock
|
||||||
|
// This prevents delay in player opening
|
||||||
|
navigation.navigate('Player', {
|
||||||
|
uri: stream.url,
|
||||||
|
title: metadata?.name || '',
|
||||||
|
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||||
|
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||||
|
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||||
|
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||||
|
year: metadata?.year,
|
||||||
|
streamProvider: stream.name,
|
||||||
|
streamName: streamName,
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
||||||
|
imdbId: imdbId || undefined,
|
||||||
|
availableStreams: streamsToPass,
|
||||||
|
backdrop: bannerImage || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lock orientation to landscape after navigation has started
|
||||||
|
// This allows the player to open immediately while orientation is being set
|
||||||
try {
|
try {
|
||||||
// Lock orientation to landscape before navigation to prevent glitches
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
|
||||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
.catch(error => {
|
||||||
|
logger.error('[StreamsScreen] Error locking orientation after navigation:', error);
|
||||||
// Small delay to ensure orientation is set before navigation
|
});
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
// Prepare available streams for the change source feature
|
|
||||||
const streamsToPass = type === 'series' ? episodeStreams : groupedStreams;
|
|
||||||
|
|
||||||
// Determine the stream name using the same logic as StreamCard
|
|
||||||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
|
||||||
|
|
||||||
navigation.navigate('Player', {
|
|
||||||
uri: stream.url,
|
|
||||||
title: metadata?.name || '',
|
|
||||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
|
||||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
|
||||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
|
||||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
|
||||||
year: metadata?.year,
|
|
||||||
streamProvider: stream.name,
|
|
||||||
streamName: streamName,
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
|
||||||
imdbId: imdbId || undefined,
|
|
||||||
availableStreams: streamsToPass,
|
|
||||||
backdrop: bannerImage || undefined,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[StreamsScreen] Error locking orientation before navigation:', error);
|
logger.error('[StreamsScreen] Error locking orientation after navigation:', error);
|
||||||
// Fallback: navigate anyway
|
|
||||||
const streamsToPass = type === 'series' ? episodeStreams : groupedStreams;
|
|
||||||
|
|
||||||
navigation.navigate('Player', {
|
|
||||||
uri: stream.url,
|
|
||||||
title: metadata?.name || '',
|
|
||||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
|
||||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
|
||||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
|
||||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
|
||||||
year: metadata?.year,
|
|
||||||
streamProvider: stream.name,
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
|
||||||
imdbId: imdbId || undefined,
|
|
||||||
availableStreams: streamsToPass,
|
|
||||||
backdrop: bannerImage || undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]);
|
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]);
|
||||||
|
|
||||||
|
|
||||||
// Update handleStreamPress
|
// Update handleStreamPress
|
||||||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -269,14 +269,14 @@ export class TraktService {
|
||||||
private readonly SCROBBLE_EXPIRY_MS = 46 * 60 * 1000; // 46 minutes (based on Trakt's expiry window)
|
private readonly SCROBBLE_EXPIRY_MS = 46 * 60 * 1000; // 46 minutes (based on Trakt's expiry window)
|
||||||
private scrobbledTimestamps: Map<string, number> = new Map();
|
private scrobbledTimestamps: Map<string, number> = new Map();
|
||||||
|
|
||||||
// Track currently watching sessions to avoid duplicate starts
|
// Track currently watching sessions to avoid duplicate starts// Sync debouncing
|
||||||
private currentlyWatching: Set<string> = new Set();
|
private currentlyWatching: Set<string> = new Set();
|
||||||
private lastSyncTimes: Map<string, number> = new Map();
|
private lastSyncTimes: Map<string, number> = new Map();
|
||||||
private readonly SYNC_DEBOUNCE_MS = 60000; // 60 seconds
|
private readonly SYNC_DEBOUNCE_MS = 1000; // 1 second for immediate sync
|
||||||
|
|
||||||
// Debounce for stop calls
|
// Debounce for stop calls
|
||||||
private lastStopCalls: Map<string, number> = new Map();
|
private lastStopCalls: Map<string, number> = new Map();
|
||||||
private readonly STOP_DEBOUNCE_MS = 10000; // 10 seconds debounce for stop calls
|
private readonly STOP_DEBOUNCE_MS = 1000; // 1 second debounce for immediate stop calls
|
||||||
|
|
||||||
// Default completion threshold (overridden by user settings)
|
// Default completion threshold (overridden by user settings)
|
||||||
private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80%
|
private readonly DEFAULT_COMPLETION_THRESHOLD = 80; // 80%
|
||||||
|
|
@ -1336,8 +1336,8 @@ export class TraktService {
|
||||||
const watchingKey = this.getWatchingKey(contentData);
|
const watchingKey = this.getWatchingKey(contentData);
|
||||||
const lastSync = this.lastSyncTimes.get(watchingKey) || 0;
|
const lastSync = this.lastSyncTimes.get(watchingKey) || 0;
|
||||||
|
|
||||||
// Debounce API calls unless forced
|
// IMMEDIATE SYNC: Remove debouncing for instant sync, only prevent truly rapid calls (< 500ms)
|
||||||
if (!force && (now - lastSync) < this.SYNC_DEBOUNCE_MS) {
|
if (!force && (now - lastSync) < 500) {
|
||||||
return true; // Skip this sync, but return success
|
return true; // Skip this sync, but return success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1377,9 +1377,9 @@ export class TraktService {
|
||||||
const watchingKey = this.getWatchingKey(contentData);
|
const watchingKey = this.getWatchingKey(contentData);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Enhanced deduplication: Check if we recently stopped this content
|
// IMMEDIATE SYNC: Reduce debouncing for instant sync, only prevent truly duplicate calls (< 1 second)
|
||||||
const lastStopTime = this.lastStopCalls.get(watchingKey);
|
const lastStopTime = this.lastStopCalls.get(watchingKey);
|
||||||
if (lastStopTime && (now - lastStopTime) < this.STOP_DEBOUNCE_MS) {
|
if (lastStopTime && (now - lastStopTime) < 1000) {
|
||||||
logger.log(`[TraktService] Ignoring duplicate stop call for ${contentData.title} (last stop ${((now - lastStopTime) / 1000).toFixed(1)}s ago)`);
|
logger.log(`[TraktService] Ignoring duplicate stop call for ${contentData.title} (last stop ${((now - lastStopTime) / 1000).toFixed(1)}s ago)`);
|
||||||
return true; // Return success to avoid error handling
|
return true; // Return success to avoid error handling
|
||||||
}
|
}
|
||||||
|
|
@ -1588,4 +1588,4 @@ export class TraktService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a singleton instance
|
// Export a singleton instance
|
||||||
export const traktService = TraktService.getInstance();
|
export const traktService = TraktService.getInstance();
|
||||||
Loading…
Reference in a new issue