mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
cc894ff4f4
commit
f96d1f1af3
8 changed files with 161 additions and 228 deletions
21
app.json
21
app.json
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
42
patch-package.js
Normal 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.');
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
44
src/patches/react-native-video+6.12.0.patch
Normal file
44
src/patches/react-native-video+6.12.0.patch
Normal 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];
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue