Compare commits

..

No commits in common. "main" and "1.2.5" have entirely different histories.
main ... 1.2.5

501 changed files with 35216 additions and 125613 deletions

View file

@ -3,12 +3,6 @@
EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
# Remote cache for TMDB (optional)
# Set to true to use local/remote cache server, and provide URL
EXPO_PUBLIC_USE_REMOTE_CACHE=false
EXPO_PUBLIC_CACHE_SERVER_URL=http://localhost:5173
EXPO_PUBLIC_DISABLE_LOCAL_CACHE=false
# MovieBox (MoviesMod) Keys
EXPO_PUBLIC_MOVIEBOX_PRIMARY_KEY=your_moviebox_primary_key
EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY=your_tmdb_api_key_for_moviebox
@ -17,8 +11,3 @@ EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY=your_tmdb_api_key_for_moviebox
EXPO_PUBLIC_TRAKT_CLIENT_ID=your_trakt_client_id
EXPO_PUBLIC_TRAKT_CLIENT_SECRET=your_trakt_client_secret
EXPO_PUBLIC_TRAKT_REDIRECT_URI=stremioexpo://auth/trakt
# Skip Intro API (IntroDB)
# Fetches intro timestamps for TV shows to enable skip intro functionality
EXPO_PUBLIC_INTRODB_API_URL=https://api.introdb.app
EXPO_PUBLIC_DISCORD_USER_API=

4
.github/FUNDING.yml vendored
View file

@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [tapframe]
ko_fi: tapframe

View file

@ -1,217 +0,0 @@
name: Bug report
description: Report a reproducible bug (one per issue).
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug.
If we can reproduce it, we can usually fix it. This form is just to get the basics in one place.
- type: markdown
attributes:
value: |
## Quick checks
- type: checkboxes
id: checks
attributes:
label: Pre-flight checks
options:
- label: I searched existing issues and this is not a duplicate.
required: true
- label: I can reproduce this on the latest release or latest main build.
required: false
- label: This issue is limited to a single bug (not multiple unrelated problems).
required: true
- type: markdown
attributes:
value: |
## Version & device
- type: input
id: app_version
attributes:
label: App version / OTA update ID
description: Release version, commit hash, or OTA update ID. You can find your OTA update ID in Settings > App updates > Current version (hold to copy).
placeholder: "e.g. 1.2.3, main@abc1234, or an OTA ID"
validations:
required: true
- type: dropdown
id: install_method
attributes:
label: Install method
options:
- GitHub Release APK / IPA
- Expo Go
- Built from source
- Other (please describe below)
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Android phone/tablet
- iOS (iPhone/iPad)
- Android emulator
- iOS Simulator
- Other (please describe below)
validations:
required: true
- type: input
id: device_model
attributes:
label: Device model
description: "Example: iPhone 15 Pro, Pixel 8, Galaxy S23 Ultra, iPad Pro, etc."
placeholder: "e.g. iPhone 15 Pro"
validations:
required: true
- type: input
id: os_version
attributes:
label: OS version
placeholder: "e.g. Android 14, iOS 17.2"
validations:
required: true
- type: dropdown
id: player_mode
attributes:
label: Player mode
description: If you are using an external player, most playback issues must be reported to that player instead.
options:
- Internal player (iOS: KSPlayer)
- Internal player (Android: ExoPlayer)
- Internal player (Android: MPV)
- External player
- Ask every time
- Not sure
validations:
required: true
- type: markdown
attributes:
value: |
## What happened?
- type: dropdown
id: area
attributes:
label: Area (tag)
description: Pick the closest match. It helps triage.
options:
- Playback (start/stop/buffering)
- Streams / Sources (selection, loading, errors)
- Next Episode / Auto-play
- Watch Progress (resume, watched state, history)
- Subtitles (styling, sync)
- Audio tracks
- UI / Layout / Animations
- Settings
- Sync (Trakt / SIMKL / remote)
- Downloads
- Other
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Exact steps. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links.
placeholder: |
1. Open ...
2. Navigate to ...
3. Press ...
4. Observe ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
placeholder: "What you expected to happen."
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
placeholder: "What actually happened (include any on-screen error text/codes)."
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: Frequency
options:
- Always
- Often (more than 50%)
- Sometimes
- Rarely
- Once
validations:
required: true
- type: dropdown
id: regression
attributes:
label: Did this work before?
options:
- Not sure
- Yes, it used to work
- No, it never worked
validations:
required: true
- type: markdown
attributes:
value: |
## Extra context (optional)
- type: textarea
id: media_details
attributes:
label: Media details (optional)
description: Only include what you can safely share.
placeholder: |
- Content type: series/movie
- Season/Episode: S1E2
- Stream/source: (addon name / source label)
- Video format: (if known)
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logs (optional but helpful)
description: |
Not required, but super helpful for playback/crash issues.
If you can, include a short snippet from Metro bundler, Xcode, or `adb logcat`.
render: shell
placeholder: |
adb logcat -d | tail -n 300
validations:
required: false
- type: textarea
id: extra
attributes:
label: Anything else? (optional)
description: Screenshots/recordings, related issues, workarounds, etc.
validations:
required: false

View file

@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Downloads / Releases
url: https://github.com/tapframe/NuvioMobile/releases
about: Grab the latest GitHub Release APK/IPA here.
- name: Documentation
url: https://github.com/tapframe/NuvioMobile/blob/main/README.md
about: Read the README for setup and usage details.

View file

@ -1,78 +0,0 @@
name: Feature request
description: Suggest an improvement or new feature.
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
One feature request per issue, please. The more real-world your use case is, the easier it is to evaluate.
Feature requests are reviewed as product proposals first.
Please do not open a pull request for a new feature, major UX change, or broad cosmetic update unless a maintainer has explicitly approved it first.
Unapproved feature PRs will usually be closed.
- type: dropdown
id: area
attributes:
label: Area (tag)
options:
- Playback
- Streams / Sources
- Next Episode / Auto-play
- Watch Progress
- Subtitles
- Audio
- UI / Layout / Animations
- Settings
- Sync (Trakt / SIMKL / remote)
- Downloads
- Other
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem statement
description: What problem are you trying to solve?
placeholder: "I want to be able to..."
validations:
required: true
- type: textarea
id: proposed
attributes:
label: Proposed solution
description: What would you like the app to do?
validations:
required: true
- type: dropdown
id: contribution_plan
attributes:
label: Are you planning to implement this yourself?
description: Major features are usually implemented in-house unless approved first.
options:
- No, this is only a proposal
- Maybe, but only if approved first
- Yes, but I understand implementation still needs maintainer approval
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered (optional)
description: Any workarounds or other approaches you considered.
validations:
required: false
- type: textarea
id: extra
attributes:
label: Additional context (optional)
description: Mockups, examples from other apps, etc.
validations:
required: false

View file

@ -1,43 +0,0 @@
## Summary
<!-- What changed in this PR? -->
## PR type
<!-- Pick one and delete the others -->
- Bug fix
- Small maintenance improvement
- Docs fix
- Approved larger change (link approval below)
## Why
<!-- Why this change is needed. Link bug/issue/context. -->
## Policy check
<!-- Confirm these before requesting review -->
- [ ] This PR is not cosmetic only.
- [ ] This PR does not add a new major feature without prior approval.
- [ ] This PR is small in scope and focused on one problem.
- [ ] If this is a larger or directional change, I linked the issue where it was approved.
<!-- PRs that do not match this policy will usually be closed without merge. -->
## Testing
<!-- What you tested and how (manual + automated). -->
- [ ] iOS tested
- [ ] Android tested
## Screenshots / Video (UI changes only)
<!-- If UI changed, add before/after screenshots or a short clip. -->
## Breaking changes
<!-- Any breaking behavior/config/schema changes? If none, write: None -->
## Linked issues
<!-- Example: Fixes #123 -->

39
.gitignore vendored
View file

