From d0719fabec89c73d884b8ae3945ac0740760f489 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 17 Sep 2025 19:37:08 +0530 Subject: [PATCH] bug fixes --- src/components/player/VideoPlayer.tsx | 39 ++++-- src/hooks/useUpdatePopup.ts | 56 ++++++--- src/screens/StreamsScreen.tsx | 8 +- src/services/updateService.ts | 88 ++++++++++++-- src/utils/mkvDetection.ts | 169 ++++++++++++++++++++++++++ 5 files changed, 315 insertions(+), 45 deletions(-) create mode 100644 src/utils/mkvDetection.ts diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 7a2a859..7d658d5 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -32,6 +32,7 @@ import { } from './utils/playerTypes'; import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils'; import { styles } from './utils/playerStyles'; +import { isMkvStream } from '../../utils/mkvDetection'; import { SubtitleModals } from './modals/SubtitleModals'; import { AudioTrackModal } from './modals/AudioTrackModal'; // Removed ResumeOverlay usage when alwaysResume is enabled @@ -48,9 +49,16 @@ const VideoPlayer: React.FC = () => { const { streamProvider, uri, headers, forceVlc } = route.params as any; // Check if the stream is from Xprime (by provider name or URL pattern) - const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' || + const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' || (uri && /flutch.*\.workers\.dev|fsl\.fastcloud\.casa|xprime/i.test(uri)); + safeDebugLog("Stream detection", { + uri, + streamProvider, + isXprimeStream, + platform: Platform.OS + }); + // Xprime-specific headers for better compatibility (from local-scrapers-repo) const getXprimeHeaders = () => { if (!isXprimeStream) return {}; @@ -69,21 +77,28 @@ const VideoPlayer: React.FC = () => { logger.log('[VideoPlayer] Applying Xprime headers for stream:', uri); return xprimeHeaders; }; - - // Check if the file format is MKV - const lowerUri = (uri || '').toLowerCase(); - const contentType = (headers && (headers['Content-Type'] || headers['content-type'])) || ''; - const isMkvByHeader = typeof contentType === 'string' && contentType.includes('matroska'); - const isMkvByPath = lowerUri.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUri) || /format=mkv\b/.test(lowerUri) || /container=mkv\b/.test(lowerUri); - const isMkvFile = Boolean(isMkvByHeader || isMkvByPath); - + + // Detect if stream is MKV format + const isMkvFile = isMkvStream(uri, headers); + // Use AndroidVideoPlayer for: // - Android devices - // - Xprime streams on any platform + // - Xprime streams (unless it's MKV on iOS - allow VLC for better compatibility) // - Non-MKV files on iOS (unless forceVlc is set) // Use VideoPlayer (VLC) for: - // - MKV files on iOS (unless forceVlc is set) - const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc); + // - MKV files on iOS (unless forceVlc is set, even for Xprime) + const shouldUseAndroidPlayer = Platform.OS === 'android' || + (isXprimeStream && !(Platform.OS === 'ios' && isMkvFile)) || + (Platform.OS === 'ios' && !isMkvFile && !forceVlc); + + safeDebugLog("Player selection logic", { + platform: Platform.OS, + isXprimeStream, + isMkvFile, + forceVlc, + xprimeException: isXprimeStream && Platform.OS === 'ios' && isMkvFile, + shouldUseAndroidPlayer + }); if (shouldUseAndroidPlayer) { return ; } diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index a98d7f4..ffd9c5b 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -21,32 +21,33 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { const [showUpdatePopup, setShowUpdatePopup] = useState(false); const [updateInfo, setUpdateInfo] = useState({ isAvailable: false }); const [isInstalling, setIsInstalling] = useState(false); + const [hasCheckedOnStartup, setHasCheckedOnStartup] = useState(false); - const checkForUpdates = useCallback(async () => { + const checkForUpdates = useCallback(async (forceCheck = false) => { try { // Check if user has dismissed the popup for this version const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY); const currentVersion = updateInfo.manifest?.id; - - if (dismissedVersion === currentVersion) { + + if (dismissedVersion === currentVersion && !forceCheck) { return; // User already dismissed this version } - // Check if user chose "later" recently (within 24 hours) + // Check if user chose "later" recently (within 6 hours) const updateLaterTimestamp = await AsyncStorage.getItem(UPDATE_LATER_STORAGE_KEY); - if (updateLaterTimestamp) { + if (updateLaterTimestamp && !forceCheck) { const laterTime = parseInt(updateLaterTimestamp); const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - - if (now - laterTime < twentyFourHours) { + const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours + + if (now - laterTime < sixHours) { return; // User chose "later" recently } } const info = await UpdateService.checkForUpdates(); setUpdateInfo(info); - + if (info.isAvailable) { setShowUpdatePopup(true); } @@ -131,8 +132,33 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { } }, [updateInfo.manifest?.id]); - // Auto-check for updates when hook is first used + // Handle startup update check results useEffect(() => { + const handleStartupUpdateCheck = (updateInfo: UpdateInfo) => { + console.log('UpdatePopup: Received startup update check result', updateInfo); + setUpdateInfo(updateInfo); + setHasCheckedOnStartup(true); + + if (updateInfo.isAvailable) { + setShowUpdatePopup(true); + } + }; + + // Register callback for startup update check + UpdateService.onUpdateCheck(handleStartupUpdateCheck); + + // Cleanup callback on unmount + return () => { + UpdateService.offUpdateCheck(handleStartupUpdateCheck); + }; + }, []); + + // Auto-check for updates when hook is first used (fallback if startup check fails) + useEffect(() => { + if (hasCheckedOnStartup) { + return; // Already checked on startup + } + // Add a small delay to ensure the app is fully loaded const timer = setTimeout(() => { (async () => { @@ -140,9 +166,9 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY); const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0; const now = Date.now(); - const twentyFourHours = 24 * 60 * 60 * 1000; - if (now - last < twentyFourHours) { - return; // Throttle: only auto-check once per 24h + const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours + if (now - last < sixHours) { + return; // Throttle: only auto-check once per 6h } await checkForUpdates(); await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now)); @@ -150,10 +176,10 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { // ignore } })(); - }, 2000); // 2 second delay + }, 3000); // Increased delay to 3 seconds to give startup check time return () => clearTimeout(timer); - }, [checkForUpdates]); + }, [checkForUpdates, hasCheckedOnStartup]); return { showUpdatePopup, diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index db46675..efb9dea 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -46,6 +46,7 @@ import { VideoPlayerService } from '../services/videoPlayerService'; import { useSettings } from '../hooks/useSettings'; import QualityBadge from '../components/metadata/QualityBadge'; import { logger } from '../utils/logger'; +import { isMkvStream } from '../utils/mkvDetection'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png'; @@ -846,12 +847,7 @@ export const StreamsScreen = () => { let forceVlc = !!options?.forceVlc; try { if (Platform.OS === 'ios' && !forceVlc) { - // Check if the actual stream is an MKV file - const lowerUri = (stream.url || '').toLowerCase(); - const contentType = (stream.headers && ((stream.headers as any)['Content-Type'] || (stream.headers as any)['content-type'])) || ''; - const isMkvByHeader = typeof contentType === 'string' && contentType.includes('matroska'); - const isMkvByPath = lowerUri.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUri) || /format=mkv\b/.test(lowerUri) || /container=mkv\b/.test(lowerUri); - const isMkvFile = Boolean(isMkvByHeader || isMkvByPath); + const isMkvFile = isMkvStream(stream.url, stream.headers); // Special case: moviebox should always use AndroidVideoPlayer if (streamProvider === 'moviebox') { diff --git a/src/services/updateService.ts b/src/services/updateService.ts index adeaf60..7a9df2a 100644 --- a/src/services/updateService.ts +++ b/src/services/updateService.ts @@ -8,12 +8,15 @@ export interface UpdateInfo { isEmbeddedLaunch?: boolean; } +export type UpdateCheckCallback = (updateInfo: UpdateInfo) => void; + export class UpdateService { private static instance: UpdateService; private updateCheckInterval: NodeJS.Timeout | null = null; // Removed automatic periodic checks - only check on app start and manual trigger private logs: string[] = []; private readonly MAX_LOGS = 100; // Keep last 100 logs + private updateCheckCallbacks: UpdateCheckCallback[] = []; private constructor() {} @@ -28,23 +31,31 @@ export class UpdateService { * Add a log entry with timestamp - always log to console for adb logcat visibility */ private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void { - // Logging disabled intentionally - return; + const timestamp = new Date().toISOString(); + const logEntry = `[${timestamp}] [${level}] UpdateService: ${message}`; + + // Keep logs for debugging (limited to prevent memory issues) + if (this.logs.length >= this.MAX_LOGS) { + this.logs.shift(); // Remove oldest log + } + this.logs.push(logEntry); + + // Always log to console for visibility + console.log(logEntry); } /** * Get all logs */ public getLogs(): string[] { - // Logging disabled - return empty list - return []; + return [...this.logs]; } /** * Clear all logs */ public clearLogs(): void { - // Logging disabled - no-op + this.addLog('Clearing all logs', 'INFO'); this.logs = []; } @@ -52,8 +63,40 @@ export class UpdateService { * Add a test log entry (useful for debugging) */ public addTestLog(message: string): void { - // Logging disabled - no-op - return; + this.addLog(`TEST: ${message}`, 'INFO'); + } + + /** + * Register a callback to be notified when update checks complete + */ + public onUpdateCheck(callback: UpdateCheckCallback): void { + this.updateCheckCallbacks.push(callback); + this.addLog(`Registered update check callback (${this.updateCheckCallbacks.length} total)`, 'INFO'); + } + + /** + * Unregister an update check callback + */ + public offUpdateCheck(callback: UpdateCheckCallback): void { + const index = this.updateCheckCallbacks.indexOf(callback); + if (index > -1) { + this.updateCheckCallbacks.splice(index, 1); + this.addLog(`Unregistered update check callback (${this.updateCheckCallbacks.length} remaining)`, 'INFO'); + } + } + + /** + * Notify all registered callbacks about an update check result + */ + private notifyUpdateCheckCallbacks(updateInfo: UpdateInfo): void { + this.addLog(`Notifying ${this.updateCheckCallbacks.length} callback(s) about update check result`, 'INFO'); + this.updateCheckCallbacks.forEach(callback => { + try { + callback(updateInfo); + } catch (error) { + this.addLog(`Callback notification failed: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + } + }); } /** @@ -189,15 +232,13 @@ export class UpdateService { this.addLog(`Updates enabled: ${Updates.isEnabled}`, 'INFO'); this.addLog(`Runtime version: ${Updates.runtimeVersion || 'unknown'}`, 'INFO'); this.addLog(`Update URL: ${this.getUpdateUrl()}`, 'INFO'); - - try { - // Only log initialization info, don't perform automatic update checks - this.addLog('UpdateService initialized - updates will be handled manually via popup', 'INFO'); + try { // Check if we're running in a development environment if (__DEV__) { this.addLog('Running in development mode', 'WARN'); this.addLog('UpdateService initialization completed (dev mode)', 'INFO'); + return; } // Check if updates are enabled @@ -207,7 +248,30 @@ export class UpdateService { return; } - this.addLog('Updates are enabled, manual update checks only', 'INFO'); + this.addLog('Updates are enabled, performing initial update check...', 'INFO'); + + // Perform an initial update check on app startup + try { + const updateInfo = await this.checkForUpdates(); + this.addLog(`Initial update check completed - Updates available: ${updateInfo.isAvailable}`, 'INFO'); + + if (updateInfo.isAvailable) { + this.addLog('Update available! The popup will be shown to the user.', 'INFO'); + } else { + this.addLog('No updates available at startup', 'INFO'); + } + + // Notify registered callbacks about the update check result + this.notifyUpdateCheckCallbacks(updateInfo); + } catch (checkError) { + this.addLog(`Initial update check failed: ${checkError instanceof Error ? checkError.message : String(checkError)}`, 'ERROR'); + + // Notify callbacks about the failed check + this.notifyUpdateCheckCallbacks({ isAvailable: false }); + + // Don't fail initialization if update check fails + } + this.addLog('UpdateService initialization completed successfully', 'INFO'); } catch (error) { this.addLog(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); diff --git a/src/utils/mkvDetection.ts b/src/utils/mkvDetection.ts new file mode 100644 index 0000000..11e6bb9 --- /dev/null +++ b/src/utils/mkvDetection.ts @@ -0,0 +1,169 @@ +/** + * Enhanced MKV stream detection utility + * Provides multiple methods to detect if a video stream is in MKV format + */ + +export interface StreamDetectionResult { + isMkv: boolean; + method: string; + confidence: 'high' | 'medium' | 'low'; +} + +/** + * Comprehensive MKV stream detection + * Uses multiple detection methods for maximum accuracy + */ +export const detectMkvStream = (streamUri: string, streamHeaders?: Record): StreamDetectionResult => { + if (!streamUri) { + return { isMkv: false, method: 'none', confidence: 'high' }; + } + + const lowerUri = streamUri.toLowerCase(); + const contentType = (streamHeaders && (streamHeaders['Content-Type'] || streamHeaders['content-type'])) || ''; + + // Method 1: Content-Type header detection (most reliable) + if (typeof contentType === 'string') { + if (/video\/x-matroska|application\/x-matroska/i.test(contentType)) { + return { isMkv: true, method: 'content-type', confidence: 'high' }; + } + if (/matroska/i.test(contentType)) { + return { isMkv: true, method: 'content-type', confidence: 'high' }; + } + } + + // Method 2: File extension detection + const mkvExtensions = ['.mkv', '.mka', '.mks', '.mk3d']; + for (const ext of mkvExtensions) { + if (lowerUri.includes(ext)) { + return { isMkv: true, method: 'extension', confidence: 'high' }; + } + } + + // Method 3: URL parameter detection + const urlPatterns = [ + /[?&]ext=mkv\b/, + /[?&]format=mkv\b/, + /[?&]container=mkv\b/, + /[?&]codec=mkv\b/, + /[?&]format=matroska\b/, + /[?&]type=mkv\b/, + /[?&]file_format=mkv\b/ + ]; + + for (const pattern of urlPatterns) { + if (pattern.test(lowerUri)) { + return { isMkv: true, method: 'url-pattern', confidence: 'high' }; + } + } + + // Method 4: Known MKV streaming patterns (medium confidence) + const streamingPatterns = [ + /mkv|matroska/i, + /video\/x-matroska/i, + /ebml/i, // EBML is the container format MKV uses + ]; + + for (const pattern of streamingPatterns) { + if (pattern.test(lowerUri)) { + return { isMkv: true, method: 'streaming-pattern', confidence: 'medium' }; + } + } + + // Method 5: Provider-specific patterns (lower confidence) + const providerPatterns = [ + /vidsrc|embedsu|multiembed/i, // Providers that often serve MKV + ]; + + for (const pattern of providerPatterns) { + if (pattern.test(lowerUri)) { + // These providers often serve MKV, but not guaranteed + return { isMkv: true, method: 'provider-pattern', confidence: 'low' }; + } + } + + return { isMkv: false, method: 'none', confidence: 'high' }; +}; + +/** + * Async HEAD request detection for MKV + * Most reliable method but requires network request + */ +export const detectMkvViaHeadRequest = async ( + url: string, + headers?: Record, + timeoutMs: number = 2000 +): Promise => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + method: 'HEAD', + headers, + signal: controller.signal as any, + } as any); + + const contentType = response.headers.get('content-type') || ''; + + if (/video\/x-matroska|application\/x-matroska/i.test(contentType)) { + return { isMkv: true, method: 'head-request', confidence: 'high' }; + } + + if (/matroska/i.test(contentType)) { + return { isMkv: true, method: 'head-request', confidence: 'high' }; + } + + return { isMkv: false, method: 'head-request', confidence: 'high' }; + } catch (error) { + return { isMkv: false, method: 'head-request-failed', confidence: 'low' }; + } finally { + clearTimeout(timeout); + } +}; + +/** + * Combined detection: fast local detection + optional HEAD request + */ +export const detectMkvComprehensive = async ( + streamUri: string, + streamHeaders?: Record, + useHeadRequest: boolean = false, + headTimeoutMs: number = 2000 +): Promise => { + // First try fast local detection + const localResult = detectMkvStream(streamUri, streamHeaders); + + if (localResult.isMkv && localResult.confidence === 'high') { + return localResult; + } + + // If local detection is inconclusive and HEAD request is enabled, try network detection + if (useHeadRequest) { + const headResult = await detectMkvViaHeadRequest(streamUri, streamHeaders, headTimeoutMs); + + if (headResult.isMkv || headResult.method === 'head-request') { + return headResult; + } + } + + return localResult; +}; + +/** + * Simple boolean wrapper for backward compatibility + */ +export const isMkvStream = (streamUri: string, streamHeaders?: Record): boolean => { + const result = detectMkvStream(streamUri, streamHeaders); + + // Debug logging in development + if (__DEV__ && streamUri) { + console.log('[MKV Detection]', { + uri: streamUri.substring(0, 100) + (streamUri.length > 100 ? '...' : ''), + isMkv: result.isMkv, + method: result.method, + confidence: result.confidence + }); + } + + return result.isMkv; +};