diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx
index 7a2a8598c..7d658d57e 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 a98d7f41a..ffd9c5b5a 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 db46675af..efb9dea3f 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 adeaf60da..7a9df2a90 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 000000000..11e6bb90a
--- /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;
+};