diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index cd0bca05..b7fe7d8d 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -54,10 +54,18 @@ kotlin { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.activity.compose) implementation(libs.ktor.client.android) - implementation(libs.androidx.media3.exoplayer) - implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.exoplayer.hls) - implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.exoplayer.smoothstreaming) + implementation(libs.androidx.media3.exoplayer.rtsp) + implementation(libs.androidx.media3.datasource) + implementation(libs.androidx.media3.datasource.okhttp) + implementation(libs.androidx.media3.decoder) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.container) + implementation(libs.androidx.media3.extractor) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar")))) } commonMain.dependencies { implementation(libs.coil.compose) @@ -87,6 +95,11 @@ kotlin { } } +configurations.all { + exclude(group = "androidx.media3", module = "media3-exoplayer") + exclude(group = "androidx.media3", module = "media3-ui") +} + android { namespace = "com.nuvio.app" compileSdk = libs.versions.android.compileSdk.get().toInt() @@ -102,6 +115,15 @@ android { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } + jniLibs { + pickFirsts += listOf( + "lib/*/libc++_shared.so", + "lib/*/libavcodec.so", + "lib/*/libavutil.so", + "lib/*/libswscale.so", + "lib/*/libswresample.so" + ) + } } buildTypes { getByName("release") { diff --git a/composeApp/libs/lib-decoder-av1-release.aar b/composeApp/libs/lib-decoder-av1-release.aar new file mode 100644 index 00000000..3e61becc Binary files /dev/null and b/composeApp/libs/lib-decoder-av1-release.aar differ diff --git a/composeApp/libs/lib-decoder-ffmpeg-release.aar b/composeApp/libs/lib-decoder-ffmpeg-release.aar new file mode 100644 index 00000000..f85f4508 Binary files /dev/null and b/composeApp/libs/lib-decoder-ffmpeg-release.aar differ diff --git a/composeApp/libs/lib-decoder-iamf-release.aar b/composeApp/libs/lib-decoder-iamf-release.aar new file mode 100644 index 00000000..8c210fca Binary files /dev/null and b/composeApp/libs/lib-decoder-iamf-release.aar differ diff --git a/composeApp/libs/lib-decoder-mpegh-release.aar b/composeApp/libs/lib-decoder-mpegh-release.aar new file mode 100644 index 00000000..8b6f4da8 Binary files /dev/null and b/composeApp/libs/lib-decoder-mpegh-release.aar differ diff --git a/composeApp/libs/lib-exoplayer-release.aar b/composeApp/libs/lib-exoplayer-release.aar new file mode 100644 index 00000000..c6670870 Binary files /dev/null and b/composeApp/libs/lib-exoplayer-release.aar differ diff --git a/composeApp/libs/lib-ui-release.aar b/composeApp/libs/lib-ui-release.aar new file mode 100644 index 00000000..6d555a0b Binary files /dev/null and b/composeApp/libs/lib-ui-release.aar differ diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt index daaa815b..81f30c94 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt @@ -21,7 +21,14 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.extractor.DefaultExtractorsFactory +import androidx.media3.extractor.ts.DefaultTsPayloadReaderFactory +import androidx.media3.extractor.ts.TsExtractor import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import kotlinx.coroutines.delay @@ -44,12 +51,51 @@ actual fun PlatformPlayerSurface( val lifecycleOwner = LocalLifecycleOwner.current val latestOnSnapshot = rememberUpdatedState(onSnapshot) val latestOnError = rememberUpdatedState(onError) + + val playerSettings = remember { + PlayerSettingsRepository.ensureLoaded() + PlayerSettingsRepository.uiState.value + } + val exoPlayer = remember(sourceUrl) { - ExoPlayer.Builder(context).build().apply { - setMediaItem(MediaItem.fromUri(sourceUrl)) - prepare() - this.playWhenReady = playWhenReady + val renderersFactory = DefaultRenderersFactory(context) + .setExtensionRendererMode(playerSettings.decoderPriority) + .setMapDV7ToHevc(playerSettings.mapDV7ToHevc) + + val trackSelector = DefaultTrackSelector(context).apply { + setParameters( + buildUponParameters() + .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true) + ) + if (playerSettings.tunnelingEnabled) { + setParameters(buildUponParameters().setTunnelingEnabled(true)) + } } + + val loadControl = DefaultLoadControl.Builder() + .setTargetBufferBytes(100 * 1024 * 1024) + .setBufferDurationsMs( + DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, + 70_000, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + 5_000 + ) + .build() + + val extractorsFactory = DefaultExtractorsFactory() + .setTsExtractorFlags(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS) + .setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE) + + ExoPlayer.Builder(context) + .setRenderersFactory(renderersFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadControl) + .setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory)) + .build().apply { + setMediaItem(MediaItem.fromUri(sourceUrl)) + prepare() + this.playWhenReady = playWhenReady + } } val pendingSubtitleTrackIndex = remember { mutableListOf() } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt index 10ac7b2e..a67c3d71 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt @@ -13,6 +13,9 @@ actual object PlayerSettingsStorage { private const val secondaryPreferredSubtitleLanguageKey = "secondary_preferred_subtitle_language" private const val streamReuseLastLinkEnabledKey = "stream_reuse_last_link_enabled" private const val streamReuseLastLinkCacheHoursKey = "stream_reuse_last_link_cache_hours" + private const val decoderPriorityKey = "decoder_priority" + private const val mapDV7ToHevcKey = "map_dv7_to_hevc" + private const val tunnelingEnabledKey = "tunneling_enabled" private var preferences: SharedPreferences? = null @@ -124,4 +127,55 @@ actual object PlayerSettingsStorage { ?.putInt(ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey), hours) ?.apply() } + + actual fun loadDecoderPriority(): Int? = + preferences?.let { sharedPreferences -> + val key = ProfileScopedKey.of(decoderPriorityKey) + if (sharedPreferences.contains(key)) { + sharedPreferences.getInt(key, 1) + } else { + null + } + } + + actual fun saveDecoderPriority(priority: Int) { + preferences + ?.edit() + ?.putInt(ProfileScopedKey.of(decoderPriorityKey), priority) + ?.apply() + } + + actual fun loadMapDV7ToHevc(): Boolean? = + preferences?.let { sharedPreferences -> + val key = ProfileScopedKey.of(mapDV7ToHevcKey) + if (sharedPreferences.contains(key)) { + sharedPreferences.getBoolean(key, false) + } else { + null + } + } + + actual fun saveMapDV7ToHevc(enabled: Boolean) { + preferences + ?.edit() + ?.putBoolean(ProfileScopedKey.of(mapDV7ToHevcKey), enabled) + ?.apply() + } + + actual fun loadTunnelingEnabled(): Boolean? = + preferences?.let { sharedPreferences -> + val key = ProfileScopedKey.of(tunnelingEnabledKey) + if (sharedPreferences.contains(key)) { + sharedPreferences.getBoolean(key, false) + } else { + null + } + } + + actual fun saveTunnelingEnabled(enabled: Boolean) { + preferences + ?.edit() + ?.putBoolean(ProfileScopedKey.of(tunnelingEnabledKey), enabled) + ?.apply() + } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt index bd4d7708..b319674d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt @@ -12,6 +12,9 @@ data class PlayerSettingsUiState( val secondaryPreferredSubtitleLanguage: String? = null, val streamReuseLastLinkEnabled: Boolean = false, val streamReuseLastLinkCacheHours: Int = 24, + val decoderPriority: Int = 1, + val mapDV7ToHevc: Boolean = false, + val tunnelingEnabled: Boolean = false, ) object PlayerSettingsRepository { @@ -26,6 +29,9 @@ object PlayerSettingsRepository { private var secondaryPreferredSubtitleLanguage: String? = null private var streamReuseLastLinkEnabled = false private var streamReuseLastLinkCacheHours = 24 + private var decoderPriority = 1 + private var mapDV7ToHevc = false + private var tunnelingEnabled = false fun ensureLoaded() { if (hasLoaded) return @@ -45,6 +51,9 @@ object PlayerSettingsRepository { secondaryPreferredSubtitleLanguage = null streamReuseLastLinkEnabled = false streamReuseLastLinkCacheHours = 24 + decoderPriority = 1 + mapDV7ToHevc = false + tunnelingEnabled = false publish() } @@ -63,6 +72,9 @@ object PlayerSettingsRepository { normalizeLanguageCode(PlayerSettingsStorage.loadSecondaryPreferredSubtitleLanguage()) streamReuseLastLinkEnabled = PlayerSettingsStorage.loadStreamReuseLastLinkEnabled() ?: false streamReuseLastLinkCacheHours = PlayerSettingsStorage.loadStreamReuseLastLinkCacheHours() ?: 24 + decoderPriority = PlayerSettingsStorage.loadDecoderPriority() ?: 1 + mapDV7ToHevc = PlayerSettingsStorage.loadMapDV7ToHevc() ?: false + tunnelingEnabled = PlayerSettingsStorage.loadTunnelingEnabled() ?: false publish() } @@ -126,6 +138,30 @@ object PlayerSettingsRepository { PlayerSettingsStorage.saveStreamReuseLastLinkCacheHours(hours) } + fun setDecoderPriority(priority: Int) { + ensureLoaded() + if (decoderPriority == priority) return + decoderPriority = priority + publish() + PlayerSettingsStorage.saveDecoderPriority(priority) + } + + fun setMapDV7ToHevc(enabled: Boolean) { + ensureLoaded() + if (mapDV7ToHevc == enabled) return + mapDV7ToHevc = enabled + publish() + PlayerSettingsStorage.saveMapDV7ToHevc(enabled) + } + + fun setTunnelingEnabled(enabled: Boolean) { + ensureLoaded() + if (tunnelingEnabled == enabled) return + tunnelingEnabled = enabled + publish() + PlayerSettingsStorage.saveTunnelingEnabled(enabled) + } + private fun publish() { _uiState.value = PlayerSettingsUiState( showLoadingOverlay = showLoadingOverlay, @@ -135,6 +171,9 @@ object PlayerSettingsRepository { secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours, + decoderPriority = decoderPriority, + mapDV7ToHevc = mapDV7ToHevc, + tunnelingEnabled = tunnelingEnabled, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt index 719baf0c..0e8f725c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt @@ -15,4 +15,10 @@ internal expect object PlayerSettingsStorage { fun saveStreamReuseLastLinkEnabled(enabled: Boolean) fun loadStreamReuseLastLinkCacheHours(): Int? fun saveStreamReuseLastLinkCacheHours(hours: Int) + fun loadDecoderPriority(): Int? + fun saveDecoderPriority(priority: Int) + fun loadMapDV7ToHevc(): Boolean? + fun saveMapDV7ToHevc(enabled: Boolean) + fun loadTunnelingEnabled(): Boolean? + fun saveTunnelingEnabled(enabled: Boolean) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt index 98ce87b8..9328b578 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt @@ -17,8 +17,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.Language -import androidx.compose.material.icons.rounded.Subtitles import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -39,6 +37,7 @@ import com.nuvio.app.features.player.AvailableLanguageOptions import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.player.SubtitleLanguageOption import com.nuvio.app.features.player.languageLabelForCode +import com.nuvio.app.isIos internal fun LazyListScope.playbackSettingsContent( isTablet: Boolean, @@ -49,6 +48,9 @@ internal fun LazyListScope.playbackSettingsContent( secondaryPreferredSubtitleLanguage: String?, streamReuseLastLinkEnabled: Boolean, streamReuseLastLinkCacheHours: Int, + decoderPriority: Int, + mapDV7ToHevc: Boolean, + tunnelingEnabled: Boolean, ) { item { PlaybackSettingsSection( @@ -60,6 +62,9 @@ internal fun LazyListScope.playbackSettingsContent( secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours, + decoderPriority = decoderPriority, + mapDV7ToHevc = mapDV7ToHevc, + tunnelingEnabled = tunnelingEnabled, ) } } @@ -74,12 +79,16 @@ private fun PlaybackSettingsSection( secondaryPreferredSubtitleLanguage: String?, streamReuseLastLinkEnabled: Boolean, streamReuseLastLinkCacheHours: Int, + decoderPriority: Int, + mapDV7ToHevc: Boolean, + tunnelingEnabled: Boolean, ) { var showPreferredAudioDialog by remember { mutableStateOf(false) } var showSecondaryAudioDialog by remember { mutableStateOf(false) } var showPreferredSubtitleDialog by remember { mutableStateOf(false) } var showSecondarySubtitleDialog by remember { mutableStateOf(false) } var showReuseCacheDurationDialog by remember { mutableStateOf(false) } + var showDecoderPriorityDialog by remember { mutableStateOf(false) } val sectionSpacing = if (isTablet) 18.dp else 12.dp Column( @@ -97,7 +106,14 @@ private fun PlaybackSettingsSection( isTablet = isTablet, onCheckedChange = PlayerSettingsRepository::setShowLoadingOverlay, ) - SettingsGroupDivider(isTablet = isTablet) + } + } + + SettingsSection( + title = "SUBTITLE AND AUDIO", + isTablet = isTablet, + ) { + SettingsGroup(isTablet = isTablet) { SettingsNavigationRow( title = "Preferred Audio Language", description = when (preferredAudioLanguage) { @@ -105,7 +121,6 @@ private fun PlaybackSettingsSection( AudioLanguageOption.DEVICE -> "Device Language" else -> languageLabelForCode(preferredAudioLanguage) }, - icon = Icons.Rounded.Language, isTablet = isTablet, onClick = { showPreferredAudioDialog = true }, ) @@ -113,7 +128,6 @@ private fun PlaybackSettingsSection( SettingsNavigationRow( title = "Secondary Audio Language", description = languageLabelForCode(secondaryPreferredAudioLanguage), - icon = Icons.Rounded.Language, isTablet = isTablet, onClick = { showSecondaryAudioDialog = true }, ) @@ -126,7 +140,6 @@ private fun PlaybackSettingsSection( SubtitleLanguageOption.FORCED -> "Forced" else -> languageLabelForCode(preferredSubtitleLanguage) }, - icon = Icons.Rounded.Subtitles, isTablet = isTablet, onClick = { showPreferredSubtitleDialog = true }, ) @@ -134,7 +147,6 @@ private fun PlaybackSettingsSection( SettingsNavigationRow( title = "Secondary Subtitle Language", description = languageLabelForCode(secondaryPreferredSubtitleLanguage), - icon = Icons.Rounded.Subtitles, isTablet = isTablet, onClick = { showSecondarySubtitleDialog = true }, ) @@ -158,13 +170,49 @@ private fun PlaybackSettingsSection( SettingsNavigationRow( title = "Last Link Cache Duration", description = formatReuseCacheDuration(streamReuseLastLinkCacheHours), - icon = Icons.Rounded.Language, isTablet = isTablet, onClick = { showReuseCacheDurationDialog = true }, ) } } } + + if (!isIos) { + SettingsSection( + title = "DECODER", + isTablet = isTablet, + ) { + SettingsGroup(isTablet = isTablet) { + SettingsNavigationRow( + title = "Decoder Priority", + description = when (decoderPriority) { + 0 -> "Device Only" + 1 -> "Prefer Device" + 2 -> "Prefer App (FFmpeg)" + else -> "Prefer Device" + }, + isTablet = isTablet, + onClick = { showDecoderPriorityDialog = true }, + ) + SettingsGroupDivider(isTablet = isTablet) + SettingsSwitchRow( + title = "Map DV7 to HEVC", + description = "Dolby Vision Profile 7 to HEVC fallback for unsupported devices.", + checked = mapDV7ToHevc, + isTablet = isTablet, + onCheckedChange = PlayerSettingsRepository::setMapDV7ToHevc, + ) + SettingsGroupDivider(isTablet = isTablet) + SettingsSwitchRow( + title = "Tunneled Playback", + description = "Enable tunneled playback for lower latency audio/video sync.", + checked = tunnelingEnabled, + isTablet = isTablet, + onCheckedChange = PlayerSettingsRepository::setTunnelingEnabled, + ) + } + } + } } if (showPreferredAudioDialog) { @@ -249,6 +297,17 @@ private fun PlaybackSettingsSection( onDismiss = { showReuseCacheDurationDialog = false }, ) } + + if (showDecoderPriorityDialog) { + DecoderPriorityDialog( + selectedPriority = decoderPriority, + onPrioritySelected = { priority -> + PlayerSettingsRepository.setDecoderPriority(priority) + showDecoderPriorityDialog = false + }, + onDismiss = { showDecoderPriorityDialog = false }, + ) + } } private fun formatReuseCacheDuration(hours: Int): String = when { @@ -441,3 +500,94 @@ private fun ReuseCacheDurationDialog( } } } + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun DecoderPriorityDialog( + selectedPriority: Int, + onPrioritySelected: (Int) -> Unit, + onDismiss: () -> Unit, +) { + val options = listOf( + 0 to "Device Only", + 1 to "Prefer Device", + 2 to "Prefer App (FFmpeg)", + ) + + BasicAlertDialog( + onDismissRequest = onDismiss, + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Decoder Priority", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + options.forEach { (priority, label) -> + val isSelected = priority == selectedPriority + val containerColor = if (isSelected) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f) + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { onPrioritySelected(priority) }, + shape = RoundedCornerShape(12.dp), + color = containerColor, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Box( + modifier = Modifier.size(24.dp), + contentAlignment = Alignment.Center, + ) { + if (isSelected) { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "Tap outside to close", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsComponents.kt index 7c547565..31a62cd5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsComponents.kt @@ -181,7 +181,7 @@ internal fun SettingsSection( internal fun SettingsNavigationRow( title: String, description: String, - icon: ImageVector, + icon: ImageVector? = null, isTablet: Boolean, onClick: () -> Unit, ) { @@ -204,24 +204,26 @@ internal fun SettingsNavigationRow( .widthIn(max = if (isTablet) 560.dp else 320.dp), verticalAlignment = Alignment.CenterVertically, ) { - Surface( - modifier = Modifier.size(iconSize), - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), - shape = RoundedCornerShape(iconRadius), - ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + if (icon != null) { + Surface( + modifier = Modifier.size(iconSize), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + shape = RoundedCornerShape(iconRadius), ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } } + Spacer(modifier = Modifier.width(if (isTablet) 16.dp else 14.dp)) } - Spacer(modifier = Modifier.width(if (isTablet) 16.dp else 14.dp)) Column { Text( text = title, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index e03ad678..9e47197b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -83,6 +83,9 @@ fun SettingsScreen( secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = playerSettingsUiState.streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = playerSettingsUiState.streamReuseLastLinkCacheHours, + decoderPriority = playerSettingsUiState.decoderPriority, + mapDV7ToHevc = playerSettingsUiState.mapDV7ToHevc, + tunnelingEnabled = playerSettingsUiState.tunnelingEnabled, selectedTheme = selectedTheme, onThemeSelected = ThemeSettingsRepository::setTheme, amoledEnabled = amoledEnabled, @@ -104,6 +107,9 @@ fun SettingsScreen( secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = playerSettingsUiState.streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = playerSettingsUiState.streamReuseLastLinkCacheHours, + decoderPriority = playerSettingsUiState.decoderPriority, + mapDV7ToHevc = playerSettingsUiState.mapDV7ToHevc, + tunnelingEnabled = playerSettingsUiState.tunnelingEnabled, selectedTheme = selectedTheme, onThemeSelected = ThemeSettingsRepository::setTheme, amoledEnabled = amoledEnabled, @@ -129,6 +135,9 @@ private fun MobileSettingsScreen( secondaryPreferredSubtitleLanguage: String?, streamReuseLastLinkEnabled: Boolean, streamReuseLastLinkCacheHours: Int, + decoderPriority: Int, + mapDV7ToHevc: Boolean, + tunnelingEnabled: Boolean, selectedTheme: AppTheme, onThemeSelected: (AppTheme) -> Unit, amoledEnabled: Boolean, @@ -166,6 +175,9 @@ private fun MobileSettingsScreen( secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours, + decoderPriority = decoderPriority, + mapDV7ToHevc = mapDV7ToHevc, + tunnelingEnabled = tunnelingEnabled, ) SettingsPage.Appearance -> appearanceSettingsContent( isTablet = false, @@ -195,6 +207,9 @@ private fun TabletSettingsScreen( secondaryPreferredSubtitleLanguage: String?, streamReuseLastLinkEnabled: Boolean, streamReuseLastLinkCacheHours: Int, + decoderPriority: Int, + mapDV7ToHevc: Boolean, + tunnelingEnabled: Boolean, selectedTheme: AppTheme, onThemeSelected: (AppTheme) -> Unit, amoledEnabled: Boolean, @@ -281,6 +296,9 @@ private fun TabletSettingsScreen( secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage, streamReuseLastLinkEnabled = streamReuseLastLinkEnabled, streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours, + decoderPriority = decoderPriority, + mapDV7ToHevc = mapDV7ToHevc, + tunnelingEnabled = tunnelingEnabled, ) SettingsPage.Appearance -> appearanceSettingsContent( isTablet = true, diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt index 720d75ad..d7ea0866 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt @@ -11,6 +11,9 @@ actual object PlayerSettingsStorage { private const val secondaryPreferredSubtitleLanguageKey = "secondary_preferred_subtitle_language" private const val streamReuseLastLinkEnabledKey = "stream_reuse_last_link_enabled" private const val streamReuseLastLinkCacheHoursKey = "stream_reuse_last_link_cache_hours" + private const val decoderPriorityKey = "decoder_priority" + private const val mapDV7ToHevcKey = "map_dv7_to_hevc" + private const val tunnelingEnabledKey = "tunneling_enabled" actual fun loadShowLoadingOverlay(): Boolean? { val defaults = NSUserDefaults.standardUserDefaults @@ -105,4 +108,46 @@ actual object PlayerSettingsStorage { actual fun saveStreamReuseLastLinkCacheHours(hours: Int) { NSUserDefaults.standardUserDefaults.setInteger(hours.toLong(), forKey = ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey)) } + + actual fun loadDecoderPriority(): Int? { + val defaults = NSUserDefaults.standardUserDefaults + val key = ProfileScopedKey.of(decoderPriorityKey) + return if (defaults.objectForKey(key) != null) { + defaults.integerForKey(key).toInt() + } else { + null + } + } + + actual fun saveDecoderPriority(priority: Int) { + NSUserDefaults.standardUserDefaults.setInteger(priority.toLong(), forKey = ProfileScopedKey.of(decoderPriorityKey)) + } + + actual fun loadMapDV7ToHevc(): Boolean? { + val defaults = NSUserDefaults.standardUserDefaults + val key = ProfileScopedKey.of(mapDV7ToHevcKey) + return if (defaults.objectForKey(key) != null) { + defaults.boolForKey(key) + } else { + null + } + } + + actual fun saveMapDV7ToHevc(enabled: Boolean) { + NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(mapDV7ToHevcKey)) + } + + actual fun loadTunnelingEnabled(): Boolean? { + val defaults = NSUserDefaults.standardUserDefaults + val key = ProfileScopedKey.of(tunnelingEnabledKey) + return if (defaults.objectForKey(key) != null) { + defaults.boolForKey(key) + } else { + null + } + } + + actual fun saveTunnelingEnabled(enabled: Boolean) { + NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(tunnelingEnabledKey)) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index febb5cf4..8df4c424 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ kotlin = "2.3.0" kotlinx-serialization = "1.8.1" ktor = "3.4.1" material3 = "1.10.0-alpha05" -androidx-media3 = "1.9.2" +androidx-media3 = "1.10.0-rc01" supabase = "3.4.1" [libraries] @@ -47,9 +47,18 @@ ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "k kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } -androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" } androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" } +androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" } +androidx-media3-exoplayer-smoothstreaming = { module = "androidx.media3:media3-exoplayer-smoothstreaming", version.ref = "androidx-media3" } +androidx-media3-exoplayer-rtsp = { module = "androidx.media3:media3-exoplayer-rtsp", version.ref = "androidx-media3" } +androidx-media3-datasource = { module = "androidx.media3:media3-datasource", version.ref = "androidx-media3" } +androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" } +androidx-media3-decoder = { module = "androidx.media3:media3-decoder", version.ref = "androidx-media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } +androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" } +androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "androidx-media3" } +androidx-media3-container = { module = "androidx.media3:media3-container", version.ref = "androidx-media3" } +androidx-media3-extractor = { module = "androidx.media3:media3-extractor", version.ref = "androidx-media3" } supabase-postgrest = { module = "io.github.jan-tennert.supabase:postgrest-kt", version.ref = "supabase" } supabase-auth = { module = "io.github.jan-tennert.supabase:auth-kt", version.ref = "supabase" } supabase-functions = { module = "io.github.jan-tennert.supabase:functions-kt", version.ref = "supabase" }