mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: enhance player settings with decoder options and improve media source handling
This commit is contained in:
parent
5612ec06fd
commit
dd33373f67
16 changed files with 424 additions and 33 deletions
|
|
@ -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") {
|
||||
|
|
|
|||
BIN
composeApp/libs/lib-decoder-av1-release.aar
Normal file
BIN
composeApp/libs/lib-decoder-av1-release.aar
Normal file
Binary file not shown.
BIN
composeApp/libs/lib-decoder-ffmpeg-release.aar
Normal file
BIN
composeApp/libs/lib-decoder-ffmpeg-release.aar
Normal file
Binary file not shown.
BIN
composeApp/libs/lib-decoder-iamf-release.aar
Normal file
BIN
composeApp/libs/lib-decoder-iamf-release.aar
Normal file
Binary file not shown.
BIN
composeApp/libs/lib-decoder-mpegh-release.aar
Normal file
BIN
composeApp/libs/lib-decoder-mpegh-release.aar
Normal file
Binary file not shown.
BIN
composeApp/libs/lib-exoplayer-release.aar
Normal file
BIN
composeApp/libs/lib-exoplayer-release.aar
Normal file
Binary file not shown.
BIN
composeApp/libs/lib-ui-release.aar
Normal file
BIN
composeApp/libs/lib-ui-release.aar
Normal file
Binary file not shown.
|
|
@ -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>() }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue