mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-11 20:40:42 +00:00
bug fixes
This commit is contained in:
parent
b21efa0df0
commit
d0719fabec
5 changed files with 315 additions and 45 deletions
|
|
@ -32,6 +32,7 @@ import {
|
||||||
} from './utils/playerTypes';
|
} from './utils/playerTypes';
|
||||||
import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils';
|
import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils';
|
||||||
import { styles } from './utils/playerStyles';
|
import { styles } from './utils/playerStyles';
|
||||||
|
import { isMkvStream } from '../../utils/mkvDetection';
|
||||||
import { SubtitleModals } from './modals/SubtitleModals';
|
import { SubtitleModals } from './modals/SubtitleModals';
|
||||||
import { AudioTrackModal } from './modals/AudioTrackModal';
|
import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||||
// Removed ResumeOverlay usage when alwaysResume is enabled
|
// Removed ResumeOverlay usage when alwaysResume is enabled
|
||||||
|
|
@ -48,9 +49,16 @@ const VideoPlayer: React.FC = () => {
|
||||||
const { streamProvider, uri, headers, forceVlc } = route.params as any;
|
const { streamProvider, uri, headers, forceVlc } = route.params as any;
|
||||||
|
|
||||||
// Check if the stream is from Xprime (by provider name or URL pattern)
|
// 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));
|
(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)
|
// Xprime-specific headers for better compatibility (from local-scrapers-repo)
|
||||||
const getXprimeHeaders = () => {
|
const getXprimeHeaders = () => {
|
||||||
if (!isXprimeStream) return {};
|
if (!isXprimeStream) return {};
|
||||||
|
|
@ -69,21 +77,28 @@ const VideoPlayer: React.FC = () => {
|
||||||
logger.log('[VideoPlayer] Applying Xprime headers for stream:', uri);
|
logger.log('[VideoPlayer] Applying Xprime headers for stream:', uri);
|
||||||
return xprimeHeaders;
|
return xprimeHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the file format is MKV
|
// Detect if stream is MKV format
|
||||||
const lowerUri = (uri || '').toLowerCase();
|
const isMkvFile = isMkvStream(uri, headers);
|
||||||
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);
|
|
||||||
|
|
||||||
// Use AndroidVideoPlayer for:
|
// Use AndroidVideoPlayer for:
|
||||||
// - Android devices
|
// - 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)
|
// - Non-MKV files on iOS (unless forceVlc is set)
|
||||||
// Use VideoPlayer (VLC) for:
|
// Use VideoPlayer (VLC) for:
|
||||||
// - MKV files on iOS (unless forceVlc is set)
|
// - MKV files on iOS (unless forceVlc is set, even for Xprime)
|
||||||
const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc);
|
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) {
|
if (shouldUseAndroidPlayer) {
|
||||||
return <AndroidVideoPlayer />;
|
return <AndroidVideoPlayer />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,32 +21,33 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
|
||||||
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
|
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({ isAvailable: false });
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({ isAvailable: false });
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
|
const [hasCheckedOnStartup, setHasCheckedOnStartup] = useState(false);
|
||||||
|
|
||||||
const checkForUpdates = useCallback(async () => {
|
const checkForUpdates = useCallback(async (forceCheck = false) => {
|
||||||
try {
|
try {
|
||||||
// Check if user has dismissed the popup for this version
|
// Check if user has dismissed the popup for this version
|
||||||
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
|
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
|
||||||
const currentVersion = updateInfo.manifest?.id;
|
const currentVersion = updateInfo.manifest?.id;
|
||||||
|
|
||||||
if (dismissedVersion === currentVersion) {
|
if (dismissedVersion === currentVersion && !forceCheck) {
|
||||||
return; // User already dismissed this version
|
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);
|
const updateLaterTimestamp = await AsyncStorage.getItem(UPDATE_LATER_STORAGE_KEY);
|
||||||
if (updateLaterTimestamp) {
|
if (updateLaterTimestamp && !forceCheck) {
|
||||||
const laterTime = parseInt(updateLaterTimestamp);
|
const laterTime = parseInt(updateLaterTimestamp);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours
|
||||||
|
|
||||||
if (now - laterTime < twentyFourHours) {
|
if (now - laterTime < sixHours) {
|
||||||
return; // User chose "later" recently
|
return; // User chose "later" recently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await UpdateService.checkForUpdates();
|
const info = await UpdateService.checkForUpdates();
|
||||||
setUpdateInfo(info);
|
setUpdateInfo(info);
|
||||||
|
|
||||||
if (info.isAvailable) {
|
if (info.isAvailable) {
|
||||||
setShowUpdatePopup(true);
|
setShowUpdatePopup(true);
|
||||||
}
|
}
|
||||||
|
|
@ -131,8 +132,33 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
|
||||||
}
|
}
|
||||||
}, [updateInfo.manifest?.id]);
|
}, [updateInfo.manifest?.id]);
|
||||||
|
|
||||||
// Auto-check for updates when hook is first used
|
// Handle startup update check results
|
||||||
useEffect(() => {
|
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
|
// Add a small delay to ensure the app is fully loaded
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -140,9 +166,9 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
|
||||||
const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY);
|
const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY);
|
||||||
const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0;
|
const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours
|
||||||
if (now - last < twentyFourHours) {
|
if (now - last < sixHours) {
|
||||||
return; // Throttle: only auto-check once per 24h
|
return; // Throttle: only auto-check once per 6h
|
||||||
}
|
}
|
||||||
await checkForUpdates();
|
await checkForUpdates();
|
||||||
await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now));
|
await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now));
|
||||||
|
|
@ -150,10 +176,10 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, 2000); // 2 second delay
|
}, 3000); // Increased delay to 3 seconds to give startup check time
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [checkForUpdates]);
|
}, [checkForUpdates, hasCheckedOnStartup]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showUpdatePopup,
|
showUpdatePopup,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import { VideoPlayerService } from '../services/videoPlayerService';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import QualityBadge from '../components/metadata/QualityBadge';
|
import QualityBadge from '../components/metadata/QualityBadge';
|
||||||
import { logger } from '../utils/logger';
|
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 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';
|
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;
|
let forceVlc = !!options?.forceVlc;
|
||||||
try {
|
try {
|
||||||
if (Platform.OS === 'ios' && !forceVlc) {
|
if (Platform.OS === 'ios' && !forceVlc) {
|
||||||
// Check if the actual stream is an MKV file
|
const isMkvFile = isMkvStream(stream.url, stream.headers);
|
||||||
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);
|
|
||||||
|
|
||||||
// Special case: moviebox should always use AndroidVideoPlayer
|
// Special case: moviebox should always use AndroidVideoPlayer
|
||||||
if (streamProvider === 'moviebox') {
|
if (streamProvider === 'moviebox') {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,15 @@ export interface UpdateInfo {
|
||||||
isEmbeddedLaunch?: boolean;
|
isEmbeddedLaunch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateCheckCallback = (updateInfo: UpdateInfo) => void;
|
||||||
|
|
||||||
export class UpdateService {
|
export class UpdateService {
|
||||||
private static instance: UpdateService;
|
private static instance: UpdateService;
|
||||||
private updateCheckInterval: NodeJS.Timeout | null = null;
|
private updateCheckInterval: NodeJS.Timeout | null = null;
|
||||||
// Removed automatic periodic checks - only check on app start and manual trigger
|
// Removed automatic periodic checks - only check on app start and manual trigger
|
||||||
private logs: string[] = [];
|
private logs: string[] = [];
|
||||||
private readonly MAX_LOGS = 100; // Keep last 100 logs
|
private readonly MAX_LOGS = 100; // Keep last 100 logs
|
||||||
|
private updateCheckCallbacks: UpdateCheckCallback[] = [];
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
|
|
@ -28,23 +31,31 @@ export class UpdateService {
|
||||||
* Add a log entry with timestamp - always log to console for adb logcat visibility
|
* Add a log entry with timestamp - always log to console for adb logcat visibility
|
||||||
*/
|
*/
|
||||||
private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void {
|
private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void {
|
||||||
// Logging disabled intentionally
|
const timestamp = new Date().toISOString();
|
||||||
return;
|
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
|
* Get all logs
|
||||||
*/
|
*/
|
||||||
public getLogs(): string[] {
|
public getLogs(): string[] {
|
||||||
// Logging disabled - return empty list
|
return [...this.logs];
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all logs
|
* Clear all logs
|
||||||
*/
|
*/
|
||||||
public clearLogs(): void {
|
public clearLogs(): void {
|
||||||
// Logging disabled - no-op
|
this.addLog('Clearing all logs', 'INFO');
|
||||||
this.logs = [];
|
this.logs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,8 +63,40 @@ export class UpdateService {
|
||||||
* Add a test log entry (useful for debugging)
|
* Add a test log entry (useful for debugging)
|
||||||
*/
|
*/
|
||||||
public addTestLog(message: string): void {
|
public addTestLog(message: string): void {
|
||||||
// Logging disabled - no-op
|
this.addLog(`TEST: ${message}`, 'INFO');
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(`Updates enabled: ${Updates.isEnabled}`, 'INFO');
|
||||||
this.addLog(`Runtime version: ${Updates.runtimeVersion || 'unknown'}`, 'INFO');
|
this.addLog(`Runtime version: ${Updates.runtimeVersion || 'unknown'}`, 'INFO');
|
||||||
this.addLog(`Update URL: ${this.getUpdateUrl()}`, '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
|
// Check if we're running in a development environment
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
this.addLog('Running in development mode', 'WARN');
|
this.addLog('Running in development mode', 'WARN');
|
||||||
this.addLog('UpdateService initialization completed (dev mode)', 'INFO');
|
this.addLog('UpdateService initialization completed (dev mode)', 'INFO');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if updates are enabled
|
// Check if updates are enabled
|
||||||
|
|
@ -207,7 +248,30 @@ export class UpdateService {
|
||||||
return;
|
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');
|
this.addLog('UpdateService initialization completed successfully', 'INFO');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.addLog(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`, 'ERROR');
|
this.addLog(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`, 'ERROR');
|
||||||
|
|
|
||||||
169
src/utils/mkvDetection.ts
Normal file
169
src/utils/mkvDetection.ts
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue