feat: enhance player settings with decoder options and improve media source handling

This commit is contained in:
tapframe 2026-03-29 17:43:04 +05:30
parent 5612ec06fd
commit dd33373f67
16 changed files with 424 additions and 33 deletions

View file

@ -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") {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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<Int>() }

View file

@ -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()
}
}

View file

@ -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,
)
}
}

View file

@ -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)
}

View file

@ -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,
)
}
}
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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))
}
}

View file

@ -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" }