mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 16:51:57 +00:00
added external player selection for downloads
This commit is contained in:
parent
e160bf6fe0
commit
01953af578
3 changed files with 223 additions and 76 deletions
|
|
@ -87,6 +87,7 @@ export interface AppSettings {
|
|||
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
|
||||
streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
|
||||
enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile
|
||||
useExternalPlayerForDownloads: boolean; // Enable/disable external player for downloaded content
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -122,6 +123,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
alwaysResume: true,
|
||||
// Downloads
|
||||
enableDownloads: false,
|
||||
useExternalPlayerForDownloads: false,
|
||||
// Theme defaults
|
||||
themeId: 'default',
|
||||
customThemes: [],
|
||||
|
|
@ -202,7 +204,7 @@ export const useSettings = () => {
|
|||
if (scoped) {
|
||||
try {
|
||||
merged = JSON.parse(scoped);
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Alert,
|
||||
Platform,
|
||||
Clipboard,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
|
|
@ -28,6 +29,8 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { VideoPlayerService } from '../services/videoPlayerService';
|
||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
|
|
@ -129,7 +132,7 @@ const DownloadItemComponent: React.FC<{
|
|||
|
||||
const formatBytes = (bytes?: number) => {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const sizes = ['B','KB','MB','GB','TB'];
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const v = bytes / Math.pow(1024, i);
|
||||
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${sizes[i]}`;
|
||||
|
|
@ -234,7 +237,7 @@ const DownloadItemComponent: React.FC<{
|
|||
<View style={styles.downloadHeader}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.downloadTitle, { color: currentTheme.colors.text }]} numberOfLines={1}>
|
||||
{item.title}{item.type === 'series' && item.season && item.episode ? ` S${String(item.season).padStart(2,'0')}E${String(item.episode).padStart(2,'0')}` : ''}
|
||||
{item.title}{item.type === 'series' && item.season && item.episode ? ` S${String(item.season).padStart(2, '0')}E${String(item.episode).padStart(2, '0')}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
@ -342,6 +345,7 @@ const DownloadItemComponent: React.FC<{
|
|||
const DownloadsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { top: safeAreaTop } = useSafeAreaInsets();
|
||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
|
|
@ -394,7 +398,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
setIsRefreshing(false);
|
||||
}, []);
|
||||
|
||||
const handleDownloadPress = useCallback((item: DownloadItem) => {
|
||||
const handleDownloadPress = useCallback(async (item: DownloadItem) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
if (item.status !== 'completed') {
|
||||
Alert.alert('Download not ready', 'Please wait until the download completes.');
|
||||
|
|
@ -411,6 +415,102 @@ const DownloadsScreen: React.FC = () => {
|
|||
const isMp4 = /\.mp4(\?|$)/i.test(lower);
|
||||
const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : undefined;
|
||||
|
||||
// Use external player if enabled in settings
|
||||
if (settings.useExternalPlayerForDownloads) {
|
||||
if (Platform.OS === 'android') {
|
||||
try {
|
||||
// Use VideoPlayerService for Android external playback
|
||||
const success = await VideoPlayerService.playVideo(uri, {
|
||||
useExternalPlayer: true,
|
||||
title: item.title,
|
||||
episodeTitle: item.type === 'series' ? item.episodeTitle : undefined,
|
||||
episodeNumber: item.type === 'series' && item.season && item.episode ? `S${item.season}E${item.episode}` : undefined,
|
||||
});
|
||||
|
||||
if (success) return;
|
||||
// Fall through to internal player if external fails
|
||||
} catch (error) {
|
||||
console.error('External player failed:', error);
|
||||
// Fall through to internal player
|
||||
}
|
||||
} else if (Platform.OS === 'ios') {
|
||||
const streamUrl = encodeURIComponent(uri);
|
||||
let externalPlayerUrls: string[] = [];
|
||||
|
||||
switch (settings.preferredPlayer) {
|
||||
case 'vlc':
|
||||
externalPlayerUrls = [
|
||||
`vlc://${uri}`,
|
||||
`vlc-x-callback://x-callback-url/stream?url=${streamUrl}`,
|
||||
`vlc://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'outplayer':
|
||||
externalPlayerUrls = [
|
||||
`outplayer://${uri}`,
|
||||
`outplayer://${streamUrl}`,
|
||||
`outplayer://play?url=${streamUrl}`,
|
||||
`outplayer://stream?url=${streamUrl}`,
|
||||
`outplayer://play/browser?url=${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'infuse':
|
||||
externalPlayerUrls = [
|
||||
`infuse://x-callback-url/play?url=${streamUrl}`,
|
||||
`infuse://play?url=${streamUrl}`,
|
||||
`infuse://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'vidhub':
|
||||
externalPlayerUrls = [
|
||||
`vidhub://play?url=${streamUrl}`,
|
||||
`vidhub://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'infuse_livecontainer':
|
||||
const infuseUrls = [
|
||||
`infuse://x-callback-url/play?url=${streamUrl}`,
|
||||
`infuse://play?url=${streamUrl}`,
|
||||
`infuse://${streamUrl}`
|
||||
];
|
||||
externalPlayerUrls = infuseUrls.map(infuseUrl => {
|
||||
const encoded = Buffer.from(infuseUrl).toString('base64');
|
||||
return `livecontainer://open-url?url=${encoded}`;
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// Internal logic will handle 'internal' choice
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings.preferredPlayer !== 'internal') {
|
||||
// Try each URL format in sequence
|
||||
const tryNextUrl = (index: number) => {
|
||||
if (index >= externalPlayerUrls.length) {
|
||||
// Fallback to internal player if all external attempts fail
|
||||
openInternalPlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = externalPlayerUrls[index];
|
||||
Linking.openURL(url)
|
||||
.catch(() => tryNextUrl(index + 1));
|
||||
};
|
||||
|
||||
if (externalPlayerUrls.length > 0) {
|
||||
tryNextUrl(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openInternalPlayer = () => {
|
||||
// Build episodeId for series progress tracking (format: contentId:season:episode)
|
||||
const episodeId = item.type === 'series' && item.season && item.episode
|
||||
? `${item.contentId}:${item.season}:${item.episode}`
|
||||
|
|
@ -437,7 +537,10 @@ const DownloadsScreen: React.FC = () => {
|
|||
backdrop: undefined,
|
||||
videoType,
|
||||
} as any);
|
||||
}, [navigation]);
|
||||
};
|
||||
|
||||
openInternalPlayer();
|
||||
}, [navigation, settings]);
|
||||
|
||||
const handleDownloadAction = useCallback((item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => {
|
||||
if (action === 'pause') pauseDownload(item.id);
|
||||
|
|
@ -624,10 +727,10 @@ const DownloadsScreen: React.FC = () => {
|
|||
<CustomAlert
|
||||
visible={showRemoveAlert}
|
||||
title="Remove Download"
|
||||
message={pendingRemoveItem ? `Remove \"${pendingRemoveItem.title}\"${pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2,'0')}E${String(pendingRemoveItem.episode).padStart(2,'0')}` : ''}?` : 'Remove this download?'}
|
||||
message={pendingRemoveItem ? `Remove \"${pendingRemoveItem.title}\"${pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2, '0')}E${String(pendingRemoveItem.episode).padStart(2, '0')}` : ''}?` : 'Remove this download?'}
|
||||
actions={[
|
||||
{ label: 'Cancel', onPress: () => setShowRemoveAlert(false) },
|
||||
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: { } },
|
||||
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} },
|
||||
]}
|
||||
onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -322,6 +322,48 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* External Player for Downloads */}
|
||||
{((Platform.OS === 'android' && settings.useExternalPlayer) ||
|
||||
(Platform.OS === 'ios' && settings.preferredPlayer !== 'internal')) && (
|
||||
<View style={[styles.settingItem, styles.settingItemBorder, { borderBottomWidth: 0, borderTopWidth: 1, borderTopColor: 'rgba(255,255,255,0.08)' }]}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
{ backgroundColor: 'rgba(255,255,255,0.1)' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name="open-in-new"
|
||||
size={20}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settingText}>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingTitle,
|
||||
{ color: currentTheme.colors.text },
|
||||
]}
|
||||
>
|
||||
External Player for Downloads
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingDescription,
|
||||
{ color: currentTheme.colors.textMuted },
|
||||
]}
|
||||
>
|
||||
Play downloaded content in your preferred external player.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.useExternalPlayerForDownloads}
|
||||
onValueChange={(value) => updateSetting('useExternalPlayerForDownloads', value)}
|
||||
thumbColor={settings.useExternalPlayerForDownloads ? currentTheme.colors.primary : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
|
|
|||
Loading…
Reference in a new issue