bug fixes

This commit is contained in:
tapframe 2025-09-17 19:37:08 +05:30
parent b21efa0df0
commit d0719fabec
5 changed files with 315 additions and 45 deletions

View file

@ -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 <AndroidVideoPlayer />;
}

View file

@ -21,32 +21,33 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({ 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,

View file

@ -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') {

View file

@ -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');

169
src/utils/mkvDetection.ts Normal file
View file

@ -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<string, string>): 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<string, string>,
timeoutMs: number = 2000
): Promise<StreamDetectionResult> => {
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<string, string>,
useHeadRequest: boolean = false,
headTimeoutMs: number = 2000
): Promise<StreamDetectionResult> => {
// 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<string, string>): 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;
};