Update app configuration and improve stream handling; add local network and microphone usage descriptions in app.json, enhance stream loading logic in useMetadata by removing external source requests, and implement error handling in VideoPlayer. Update StreamsScreen to filter out deprecated sources for better clarity.

This commit is contained in:
tapframe 2025-05-01 20:26:43 +05:30
parent cc894ff4f4
commit f96d1f1af3
8 changed files with 161 additions and 228 deletions

View file

@ -18,9 +18,26 @@
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
}
},
"NSBonjourServices": [
"_http._tcp"
],
"NSLocalNetworkUsageDescription": "App uses the local network to discover and connect to devices.",
"NSMicrophoneUsageDescription": "This app does not require microphone access.",
"UIBackgroundModes": ["audio"],
"LSSupportsOpeningDocumentsInPlace": true,
"UIFileSharingEnabled": true
},
"bundleIdentifier": "com.nuvio.app"
"bundleIdentifier": "com.nuvio.app",
"associatedDomains": [],
"documentTypes": [
{
"name": "Matroska Video",
"role": "viewer",
"utis": ["org.matroska.mkv"],
"extensions": ["mkv"]
}
]
},
"android": {
"adaptiveIcon": {

View file

@ -6,7 +6,8 @@
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
"web": "expo start --web",
"postinstall": "node patch-package.js"
},
"dependencies": {
"@expo/metro-runtime": "~4.0.1",

42
patch-package.js Normal file
View file

@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Directory containing patches
const patchesDir = path.join(__dirname, 'src/patches');
// Check if the directory exists
if (!fs.existsSync(patchesDir)) {
console.error(`Patches directory not found: ${patchesDir}`);
process.exit(1);
}
// Get all patch files
const patches = fs.readdirSync(patchesDir).filter(file => file.endsWith('.patch'));
if (patches.length === 0) {
console.log('No patch files found.');
process.exit(0);
}
console.log(`Found ${patches.length} patch files.`);
// Apply each patch
patches.forEach(patchFile => {
const patchPath = path.join(patchesDir, patchFile);
console.log(`Applying patch: ${patchFile}`);
try {
// Use the patch command to apply the patch file
execSync(`patch -p1 < ${patchPath}`, {
stdio: 'inherit',
cwd: process.cwd()
});
console.log(`✅ Successfully applied patch: ${patchFile}`);
} catch (error) {
console.error(`❌ Failed to apply patch ${patchFile}:`, error.message);
// Continue with other patches even if one fails
}
});
console.log('Patch process completed.');

View file

@ -577,9 +577,12 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
const loadStreams = async () => {
const startTime = Date.now();
try {
console.log('🚀 [loadStreams] START - Loading movie streams for:', id);
console.log('🚀 [loadStreams] START - Loading streams for:', id);
updateLoadingState();
// Always clear streams first to ensure we don't show stale data
setGroupedStreams({});
// Get TMDB ID for external sources first before starting parallel requests
console.log('🔍 [loadStreams] Getting TMDB ID for:', id);
let tmdbId;
@ -598,70 +601,18 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
console.log('🔄 [loadStreams] Starting stream requests');
const fetchPromises = [];
// Start Stremio request using the new callback method
// We don't push this promise anymore, as results are handled by callback
// Start Stremio request using the callback method
processStremioSource(type, id, false);
// Start Source 1 request if we have a TMDB ID
if (tmdbId) {
const source1Promise = processExternalSource('source1', (async () => {
try {
const streams = await fetchExternalStreams(
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}`,
'Source 1'
);
if (streams.length > 0) {
return {
'source_1': {
addonName: 'Source 1',
streams
}
};
}
return {};
} catch (error) {
console.error('❌ [loadStreams:source1] Error fetching Source 1 streams:', error);
return {};
}
})(), false);
fetchPromises.push(source1Promise);
}
// No external sources are used anymore
const fetchPromises: Promise<any>[] = [];
// Start Source 2 request if we have a TMDB ID
if (tmdbId) {
const source2Promise = processExternalSource('source2', (async () => {
try {
const streams = await fetchExternalStreams(
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}`,
'Source 2'
);
if (streams.length > 0) {
return {
'source_2': {
addonName: 'Source 2',
streams
}
};
}
return {};
} catch (error) {
console.error('❌ [loadStreams:source2] Error fetching Source 2 streams:', error);
return {};
}
})(), false);
fetchPromises.push(source2Promise);
}
// Wait only for external promises now
// Wait only for external promises now (none in this case)
const results = await Promise.allSettled(fetchPromises);
const totalTime = Date.now() - startTime;
console.log(`✅ [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
const sourceTypes = ['source1', 'source2']; // Removed 'stremio'
const sourceTypes: string[] = []; // No external sources
results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`);
@ -729,72 +680,19 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
console.log('🔄 [loadEpisodeStreams] Starting stream requests');
const fetchPromises = [];
const fetchPromises: Promise<any>[] = [];
// Start Stremio request using the new callback method
// We don't push this promise anymore
// Start Stremio request using the callback method
processStremioSource('series', episodeId, true);
// Start Source 1 request if we have a TMDB ID
if (tmdbId) {
const source1Promise = processExternalSource('source1', (async () => {
try {
const streams = await fetchExternalStreams(
`https://nice-month-production.up.railway.app/embedsu/${tmdbId}${episodeQuery}`,
'Source 1',
true
);
if (streams.length > 0) {
return {
'source_1': {
addonName: 'Source 1',
streams
}
};
}
return {};
} catch (error) {
console.error('❌ [loadEpisodeStreams:source1] Error fetching Source 1 streams:', error);
return {};
}
})(), true);
fetchPromises.push(source1Promise);
}
// No external sources are used anymore
// Start Source 2 request if we have a TMDB ID
if (tmdbId) {
const source2Promise = processExternalSource('source2', (async () => {
try {
const streams = await fetchExternalStreams(
`https://vidsrc-api-js-phz6.onrender.com/embedsu/${tmdbId}${episodeQuery}`,
'Source 2',
true
);
if (streams.length > 0) {
return {
'source_2': {
addonName: 'Source 2',
streams
}
};
}
return {};
} catch (error) {
console.error('❌ [loadEpisodeStreams:source2] Error fetching Source 2 streams:', error);
return {};
}
})(), true);
fetchPromises.push(source2Promise);
}
// Wait only for external promises now
// Wait only for external promises now (none in this case)
const results = await Promise.allSettled(fetchPromises);
const totalTime = Date.now() - startTime;
console.log(`✅ [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
const sourceTypes = ['source1', 'source2']; // Removed 'stremio'
const sourceTypes: string[] = []; // No external sources
results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);
@ -834,97 +732,6 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
}
};
const fetchExternalStreams = async (url: string, sourceName: string, isEpisode = false) => {
try {
console.log(`\n🌐 [${sourceName}] Starting fetch request...`);
console.log(`📍 URL: ${url}`);
// Add proper headers to ensure we get JSON response
const headers = {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
};
console.log('📋 Request Headers:', headers);
// Make the fetch request
console.log(`⏳ [${sourceName}] Making fetch request...`);
const response = await fetch(url, { headers });
console.log(`✅ [${sourceName}] Response received`);
console.log(`📊 Status: ${response.status} ${response.statusText}`);
console.log(`🔤 Content-Type:`, response.headers.get('content-type'));
// Check if response is ok
if (!response.ok) {
console.error(`❌ [${sourceName}] HTTP error: ${response.status}`);
console.error(`📝 Status Text: ${response.statusText}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
// Try to parse JSON
console.log(`📑 [${sourceName}] Reading response body...`);
const text = await response.text();
console.log(`📄 [${sourceName}] Response body (first 300 chars):`, text.substring(0, 300));
let data;
try {
console.log(`🔄 [${sourceName}] Parsing JSON...`);
data = JSON.parse(text);
console.log(`✅ [${sourceName}] JSON parsed successfully`);
} catch (e) {
console.error(`❌ [${sourceName}] JSON parse error:`, e);
console.error(`📝 [${sourceName}] Raw response:`, text.substring(0, 200));
throw new Error('Invalid JSON response');
}
// Transform the response
console.log(`🔄 [${sourceName}] Processing sources...`);
if (data && data.sources && Array.isArray(data.sources)) {
console.log(`📦 [${sourceName}] Found ${data.sources.length} source(s)`);
const transformedStreams = [];
for (const source of data.sources) {
console.log(`\n📂 [${sourceName}] Processing source:`, source);
if (source.files && Array.isArray(source.files)) {
console.log(`📁 [${sourceName}] Found ${source.files.length} file(s) in source`);
for (const file of source.files) {
console.log(`🎥 [${sourceName}] Processing file:`, file);
const stream = {
url: file.file,
title: `${sourceName} - ${file.quality || 'Unknown'}`,
name: `${sourceName} - ${file.quality || 'Unknown'}`,
behaviorHints: {
notWebReady: false,
headers: source.headers || {}
}
};
console.log(`✨ [${sourceName}] Created stream:`, stream);
transformedStreams.push(stream);
}
} else {
console.log(`⚠️ [${sourceName}] No files array found in source or invalid format`);
}
}
console.log(`\n🎉 [${sourceName}] Successfully processed ${transformedStreams.length} stream(s)`);
return transformedStreams;
}
console.log(`⚠️ [${sourceName}] No valid sources found in response`);
return [];
} catch (error) {
console.error(`\n❌ [${sourceName}] Error fetching streams:`, error);
console.error(`📍 URL: ${url}`);
if (error instanceof Error) {
console.error(`💥 Error name: ${error.name}`);
console.error(`💥 Error message: ${error.message}`);
console.error(`💥 Stack trace: ${error.stack}`);
}
return [];
}
};
const handleSeasonChange = useCallback((seasonNumber: number) => {
if (selectedSeason === seasonNumber) return;

View file

@ -0,0 +1,44 @@
diff --git a/node_modules/react-native-video/ios/Video/RCTVideo.m b/node_modules/react-native-video/ios/Video/RCTVideo.m
index 79d88de..a28a21e 100644
--- a/node_modules/react-native-video/ios/Video/RCTVideo.m
+++ b/node_modules/react-native-video/ios/Video/RCTVideo.m
@@ -1023,7 +1023,9 @@ static NSString *const statusKeyPath = @"status";
/* The player used to render the video */
AVPlayer *_player;
- AVPlayerLayer *_playerLayer;
+ // Use strong reference instead of weak to prevent deallocation issues
+ __strong AVPlayerLayer *_playerLayer;
+
NSURL *_videoURL;
/* IOS < 10 seek optimization */
@@ -1084,7 +1086,16 @@ - (void)removeFromSuperview
_player = nil;
_playerItem = nil;
- _playerLayer = nil;
+
+ // Properly clean up the player layer
+ if (_playerLayer) {
+ [_playerLayer removeFromSuperlayer];
+ // Set animation keys to nil before releasing to avoid crashes
+ [_playerLayer removeAllAnimations];
+ _playerLayer = nil;
+ }
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - App lifecycle handlers
@@ -1116,7 +1127,8 @@ - (void)applicationDidEnterBackground:(NSNotification *)notification
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
- if (_playInBackground || _playWhenInactive || _paused) return;
+ // Resume playback even if originally playing in background
+ if (_paused) return;
[_player play];
[_player setRate:_rate];
}

View file

@ -542,14 +542,12 @@ export const StreamsScreen = () => {
if (indexB !== -1) return 1;
return 0;
})
.filter(provider => provider !== 'source_1' && provider !== 'source_2') // Filter out source_1 and source_2
.map(provider => {
const addonInfo = streams[provider];
const installedAddon = installedAddons.find(addon => addon.id === provider);
let displayName = provider;
if (provider === 'external_sources') displayName = 'External Sources';
else if (installedAddon) displayName = installedAddon.name;
if (installedAddon) displayName = installedAddon.name;
else if (addonInfo?.addonName) displayName = addonInfo.addonName;
return { id: provider, name: displayName };
@ -562,10 +560,6 @@ export const StreamsScreen = () => {
const installedAddons = stremioService.getInstalledAddons();
return Object.entries(streams)
.filter(([addonId]) => {
// Filter out source_1 and source_2
return addonId !== 'source_1' && addonId !== 'source_2';
})
.sort(([addonIdA], [addonIdB]) => {
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);

View file

@ -633,6 +633,12 @@ const VideoPlayer: React.FC = () => {
`);
};
// Add onError handler
const handleError = (error: any) => {
logger.error('[VideoPlayer] Playback Error:', error);
// Optionally, you could show an error message to the user here
};
return (
<View style={styles.container}>
<TouchableOpacity
@ -659,6 +665,7 @@ const VideoPlayer: React.FC = () => {
onTextTracks={onTextTracks}
onBuffer={onBuffer}
onLoadStart={onLoadStart}
onError={handleError}
/>
{/* Slider Container with buffer indicator */}

View file

@ -558,21 +558,42 @@ class StremioService {
}
// Log the detailed resources structure for debugging
// logger.log(`📋 [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources)); // Verbose, uncomment if needed
logger.log(`📋 [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources));
// Check if the addon has a stream resource for this type
const hasStreamResource = addon.resources.some(
resource => {
const result = resource.name === 'stream' && resource.types && resource.types.includes(type);
// logger.log(`🔎 [getStreams] Addon ${addon.id} resource ${resource.name}: supports ${type}? ${result}`); // Verbose
return result;
}
);
let hasStreamResource = false;
// Handle both resource formats:
// 1. Standard format: array of ResourceObjects with name and types properties
// 2. Simple format: string array ["stream"] with separate types array in the addon
// Check for standard format (array of objects)
if (addon.resources.length > 0 && typeof addon.resources[0] === 'object') {
// Type-safe check for standard format (objects with name/types)
hasStreamResource = addon.resources.some(
resource => {
if (typeof resource === 'object' && resource !== null) {
const typedResource = resource as ResourceObject;
return typedResource.name === 'stream' &&
Array.isArray(typedResource.types) &&
typedResource.types.includes(type);
}
return false;
}
);
}
// Check for simple format (string array)
else if (Array.isArray(addon.resources) && addon.types) {
// Check if resources array contains 'stream' and types array includes the desired type
hasStreamResource =
(addon.resources as unknown as string[]).includes('stream') &&
Array.isArray(addon.types) &&
addon.types.includes(type);
}
if (!hasStreamResource) {
// logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`); // Verbose
logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`);
} else {
// logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type}`); // Verbose
logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type}`);
}
return hasStreamResource;