@ -2,9 +2,6 @@
# dependencies
node_modules/
# Un-ignore specific react-native-video source files we patch
!node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
# Expo
@ -34,12 +31,8 @@ yarn-error.*
*.pem
# local env files
.env*.local
.env
# Sentry
ios/sentry.properties
android/sentry.properties
.env*.local
# typescript
*.tsbuildinfo
@ -54,7 +47,6 @@ android/build/
android/.gradle/
android/app/libs/*.aar
!android/app/libs/lib-decoder-ffmpeg-release.aar
!android/app/libs/libmpv-release.aar
HEATING_OPTIMIZATIONS.md
# sliderreadme.md
.cursor/mcp.json
@ -74,36 +66,9 @@ sliderreadme.md
bottomsheet.md
fastimage.md
## Backup directories
# Backup directories
backup_sdk54_upgrade/
SDK54_UPGRADE_SUMMARY.md
SDK54_UPGRADE_SUMMARY.md
build-and-publish-app-releases.sh
bottomnav.md
/TrailerServices
mmkv.md
fix-android-scroll-lag-summary.md
server/cache-server
server/campaign-manager
server/sync-service
carousal.md
node_modules
expofs.md
ios/sentry.properties
android/sentry.properties
Stremio addons refer
trakt-docs
trakt-docss
# Removed submodules (kept locally)
libmpv-android/
mpv-android/
mpvKt/
# Torrent libraries
LibTorrent/
iTorrent/
simkl-docss
downloader.md
server
Deliverables 2

View file

@ -1,3 +1,2 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View file

@ -1,10 +0,0 @@
# Nuvio Alpha Build 2
This is the second alpha release of Nuvio!
## What's New
- **Intro Submission:** You can now submit intro timestamps directly to IntroDB!
- **Bug Fixes:** Various improvements and stability fixes.
## Installation
Download the attached APK and install it on your Android device.

211
App.tsx
View file

@ -11,26 +11,21 @@ import {
StyleSheet,
I18nManager,
Platform,
LogBox,
Linking
LogBox
} from 'react-native';
import './src/i18n'; // Initialize i18n
import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import { Provider as PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { enableScreens, enableFreeze } from 'react-native-screens';
import AppNavigator, {
import { enableScreens } from 'react-native-screens';
import AppNavigator, {
CustomNavigationDarkTheme,
CustomDarkTheme
} from './src/navigation/AppNavigator';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import 'react-native-reanimated';
import { CatalogProvider } from './src/contexts/CatalogContext';
import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext';
import { SimklProvider } from './src/contexts/SimklContext';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { TrailerProvider } from './src/contexts/TrailerContext';
import { DownloadsProvider } from './src/contexts/DownloadsContext';
@ -39,54 +34,25 @@ import UpdatePopup from './src/components/UpdatePopup';
import MajorUpdateOverlay from './src/components/MajorUpdateOverlay';
import { useGithubMajorUpdate } from './src/hooks/useGithubMajorUpdate';
import { useUpdatePopup } from './src/hooks/useUpdatePopup';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Sentry from '@sentry/react-native';
import UpdateService from './src/services/updateService';
import { memoryMonitorService } from './src/services/memoryMonitorService';
import { aiService } from './src/services/aiService';
import { AccountProvider, useAccount } from './src/contexts/AccountContext';
import { ToastProvider } from './src/contexts/ToastContext';
import { mmkvStorage } from './src/services/mmkvStorage';
import { CampaignManager } from './src/components/promotions/CampaignManager';
import { isErrorReportingEnabledSync } from './src/services/telemetryService';
import { supabaseSyncService } from './src/services/supabaseSyncService';
// Initialize Sentry with privacy-first defaults
// Settings are loaded from telemetryService and can be controlled by user
// Note: Full dynamic control requires app restart as Sentry initializes at startup
Sentry.init({
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
// Privacy-first: Disable PII by default (IP address, cookies, user data)
// Users can opt-in via Privacy Settings if they choose
sendDefaultPii: false,
// Adds more context data to events (IP address, cookies, user, etc.)
// For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/
sendDefaultPii: true,
// Session Replay completely disabled by default for privacy
// This prevents screen recording without explicit user consent
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
// Only include feedback integration (user-initiated, not automatic)
// Configure Session Replay conservatively to avoid startup overhead in production
replaysSessionSampleRate: __DEV__ ? 0.1 : 0,
replaysOnErrorSampleRate: __DEV__ ? 1 : 0,
integrations: [Sentry.feedbackIntegration()],
// beforeSend hook to respect user's telemetry preferences
// Uses synchronous MMKV read to check preference immediately
beforeSend: (event) => {
// Check if error reporting is disabled (synchronous check)
if (!isErrorReportingEnabledSync()) {
// Drop the event - user has opted out
return null;
}
return event;
},
// beforeSendTransaction hook for performance monitoring
beforeSendTransaction: (event) => {
if (!isErrorReportingEnabledSync()) {
return null;
}
return event;
},
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: __DEV__,
});
@ -104,8 +70,6 @@ LogBox.ignoreLogs([
// This fixes many navigation layout issues by using native screen containers
enableScreens(true);
// Freeze non-focused screens to stop background re-renders
enableFreeze(true);
// Inner app component that uses the theme context
const ThemedApp = () => {
@ -115,12 +79,12 @@ const ThemedApp = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const engine = (global as any).HermesInternal ? 'Hermes' : 'JSC';
console.log('JS Engine:', engine);
} catch { }
} catch {}
}, []);
const { currentTheme } = useTheme();
const [isAppReady, setIsAppReady] = useState(false);
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null);
// Update popup functionality
const {
showUpdatePopup,
@ -133,84 +97,38 @@ const ThemedApp = () => {
// GitHub major/minor release overlay
const githubUpdate = useGithubMajorUpdate();
const [isDownloadingGitHub, setIsDownloadingGitHub] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const handleGithubUpdateAction = async () => {
console.log('handleGithubUpdateAction triggered. Release data exists:', !!githubUpdate.releaseData);
if (Platform.OS === 'android') {
setIsDownloadingGitHub(true);
setDownloadProgress(0);
try {
const { default: AndroidUpdateService } = await import('./src/services/androidUpdateService');
if (githubUpdate.releaseData) {
console.log('Calling AndroidUpdateService with:', githubUpdate.releaseData.tag_name);
const success = await AndroidUpdateService.downloadAndInstallUpdate(
githubUpdate.releaseData,
(progress) => {
setDownloadProgress(progress);
}
);
console.log('AndroidUpdateService result:', success);
if (!success) {
console.log('Update failed, falling back to browser');
// If download fails or no APK found, fallback to browser
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
}
} else if (githubUpdate.releaseUrl) {
console.log('No release data, falling back to browser');
Linking.openURL(githubUpdate.releaseUrl);
}
} catch (error) {
console.error('Failed to update via Android service', error);
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
} finally {
setIsDownloadingGitHub(false);
setDownloadProgress(0);
}
} else {
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
}
};
// Check onboarding status and initialize services
useEffect(() => {
const initializeApp = async () => {
try {
// Check onboarding status
const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding');
const onboardingCompleted = await AsyncStorage.getItem('hasCompletedOnboarding');
setHasCompletedOnboarding(onboardingCompleted === 'true');
// Initialize Supabase auth/session and start background sync.
// This is intentionally non-blocking for app startup UX.
supabaseSyncService
.initialize()
.then(() => supabaseSyncService.startupSync())
.catch((error) => {
console.warn('[App] Supabase sync bootstrap failed:', error);
});
// Initialize update service
await UpdateService.initialize();
// Initialize update service (skip on Android to prevent update checks)
if (Platform.OS !== 'android') {
await UpdateService.initialize();
}
// Initialize memory monitoring service to prevent OutOfMemoryError
memoryMonitorService; // Just accessing it starts the monitoring
console.log('Memory monitoring service initialized');
// Initialize AI service
await aiService.initialize();
console.log('AI service initialized');
} catch (error) {
console.error('Error initializing app:', error);
// Default to showing onboarding if we can't check
setHasCompletedOnboarding(false);
}
};
initializeApp();
}, []);
// Create custom themes based on current theme
const customDarkTheme = {
...CustomDarkTheme,
@ -219,7 +137,7 @@ const ThemedApp = () => {
primary: currentTheme.colors.primary,
}
};
const customNavigationTheme = {
...CustomNavigationDarkTheme,
colors: {
@ -234,44 +152,33 @@ const ThemedApp = () => {
const handleSplashComplete = () => {
setIsAppReady(true);
};
// Navigation reference
const navigationRef = React.useRef<any>(null);
// Don't render anything until we know the onboarding status
const shouldShowApp = isAppReady && hasCompletedOnboarding !== null;
const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding';
return (
<AccountProvider>
<PaperProvider theme={customDarkTheme}>
<NavigationContainer
ref={navigationRef}
<NavigationContainer
theme={customNavigationTheme}
linking={{
prefixes: ['nuvio://'],
config: {
screens: {
ScraperSettings: {
path: 'repo',
},
},
},
}}
linking={undefined}
>
<DownloadsProvider>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar style="light" />
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
<UpdatePopup
visible={showUpdatePopup}
updateInfo={updateInfo}
onUpdateNow={handleUpdateNow}
onUpdateLater={handleUpdateLater}
onDismiss={handleDismiss}
isInstalling={isInstalling}
/>
{Platform.OS === 'ios' && (
<UpdatePopup
visible={showUpdatePopup}
updateInfo={updateInfo}
onUpdateNow={handleUpdateNow}
onUpdateLater={handleUpdateLater}
onDismiss={handleDismiss}
isInstalling={isInstalling}
/>
)}
<MajorUpdateOverlay
visible={githubUpdate.visible}
latestTag={githubUpdate.latestTag}
@ -279,11 +186,7 @@ const ThemedApp = () => {
releaseUrl={githubUpdate.releaseUrl}
onDismiss={githubUpdate.onDismiss}
onLater={githubUpdate.onLater}
onUpdateAction={handleGithubUpdateAction}
isDownloading={isDownloadingGitHub}
downloadProgress={downloadProgress}
/>
<CampaignManager />
</View>
</DownloadsProvider>
</NavigationContainer>
@ -294,27 +197,19 @@ const ThemedApp = () => {
function App(): React.JSX.Element {
return (
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<GenreProvider>
<CatalogProvider>
<TraktProvider>
<SimklProvider>
<ThemeProvider>
<TrailerProvider>
<ToastProvider>
<ThemedApp />
</ToastProvider>
</TrailerProvider>
</ThemeProvider>
</SimklProvider>
</TraktProvider>
</CatalogProvider>
</GenreProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<GenreProvider>
<CatalogProvider>
<TraktProvider>
<ThemeProvider>
<TrailerProvider>
<ThemedApp />
</TrailerProvider>
</ThemeProvider>
</TraktProvider>
</CatalogProvider>
</GenreProvider>
</GestureHandlerRootView>
);
}
@ -324,4 +219,4 @@ const styles = StyleSheet.create({
},
});
export default Sentry.wrap(App);
export default Sentry.wrap(App);

View file

@ -1,80 +0,0 @@
# Contributing
Thanks for helping improve NuvioMobile.
## PR policy
Pull requests are currently intended for:
- Reproducible bug fixes
- Small stability improvements
- Minor maintenance work
- Small documentation fixes that improve accuracy
Pull requests are generally **not** accepted for:
- New major features
- Product direction changes
- Large UX / UI redesigns
- Cosmetic-only changes
- Refactors without a clear user-facing or maintenance benefit
For feature ideas and bigger changes, please open an issue first. Feature implementation is usually kept in-house unless it has been discussed and explicitly approved beforehand.
## Where to ask questions
- Use **Issues** for bugs, feature requests, setup help, and general support.
## Bug reports (rules)
To keep issues fixable, bug reports should include:
- App version or OTA update ID (Settings > App updates > Current version, hold to copy)
- Platform + device model + OS version (Android/iOS)
- Install method (release APK/IPA / Expo Go / built from source)
- Steps to reproduce (exact steps)
- Expected vs actual behavior
- Frequency (always/sometimes/once)
Logs are **optional**, but they help a lot for playback/crash issues.
### How to capture logs (optional)
If you can, reproduce the issue once, then attach a short log snippet from around the time it happened:
For Android:
```sh
adb logcat -d | tail -n 300
```
For iOS/Metro:
```sh
# Copy from your Metro bundler output or Xcode console
```
If the issue is a crash, also include any stack trace shown by Android Studio, Xcode, or `adb logcat`.
## Feature requests (rules)
Please include:
- The problem you are solving (use case)
- Your proposed solution
- Alternatives considered (if any)
Opening a feature request does **not** mean a pull request will be accepted for it. If the feature affects product scope, UX direction, or adds a significant new surface area, do not start implementation unless a maintainer explicitly approves it first.
## Before opening a PR
Please make sure your PR is all of the following:
- Small in scope
- Focused on one problem
- Clearly aligned with the current direction of the project
- Not cosmetic-only
- Not a new major feature unless it was discussed and approved first
PRs that do not fit this policy will usually be closed without merge so review time can stay focused on bugs, regressions, and small improvements.
## One issue per problem
Please open separate issues for separate bugs/features. It makes tracking, fixing, and closing issues much faster.

195
README.md
View file

@ -1,76 +1,175 @@
<!-- Improved compatibility of back to top link -->
<a id="readme-top"></a>
<!-- PROJECT SHIELDS -->
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![License][license-shield]][license-url]
<!-- PROJECT LOGO -->
<br />
<div align="center">
<img src="https://github.com/tapframe/NuvioTV/blob/main/assets/brand/app_logo_wordmark.png" alt="Nuvio" width="300" />
<br />
<br />
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![License][license-shield]][license-url]
<p>
A modern media hub for Android and iOS built with React Native and Expo.
<img src="assets/titlelogo.png" alt="Nuvio Logo" width="120" />
<h1 align="center">🎬 Nuvio Media Hub</h1>
<p align="center">
A modern media hub built with React Native and Expo
<br />
Stremio Addon ecosystem • Cross-platform
Stremio Addon ecosystem • Crossplatform • Offline metadata & sync
<br />
<br />
<a href="#getting-started"><strong>Get Started »</strong></a>
<br />
<br />
<a href="#demo">View Screenshots</a>
·
<a href="https://github.com/tapframe/NuvioStreaming/issues/new?labels=bug&template=bug_report.md">Report Bug</a>
·
<a href="https://github.com/tapframe/NuvioStreaming/issues/new?labels=enhancement&template=feature_request.md">Request Feature</a>
</p>
</div>
## About
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
</li>
<li><a href="#demo">Screenshots</a></li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#installation">Installation</a></li>
<li><a href="#build">Build</a></li>
</ul>
</li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#support">Support</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
<li><a href="#built-with">Built With</a></li>
</ol>
</details>
Nuvio Media Hub is a cross-platform app for managing and discovering media, with a playback-focused interface that can integrate with the Stremio addon ecosystem through user-installed extensions.
<!-- ABOUT THE PROJECT -->
## About The Project
## Installation
Nuvio Media Hub is a crossplatform app for managing, discovering, and streaming your media via a flexible addon ecosystem. Built with React Native + Expo, it integrates providers and sync services while keeping a simple, fast UI.
### Android
Download the latest APK from [GitHub Releases](https://github.com/tapframe/NuvioStreaming/releases/latest).
<!-- DEMO / SCREENSHOTS -->
## Demo
<a id="demo"></a>
### iOS
| Home | Details |
|:----:|:-------:|
| ![Home](screenshots/Simulator%20Screenshot%20-%20iPhone%2016%20Pro%20-%202025-08-27%20at%2021.08.32-portrait.png) | ![Details](screenshots/WhatsApp%20Image%202025-09-02%20at%2000.24.31-portrait.png) |
- [TestFlight](https://testflight.apple.com/join/QkKMGRqp)
- [AltStore](https://tinyurl.com/NuvioAltstore)
- [SideStore](https://tinyurl.com/NuvioSidestore)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
**Manual source:** `https://raw.githubusercontent.com/tapframe/NuvioStreaming/main/nuvio-source.json`
<!-- GETTING STARTED -->
## Getting Started
## Development
Follow the steps below to run the app locally.
### Installation
```bash
git clone https://github.com/tapframe/NuvioStreaming.git
cd NuvioStreaming
npm install --legacy-peer-deps
npx expo prebuild
npx expo run:android
# or
npx expo run:ios
npm install
# If you hit peer dependency conflicts:
# npm install --legacy-peer-deps
npx expo start
```
## Legal & DMCA
### Build
Nuvio functions solely as a client-side interface for browsing metadata and playing media provided by user-installed extensions and/or user-provided sources. It is intended for content the user owns or is otherwise authorized to access.
```bash
npx expo prebuild
npx expo run:android # Android
npx expo run:ios # iOS
```
Nuvio is not affiliated with any third-party extensions, catalogs, sources, or content providers. It does not host, store, or distribute any media content.
<details>
<summary>Alternative iOS Installation</summary>
For comprehensive legal information, including our full disclaimer, third-party extension policy, and DMCA/Copyright information, please visit our **[Legal & Disclaimer Page](https://nuvioapp.space/legal)**.
### AltStore
<img src="https://upload.wikimedia.org/wikipedia/commons/2/20/AltStore_logo.png" width="24" height="24" align="left"> [![Add to AltStore](https://img.shields.io/badge/Add%20to-AltStore-blue?style=for-the-badge)](https://tinyurl.com/NuvioAltstore)
### SideStore
<img src="https://github.com/SideStore/assets/blob/main/icon.png?raw=true" width="24" height="24" align="left"> [![Add to SideStore](https://img.shields.io/badge/Add%20to-SideStore-green?style=for-the-badge)](https://tinyurl.com/NuvioSidestore)
**Manual URL:** `https://raw.githubusercontent.com/tapframe/NuvioStreaming/main/nuvio-source.json`
</details>
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Contributing
Contributions make the opensource community amazing! Any contributions are greatly appreciated.
1. Fork the project
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Support
If you find Nuvio helpful, consider supporting development:
* **KoFi** `https://ko-fi.com/tapframe`
* **GitHub Star** Star the repo to show support
* **Share** Tell others about the project
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## License
Distributed under the GNU GPLv3 License. See `LICENSE` for more information.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Contact
**Project Links:**
* GitHub: `https://github.com/tapframe`
* Issues: `https://github.com/tapframe/NuvioStreaming/issues`
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Acknowledgments
* [React Native](https://reactnative.dev/)
* [Expo](https://expo.dev/)
* [TypeScript](https://www.typescriptlang.org/)
* Community contributors and testers
**Disclaimer:** This application functions as a media hub with addon/plugin support. It does not contain any builtin content or host media content. Content access is only available through userinstalled plugins and addons. Any legal concerns should be directed to the specific websites providing the content.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Built With
- React Native
- Expo
- TypeScript
<p align="left">
<a href="https://skillicons.dev">
<img src="https://skillicons.dev/icons?i=react,typescript,nodejs,expo,github,githubactions&theme=light&perline=6" />
</a>
<br/>
React Native • Expo • TypeScript
</p>
## Star History
<a href="https://www.star-history.com/#tapframe/NuvioStreaming&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&legend=top-left" />
</picture>
</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
[contributors-shield]: https://img.shields.io/github/contributors/tapframe/NuvioStreaming.svg?style=for-the-badge
@ -82,4 +181,4 @@ For comprehensive legal information, including our full disclaimer, third-party
[issues-shield]: https://img.shields.io/github/issues/tapframe/NuvioStreaming.svg?style=for-the-badge
[issues-url]: https://github.com/tapframe/NuvioStreaming/issues
[license-shield]: https://img.shields.io/github/license/tapframe/NuvioStreaming.svg?style=for-the-badge
[license-url]: http://www.gnu.org/licenses/gpl-3.0.en.html
[license-url]: http://www.gnu.org/licenses/gpl-3.0.en.html

View file

@ -81,6 +81,8 @@ def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBu
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
// apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")
android {
ndkVersion rootProject.ext.ndkVersion
@ -92,8 +94,8 @@ android {
applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 38
versionName "1.4.2"
versionCode 20
versionName "1.2.5"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
@ -115,7 +117,7 @@ android {
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseVersionCode = 38 // Current versionCode 38 from defaultConfig
def baseVersionCode = 20 // Current versionCode from defaultConfig
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def versionCode = baseVersionCode * 100 // Base multiplier
@ -182,14 +184,7 @@ android {
}
}
configurations.all {
exclude group: 'com.caverock', module: 'androidsvg'
}
dependencies {
// @generated begin react-native-google-cast-dependencies - expo prebuild (DO NOT MODIFY) sync-3822a3c86222e7aca74039b551612aab7e75365d
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
// @generated end react-native-google-cast-dependencies
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
@ -219,14 +214,4 @@ dependencies {
// Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3
implementation files("libs/lib-decoder-ffmpeg-release.aar")
// MPV Player library
implementation files("libs/libmpv-release.aar")
// Google Cast Framework
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
}
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

Binary file not shown.

View file

@ -1,8 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="dev.jdtech.mpv"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@ -16,14 +14,12 @@
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="com.reactnative.googlecast.GoogleCastOptionsProvider"/>
<meta-data android:name="com.reactnative.googlecast.RECEIVER_APPLICATION_ID" android:value="CC1AD845"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://ota.nuvioapp.space/api/manifest"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified" android:supportsPictureInPicture="true" android:resizeableActivity="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View file

@ -9,7 +9,6 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
import com.reactnative.googlecast.api.RNGCCastContext
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -18,12 +17,6 @@ class MainActivity : ReactActivity() {
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
super.onCreate(null)
// @generated begin react-native-google-cast-onCreate - expo prebuild (DO NOT MODIFY) sync-489050f2bf9933a98bbd9d93137016ae14c22faa
RNGCCastContext.getSharedInstance(this)
// @generated end react-native-google-cast-onCreate
// Initialize Google Cast context
RNGCCastContext.getSharedInstance(this)
}
/**

View file

@ -15,7 +15,6 @@ import com.facebook.react.defaults.DefaultReactNativeHost
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import com.nuvio.app.mpv.MpvPackage
class MainApplication : Application(), ReactApplication {
@ -25,7 +24,7 @@ class MainApplication : Application(), ReactApplication {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(com.nuvio.app.mpv.MpvPackage())
// add(MyReactNativePackage())
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"

View file

@ -1,633 +0,0 @@
package com.nuvio.app.mpv
import android.content.Context
import android.graphics.SurfaceTexture
import android.util.AttributeSet
import android.util.Log
import android.view.Surface
import android.view.TextureView
import dev.jdtech.mpv.MPVLib
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
class MPVView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextureView(context, attrs, defStyleAttr), TextureView.SurfaceTextureListener, MPVLib.EventObserver {
companion object {
private const val TAG = "MPVView"
}
private var isMpvInitialized = false
private var pendingDataSource: String? = null
private var isPaused: Boolean = true
private var surface: Surface? = null
private var httpHeaders: Map<String, String>? = null
// Decoder mode setting: 'auto', 'sw', 'hw', 'hw+' (default: auto)
var decoderMode: String = "auto"
// GPU mode setting: 'gpu', 'gpu-next' (default: gpu)
var gpuMode: String = "gpu"
// Flag to track if onLoad has been fired (prevents multiple fires for HLS streams)
private var hasLoadEventFired: Boolean = false
// Event listener for React Native
var onLoadCallback: ((duration: Double, width: Int, height: Int) -> Unit)? = null
var onProgressCallback: ((position: Double, duration: Double) -> Unit)? = null
var onEndCallback: (() -> Unit)? = null
var onErrorCallback: ((message: String) -> Unit)? = null
var onTracksChangedCallback: ((audioTracks: List<Map<String, Any>>, subtitleTracks: List<Map<String, Any>>) -> Unit)? = null
private var resumeOnForeground = false
private val lifeCycleListener = object : LifecycleEventListener {
override fun onHostPause() {
resumeOnForeground = !isPaused;
if(resumeOnForeground) {
Log.d(TAG, "App backgrounded — pausing MPV")
setPaused(true)
}
}
override fun onHostResume() {
if(resumeOnForeground) {
setPaused(false)
resumeOnForeground = false
}
}
override fun onHostDestroy() {}
}
init {
surfaceTextureListener = this
isOpaque = false
(context as? ReactContext)?.addLifecycleEventListener(lifeCycleListener)
}
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG, "Surface texture available: ${width}x${height}")
try {
surface = Surface(surfaceTexture)
MPVLib.create(context.applicationContext)
initOptions()
MPVLib.init()
MPVLib.attachSurface(surface!!)
MPVLib.addObserver(this)
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
observeProperties()
isMpvInitialized = true
// If a data source was set before surface was ready, load it now
// Headers are already applied in initOptions() before init()
pendingDataSource?.let { url ->
loadFile(url)
pendingDataSource = null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize MPV", e)
onErrorCallback?.invoke("MPV initialization failed: ${e.message}")
}
}
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG, "Surface texture size changed: ${width}x${height}")
if (isMpvInitialized) {
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
}
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
Log.d(TAG, "Surface texture destroyed")
(context as? ReactContext)?.removeLifecycleEventListener(lifeCycleListener)
if (isMpvInitialized) {
MPVLib.removeObserver(this)
MPVLib.detachSurface()
MPVLib.destroy()
isMpvInitialized = false
}
surface?.release()
surface = null
return true
}
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
// Called when the SurfaceTexture is updated via updateTexImage()
}
private fun initOptions() {
MPVLib.setOptionString("profile", "fast")
// GPU rendering mode (gpu or gpu-next)
MPVLib.setOptionString("vo", gpuMode)
MPVLib.setOptionString("gpu-context", "android")
MPVLib.setOptionString("opengl-es", "yes")
// Decoder mode mapping (same as mpvKt)
val hwdecValue = when (decoderMode) {
"auto" -> "auto-copy" // Best balance: HW decode, copy to CPU for filters
"sw" -> "no" // Software decoding only
"hw" -> "mediacodec-copy" // HW decode with copy (safer)
"hw+" -> "mediacodec" // Full HW decode (fastest, may have issues)
else -> "auto-copy"
}
Log.d(TAG, "Decoder mode: $decoderMode, hwdec value: $hwdecValue, GPU mode: $gpuMode")
MPVLib.setOptionString("hwdec", hwdecValue)
// Note: Not setting hwdec-codecs explicitly - let mpv use defaults
MPVLib.setOptionString("target-colorspace-hint", "yes")
// HDR and Dolby Vision support
// target-prim: Signal target display primaries (auto = passthrough when display supports)
MPVLib.setOptionString("target-prim", "auto")
// target-trc: Signal target transfer characteristics (auto = passthrough when display supports)
MPVLib.setOptionString("target-trc", "auto")
// tone-mapping: How to handle HDR/DV content on SDR displays (auto = best automatic choice)
MPVLib.setOptionString("tone-mapping", "auto")
// hdr-compute-peak: Compute peak brightness for better tone mapping
MPVLib.setOptionString("hdr-compute-peak", "auto")
// Allow DV Profile 5 (HEVC with RPU) to be decoded by hardware decoder
MPVLib.setOptionString("vd-lavc-o", "strict=-2")
// Workaround for https://github.com/mpv-player/mpv/issues/14651
MPVLib.setOptionString("vd-lavc-film-grain", "cpu")
MPVLib.setOptionString("ao", "audiotrack,opensles")
// Limit demuxer cache based on Android version (like mpvKt)
val cacheMegs = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) 64 else 32
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
MPVLib.setOptionString("cache", "yes")
MPVLib.setOptionString("cache-secs", "30")
MPVLib.setOptionString("network-timeout", "60")
MPVLib.setOptionString("ytdl", "no")
applyHttpHeadersAsOptions()
MPVLib.setOptionString("tls-verify", "no")
MPVLib.setOptionString("http-reconnect", "yes")
MPVLib.setOptionString("stream-reconnect", "yes")
MPVLib.setOptionString("demuxer-lavf-o", "live_start_index=0,prefer_x_start=1,http_persistent=1")
MPVLib.setOptionString("demuxer-seekable-cache", "yes")
MPVLib.setOptionString("force-seekable", "yes")
MPVLib.setOptionString("sub-auto", "fuzzy")
MPVLib.setOptionString("sub-visibility", "yes")
MPVLib.setOptionString("sub-font-size", "48")
MPVLib.setOptionString("sub-pos", "100")
MPVLib.setOptionString("sub-color", "#FFFFFFFF")
MPVLib.setOptionString("sub-border-size", "3")
MPVLib.setOptionString("sub-border-color", "#FF000000")
MPVLib.setOptionString("sub-shadow-offset", "2")
MPVLib.setOptionString("sub-shadow-color", "#80000000")
MPVLib.setOptionString("osd-fonts-dir", "/system/fonts")
MPVLib.setOptionString("sub-fonts-dir", "/system/fonts")
MPVLib.setOptionString("sub-font", "Roboto")
MPVLib.setOptionString("embeddedfonts", "yes")
MPVLib.setOptionString("sub-codepage", "auto")
MPVLib.setOptionString("blend-subtitles", "no")
MPVLib.setOptionString("sub-use-margins", "yes")
MPVLib.setOptionString("sub-ass-override", "force")
MPVLib.setOptionString("sub-scale", "1.0")
MPVLib.setOptionString("sub-fix-timing", "yes")
MPVLib.setOptionString("osc", "no")
MPVLib.setOptionString("osd-level", "1")
MPVLib.setOptionString("sid", "auto")
MPVLib.setOptionString("terminal", "no")
MPVLib.setOptionString("input-default-bindings", "no")
}
private fun observeProperties() {
// MPV format constants (from MPVLib source)
val MPV_FORMAT_NONE = 0
val MPV_FORMAT_FLAG = 3
val MPV_FORMAT_INT64 = 4
val MPV_FORMAT_DOUBLE = 5
MPVLib.observeProperty("time-pos", MPV_FORMAT_DOUBLE)
MPVLib.observeProperty("duration/full", MPV_FORMAT_DOUBLE) // Use /full for complete HLS duration
MPVLib.observeProperty("pause", MPV_FORMAT_FLAG)
MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG)
MPVLib.observeProperty("eof-reached", MPV_FORMAT_FLAG)
MPVLib.observeProperty("video-params/aspect", MPV_FORMAT_DOUBLE)
MPVLib.observeProperty("width", MPV_FORMAT_INT64)
MPVLib.observeProperty("height", MPV_FORMAT_INT64)
MPVLib.observeProperty("track-list", MPV_FORMAT_NONE)
// Observe subtitle properties for debugging
MPVLib.observeProperty("sid", MPV_FORMAT_INT64)
MPVLib.observeProperty("sub-visibility", MPV_FORMAT_FLAG)
MPVLib.observeProperty("sub-text", MPV_FORMAT_NONE)
}
private fun loadFile(url: String) {
Log.d(TAG, "Loading file: $url")
// Reset load event flag for new file
hasLoadEventFired = false
// Re-apply headers before loading to ensure segments/keys use the correct headers
applyHttpHeadersAsOptions()
MPVLib.command(arrayOf("loadfile", url))
}
// Public API
fun setDataSource(url: String) {
if (isMpvInitialized) {
// Headers were already set during initialization in initOptions()
loadFile(url)
} else {
pendingDataSource = url
}
}
fun setHeaders(headers: Map<String, String>?) {
httpHeaders = headers
Log.d(TAG, "Headers set: $headers")
if (isMpvInitialized) {
applyHttpHeadersAsOptions()
}
}
private fun applyHttpHeadersAsOptions() {
// Find User-Agent (case-insensitive)
val userAgentKey = httpHeaders?.keys?.find { it.equals("User-Agent", ignoreCase = true) }
val userAgent = userAgentKey?.let { httpHeaders?.get(it) }
?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
Log.d(TAG, "Setting User-Agent: $userAgent")
if (isMpvInitialized) {
MPVLib.setPropertyString("user-agent", userAgent)
} else {
MPVLib.setOptionString("user-agent", userAgent)
}
httpHeaders?.let { headers ->
val otherHeaders = headers.filterKeys { !it.equals("User-Agent", ignoreCase = true) }
if (otherHeaders.isNotEmpty()) {
// Use newline separator for http-header-fields as it's the standard for mpv
val headerString = otherHeaders.map { (key, value) -> "$key: $value" }.joinToString("\n")
Log.d(TAG, "Setting additional headers:\n$headerString")
if (isMpvInitialized) {
MPVLib.setPropertyString("http-header-fields", headerString)
} else {
MPVLib.setOptionString("http-header-fields", headerString)
}
} else if (isMpvInitialized) {
MPVLib.setPropertyString("http-header-fields", "")
}
}
}
fun setPaused(paused: Boolean) {
isPaused = paused
if (isMpvInitialized) {
MPVLib.setPropertyBoolean("pause", paused)
}
}
fun seekTo(positionSeconds: Double) {
Log.d(TAG, "seekTo called: positionSeconds=$positionSeconds, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
Log.d(TAG, "Executing MPV seek command: seek $positionSeconds absolute")
MPVLib.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
}
}
fun setSpeed(speed: Double) {
if (isMpvInitialized) {
MPVLib.setPropertyDouble("speed", speed)
}
}
fun setVolume(volume: Double) {
if (isMpvInitialized) {
// MPV volume is 0-100
MPVLib.setPropertyDouble("volume", volume * 100.0)
}
}
fun setAudioTrack(trackId: Int) {
if (isMpvInitialized) {
if (trackId == -1) {
MPVLib.setPropertyString("aid", "no")
} else {
MPVLib.setPropertyInt("aid", trackId)
}
}
}
fun setSubtitleTrack(trackId: Int) {
Log.d(TAG, "setSubtitleTrack called: trackId=$trackId, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
if (trackId == -1) {
Log.d(TAG, "Disabling subtitles (sid=no)")
MPVLib.setPropertyString("sid", "no")
MPVLib.setPropertyString("sub-visibility", "no")
} else {
Log.d(TAG, "Setting subtitle track to: $trackId")
MPVLib.setPropertyInt("sid", trackId)
// Ensure subtitles are visible
MPVLib.setPropertyString("sub-visibility", "yes")
// Debug: Verify the subtitle was set correctly
val currentSid = MPVLib.getPropertyInt("sid")
val subVisibility = MPVLib.getPropertyString("sub-visibility")
val subDelay = MPVLib.getPropertyDouble("sub-delay")
val subScale = MPVLib.getPropertyDouble("sub-scale")
Log.d(TAG, "After setting - sid=$currentSid, sub-visibility=$subVisibility, sub-delay=$subDelay, sub-scale=$subScale")
}
}
}
fun setResizeMode(mode: String) {
Log.d(TAG, "setResizeMode called: mode=$mode, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
when (mode) {
"contain" -> {
// Letterbox - show entire video with black bars
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
"cover" -> {
// Fill/crop - zoom to fill, cropping edges
MPVLib.setPropertyDouble("panscan", 1.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
"stretch" -> {
// Stretch - disable aspect ratio
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "no")
}
else -> {
// Default to contain
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
}
}
}
// Subtitle Styling Methods
fun setSubtitleSize(size: Int) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle size: $size")
MPVLib.setPropertyInt("sub-font-size", size)
}
}
fun setSubtitleColor(color: String) {
if (isMpvInitialized) {
// MPV expects color in #AARRGGBB format, but we receive #RRGGBB
// Convert to MPV format with full opacity
val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color
Log.d(TAG, "Setting subtitle color: $mpvColor")
MPVLib.setPropertyString("sub-color", mpvColor)
}
}
fun setSubtitleBackgroundColor(color: String, opacity: Float) {
if (isMpvInitialized) {
// Convert opacity (0-1) to hex (00-FF)
val alphaHex = (opacity * 255).toInt().coerceIn(0, 255).let {
String.format("%02X", it)
}
// MPV format: #AARRGGBB
val baseColor = if (color.startsWith("#")) color.substring(1) else color
val mpvColor = "#${alphaHex}${baseColor.takeLast(6)}"
Log.d(TAG, "Setting subtitle background: $mpvColor (opacity: $opacity)")
MPVLib.setPropertyString("sub-back-color", mpvColor)
}
}
fun setSubtitleBorderSize(size: Int) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle border size: $size")
MPVLib.setPropertyInt("sub-border-size", size)
}
}
fun setSubtitleBorderColor(color: String) {
if (isMpvInitialized) {
val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color
Log.d(TAG, "Setting subtitle border color: $mpvColor")
MPVLib.setPropertyString("sub-border-color", mpvColor)
}
}
fun setSubtitleShadow(enabled: Boolean, offset: Int) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle shadow: enabled=$enabled, offset=$offset")
if (enabled) {
MPVLib.setPropertyInt("sub-shadow-offset", offset)
MPVLib.setPropertyString("sub-shadow-color", "#80000000")
} else {
MPVLib.setPropertyInt("sub-shadow-offset", 0)
}
}
}
fun setSubtitlePosition(pos: Int) {
if (isMpvInitialized) {
// sub-pos: 0=top, 100=bottom, can go beyond 100 for more offset
// UI sends bottomOffset (0=at bottom, higher=more up from bottom)
// Convert: MPV pos = 100 - (bottomOffset / screenHeightFactor)
// Simplified: just pass pos directly, UI should convert
Log.d(TAG, "Setting subtitle position: $pos")
MPVLib.setPropertyInt("sub-pos", pos)
}
}
fun setSubtitleDelay(delaySec: Double) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle delay: $delaySec seconds")
MPVLib.setPropertyDouble("sub-delay", delaySec)
}
}
fun setSubtitleScale(scale: Double) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle scale: $scale")
MPVLib.setPropertyDouble("sub-scale", scale)
}
}
fun setSubtitleAlignment(align: String) {
if (isMpvInitialized) {
// MPV sub-justify values: left, center, right, auto
val mpvAlign = when (align) {
"left" -> "left"
"right" -> "right"
"center" -> "center"
else -> "center"
}
Log.d(TAG, "Setting subtitle alignment: $mpvAlign")
MPVLib.setPropertyString("sub-justify", mpvAlign)
}
}
fun setSubtitleBold(bold: Boolean) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle bold: $bold")
MPVLib.setPropertyString("sub-bold", if (bold) "yes" else "no")
}
}
fun setSubtitleItalic(italic: Boolean) {
if (isMpvInitialized) {
Log.d(TAG, "Setting subtitle italic: $italic")
MPVLib.setPropertyString("sub-italic", if (italic) "yes" else "no")
}
}
// MPVLib.EventObserver implementation
override fun eventProperty(property: String) {
Log.d(TAG, "Property changed: $property")
when (property) {
"track-list" -> {
// Parse track list and notify React Native
parseAndSendTracks()
}
}
}
private fun parseAndSendTracks() {
try {
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: 0
Log.d(TAG, "Track count: $trackCount")
val audioTracks = mutableListOf<Map<String, Any>>()
val subtitleTracks = mutableListOf<Map<String, Any>>()
for (i in 0 until trackCount) {
val type = MPVLib.getPropertyString("track-list/$i/type") ?: continue
val id = MPVLib.getPropertyInt("track-list/$i/id") ?: continue
val title = MPVLib.getPropertyString("track-list/$i/title") ?: ""
val lang = MPVLib.getPropertyString("track-list/$i/lang") ?: ""
val codec = MPVLib.getPropertyString("track-list/$i/codec") ?: ""
val trackName = when {
title.isNotEmpty() -> title
lang.isNotEmpty() -> lang.uppercase()
else -> "Track $id"
}
val track = mapOf(
"id" to id,
"name" to trackName,
"language" to lang,
"codec" to codec
)
when (type) {
"audio" -> {
Log.d(TAG, "Found audio track: $track")
audioTracks.add(track)
}
"sub" -> {
Log.d(TAG, "Found subtitle track: $track")
subtitleTracks.add(track)
}
}
}
Log.d(TAG, "Sending tracks - Audio: ${audioTracks.size}, Subtitles: ${subtitleTracks.size}")
onTracksChangedCallback?.invoke(audioTracks, subtitleTracks)
} catch (e: Exception) {
Log.e(TAG, "Error parsing tracks", e)
}
}
override fun eventProperty(property: String, value: Long) {
Log.d(TAG, "Property $property = $value (Long)")
}
override fun eventProperty(property: String, value: Double) {
Log.d(TAG, "Property $property = $value (Double)")
when (property) {
"time-pos" -> {
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
onProgressCallback?.invoke(value, duration)
}
"duration/full", "duration" -> {
// Only fire onLoad once when video dimensions are available
// For HLS streams, duration updates incrementally as segments are fetched
if (!hasLoadEventFired) {
val width = MPVLib.getPropertyInt("width") ?: 0
val height = MPVLib.getPropertyInt("height") ?: 0
// Wait until we have valid dimensions before firing onLoad
if (width > 0 && height > 0 && value > 0) {
hasLoadEventFired = true
Log.d(TAG, "Firing onLoad event: duration=$value, width=$width, height=$height")
onLoadCallback?.invoke(value, width, height)
}
}
}
}
}
override fun eventProperty(property: String, value: Boolean) {
Log.d(TAG, "Property $property = $value (Boolean)")
when (property) {
"eof-reached" -> {
if (value) {
onEndCallback?.invoke()
}
}
}
}
override fun eventProperty(property: String, value: String) {
Log.d(TAG, "Property $property = $value (String)")
}
override fun event(eventId: Int) {
Log.d(TAG, "Event: $eventId")
// MPV event constants (from MPVLib source)
val MPV_EVENT_FILE_LOADED = 8
val MPV_EVENT_END_FILE = 7
when (eventId) {
MPV_EVENT_FILE_LOADED -> {
// File is loaded, start playback if not paused
if (!isPaused) {
MPVLib.setPropertyBoolean("pause", false)
}
}
MPV_EVENT_END_FILE -> {
Log.d(TAG, "MPV_EVENT_END_FILE")
// Heuristic: If duration is effectively 0 at end of file, it's a load error
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
val timePos = MPVLib.getPropertyDouble("time-pos") ?: 0.0
val eofReached = MPVLib.getPropertyBoolean("eof-reached") ?: false
Log.d(TAG, "End stats - Duration: $duration, Time: $timePos, EOF: $eofReached")
if (duration < 1.0 && !eofReached) {
val customError = "Unable to play media. Source may be unreachable."
Log.e(TAG, "Playback error detected (heuristic): $customError")
onErrorCallback?.invoke(customError)
} else {
onEndCallback?.invoke()
}
}
}
}
}

View file

@ -1,16 +0,0 @@
package com.nuvio.app.mpv
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class MpvPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(MpvPlayerViewManager(reactContext))
}
}

View file

@ -1,241 +0,0 @@
package com.nuvio.app.mpv
import android.graphics.Color
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter
class MpvPlayerViewManager(
private val reactContext: ReactApplicationContext
) : SimpleViewManager<MPVView>() {
companion object {
const val REACT_CLASS = "MpvPlayer"
// Commands
const val COMMAND_SEEK = 1
const val COMMAND_SET_AUDIO_TRACK = 2
const val COMMAND_SET_SUBTITLE_TRACK = 3
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(context: ThemedReactContext): MPVView {
val view = MPVView(context)
// Note: Do NOT set background color - it will block the SurfaceView content
// Set up event callbacks
view.onLoadCallback = { duration, width, height ->
val event = Arguments.createMap().apply {
putDouble("duration", duration)
putInt("width", width)
putInt("height", height)
}
sendEvent(context, view.id, "onLoad", event)
}
view.onProgressCallback = { position, duration ->
val event = Arguments.createMap().apply {
putDouble("currentTime", position)
putDouble("duration", duration)
}
sendEvent(context, view.id, "onProgress", event)
}
view.onEndCallback = {
sendEvent(context, view.id, "onEnd", Arguments.createMap())
}
view.onErrorCallback = { message ->
val event = Arguments.createMap().apply {
putString("error", message)
}
sendEvent(context, view.id, "onError", event)
}
view.onTracksChangedCallback = { audioTracks, subtitleTracks ->
val event = Arguments.createMap().apply {
val audioArray = Arguments.createArray()
audioTracks.forEach { track ->
val trackMap = Arguments.createMap().apply {
putInt("id", track["id"] as Int)
putString("name", track["name"] as String)
putString("language", track["language"] as String)
putString("codec", track["codec"] as String)
}
audioArray.pushMap(trackMap)
}
putArray("audioTracks", audioArray)
val subtitleArray = Arguments.createArray()
subtitleTracks.forEach { track ->
val trackMap = Arguments.createMap().apply {
putInt("id", track["id"] as Int)
putString("name", track["name"] as String)
putString("language", track["language"] as String)
putString("codec", track["codec"] as String)
}
subtitleArray.pushMap(trackMap)
}
putArray("subtitleTracks", subtitleArray)
}
sendEvent(context, view.id, "onTracksChanged", event)
}
return view
}
private fun sendEvent(context: ThemedReactContext, viewId: Int, eventName: String, params: com.facebook.react.bridge.WritableMap) {
context.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(viewId, eventName, params)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return MapBuilder.builder<String, Any>()
.put("onLoad", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onLoad")))
.put("onProgress", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onProgress")))
.put("onEnd", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onEnd")))
.put("onError", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onError")))
.put("onTracksChanged", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onTracksChanged")))
.build()
}
override fun getCommandsMap(): Map<String, Int> {
return MapBuilder.of(
"seek", COMMAND_SEEK,
"setAudioTrack", COMMAND_SET_AUDIO_TRACK,
"setSubtitleTrack", COMMAND_SET_SUBTITLE_TRACK
)
}
override fun receiveCommand(view: MPVView, commandId: String?, args: ReadableArray?) {
android.util.Log.d("MpvPlayerViewManager", "receiveCommand: $commandId, args: $args")
when (commandId) {
"seek" -> {
val position = args?.getDouble(0)
android.util.Log.d("MpvPlayerViewManager", "Seek command received: position=$position")
position?.let { view.seekTo(it) }
}
"setAudioTrack" -> {
args?.getInt(0)?.let { view.setAudioTrack(it) }
}
"setSubtitleTrack" -> {
args?.getInt(0)?.let { view.setSubtitleTrack(it) }
}
}
}
// React Props
@ReactProp(name = "source")
fun setSource(view: MPVView, source: String?) {
source?.let { view.setDataSource(it) }
}
@ReactProp(name = "paused")
fun setPaused(view: MPVView, paused: Boolean) {
view.setPaused(paused)
}
@ReactProp(name = "volume", defaultFloat = 1.0f)
fun setVolume(view: MPVView, volume: Float) {
view.setVolume(volume.toDouble())
}
@ReactProp(name = "rate", defaultFloat = 1.0f)
fun setRate(view: MPVView, rate: Float) {
view.setSpeed(rate.toDouble())
}
// Handle backgroundColor prop to prevent crash from React Native style system
@ReactProp(name = "backgroundColor", customType = "Color")
fun setBackgroundColor(view: MPVView, color: Int?) {
// Intentionally ignoring - background color would block the TextureView content
// Leave the view transparent
}
@ReactProp(name = "resizeMode")
fun setResizeMode(view: MPVView, resizeMode: String?) {
view.setResizeMode(resizeMode ?: "contain")
}
@ReactProp(name = "headers")
fun setHeaders(view: MPVView, headers: com.facebook.react.bridge.ReadableMap?) {
if (headers != null) {
val headerMap = mutableMapOf<String, String>()
val iterator = headers.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
headers.getString(key)?.let { value ->
headerMap[key] = value
}
}
view.setHeaders(headerMap)
} else {
view.setHeaders(null)
}
}
@ReactProp(name = "decoderMode")
fun setDecoderMode(view: MPVView, decoderMode: String?) {
view.decoderMode = decoderMode ?: "auto"
}
@ReactProp(name = "gpuMode")
fun setGpuMode(view: MPVView, gpuMode: String?) {
view.gpuMode = gpuMode ?: "gpu"
}
// Subtitle Styling Props
@ReactProp(name = "subtitleSize", defaultInt = 48)
fun setSubtitleSize(view: MPVView, size: Int) {
view.setSubtitleSize(size)
}
@ReactProp(name = "subtitleColor")
fun setSubtitleColor(view: MPVView, color: String?) {
view.setSubtitleColor(color ?: "#FFFFFF")
}
@ReactProp(name = "subtitleBackgroundOpacity", defaultFloat = 0.0f)
fun setSubtitleBackgroundOpacity(view: MPVView, opacity: Float) {
// Black background with user-specified opacity
view.setSubtitleBackgroundColor("#000000", opacity)
}
@ReactProp(name = "subtitleBorderSize", defaultInt = 3)
fun setSubtitleBorderSize(view: MPVView, size: Int) {
view.setSubtitleBorderSize(size)
}
@ReactProp(name = "subtitleBorderColor")
fun setSubtitleBorderColor(view: MPVView, color: String?) {
view.setSubtitleBorderColor(color ?: "#000000")
}
@ReactProp(name = "subtitleShadowEnabled", defaultBoolean = true)
fun setSubtitleShadowEnabled(view: MPVView, enabled: Boolean) {
view.setSubtitleShadow(enabled, if (enabled) 2 else 0)
}
@ReactProp(name = "subtitlePosition", defaultInt = 100)
fun setSubtitlePosition(view: MPVView, pos: Int) {
view.setSubtitlePosition(pos)
}
@ReactProp(name = "subtitleDelay", defaultFloat = 0.0f)
fun setSubtitleDelay(view: MPVView, delay: Float) {
view.setSubtitleDelay(delay.toDouble())
}
@ReactProp(name = "subtitleAlignment")
fun setSubtitleAlignment(view: MPVView, align: String?) {
view.setSubtitleAlignment(align ?: "center")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -3,5 +3,5 @@
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
<string name="expo_runtime_version">1.4.2</string>
<string name="expo_runtime_version">1.2.5</string>
</resources>

View file

@ -1,14 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 35
castFrameworkVersion = "22.1.0"
ndkVersion = "29.0.14206865" // Required for libmpv AAR built with NDK r29
}
repositories {
google()
mavenCentral()

View file

@ -12,9 +12,6 @@
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# Enable Gradle Build Cache
org.gradle.caching=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
"version": "1.4.2",
"version": "1.2.5",
"orientation": "default",
"backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
@ -10,43 +10,32 @@
"scheme": "nuvio",
"newArchEnabled": true,
"splash": {
"image": "./src/assets/splash-icon-new.png",
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#020404"
},
"ios": {
"supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"buildNumber": "38",
"buildNumber": "20",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
},
"NSBonjourServices": [
"_http._tcp",
"_googlecast._tcp",
"_CC1AD845._googlecast._tcp"
"_http._tcp"
],
"NSLocalNetworkUsageDescription": "Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers.",
"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,
"LSApplicationQueriesSchemes": [
"vlc",
"vlc-x-callback",
"infuse",
"outplayer",
"open-vidhub",
"livecontainer"
]
"UIFileSharingEnabled": true
},
"bundleIdentifier": "com.nuvio.hub",
"bundleIdentifier": "com.nuvio.app",
"associatedDomains": [],
"jsEngine": "hermes",
"appleTeamId": "8QBDZ766S3"
"jsEngine": "hermes"
},
"android": {
"adaptiveIcon": {
@ -56,11 +45,10 @@
"icon": "./assets/android/mipmap-xxxhdpi/ic_launcher.png",
"permissions": [
"INTERNET",
"WAKE_LOCK",
"android.permission.WRITE_SETTINGS"
"WAKE_LOCK"
],
"package": "com.nuvio.app",
"versionCode": 38,
"versionCode": 20,
"architectures": [
"arm64-v8a",
"armeabi-v7a",
@ -69,6 +57,7 @@
],
"jsEngine": "hermes"
},
"extra": {
"eas": {
"projectId": "909107b8-fe61-45ce-b02f-b02510d306a6"
@ -76,7 +65,6 @@
},
"owner": "nayifleo",
"plugins": [
"expo-live-activity",
[
"@sentry/react-native/expo",
{
@ -85,12 +73,6 @@
"organization": "tapframe"
}
],
[
"@kesha-antonov/react-native-background-downloader",
{
"skipMmkvDependency": true
}
],
"expo-localization",
[
"expo-updates",
@ -98,21 +80,21 @@
"username": "nayifleo"
}
],
"react-native-bottom-tabs",
[
"react-native-google-cast",
"expo-libvlc-player",
{
"receiverAppId": "CC1AD845",
"iosStartDiscoveryAfterFirstTapOnCastButton": true
"localNetworkPermission": "Allow $(PRODUCT_NAME) to access your local network",
"supportsBackgroundPlayback": true
}
]
],
"react-native-bottom-tabs"
],
"updates": {
"enabled": true,
"checkAutomatically": "ON_ERROR_RECOVERY",
"fallbackToCacheTimeout": 30000,
"url": "https://ota.nuvioapp.space/api/manifest"
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
},
"runtimeVersion": "1.4.2"
"runtimeVersion": "1.2.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
<color name="ic_launcher_background">#151515</color>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -1,128 +1,128 @@
{
"images": [
"images":[
{
"idiom": "iphone",
"size": "20x20",
"scale": "2x",
"filename": "Icon-App-20x20@2x.png"
"idiom":"iphone",
"size":"20x20",
"scale":"2x",
"filename":"Icon-App-20x20@2x.png"
},
{
"idiom": "iphone",
"size": "20x20",
"scale": "3x",
"filename": "Icon-App-20x20@3x.png"
"idiom":"iphone",
"size":"20x20",
"scale":"3x",
"filename":"Icon-App-20x20@3x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "1x",
"filename": "Icon-App-29x29@1x.png"
"idiom":"iphone",
"size":"29x29",
"scale":"1x",
"filename":"Icon-App-29x29@1x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "2x",
"filename": "Icon-App-29x29@2x.png"
"idiom":"iphone",
"size":"29x29",
"scale":"2x",
"filename":"Icon-App-29x29@2x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "3x",
"filename": "Icon-App-29x29@3x.png"
"idiom":"iphone",
"size":"29x29",
"scale":"3x",
"filename":"Icon-App-29x29@3x.png"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "2x",
"filename": "Icon-App-40x40@2x.png"
"idiom":"iphone",
"size":"40x40",
"scale":"2x",
"filename":"Icon-App-40x40@2x.png"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "3x",
"filename": "Icon-App-40x40@3x.png"
"idiom":"iphone",
"size":"40x40",
"scale":"3x",
"filename":"Icon-App-40x40@3x.png"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "2x",
"filename": "Icon-App-60x60@2x.png"
"idiom":"iphone",
"size":"60x60",
"scale":"2x",
"filename":"Icon-App-60x60@2x.png"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "3x",
"filename": "Icon-App-60x60@3x.png"
"idiom":"iphone",
"size":"60x60",
"scale":"3x",
"filename":"Icon-App-60x60@3x.png"
},
{
"idiom": "iphone",
"size": "76x76",
"scale": "2x",
"filename": "Icon-App-76x76@2x.png"
"idiom":"iphone",
"size":"76x76",
"scale":"2x",
"filename":"Icon-App-76x76@2x.png"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "1x",
"filename": "Icon-App-20x20@1x.png"
"idiom":"ipad",
"size":"20x20",
"scale":"1x",
"filename":"Icon-App-20x20@1x.png"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "2x",
"filename": "Icon-App-20x20@2x.png"
"idiom":"ipad",
"size":"20x20",
"scale":"2x",
"filename":"Icon-App-20x20@2x.png"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "1x",
"filename": "Icon-App-29x29@1x.png"
"idiom":"ipad",
"size":"29x29",
"scale":"1x",
"filename":"Icon-App-29x29@1x.png"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "2x",
"filename": "Icon-App-29x29@2x.png"
"idiom":"ipad",
"size":"29x29",
"scale":"2x",
"filename":"Icon-App-29x29@2x.png"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "1x",
"filename": "Icon-App-40x40@1x.png"
"idiom":"ipad",
"size":"40x40",
"scale":"1x",
"filename":"Icon-App-40x40@1x.png"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "2x",
"filename": "Icon-App-40x40@2x.png"
"idiom":"ipad",
"size":"40x40",
"scale":"2x",
"filename":"Icon-App-40x40@2x.png"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "1x",
"filename": "Icon-App-76x76@1x.png"
"idiom":"ipad",
"size":"76x76",
"scale":"1x",
"filename":"Icon-App-76x76@1x.png"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "2x",
"filename": "Icon-App-76x76@2x.png"
"idiom":"ipad",
"size":"76x76",
"scale":"2x",
"filename":"Icon-App-76x76@2x.png"
},
{
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x",
"filename": "Icon-App-83.5x83.5@2x.png"
"idiom":"ipad",
"size":"83.5x83.5",
"scale":"2x",
"filename":"Icon-App-83.5x83.5@2x.png"
},
{
"size": "1024x1024",
"idiom": "ios-marketing",
"scale": "1x",
"filename": "ItunesArtwork@2x.png"
"size" : "1024x1024",
"idiom" : "ios-marketing",
"scale" : "1x",
"filename" : "ItunesArtwork@2x.png"
}
],
"info": {
"version": 1,
"author": "easyappicon"
"info":{
"version":1,
"author":"easyappicon"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Some files were not shown because too many files have changed in this diff Show more