From af871aee94c988a814252e5357d0e157ee3fdad3 Mon Sep 17 00:00:00 2001 From: SunnyVishnu3 Date: Sat, 23 May 2026 22:00:45 +0530 Subject: [PATCH 01/16] EXOPLAYER SUPPORT ported from oneplayer nextlib from https://github.com/FoxNick/nextlib --- app/build.gradle.kts | 19 + app/src/main/AndroidManifest.xml | 18 + app/src/main/java/app/gyrolet/mpvrx/App.kt | 1 + .../mpvrx/preferences/PlayerPreferences.kt | 2 + .../java/app/gyrolet/mpvrx/ui/icons/Icons.kt | 5 + .../mpvrx/ui/preferences/PreferencesScreen.kt | 373 ++++++-- .../gyrolet/mpvrx/utils/media/MediaUtils.kt | 16 +- .../mpvrx/exoplayer/ExoPlayerActivity.kt | 873 ++++++++++++++++++ .../mpvrx/exoplayer/MediaSynchronizer.kt | 8 + .../app/gyrolet/mpvrx/exoplayer/PlayerApi.kt | 72 ++ .../repository/FolderPlaybackAnchorKey.kt | 28 + .../repository/LocalMediaRepositoryImpl.kt | 225 +++++ .../repository/LocalSubtitleFontRepository.kt | 275 ++++++ .../repository/SubtitleFontFileValidator.kt | 13 + .../exoplayer/core/ui/components/Buttons.kt | 38 + .../ui/components/ClickablePreferenceItem.kt | 47 + .../core/ui/components/NextDialog.kt | 62 ++ .../core/ui/components/NextSwitch.kt | 39 + .../core/ui/components/NextTopAppBar.kt | 79 ++ .../core/ui/components/OptionsDialog.kt | 38 + .../core/ui/components/PreferenceItem.kt | 174 ++++ .../core/ui/components/PreferenceSlider.kt | 69 ++ .../core/ui/components/PreferenceSwitch.kt | 49 + .../components/PreferenceSwitchWithDivider.kt | 96 ++ .../core/ui/components/RadioTextButton.kt | 42 + .../core/ui/components/SubtitleStylePanel.kt | 157 ++++ .../mpvrx/exoplayer/di/ExoPlayerModule.kt | 125 +++ .../feature/player/PlayerContentFrame.kt | 188 ++++ .../feature/player/PlayerControlsDragDrop.kt | 193 ++++ .../feature/player/buttons/RepeatButton.kt | 67 ++ .../feature/player/buttons/ShuffleButton.kt | 53 ++ .../player/datasource/FtpDataSource.kt | 127 +++ .../player/datasource/SmbDataSource.kt | 162 ++++ .../player/service/DecoderPriorityPolicy.kt | 42 + .../player/service/VideoEffectsPolicy.kt | 5 + .../player/service/VideoEffectsState.kt | 9 + .../player/service/VideoFilterPreferences.kt | 81 ++ .../player/service/VideoFilterTransition.kt | 49 + .../player/service/VideoFiltersEffect.kt | 233 +++++ .../feature/player/state/BrightnessState.kt | 60 ++ .../NormalizingAssMatroskaExtractor.kt | 318 +++++++ .../player/subtitle/SubtitleFontPolicy.kt | 17 + .../player/ui/DecoderPrioritySelectorView.kt | 80 ++ .../feature/player/ui/MenuOverlayView.kt | 132 +++ .../feature/player/ui/MenuRootContent.kt | 167 ++++ .../player/ui/PlaybackSpeedSelectorView.kt | 186 ++++ .../feature/player/ui/PlayerGestures.kt | 172 ++++ .../feature/player/ui/PlaylistView.kt | 292 ++++++ .../feature/player/ui/ShutterView.kt | 17 + .../player/ui/SleepTimerSelectorView.kt | 140 +++ .../feature/player/ui/SubtitleSelectorView.kt | 415 +++++++++ .../feature/player/ui/SubtitleView.kt | 203 ++++ .../ui/VideoContentScaleSelectorView.kt | 84 ++ .../ui/controls/ControlsBottomModernView.kt | 233 +++++ .../player/ui/controls/ControlsBottomView.kt | 410 ++++++++ .../ui/controls/ControlsTopModernView.kt | 76 ++ .../player/ui/controls/ControlsTopView.kt | 223 +++++ .../PlayerCustomizableControlButton.kt | 387 ++++++++ .../feature/player/utils/PlayerApi.kt | 73 ++ .../settings/extensions/Extensions.kt | 91 ++ .../screens/audio/AudioPreferencesScreen.kt | 246 +++++ .../audio/AudioPreferencesViewModel.kt | 144 +++ .../decoder/DecoderPreferencesScreen.kt | 353 +++++++ .../decoder/DecoderPreferencesViewModel.kt | 186 ++++ .../general/GeneralPreferencesScreen.kt | 211 +++++ .../general/GeneralPreferencesViewModel.kt | 122 +++ .../gesture/GesturePreferencesScreen.kt | 363 ++++++++ .../gesture/GesturePreferencesViewModel.kt | 211 +++++ .../MediaLibraryPreferencesScreen.kt | 151 +++ .../MediaLibraryPreferencesViewModel.kt | 105 +++ .../screens/player/PlayerPreferencesScreen.kt | 355 +++++++ .../player/PlayerPreferencesViewModel.kt | 219 +++++ .../subtitle/SubtitlePreferencesScreen.kt | 320 +++++++ .../subtitle/SubtitlePreferencesViewModel.kt | 226 +++++ .../exoplayer/settings/utils/LocalesHelper.kt | 51 + app/src/main/res/values/arrays_exoplayer.xml | 32 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 467 ++++++++++ gradle/libs.versions.toml | 35 +- 79 files changed, 11643 insertions(+), 87 deletions(-) create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/ExoPlayerActivity.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/MediaSynchronizer.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/PlayerApi.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/data/repository/FolderPlaybackAnchorKey.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/data/repository/LocalMediaRepositoryImpl.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/data/repository/LocalSubtitleFontRepository.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/data/repository/SubtitleFontFileValidator.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/Buttons.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/ClickablePreferenceItem.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/NextDialog.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/NextSwitch.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/NextTopAppBar.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/OptionsDialog.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/PreferenceItem.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/PreferenceSlider.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/PreferenceSwitch.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/PreferenceSwitchWithDivider.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/RadioTextButton.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/core/ui/components/SubtitleStylePanel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/di/ExoPlayerModule.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/PlayerContentFrame.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/PlayerControlsDragDrop.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/buttons/RepeatButton.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/buttons/ShuffleButton.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/datasource/FtpDataSource.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/datasource/SmbDataSource.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/DecoderPriorityPolicy.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/VideoEffectsPolicy.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/VideoEffectsState.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/VideoFilterPreferences.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/VideoFilterTransition.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/service/VideoFiltersEffect.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/state/BrightnessState.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/subtitle/NormalizingAssMatroskaExtractor.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/subtitle/SubtitleFontPolicy.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/DecoderPrioritySelectorView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/MenuOverlayView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/MenuRootContent.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/PlaybackSpeedSelectorView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/PlayerGestures.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/PlaylistView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/ShutterView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/SleepTimerSelectorView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/SubtitleSelectorView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/SubtitleView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/VideoContentScaleSelectorView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/controls/ControlsBottomModernView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/controls/ControlsBottomView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/controls/ControlsTopModernView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/controls/ControlsTopView.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/ui/controls/PlayerCustomizableControlButton.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/feature/player/utils/PlayerApi.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/extensions/Extensions.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/audio/AudioPreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/audio/AudioPreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/decoder/DecoderPreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/decoder/DecoderPreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/general/GeneralPreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/general/GeneralPreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/gesture/GesturePreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/gesture/GesturePreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/medialibrary/MediaLibraryPreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/medialibrary/MediaLibraryPreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/player/PlayerPreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/player/PlayerPreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/subtitle/SubtitlePreferencesScreen.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/screens/subtitle/SubtitlePreferencesViewModel.kt create mode 100644 app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/settings/utils/LocalesHelper.kt create mode 100644 app/src/main/res/values/arrays_exoplayer.xml create mode 100644 app/src/main/res/values/colors.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7e3836854..19c747f56 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -197,8 +197,11 @@ dependencies { implementation(libs.androidx.compose.animation.graphics) implementation(libs.mediasession) implementation(libs.androidx.documentfile) + implementation(libs.androidx.datastore.core) + implementation(libs.androidx.datastore.preferences) implementation(libs.bundles.coil) + implementation(platform(libs.koin.bom)) implementation(libs.bundles.koin) @@ -212,11 +215,21 @@ dependencies { implementation(libs.room.ktx) implementation(libs.kotlinx.immutable.collections) + implementation(libs.kotlinx.coroutines.guava) implementation(libs.kotlinx.serialization.json) + implementation(libs.okhttp) implementation(libs.jsoup) implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.datasource.okhttp) implementation(libs.androidx.media3.effect) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.rtsp) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.ui.compose) implementation(libs.androidx.media3.transformer) implementation(platform(libs.sora.editor.bom)) implementation(libs.sora.editor) @@ -226,11 +239,17 @@ dependencies { coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.truetype.parser) + implementation(libs.juniversalchardet) + implementation(libs.ass.media) implementation(libs.fsaf) + + implementation(libs.mediainfo.lib) implementation("com.llamatik:library:1.4.0") implementation(files("libs/mpvlib.aar")) + implementation(files("libs/media3ext-release.aar")) + // Network protocol libraries implementation(libs.smbj) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3959fcf22..90eef724a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -332,6 +332,24 @@ + + + + + + + + () + val enableExoPlayer by playerPreferences.enableExoPlayer.collectAsState() + Scaffold( topBar = { TopAppBar( @@ -103,118 +116,314 @@ object PreferencesScreen : Screen { } // ── 1. Appearance ───────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_appearance)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_appearance_title)) }, - summary = { Text(stringResource(R.string.pref_appearance_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Outlined.Palette, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(AppearancePreferencesScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_appearance)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_appearance_title)) }, + summary = { + Text( + stringResource(R.string.pref_appearance_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Outlined.Palette, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(AppearancePreferencesScreen) }, + ) + } } } // ── 2. Playback ─────────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_playback)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_player)) }, - summary = { Text(stringResource(R.string.pref_player_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Default.Slideshow, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(PlayerPreferencesScreen) }, - ) - PreferenceDivider() - Preference( - title = { Text(stringResource(R.string.pref_decoder)) }, - summary = { Text(stringResource(R.string.pref_decoder_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Default.DeveloperBoard, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(DecoderPreferencesScreen) }, - ) - PreferenceDivider() - Preference( - title = { Text(stringResource(R.string.pref_audio)) }, - summary = { Text(stringResource(R.string.pref_audio_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Outlined.Audiotrack, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(AudioPreferencesScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_playback)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_player)) }, + summary = { + Text( + stringResource(R.string.pref_player_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Default.Slideshow, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(PlayerPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text(stringResource(R.string.pref_decoder)) }, + summary = { + Text( + stringResource(R.string.pref_decoder_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Default.DeveloperBoard, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(DecoderPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text(stringResource(R.string.pref_audio)) }, + summary = { + Text( + stringResource(R.string.pref_audio_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Outlined.Audiotrack, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(AudioPreferencesScreen) }, + ) + } } } // ── 3. Gestures & Controls ──────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_gestures_controls)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_gesture)) }, - summary = { Text(stringResource(R.string.pref_gesture_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Outlined.Gesture, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(GesturePreferencesScreen) }, - ) - PreferenceDivider() - Preference( - title = { Text(stringResource(R.string.pref_layout_title)) }, - summary = { Text(stringResource(R.string.pref_layout_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Default.GridView, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(PlayerControlsPreferencesScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_gestures_controls)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_gesture)) }, + summary = { + Text( + stringResource(R.string.pref_gesture_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Outlined.Gesture, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(GesturePreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text(stringResource(R.string.pref_layout_title)) }, + summary = { + Text( + stringResource(R.string.pref_layout_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Default.GridView, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(PlayerControlsPreferencesScreen) }, + ) + } } } // ── 4. Subtitles ────────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_subtitles)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_subtitles)) }, - summary = { Text(stringResource(R.string.pref_subtitles_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Outlined.Subtitles, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(SubtitlesPreferencesScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_subtitles)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_subtitles)) }, + summary = { + Text( + stringResource(R.string.pref_subtitles_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Outlined.Subtitles, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(SubtitlesPreferencesScreen) }, + ) + } } } // ── 5. Storage ──────────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_storage)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_folders_title)) }, - summary = { Text(stringResource(R.string.pref_section_storage_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Outlined.Folder, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(FoldersPreferencesScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_storage)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_folders_title)) }, + summary = { + Text( + stringResource(R.string.pref_section_storage_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Outlined.Folder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(FoldersPreferencesScreen) }, + ) + } } } // ── 6. AI Integration ────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_ai)) } - item { - PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_section_ai_title)) }, - summary = { Text(stringResource(R.string.pref_section_ai_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Default.AutoAwesome, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(AiIntegrationScreen) }, - ) + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_ai)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_section_ai_title)) }, + summary = { + Text( + stringResource(R.string.pref_section_ai_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Default.AutoAwesome, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(AiIntegrationScreen) }, + ) + } } } // ── 7. Advanced ─────────────────────────────────────────────────── - item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_advanced)) } + if (!enableExoPlayer) { + item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_advanced)) } + item { + PreferenceCard { + Preference( + title = { Text(stringResource(R.string.pref_advanced)) }, + summary = { + Text( + stringResource(R.string.pref_advanced_summary), + color = MaterialTheme.colorScheme.outline + ) + }, + icon = { + Icon( + Icons.Alternatives.AdvancedSettings, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + onClick = { backstack.add(AdvancedPreferencesScreen) }, + ) + } + } + } + + // ── 8. ExoPlayer ────────────────────────────────────────────────── + item { PreferenceSectionHeader(title = "ExoPlayer") } item { PreferenceCard { - Preference( - title = { Text(stringResource(R.string.pref_advanced)) }, - summary = { Text(stringResource(R.string.pref_advanced_summary), color = MaterialTheme.colorScheme.outline) }, - icon = { Icon(Icons.Alternatives.AdvancedSettings, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, - onClick = { backstack.add(AdvancedPreferencesScreen) }, + me.zhanghai.compose.preference.SwitchPreference( + value = enableExoPlayer, + onValueChange = { playerPreferences.enableExoPlayer.set(it) }, + title = { Text("Enable ExoPlayer") }, + summary = { Text("Use ExoPlayer as the video engine instead of mpv") }, + icon = { + Icon( + Icons.Default.PlayCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, ) + + if (enableExoPlayer) { + // Ported settings from One-Player + Preference( + title = { Text("Media Library") }, + summary = { Text("Ignore .nomedia, thumbnail generation", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Default.Movie, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoMediaLibraryPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("Player") }, + summary = { Text("Resume, speed, loop mode, orientation", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Default.PlayArrow, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoPlayerPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("Gestures") }, + summary = { Text("Double tap, seek, zoom, volume, brightness", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Outlined.Gesture, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoGesturePreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("Video Processing") }, + summary = { Text("Decoder priority, video filters", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Default.DeveloperBoard, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoDecoderPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("Audio") }, + summary = { Text("Preferred languages, focus, volume memory", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Default.Audiotrack, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoAudioPreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("Subtitle") }, + summary = { Text("Font, auto-load, encoding, appearance", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Default.Subtitles, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoSubtitlePreferencesScreen) }, + ) + PreferenceDivider() + Preference( + title = { Text("General") }, + summary = { Text("Cache, backup, restore", color = MaterialTheme.colorScheme.outline) }, + icon = { Icon(Icons.Outlined.Settings, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, + onClick = { backstack.add(ExoGeneralPreferencesScreen) }, + ) + } } } - // ── 7. About ────────────────────────────────────────────────────── + // ── 9. About ────────────────────────────────────────────────────── item { PreferenceSectionHeader(title = stringResource(R.string.pref_section_about)) } item { PreferenceCard { diff --git a/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt b/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt index d11c1db89..492a68a80 100644 --- a/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt +++ b/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt @@ -6,7 +6,10 @@ import android.net.Uri import androidx.core.content.FileProvider import androidx.core.net.toUri import app.gyrolet.mpvrx.domain.media.model.Video +import app.gyrolet.mpvrx.exoplayer.ExoPlayerActivity import app.gyrolet.mpvrx.ui.player.PlayerActivity +import app.gyrolet.mpvrx.preferences.PlayerPreferences +import org.koin.java.KoinJavaComponent.inject import app.gyrolet.mpvrx.ui.player.PlayerLookupHints import app.gyrolet.mpvrx.utils.history.RecentlyPlayedOps import `is`.xyz.mpv.Utils @@ -42,6 +45,15 @@ data class PlaybackSubtitleTrack( * bypassing MediaUtils. */ object MediaUtils { + private val playerPreferences: PlayerPreferences by inject(PlayerPreferences::class.java) + + private fun getPlayerActivityClass(): Class<*> { + return if (playerPreferences.enableExoPlayer.get()) { + ExoPlayerActivity::class.java + } else { + PlayerActivity::class.java + } + } /** * Play video content from any source. * @@ -68,7 +80,7 @@ object MediaUtils { when (source) { is Video -> { val intent = Intent(Intent.ACTION_VIEW, source.uri) - intent.setClass(context, PlayerActivity::class.java) + intent.setClass(context, getPlayerActivityClass()) intent.putExtra("internal_launch", true) // Enables subtitle autoload applyPlaybackExtras( intent = intent, @@ -113,7 +125,7 @@ object MediaUtils { } val intent = Intent(Intent.ACTION_VIEW, uri) - intent.setClass(context, PlayerActivity::class.java) + intent.setClass(context, getPlayerActivityClass()) intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) applyPlaybackExtras( diff --git a/app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/ExoPlayerActivity.kt b/app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/ExoPlayerActivity.kt new file mode 100644 index 000000000..335d47958 --- /dev/null +++ b/app/src/main/kotlin/app/gyrolet/mpvrx/exoplayer/ExoPlayerActivity.kt @@ -0,0 +1,873 @@ +package app.gyrolet.mpvrx.exoplayer + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.graphics.Color +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.provider.MediaStore +import android.view.KeyEvent +import android.view.PixelCopy +import android.view.SurfaceView +import android.view.View +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.getValue +import org.koin.androidx.viewmodel.ext.android.viewModel +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.core.content.ContextCompat +import androidx.core.content.IntentCompat +import androidx.core.graphics.createBitmap +import androidx.core.util.Consumer +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.google.common.util.concurrent.ListenableFuture +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.guava.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import app.gyrolet.mpvrx.exoplayer.core.common.Logger +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.applyNavigationBarStyle +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.applyPrivacyProtection +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.canonicalPathOrSelf +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.getFilenameFromUri +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.getMediaContentUri +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.isSubtitleExtension +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.resolvePrivacyPreviewScrim +import app.gyrolet.mpvrx.exoplayer.core.common.extensions.scanFileForContentUri +import app.gyrolet.mpvrx.exoplayer.core.model.ScreenOrientation +import app.gyrolet.mpvrx.exoplayer.core.model.Video +import app.gyrolet.mpvrx.ui.theme.MpvrxTheme +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.OpenDocumentWithInitialUri +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.copy +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.registerForSuspendActivityResult +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.setMetadataExtras +import app.gyrolet.mpvrx.exoplayer.core.data.repository.OnlineSubtitleRepository +import app.gyrolet.mpvrx.exoplayer.core.data.repository.InvalidOnlineSubtitleSchemeException +import app.gyrolet.mpvrx.exoplayer.core.data.repository.InvalidOnlineSubtitleUrlException +import app.gyrolet.mpvrx.exoplayer.core.data.repository.InvalidOnlineSubtitleExtensionException +import app.gyrolet.mpvrx.exoplayer.core.data.repository.OnlineSubtitleTooLargeException +import app.gyrolet.mpvrx.exoplayer.core.data.repository.EmptyOnlineSubtitleException +import app.gyrolet.mpvrx.exoplayer.core.data.repository.OnlineSubtitleDownloadFailedException +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.toActivityOrientation +import app.gyrolet.mpvrx.exoplayer.feature.player.extensions.uriToSubtitleConfiguration +import app.gyrolet.mpvrx.exoplayer.feature.player.service.addSubtitleTrack +import app.gyrolet.mpvrx.exoplayer.feature.player.service.stopPlayerSession +import app.gyrolet.mpvrx.exoplayer.feature.player.MediaPlayerScreen +import app.gyrolet.mpvrx.exoplayer.feature.player.PlayerViewModel +import app.gyrolet.mpvrx.R +import org.koin.android.ext.android.inject + +internal data class PlaybackPlaylist( + val items: List, + val currentIndex: Int, +) + +internal data class PlaybackTarget( + val sourceUriString: String, + val playbackUriString: String, + val currentPath: String?, +) + +internal fun buildPlaybackPlaylistFromItems( + playlistItems: List, + playbackTarget: PlaybackTarget, +): PlaybackPlaylist { + if (playlistItems.isEmpty()) { + return PlaybackPlaylist( + items = listOf(playbackTarget.playbackUriString), + currentIndex = 0, + ) + } + + val currentIndex = playlistItems.indexOfFirst { uriString -> + uriString == playbackTarget.playbackUriString || + uriString == playbackTarget.sourceUriString + } + if (currentIndex >= 0) { + return PlaybackPlaylist( + items = playlistItems, + currentIndex = currentIndex, + ) + } + + return PlaybackPlaylist( + items = listOf(playbackTarget.playbackUriString) + playlistItems, + currentIndex = 0, + ) +} + +internal fun buildPlaybackPlaylist( + playlistVideos: List