mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 01:32:08 +00:00
feat(ios): adding custom tuning for mpv
This commit is contained in:
parent
a58f172010
commit
873f81a954
13 changed files with 1322 additions and 1 deletions
|
|
@ -56,6 +56,20 @@ actual object PlayerSettingsStorage {
|
|||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
private const val useLibassKey = "use_libass"
|
||||
private const val libassRenderTypeKey = "libass_render_type"
|
||||
private const val iosVideoOutputPresetKey = "ios_video_output_preset"
|
||||
private const val iosToneMappingModeKey = "ios_tone_mapping_mode"
|
||||
private const val iosTargetPrimariesKey = "ios_target_primaries"
|
||||
private const val iosTargetTransferKey = "ios_target_transfer"
|
||||
private const val iosHardwareDecoderModeKey = "ios_hardware_decoder_mode"
|
||||
private const val iosExtendedDynamicRangeEnabledKey = "ios_extended_dynamic_range_enabled"
|
||||
private const val iosTargetColorspaceHintEnabledKey = "ios_target_colorspace_hint_enabled"
|
||||
private const val iosHdrComputePeakEnabledKey = "ios_hdr_compute_peak_enabled"
|
||||
private const val iosDebandEnabledKey = "ios_deband_enabled"
|
||||
private const val iosInterpolationEnabledKey = "ios_interpolation_enabled"
|
||||
private const val iosBrightnessKey = "ios_brightness"
|
||||
private const val iosContrastKey = "ios_contrast"
|
||||
private const val iosSaturationKey = "ios_saturation"
|
||||
private const val iosGammaKey = "ios_gamma"
|
||||
private val syncKeys = listOf(
|
||||
showLoadingOverlayKey,
|
||||
resizeModeKey,
|
||||
|
|
@ -92,6 +106,20 @@ actual object PlayerSettingsStorage {
|
|||
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||
useLibassKey,
|
||||
libassRenderTypeKey,
|
||||
iosVideoOutputPresetKey,
|
||||
iosToneMappingModeKey,
|
||||
iosTargetPrimariesKey,
|
||||
iosTargetTransferKey,
|
||||
iosHardwareDecoderModeKey,
|
||||
iosExtendedDynamicRangeEnabledKey,
|
||||
iosTargetColorspaceHintEnabledKey,
|
||||
iosHdrComputePeakEnabledKey,
|
||||
iosDebandEnabledKey,
|
||||
iosInterpolationEnabledKey,
|
||||
iosBrightnessKey,
|
||||
iosContrastKey,
|
||||
iosSaturationKey,
|
||||
iosGammaKey,
|
||||
)
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
|
@ -652,6 +680,120 @@ actual object PlayerSettingsStorage {
|
|||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosVideoOutputPreset(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(iosVideoOutputPresetKey), null)
|
||||
|
||||
actual fun saveIosVideoOutputPreset(preset: String) {
|
||||
preferences?.edit()?.putString(ProfileScopedKey.of(iosVideoOutputPresetKey), preset)?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosToneMappingMode(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(iosToneMappingModeKey), null)
|
||||
|
||||
actual fun saveIosToneMappingMode(mode: String) {
|
||||
preferences?.edit()?.putString(ProfileScopedKey.of(iosToneMappingModeKey), mode)?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosTargetPrimaries(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(iosTargetPrimariesKey), null)
|
||||
|
||||
actual fun saveIosTargetPrimaries(primaries: String) {
|
||||
preferences?.edit()?.putString(ProfileScopedKey.of(iosTargetPrimariesKey), primaries)?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosTargetTransfer(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(iosTargetTransferKey), null)
|
||||
|
||||
actual fun saveIosTargetTransfer(transfer: String) {
|
||||
preferences?.edit()?.putString(ProfileScopedKey.of(iosTargetTransferKey), transfer)?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosHardwareDecoderMode(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(iosHardwareDecoderModeKey), null)
|
||||
|
||||
actual fun saveIosHardwareDecoderMode(mode: String) {
|
||||
preferences?.edit()?.putString(ProfileScopedKey.of(iosHardwareDecoderModeKey), mode)?.apply()
|
||||
}
|
||||
|
||||
private fun loadIosBoolean(keyBase: String, defaultValue: Boolean): Boolean? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val key = ProfileScopedKey.of(keyBase)
|
||||
if (sharedPreferences.contains(key)) sharedPreferences.getBoolean(key, defaultValue) else null
|
||||
}
|
||||
|
||||
private fun saveIosBoolean(keyBase: String, enabled: Boolean) {
|
||||
preferences?.edit()?.putBoolean(ProfileScopedKey.of(keyBase), enabled)?.apply()
|
||||
}
|
||||
|
||||
private fun loadIosInt(keyBase: String): Int? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val key = ProfileScopedKey.of(keyBase)
|
||||
if (sharedPreferences.contains(key)) sharedPreferences.getInt(key, 0) else null
|
||||
}
|
||||
|
||||
private fun saveIosInt(keyBase: String, value: Int) {
|
||||
preferences?.edit()?.putInt(ProfileScopedKey.of(keyBase), value)?.apply()
|
||||
}
|
||||
|
||||
actual fun loadIosExtendedDynamicRangeEnabled(): Boolean? =
|
||||
loadIosBoolean(iosExtendedDynamicRangeEnabledKey, true)
|
||||
|
||||
actual fun saveIosExtendedDynamicRangeEnabled(enabled: Boolean) {
|
||||
saveIosBoolean(iosExtendedDynamicRangeEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosTargetColorspaceHintEnabled(): Boolean? =
|
||||
loadIosBoolean(iosTargetColorspaceHintEnabledKey, true)
|
||||
|
||||
actual fun saveIosTargetColorspaceHintEnabled(enabled: Boolean) {
|
||||
saveIosBoolean(iosTargetColorspaceHintEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosHdrComputePeakEnabled(): Boolean? =
|
||||
loadIosBoolean(iosHdrComputePeakEnabledKey, true)
|
||||
|
||||
actual fun saveIosHdrComputePeakEnabled(enabled: Boolean) {
|
||||
saveIosBoolean(iosHdrComputePeakEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosDebandEnabled(): Boolean? =
|
||||
loadIosBoolean(iosDebandEnabledKey, false)
|
||||
|
||||
actual fun saveIosDebandEnabled(enabled: Boolean) {
|
||||
saveIosBoolean(iosDebandEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosInterpolationEnabled(): Boolean? =
|
||||
loadIosBoolean(iosInterpolationEnabledKey, false)
|
||||
|
||||
actual fun saveIosInterpolationEnabled(enabled: Boolean) {
|
||||
saveIosBoolean(iosInterpolationEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosBrightness(): Int? = loadIosInt(iosBrightnessKey)
|
||||
|
||||
actual fun saveIosBrightness(value: Int) {
|
||||
saveIosInt(iosBrightnessKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosContrast(): Int? = loadIosInt(iosContrastKey)
|
||||
|
||||
actual fun saveIosContrast(value: Int) {
|
||||
saveIosInt(iosContrastKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosSaturation(): Int? = loadIosInt(iosSaturationKey)
|
||||
|
||||
actual fun saveIosSaturation(value: Int) {
|
||||
saveIosInt(iosSaturationKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosGamma(): Int? = loadIosInt(iosGammaKey)
|
||||
|
||||
actual fun saveIosGamma(value: Int) {
|
||||
saveIosInt(iosGammaKey, value)
|
||||
}
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
||||
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
||||
|
|
@ -688,6 +830,20 @@ actual object PlayerSettingsStorage {
|
|||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
||||
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
|
||||
loadIosVideoOutputPreset()?.let { put(iosVideoOutputPresetKey, encodeSyncString(it)) }
|
||||
loadIosToneMappingMode()?.let { put(iosToneMappingModeKey, encodeSyncString(it)) }
|
||||
loadIosTargetPrimaries()?.let { put(iosTargetPrimariesKey, encodeSyncString(it)) }
|
||||
loadIosTargetTransfer()?.let { put(iosTargetTransferKey, encodeSyncString(it)) }
|
||||
loadIosHardwareDecoderMode()?.let { put(iosHardwareDecoderModeKey, encodeSyncString(it)) }
|
||||
loadIosExtendedDynamicRangeEnabled()?.let { put(iosExtendedDynamicRangeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosTargetColorspaceHintEnabled()?.let { put(iosTargetColorspaceHintEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosHdrComputePeakEnabled()?.let { put(iosHdrComputePeakEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosDebandEnabled()?.let { put(iosDebandEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosInterpolationEnabled()?.let { put(iosInterpolationEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosBrightness()?.let { put(iosBrightnessKey, encodeSyncInt(it)) }
|
||||
loadIosContrast()?.let { put(iosContrastKey, encodeSyncInt(it)) }
|
||||
loadIosSaturation()?.let { put(iosSaturationKey, encodeSyncInt(it)) }
|
||||
loadIosGamma()?.let { put(iosGammaKey, encodeSyncInt(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
|
|
@ -732,5 +888,19 @@ actual object PlayerSettingsStorage {
|
|||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
||||
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
|
||||
payload.decodeSyncString(iosVideoOutputPresetKey)?.let(::saveIosVideoOutputPreset)
|
||||
payload.decodeSyncString(iosToneMappingModeKey)?.let(::saveIosToneMappingMode)
|
||||
payload.decodeSyncString(iosTargetPrimariesKey)?.let(::saveIosTargetPrimaries)
|
||||
payload.decodeSyncString(iosTargetTransferKey)?.let(::saveIosTargetTransfer)
|
||||
payload.decodeSyncString(iosHardwareDecoderModeKey)?.let(::saveIosHardwareDecoderMode)
|
||||
payload.decodeSyncBoolean(iosExtendedDynamicRangeEnabledKey)?.let(::saveIosExtendedDynamicRangeEnabled)
|
||||
payload.decodeSyncBoolean(iosTargetColorspaceHintEnabledKey)?.let(::saveIosTargetColorspaceHintEnabled)
|
||||
payload.decodeSyncBoolean(iosHdrComputePeakEnabledKey)?.let(::saveIosHdrComputePeakEnabled)
|
||||
payload.decodeSyncBoolean(iosDebandEnabledKey)?.let(::saveIosDebandEnabled)
|
||||
payload.decodeSyncBoolean(iosInterpolationEnabledKey)?.let(::saveIosInterpolationEnabled)
|
||||
payload.decodeSyncInt(iosBrightnessKey)?.let(::saveIosBrightness)
|
||||
payload.decodeSyncInt(iosContrastKey)?.let(::saveIosContrast)
|
||||
payload.decodeSyncInt(iosSaturationKey)?.let(::saveIosSaturation)
|
||||
payload.decodeSyncInt(iosGammaKey)?.let(::saveIosGamma)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,317 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun IosVideoSettingsModal(
|
||||
visible: Boolean,
|
||||
settings: PlayerSettingsUiState,
|
||||
onSettingsChanged: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(tween(200)),
|
||||
exit = fadeOut(tween(200)),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onDismiss,
|
||||
)
|
||||
.background(colorScheme.scrim.copy(alpha = 0.56f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val maxH = maxHeight
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically(tween(300)) { it / 3 } + fadeIn(tween(300)),
|
||||
exit = slideOutVertically(tween(250)) { it / 3 } + fadeOut(tween(250)),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 460.dp)
|
||||
.fillMaxWidth(0.92f)
|
||||
.heightIn(max = maxH * 0.95f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(colorScheme.surface)
|
||||
.border(1.dp, colorScheme.outlineVariant.copy(alpha = 0.8f), RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {},
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Video",
|
||||
color = colorScheme.onSurface,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
TextButton(onClick = {
|
||||
PlayerSettingsRepository.resetIosVideoOutputTuning()
|
||||
onSettingsChanged()
|
||||
}) {
|
||||
Text("Reset tuning")
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
OptionGroup(
|
||||
title = "Output preset",
|
||||
options = IosVideoOutputPreset.entries,
|
||||
selected = settings.iosVideoOutputPreset,
|
||||
label = { it.label },
|
||||
description = { it.description },
|
||||
onSelect = {
|
||||
PlayerSettingsRepository.setIosVideoOutputPreset(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
|
||||
ToggleRow(
|
||||
title = "HDR peak detection",
|
||||
description = "Estimate HDR peak brightness when metadata is bad or missing.",
|
||||
checked = settings.iosHdrComputePeakEnabled,
|
||||
onCheckedChange = {
|
||||
PlayerSettingsRepository.setIosHdrComputePeakEnabled(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
|
||||
OptionGroup(
|
||||
title = "Tone mapping",
|
||||
options = IosToneMappingMode.entries,
|
||||
selected = settings.iosToneMappingMode,
|
||||
label = { it.label },
|
||||
onSelect = {
|
||||
PlayerSettingsRepository.setIosToneMappingMode(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
|
||||
ToggleRow(
|
||||
title = "Deband",
|
||||
description = "Reduce color banding at a small performance cost.",
|
||||
checked = settings.iosDebandEnabled,
|
||||
onCheckedChange = {
|
||||
PlayerSettingsRepository.setIosDebandEnabled(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
ToggleRow(
|
||||
title = "Frame interpolation",
|
||||
description = "Smooth motion when mpv can use display sync cleanly.",
|
||||
checked = settings.iosInterpolationEnabled,
|
||||
onCheckedChange = {
|
||||
PlayerSettingsRepository.setIosInterpolationEnabled(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
|
||||
PictureSlider(
|
||||
title = "Brightness",
|
||||
value = settings.iosBrightness,
|
||||
onValueChanged = {
|
||||
PlayerSettingsRepository.setIosBrightness(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
PictureSlider(
|
||||
title = "Contrast",
|
||||
value = settings.iosContrast,
|
||||
onValueChanged = {
|
||||
PlayerSettingsRepository.setIosContrast(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
PictureSlider(
|
||||
title = "Saturation",
|
||||
value = settings.iosSaturation,
|
||||
onValueChanged = {
|
||||
PlayerSettingsRepository.setIosSaturation(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
PictureSlider(
|
||||
title = "Gamma",
|
||||
value = settings.iosGamma,
|
||||
onValueChanged = {
|
||||
PlayerSettingsRepository.setIosGamma(it)
|
||||
onSettingsChanged()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(
|
||||
title: String,
|
||||
description: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(text = title, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold)
|
||||
Text(text = description, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp)
|
||||
}
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PictureSlider(
|
||||
title: String,
|
||||
value: Int,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(text = value.toString(), color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Slider(
|
||||
value = value.toFloat(),
|
||||
onValueChange = { onValueChanged(it.roundToInt().coerceIn(-50, 50)) },
|
||||
valueRange = -50f..50f,
|
||||
steps = 99,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> OptionGroup(
|
||||
title: String,
|
||||
options: List<T>,
|
||||
selected: T,
|
||||
label: (T) -> String,
|
||||
description: ((T) -> String)? = null,
|
||||
onSelect: (T) -> Unit,
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
options.forEach { option ->
|
||||
val isSelected = option == selected
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect(option) },
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = label(option), color = MaterialTheme.colorScheme.onSurface)
|
||||
val subtitle = description?.invoke(option)
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.material.icons.rounded.LockOpen
|
|||
import androidx.compose.material.icons.rounded.Replay10
|
||||
import androidx.compose.material.icons.rounded.Speed
|
||||
import androidx.compose.material.icons.rounded.SwapHoriz
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.material.icons.rounded.VideoLibrary
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -83,6 +84,7 @@ internal fun PlayerControlsShell(
|
|||
onSpeedClick: () -> Unit,
|
||||
onSubtitleClick: () -> Unit,
|
||||
onAudioClick: () -> Unit,
|
||||
onVideoSettingsClick: (() -> Unit)? = null,
|
||||
onSourcesClick: (() -> Unit)? = null,
|
||||
onEpisodesClick: (() -> Unit)? = null,
|
||||
onSubmitIntroClick: (() -> Unit)? = null,
|
||||
|
|
@ -182,6 +184,7 @@ internal fun PlayerControlsShell(
|
|||
onSpeedClick = onSpeedClick,
|
||||
onSubtitleClick = onSubtitleClick,
|
||||
onAudioClick = onAudioClick,
|
||||
onVideoSettingsClick = onVideoSettingsClick,
|
||||
onSourcesClick = onSourcesClick,
|
||||
onEpisodesClick = onEpisodesClick,
|
||||
modifier = Modifier
|
||||
|
|
@ -469,6 +472,7 @@ private fun ProgressControls(
|
|||
onSpeedClick: () -> Unit,
|
||||
onSubtitleClick: () -> Unit,
|
||||
onAudioClick: () -> Unit,
|
||||
onVideoSettingsClick: (() -> Unit)? = null,
|
||||
onSourcesClick: (() -> Unit)? = null,
|
||||
onEpisodesClick: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -538,6 +542,13 @@ private fun ProgressControls(
|
|||
painter = audioPainter,
|
||||
onClick = onAudioClick,
|
||||
)
|
||||
if (onVideoSettingsClick != null) {
|
||||
PlayerActionPillButton(
|
||||
label = "Video",
|
||||
icon = Icons.Rounded.Tune,
|
||||
onClick = onVideoSettingsClick,
|
||||
)
|
||||
}
|
||||
if (onSourcesClick != null) {
|
||||
PlayerActionPillButton(
|
||||
label = stringResource(Res.string.compose_player_sources),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface PlayerEngineController {
|
|||
fun clearExternalSubtitle()
|
||||
fun clearExternalSubtitleAndSelect(trackIndex: Int)
|
||||
fun applySubtitleStyle(style: SubtitleStyleState) {}
|
||||
fun configureIosVideoOutput(settings: PlayerSettingsUiState) {}
|
||||
}
|
||||
|
||||
internal fun sanitizePlaybackHeaders(headers: Map<String, String>?): Map<String, String> {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,73 @@ enum class PlayerResizeMode {
|
|||
Zoom,
|
||||
}
|
||||
|
||||
enum class IosVideoOutputPreset(
|
||||
val label: String,
|
||||
val description: String,
|
||||
) {
|
||||
NativeEdr(
|
||||
label = "Native EDR",
|
||||
description = "Best for HDR-capable iPhones and iPads.",
|
||||
),
|
||||
SdrToneMapped(
|
||||
label = "SDR tone mapped",
|
||||
description = "More predictable whites and blacks on SDR-style output.",
|
||||
),
|
||||
Compatibility(
|
||||
label = "Compatibility",
|
||||
description = "Closest to the older iOS MPV behavior.",
|
||||
),
|
||||
Custom(
|
||||
label = "Custom",
|
||||
description = "Use your advanced values below.",
|
||||
),
|
||||
}
|
||||
|
||||
enum class IosToneMappingMode(
|
||||
val mpvValue: String,
|
||||
val label: String,
|
||||
) {
|
||||
Auto("auto", "Auto"),
|
||||
Bt2390("bt.2390", "BT.2390"),
|
||||
Mobius("mobius", "Mobius"),
|
||||
Reinhard("reinhard", "Reinhard"),
|
||||
Hable("hable", "Hable"),
|
||||
Gamma("gamma", "Gamma"),
|
||||
Clip("clip", "Clip"),
|
||||
}
|
||||
|
||||
enum class IosTargetPrimaries(
|
||||
val mpvValue: String,
|
||||
val label: String,
|
||||
) {
|
||||
Auto("auto", "Auto"),
|
||||
Bt709("bt.709", "BT.709"),
|
||||
DisplayP3("display-p3", "Display P3"),
|
||||
Bt2020("bt.2020", "BT.2020"),
|
||||
}
|
||||
|
||||
enum class IosTargetTransfer(
|
||||
val mpvValue: String,
|
||||
val label: String,
|
||||
) {
|
||||
Auto("auto", "Auto"),
|
||||
Srgb("srgb", "sRGB"),
|
||||
Bt1886("bt.1886", "BT.1886"),
|
||||
Gamma22("gamma2.2", "Gamma 2.2"),
|
||||
Gamma24("gamma2.4", "Gamma 2.4"),
|
||||
Pq("pq", "PQ"),
|
||||
Hlg("hlg", "HLG"),
|
||||
}
|
||||
|
||||
enum class IosHardwareDecoderMode(
|
||||
val mpvValue: String,
|
||||
val label: String,
|
||||
) {
|
||||
Auto("auto", "Auto"),
|
||||
VideoToolbox("videotoolbox", "VideoToolbox"),
|
||||
Off("no", "Off"),
|
||||
}
|
||||
|
||||
data class PlayerPlaybackSnapshot(
|
||||
val isLoading: Boolean = true,
|
||||
val isPlaying: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import com.nuvio.app.features.watchprogress.WatchProgressClock
|
|||
import com.nuvio.app.features.watchprogress.WatchProgressPlaybackSession
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
||||
import com.nuvio.app.isIos
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
@ -472,6 +473,7 @@ fun PlayerScreen(
|
|||
|
||||
var showAudioModal by remember { mutableStateOf(false) }
|
||||
var showSubtitleModal by remember { mutableStateOf(false) }
|
||||
var showVideoSettingsModal by remember { mutableStateOf(false) }
|
||||
var audioTracks by remember { mutableStateOf<List<AudioTrack>>(emptyList()) }
|
||||
var subtitleTracks by remember { mutableStateOf<List<SubtitleTrack>>(emptyList()) }
|
||||
var selectedAudioIndex by remember { mutableStateOf(-1) }
|
||||
|
|
@ -609,6 +611,7 @@ fun PlayerScreen(
|
|||
renderedGestureFeedback = null
|
||||
showAudioModal = false
|
||||
showSubtitleModal = false
|
||||
showVideoSettingsModal = false
|
||||
showSourcesPanel = false
|
||||
showEpisodesPanel = false
|
||||
episodeStreamsPanelState = EpisodeStreamsPanelState()
|
||||
|
|
@ -1805,6 +1808,14 @@ fun PlayerScreen(
|
|||
refreshTracks()
|
||||
showAudioModal = true
|
||||
},
|
||||
onVideoSettingsClick = if (isIos) {
|
||||
{
|
||||
showVideoSettingsModal = true
|
||||
controlsVisible = true
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onSourcesClick = if (activeVideoId != null) { { openSourcesPanel() } } else null,
|
||||
onEpisodesClick = if (isSeries) { { openEpisodesPanel() } } else null,
|
||||
onSubmitIntroClick = if (isSeries && playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introDbApiKey.isNotBlank()) { { showSubmitIntroModal = true } } else null,
|
||||
|
|
@ -1973,6 +1984,15 @@ fun PlayerScreen(
|
|||
onDismiss = { showSubtitleModal = false },
|
||||
)
|
||||
|
||||
IosVideoSettingsModal(
|
||||
visible = showVideoSettingsModal,
|
||||
settings = playerSettingsUiState,
|
||||
onSettingsChanged = {
|
||||
playerController?.configureIosVideoOutput(PlayerSettingsRepository.uiState.value)
|
||||
},
|
||||
onDismiss = { showVideoSettingsModal = false },
|
||||
)
|
||||
|
||||
// Sources Panel
|
||||
PlayerSourcesPanel(
|
||||
visible = showSourcesPanel,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,20 @@ data class PlayerSettingsUiState(
|
|||
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
||||
val useLibass: Boolean = false,
|
||||
val libassRenderType: String = "CUES",
|
||||
val iosVideoOutputPreset: IosVideoOutputPreset = IosVideoOutputPreset.NativeEdr,
|
||||
val iosToneMappingMode: IosToneMappingMode = IosToneMappingMode.Auto,
|
||||
val iosTargetPrimaries: IosTargetPrimaries = IosTargetPrimaries.Auto,
|
||||
val iosTargetTransfer: IosTargetTransfer = IosTargetTransfer.Auto,
|
||||
val iosHardwareDecoderMode: IosHardwareDecoderMode = IosHardwareDecoderMode.Auto,
|
||||
val iosExtendedDynamicRangeEnabled: Boolean = true,
|
||||
val iosTargetColorspaceHintEnabled: Boolean = true,
|
||||
val iosHdrComputePeakEnabled: Boolean = true,
|
||||
val iosDebandEnabled: Boolean = false,
|
||||
val iosInterpolationEnabled: Boolean = false,
|
||||
val iosBrightness: Int = 0,
|
||||
val iosContrast: Int = 0,
|
||||
val iosSaturation: Int = 0,
|
||||
val iosGamma: Int = 0,
|
||||
)
|
||||
|
||||
object PlayerSettingsRepository {
|
||||
|
|
@ -84,6 +98,20 @@ object PlayerSettingsRepository {
|
|||
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
private var useLibass = false
|
||||
private var libassRenderType = "CUES"
|
||||
private var iosVideoOutputPreset = IosVideoOutputPreset.NativeEdr
|
||||
private var iosToneMappingMode = IosToneMappingMode.Auto
|
||||
private var iosTargetPrimaries = IosTargetPrimaries.Auto
|
||||
private var iosTargetTransfer = IosTargetTransfer.Auto
|
||||
private var iosHardwareDecoderMode = IosHardwareDecoderMode.Auto
|
||||
private var iosExtendedDynamicRangeEnabled = true
|
||||
private var iosTargetColorspaceHintEnabled = true
|
||||
private var iosHdrComputePeakEnabled = true
|
||||
private var iosDebandEnabled = false
|
||||
private var iosInterpolationEnabled = false
|
||||
private var iosBrightness = 0
|
||||
private var iosContrast = 0
|
||||
private var iosSaturation = 0
|
||||
private var iosGamma = 0
|
||||
|
||||
fun ensureLoaded() {
|
||||
if (hasLoaded) return
|
||||
|
|
@ -130,6 +158,20 @@ object PlayerSettingsRepository {
|
|||
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||
useLibass = false
|
||||
libassRenderType = "CUES"
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.NativeEdr
|
||||
iosToneMappingMode = IosToneMappingMode.Auto
|
||||
iosTargetPrimaries = IosTargetPrimaries.Auto
|
||||
iosTargetTransfer = IosTargetTransfer.Auto
|
||||
iosHardwareDecoderMode = IosHardwareDecoderMode.Auto
|
||||
iosExtendedDynamicRangeEnabled = true
|
||||
iosTargetColorspaceHintEnabled = true
|
||||
iosHdrComputePeakEnabled = true
|
||||
iosDebandEnabled = false
|
||||
iosInterpolationEnabled = false
|
||||
iosBrightness = 0
|
||||
iosContrast = 0
|
||||
iosSaturation = 0
|
||||
iosGamma = 0
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +246,30 @@ object PlayerSettingsRepository {
|
|||
nextEpisodeThresholdMinutesBeforeEnd = PlayerSettingsStorage.loadNextEpisodeThresholdMinutesBeforeEnd() ?: 2f
|
||||
useLibass = PlayerSettingsStorage.loadUseLibass() ?: false
|
||||
libassRenderType = PlayerSettingsStorage.loadLibassRenderType() ?: "CUES"
|
||||
iosVideoOutputPreset = PlayerSettingsStorage.loadIosVideoOutputPreset()
|
||||
?.let { runCatching { IosVideoOutputPreset.valueOf(it) }.getOrNull() }
|
||||
?: IosVideoOutputPreset.NativeEdr
|
||||
iosToneMappingMode = PlayerSettingsStorage.loadIosToneMappingMode()
|
||||
?.let { runCatching { IosToneMappingMode.valueOf(it) }.getOrNull() }
|
||||
?: IosToneMappingMode.Auto
|
||||
iosTargetPrimaries = PlayerSettingsStorage.loadIosTargetPrimaries()
|
||||
?.let { runCatching { IosTargetPrimaries.valueOf(it) }.getOrNull() }
|
||||
?: IosTargetPrimaries.Auto
|
||||
iosTargetTransfer = PlayerSettingsStorage.loadIosTargetTransfer()
|
||||
?.let { runCatching { IosTargetTransfer.valueOf(it) }.getOrNull() }
|
||||
?: IosTargetTransfer.Auto
|
||||
iosHardwareDecoderMode = PlayerSettingsStorage.loadIosHardwareDecoderMode()
|
||||
?.let { runCatching { IosHardwareDecoderMode.valueOf(it) }.getOrNull() }
|
||||
?: IosHardwareDecoderMode.Auto
|
||||
iosExtendedDynamicRangeEnabled = PlayerSettingsStorage.loadIosExtendedDynamicRangeEnabled() ?: true
|
||||
iosTargetColorspaceHintEnabled = PlayerSettingsStorage.loadIosTargetColorspaceHintEnabled() ?: true
|
||||
iosHdrComputePeakEnabled = PlayerSettingsStorage.loadIosHdrComputePeakEnabled() ?: true
|
||||
iosDebandEnabled = PlayerSettingsStorage.loadIosDebandEnabled() ?: false
|
||||
iosInterpolationEnabled = PlayerSettingsStorage.loadIosInterpolationEnabled() ?: false
|
||||
iosBrightness = PlayerSettingsStorage.loadIosBrightness() ?: 0
|
||||
iosContrast = PlayerSettingsStorage.loadIosContrast() ?: 0
|
||||
iosSaturation = PlayerSettingsStorage.loadIosSaturation() ?: 0
|
||||
iosGamma = PlayerSettingsStorage.loadIosGamma() ?: 0
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -498,6 +564,164 @@ object PlayerSettingsRepository {
|
|||
PlayerSettingsStorage.saveLibassRenderType(renderType)
|
||||
}
|
||||
|
||||
fun setIosVideoOutputPreset(preset: IosVideoOutputPreset) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = preset
|
||||
when (preset) {
|
||||
IosVideoOutputPreset.NativeEdr -> {
|
||||
iosExtendedDynamicRangeEnabled = true
|
||||
iosTargetColorspaceHintEnabled = true
|
||||
iosHdrComputePeakEnabled = true
|
||||
iosToneMappingMode = IosToneMappingMode.Auto
|
||||
iosTargetPrimaries = IosTargetPrimaries.Auto
|
||||
iosTargetTransfer = IosTargetTransfer.Auto
|
||||
}
|
||||
IosVideoOutputPreset.SdrToneMapped -> {
|
||||
iosExtendedDynamicRangeEnabled = false
|
||||
iosTargetColorspaceHintEnabled = false
|
||||
iosHdrComputePeakEnabled = true
|
||||
iosToneMappingMode = IosToneMappingMode.Bt2390
|
||||
iosTargetPrimaries = IosTargetPrimaries.Bt709
|
||||
iosTargetTransfer = IosTargetTransfer.Srgb
|
||||
}
|
||||
IosVideoOutputPreset.Compatibility -> {
|
||||
iosExtendedDynamicRangeEnabled = false
|
||||
iosTargetColorspaceHintEnabled = true
|
||||
iosHdrComputePeakEnabled = false
|
||||
iosToneMappingMode = IosToneMappingMode.Auto
|
||||
iosTargetPrimaries = IosTargetPrimaries.Auto
|
||||
iosTargetTransfer = IosTargetTransfer.Auto
|
||||
}
|
||||
IosVideoOutputPreset.Custom -> Unit
|
||||
}
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosToneMappingMode(mode: IosToneMappingMode) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosToneMappingMode = mode
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosTargetPrimaries(primaries: IosTargetPrimaries) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosTargetPrimaries = primaries
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosTargetTransfer(transfer: IosTargetTransfer) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosTargetTransfer = transfer
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosHardwareDecoderMode(mode: IosHardwareDecoderMode) {
|
||||
ensureLoaded()
|
||||
iosHardwareDecoderMode = mode
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosHardwareDecoderMode(mode.name)
|
||||
}
|
||||
|
||||
fun setIosExtendedDynamicRangeEnabled(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosExtendedDynamicRangeEnabled = enabled
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosTargetColorspaceHintEnabled(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosTargetColorspaceHintEnabled = enabled
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosHdrComputePeakEnabled(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
iosVideoOutputPreset = IosVideoOutputPreset.Custom
|
||||
iosHdrComputePeakEnabled = enabled
|
||||
publish()
|
||||
saveIosVideoOutputSettings()
|
||||
}
|
||||
|
||||
fun setIosDebandEnabled(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
iosDebandEnabled = enabled
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosDebandEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setIosInterpolationEnabled(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
iosInterpolationEnabled = enabled
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosInterpolationEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setIosBrightness(value: Int) {
|
||||
ensureLoaded()
|
||||
iosBrightness = value.coerceIn(-50, 50)
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosBrightness(iosBrightness)
|
||||
}
|
||||
|
||||
fun setIosContrast(value: Int) {
|
||||
ensureLoaded()
|
||||
iosContrast = value.coerceIn(-50, 50)
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosContrast(iosContrast)
|
||||
}
|
||||
|
||||
fun setIosSaturation(value: Int) {
|
||||
ensureLoaded()
|
||||
iosSaturation = value.coerceIn(-50, 50)
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosSaturation(iosSaturation)
|
||||
}
|
||||
|
||||
fun setIosGamma(value: Int) {
|
||||
ensureLoaded()
|
||||
iosGamma = value.coerceIn(-50, 50)
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosGamma(iosGamma)
|
||||
}
|
||||
|
||||
fun resetIosVideoOutputTuning() {
|
||||
ensureLoaded()
|
||||
iosBrightness = 0
|
||||
iosContrast = 0
|
||||
iosSaturation = 0
|
||||
iosGamma = 0
|
||||
iosDebandEnabled = false
|
||||
iosInterpolationEnabled = false
|
||||
publish()
|
||||
PlayerSettingsStorage.saveIosBrightness(0)
|
||||
PlayerSettingsStorage.saveIosContrast(0)
|
||||
PlayerSettingsStorage.saveIosSaturation(0)
|
||||
PlayerSettingsStorage.saveIosGamma(0)
|
||||
PlayerSettingsStorage.saveIosDebandEnabled(false)
|
||||
PlayerSettingsStorage.saveIosInterpolationEnabled(false)
|
||||
}
|
||||
|
||||
private fun saveIosVideoOutputSettings() {
|
||||
PlayerSettingsStorage.saveIosVideoOutputPreset(iosVideoOutputPreset.name)
|
||||
PlayerSettingsStorage.saveIosToneMappingMode(iosToneMappingMode.name)
|
||||
PlayerSettingsStorage.saveIosTargetPrimaries(iosTargetPrimaries.name)
|
||||
PlayerSettingsStorage.saveIosTargetTransfer(iosTargetTransfer.name)
|
||||
PlayerSettingsStorage.saveIosExtendedDynamicRangeEnabled(iosExtendedDynamicRangeEnabled)
|
||||
PlayerSettingsStorage.saveIosTargetColorspaceHintEnabled(iosTargetColorspaceHintEnabled)
|
||||
PlayerSettingsStorage.saveIosHdrComputePeakEnabled(iosHdrComputePeakEnabled)
|
||||
}
|
||||
|
||||
private fun publish() {
|
||||
_uiState.value = PlayerSettingsUiState(
|
||||
showLoadingOverlay = showLoadingOverlay,
|
||||
|
|
@ -534,6 +758,20 @@ object PlayerSettingsRepository {
|
|||
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
||||
useLibass = useLibass,
|
||||
libassRenderType = libassRenderType,
|
||||
iosVideoOutputPreset = iosVideoOutputPreset,
|
||||
iosToneMappingMode = iosToneMappingMode,
|
||||
iosTargetPrimaries = iosTargetPrimaries,
|
||||
iosTargetTransfer = iosTargetTransfer,
|
||||
iosHardwareDecoderMode = iosHardwareDecoderMode,
|
||||
iosExtendedDynamicRangeEnabled = iosExtendedDynamicRangeEnabled,
|
||||
iosTargetColorspaceHintEnabled = iosTargetColorspaceHintEnabled,
|
||||
iosHdrComputePeakEnabled = iosHdrComputePeakEnabled,
|
||||
iosDebandEnabled = iosDebandEnabled,
|
||||
iosInterpolationEnabled = iosInterpolationEnabled,
|
||||
iosBrightness = iosBrightness,
|
||||
iosContrast = iosContrast,
|
||||
iosSaturation = iosSaturation,
|
||||
iosGamma = iosGamma,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,34 @@ internal expect object PlayerSettingsStorage {
|
|||
fun saveUseLibass(enabled: Boolean)
|
||||
fun loadLibassRenderType(): String?
|
||||
fun saveLibassRenderType(renderType: String)
|
||||
fun loadIosVideoOutputPreset(): String?
|
||||
fun saveIosVideoOutputPreset(preset: String)
|
||||
fun loadIosToneMappingMode(): String?
|
||||
fun saveIosToneMappingMode(mode: String)
|
||||
fun loadIosTargetPrimaries(): String?
|
||||
fun saveIosTargetPrimaries(primaries: String)
|
||||
fun loadIosTargetTransfer(): String?
|
||||
fun saveIosTargetTransfer(transfer: String)
|
||||
fun loadIosHardwareDecoderMode(): String?
|
||||
fun saveIosHardwareDecoderMode(mode: String)
|
||||
fun loadIosExtendedDynamicRangeEnabled(): Boolean?
|
||||
fun saveIosExtendedDynamicRangeEnabled(enabled: Boolean)
|
||||
fun loadIosTargetColorspaceHintEnabled(): Boolean?
|
||||
fun saveIosTargetColorspaceHintEnabled(enabled: Boolean)
|
||||
fun loadIosHdrComputePeakEnabled(): Boolean?
|
||||
fun saveIosHdrComputePeakEnabled(enabled: Boolean)
|
||||
fun loadIosDebandEnabled(): Boolean?
|
||||
fun saveIosDebandEnabled(enabled: Boolean)
|
||||
fun loadIosInterpolationEnabled(): Boolean?
|
||||
fun saveIosInterpolationEnabled(enabled: Boolean)
|
||||
fun loadIosBrightness(): Int?
|
||||
fun saveIosBrightness(value: Int)
|
||||
fun loadIosContrast(): Int?
|
||||
fun saveIosContrast(value: Int)
|
||||
fun loadIosSaturation(): Int?
|
||||
fun saveIosSaturation(value: Int)
|
||||
fun loadIosGamma(): Int?
|
||||
fun saveIosGamma(value: Int)
|
||||
fun exportToSyncPayload(): JsonObject
|
||||
fun replaceFromSyncPayload(payload: JsonObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ import com.nuvio.app.features.player.AudioLanguageOption
|
|||
import com.nuvio.app.features.player.AvailableLanguageOptions
|
||||
import com.nuvio.app.features.player.ExternalPlayerApp
|
||||
import com.nuvio.app.features.player.ExternalPlayerPlatform
|
||||
import com.nuvio.app.features.player.IosHardwareDecoderMode
|
||||
import com.nuvio.app.features.player.IosTargetPrimaries
|
||||
import com.nuvio.app.features.player.IosTargetTransfer
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.player.SubtitleLanguageOption
|
||||
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
||||
|
|
@ -175,6 +178,9 @@ private fun PlaybackSettingsSection(
|
|||
var showReuseCacheDurationDialog by remember { mutableStateOf(false) }
|
||||
var showDecoderPriorityDialog by remember { mutableStateOf(false) }
|
||||
var showHoldToSpeedValueDialog by remember { mutableStateOf(false) }
|
||||
var showIosHardwareDecoderDialog by remember { mutableStateOf(false) }
|
||||
var showIosTargetPrimariesDialog by remember { mutableStateOf(false) }
|
||||
var showIosTargetTransferDialog by remember { mutableStateOf(false) }
|
||||
var showLibassRenderTypeDialog by remember { mutableStateOf(false) }
|
||||
var showAutoPlayModeDialog by remember { mutableStateOf(false) }
|
||||
var showAutoPlaySourceDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -487,6 +493,52 @@ private fun PlaybackSettingsSection(
|
|||
}
|
||||
}
|
||||
|
||||
if (isIos) {
|
||||
SettingsSection(
|
||||
title = "iOS video output",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
SettingsGroup(isTablet = isTablet) {
|
||||
SettingsNavigationRow(
|
||||
title = "Hardware decoder",
|
||||
description = autoPlayPlayerSettings.iosHardwareDecoderMode.label,
|
||||
isTablet = isTablet,
|
||||
onClick = { showIosHardwareDecoderDialog = true },
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsSwitchRow(
|
||||
title = "Extended dynamic range",
|
||||
description = "Default Metal output mode for new playback sessions.",
|
||||
checked = autoPlayPlayerSettings.iosExtendedDynamicRangeEnabled,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setIosExtendedDynamicRangeEnabled,
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsSwitchRow(
|
||||
title = "Display color hint",
|
||||
description = "Let mpv target the active display color space by default.",
|
||||
checked = autoPlayPlayerSettings.iosTargetColorspaceHintEnabled,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setIosTargetColorspaceHintEnabled,
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = "Target primaries",
|
||||
description = autoPlayPlayerSettings.iosTargetPrimaries.label,
|
||||
isTablet = isTablet,
|
||||
onClick = { showIosTargetPrimariesDialog = true },
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = "Target transfer",
|
||||
description = autoPlayPlayerSettings.iosTargetTransfer.label,
|
||||
isTablet = isTablet,
|
||||
onClick = { showIosTargetTransferDialog = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isIos) {
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_playback_section_subtitle_rendering),
|
||||
|
|
@ -854,6 +906,48 @@ private fun PlaybackSettingsSection(
|
|||
)
|
||||
}
|
||||
|
||||
if (showIosHardwareDecoderDialog) {
|
||||
IosEnumSelectionDialog(
|
||||
title = "Hardware decoder",
|
||||
options = IosHardwareDecoderMode.entries,
|
||||
selected = autoPlayPlayerSettings.iosHardwareDecoderMode,
|
||||
label = { it.label },
|
||||
onSelect = {
|
||||
PlayerSettingsRepository.setIosHardwareDecoderMode(it)
|
||||
showIosHardwareDecoderDialog = false
|
||||
},
|
||||
onDismiss = { showIosHardwareDecoderDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showIosTargetPrimariesDialog) {
|
||||
IosEnumSelectionDialog(
|
||||
title = "Target primaries",
|
||||
options = IosTargetPrimaries.entries,
|
||||
selected = autoPlayPlayerSettings.iosTargetPrimaries,
|
||||
label = { it.label },
|
||||
onSelect = {
|
||||
PlayerSettingsRepository.setIosTargetPrimaries(it)
|
||||
showIosTargetPrimariesDialog = false
|
||||
},
|
||||
onDismiss = { showIosTargetPrimariesDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showIosTargetTransferDialog) {
|
||||
IosEnumSelectionDialog(
|
||||
title = "Target transfer",
|
||||
options = IosTargetTransfer.entries,
|
||||
selected = autoPlayPlayerSettings.iosTargetTransfer,
|
||||
label = { it.label },
|
||||
onSelect = {
|
||||
PlayerSettingsRepository.setIosTargetTransfer(it)
|
||||
showIosTargetTransferDialog = false
|
||||
},
|
||||
onDismiss = { showIosTargetTransferDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showLibassRenderTypeDialog) {
|
||||
LibassRenderTypeDialog(
|
||||
selectedRenderType = libassRenderType,
|
||||
|
|
@ -1318,6 +1412,94 @@ private fun DecoderPriorityDialog(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun <T> IosEnumSelectionDialog(
|
||||
title: String,
|
||||
options: List<T>,
|
||||
selected: T,
|
||||
label: (T) -> String,
|
||||
onSelect: (T) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
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 = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
options.forEach { option ->
|
||||
val isSelected = option == selected
|
||||
val containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect(option) },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = containerColor,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = label(option),
|
||||
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 = stringResource(Res.string.settings_playback_dialog_close),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun HoldToSpeedValueDialog(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ interface NuvioPlayerBridge {
|
|||
fun seekTo(positionMs: Long)
|
||||
fun seekBy(offsetMs: Long)
|
||||
fun retry()
|
||||
fun configureVideoOutput(
|
||||
hardwareDecoder: String,
|
||||
targetColorspaceHint: Boolean,
|
||||
toneMapping: String,
|
||||
hdrComputePeak: Boolean,
|
||||
targetPrimaries: String,
|
||||
targetTransfer: String,
|
||||
extendedDynamicRange: Boolean,
|
||||
deband: Boolean,
|
||||
interpolation: Boolean,
|
||||
brightness: Int,
|
||||
contrast: Int,
|
||||
saturation: Int,
|
||||
gamma: Int,
|
||||
)
|
||||
fun setPlaybackSpeed(speed: Float)
|
||||
fun setResizeMode(mode: Int) // 0=Fit, 1=Fill, 2=Zoom
|
||||
fun getAudioTrackCount(): Int
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.nuvio.app.features.player
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.interop.UIKitViewController
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -37,6 +39,9 @@ actual fun PlatformPlayerSurface(
|
|||
val latestOnControllerReady = rememberUpdatedState(onControllerReady)
|
||||
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
||||
val latestOnError = rememberUpdatedState(onError)
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
val playerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val latestPlayerSettings = rememberUpdatedState(playerSettings)
|
||||
|
||||
val bridge = remember {
|
||||
NuvioPlayerBridgeFactory.create()
|
||||
|
|
@ -71,6 +76,10 @@ actual fun PlatformPlayerSurface(
|
|||
bridge.retry()
|
||||
}
|
||||
|
||||
override fun configureIosVideoOutput(settings: PlayerSettingsUiState) {
|
||||
bridge.applyIosVideoOutputSettings(settings)
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
bridge.setPlaybackSpeed(speed)
|
||||
}
|
||||
|
|
@ -214,6 +223,7 @@ actual fun PlatformPlayerSurface(
|
|||
|
||||
// Load file and set initial state
|
||||
LaunchedEffect(bridge, sourceUrl, sourceAudioUrl, sourceHeaders) {
|
||||
bridge.applyIosVideoOutputSettings(latestPlayerSettings.value)
|
||||
bridge.loadFileWithAudio(
|
||||
sourceUrl,
|
||||
sourceAudioUrl,
|
||||
|
|
@ -242,6 +252,10 @@ actual fun PlatformPlayerSurface(
|
|||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(bridge, playerSettings) {
|
||||
bridge.applyIosVideoOutputSettings(playerSettings)
|
||||
}
|
||||
|
||||
// Polling for snapshots
|
||||
LaunchedEffect(bridge) {
|
||||
var lastReportedError: String? = null
|
||||
|
|
@ -280,6 +294,24 @@ actual fun PlatformPlayerSurface(
|
|||
)
|
||||
}
|
||||
|
||||
private fun NuvioPlayerBridge.applyIosVideoOutputSettings(settings: PlayerSettingsUiState) {
|
||||
configureVideoOutput(
|
||||
hardwareDecoder = settings.iosHardwareDecoderMode.mpvValue,
|
||||
targetColorspaceHint = settings.iosTargetColorspaceHintEnabled,
|
||||
toneMapping = settings.iosToneMappingMode.mpvValue,
|
||||
hdrComputePeak = settings.iosHdrComputePeakEnabled,
|
||||
targetPrimaries = settings.iosTargetPrimaries.mpvValue,
|
||||
targetTransfer = settings.iosTargetTransfer.mpvValue,
|
||||
extendedDynamicRange = settings.iosExtendedDynamicRangeEnabled,
|
||||
deband = settings.iosDebandEnabled,
|
||||
interpolation = settings.iosInterpolationEnabled,
|
||||
brightness = settings.iosBrightness,
|
||||
contrast = settings.iosContrast,
|
||||
saturation = settings.iosSaturation,
|
||||
gamma = settings.iosGamma,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Color.toMpvColorString(): String {
|
||||
val redInt = (red * 255f).toInt().coerceIn(0, 255)
|
||||
val greenInt = (green * 255f).toInt().coerceIn(0, 255)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,20 @@ actual object PlayerSettingsStorage {
|
|||
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||
private const val useLibassKey = "use_libass"
|
||||
private const val libassRenderTypeKey = "libass_render_type"
|
||||
private const val iosVideoOutputPresetKey = "ios_video_output_preset"
|
||||
private const val iosToneMappingModeKey = "ios_tone_mapping_mode"
|
||||
private const val iosTargetPrimariesKey = "ios_target_primaries"
|
||||
private const val iosTargetTransferKey = "ios_target_transfer"
|
||||
private const val iosHardwareDecoderModeKey = "ios_hardware_decoder_mode"
|
||||
private const val iosExtendedDynamicRangeEnabledKey = "ios_extended_dynamic_range_enabled"
|
||||
private const val iosTargetColorspaceHintEnabledKey = "ios_target_colorspace_hint_enabled"
|
||||
private const val iosHdrComputePeakEnabledKey = "ios_hdr_compute_peak_enabled"
|
||||
private const val iosDebandEnabledKey = "ios_deband_enabled"
|
||||
private const val iosInterpolationEnabledKey = "ios_interpolation_enabled"
|
||||
private const val iosBrightnessKey = "ios_brightness"
|
||||
private const val iosContrastKey = "ios_contrast"
|
||||
private const val iosSaturationKey = "ios_saturation"
|
||||
private const val iosGammaKey = "ios_gamma"
|
||||
private val syncKeys = listOf(
|
||||
showLoadingOverlayKey,
|
||||
resizeModeKey,
|
||||
|
|
@ -90,8 +104,42 @@ actual object PlayerSettingsStorage {
|
|||
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||
useLibassKey,
|
||||
libassRenderTypeKey,
|
||||
iosVideoOutputPresetKey,
|
||||
iosToneMappingModeKey,
|
||||
iosTargetPrimariesKey,
|
||||
iosTargetTransferKey,
|
||||
iosHardwareDecoderModeKey,
|
||||
iosExtendedDynamicRangeEnabledKey,
|
||||
iosTargetColorspaceHintEnabledKey,
|
||||
iosHdrComputePeakEnabledKey,
|
||||
iosDebandEnabledKey,
|
||||
iosInterpolationEnabledKey,
|
||||
iosBrightnessKey,
|
||||
iosContrastKey,
|
||||
iosSaturationKey,
|
||||
iosGammaKey,
|
||||
)
|
||||
|
||||
private fun loadBoolean(keyBase: String): Boolean? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(keyBase)
|
||||
return if (defaults.objectForKey(key) != null) defaults.boolForKey(key) else null
|
||||
}
|
||||
|
||||
private fun saveBoolean(keyBase: String, enabled: Boolean) {
|
||||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(keyBase))
|
||||
}
|
||||
|
||||
private fun loadInt(keyBase: String): Int? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(keyBase)
|
||||
return if (defaults.objectForKey(key) != null) defaults.integerForKey(key).toInt() else null
|
||||
}
|
||||
|
||||
private fun saveInt(keyBase: String, value: Int) {
|
||||
NSUserDefaults.standardUserDefaults.setInteger(value.toLong(), forKey = ProfileScopedKey.of(keyBase))
|
||||
}
|
||||
|
||||
actual fun loadShowLoadingOverlay(): Boolean? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(showLoadingOverlayKey)
|
||||
|
|
@ -552,6 +600,100 @@ actual object PlayerSettingsStorage {
|
|||
|
||||
actual fun saveLibassRenderType(renderType: String) {}
|
||||
|
||||
actual fun loadIosVideoOutputPreset(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(iosVideoOutputPresetKey))
|
||||
|
||||
actual fun saveIosVideoOutputPreset(preset: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(preset, forKey = ProfileScopedKey.of(iosVideoOutputPresetKey))
|
||||
}
|
||||
|
||||
actual fun loadIosToneMappingMode(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(iosToneMappingModeKey))
|
||||
|
||||
actual fun saveIosToneMappingMode(mode: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(mode, forKey = ProfileScopedKey.of(iosToneMappingModeKey))
|
||||
}
|
||||
|
||||
actual fun loadIosTargetPrimaries(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(iosTargetPrimariesKey))
|
||||
|
||||
actual fun saveIosTargetPrimaries(primaries: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(primaries, forKey = ProfileScopedKey.of(iosTargetPrimariesKey))
|
||||
}
|
||||
|
||||
actual fun loadIosTargetTransfer(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(iosTargetTransferKey))
|
||||
|
||||
actual fun saveIosTargetTransfer(transfer: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(transfer, forKey = ProfileScopedKey.of(iosTargetTransferKey))
|
||||
}
|
||||
|
||||
actual fun loadIosHardwareDecoderMode(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(iosHardwareDecoderModeKey))
|
||||
|
||||
actual fun saveIosHardwareDecoderMode(mode: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(mode, forKey = ProfileScopedKey.of(iosHardwareDecoderModeKey))
|
||||
}
|
||||
|
||||
actual fun loadIosExtendedDynamicRangeEnabled(): Boolean? =
|
||||
loadBoolean(iosExtendedDynamicRangeEnabledKey)
|
||||
|
||||
actual fun saveIosExtendedDynamicRangeEnabled(enabled: Boolean) {
|
||||
saveBoolean(iosExtendedDynamicRangeEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosTargetColorspaceHintEnabled(): Boolean? =
|
||||
loadBoolean(iosTargetColorspaceHintEnabledKey)
|
||||
|
||||
actual fun saveIosTargetColorspaceHintEnabled(enabled: Boolean) {
|
||||
saveBoolean(iosTargetColorspaceHintEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosHdrComputePeakEnabled(): Boolean? =
|
||||
loadBoolean(iosHdrComputePeakEnabledKey)
|
||||
|
||||
actual fun saveIosHdrComputePeakEnabled(enabled: Boolean) {
|
||||
saveBoolean(iosHdrComputePeakEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosDebandEnabled(): Boolean? =
|
||||
loadBoolean(iosDebandEnabledKey)
|
||||
|
||||
actual fun saveIosDebandEnabled(enabled: Boolean) {
|
||||
saveBoolean(iosDebandEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosInterpolationEnabled(): Boolean? =
|
||||
loadBoolean(iosInterpolationEnabledKey)
|
||||
|
||||
actual fun saveIosInterpolationEnabled(enabled: Boolean) {
|
||||
saveBoolean(iosInterpolationEnabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadIosBrightness(): Int? = loadInt(iosBrightnessKey)
|
||||
|
||||
actual fun saveIosBrightness(value: Int) {
|
||||
saveInt(iosBrightnessKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosContrast(): Int? = loadInt(iosContrastKey)
|
||||
|
||||
actual fun saveIosContrast(value: Int) {
|
||||
saveInt(iosContrastKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosSaturation(): Int? = loadInt(iosSaturationKey)
|
||||
|
||||
actual fun saveIosSaturation(value: Int) {
|
||||
saveInt(iosSaturationKey, value)
|
||||
}
|
||||
|
||||
actual fun loadIosGamma(): Int? = loadInt(iosGammaKey)
|
||||
|
||||
actual fun saveIosGamma(value: Int) {
|
||||
saveInt(iosGammaKey, value)
|
||||
}
|
||||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
||||
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
||||
|
|
@ -588,6 +730,20 @@ actual object PlayerSettingsStorage {
|
|||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
||||
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
|
||||
loadIosVideoOutputPreset()?.let { put(iosVideoOutputPresetKey, encodeSyncString(it)) }
|
||||
loadIosToneMappingMode()?.let { put(iosToneMappingModeKey, encodeSyncString(it)) }
|
||||
loadIosTargetPrimaries()?.let { put(iosTargetPrimariesKey, encodeSyncString(it)) }
|
||||
loadIosTargetTransfer()?.let { put(iosTargetTransferKey, encodeSyncString(it)) }
|
||||
loadIosHardwareDecoderMode()?.let { put(iosHardwareDecoderModeKey, encodeSyncString(it)) }
|
||||
loadIosExtendedDynamicRangeEnabled()?.let { put(iosExtendedDynamicRangeEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosTargetColorspaceHintEnabled()?.let { put(iosTargetColorspaceHintEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosHdrComputePeakEnabled()?.let { put(iosHdrComputePeakEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosDebandEnabled()?.let { put(iosDebandEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosInterpolationEnabled()?.let { put(iosInterpolationEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadIosBrightness()?.let { put(iosBrightnessKey, encodeSyncInt(it)) }
|
||||
loadIosContrast()?.let { put(iosContrastKey, encodeSyncInt(it)) }
|
||||
loadIosSaturation()?.let { put(iosSaturationKey, encodeSyncInt(it)) }
|
||||
loadIosGamma()?.let { put(iosGammaKey, encodeSyncInt(it)) }
|
||||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
|
|
@ -631,5 +787,19 @@ actual object PlayerSettingsStorage {
|
|||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
||||
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
|
||||
payload.decodeSyncString(iosVideoOutputPresetKey)?.let(::saveIosVideoOutputPreset)
|
||||
payload.decodeSyncString(iosToneMappingModeKey)?.let(::saveIosToneMappingMode)
|
||||
payload.decodeSyncString(iosTargetPrimariesKey)?.let(::saveIosTargetPrimaries)
|
||||
payload.decodeSyncString(iosTargetTransferKey)?.let(::saveIosTargetTransfer)
|
||||
payload.decodeSyncString(iosHardwareDecoderModeKey)?.let(::saveIosHardwareDecoderMode)
|
||||
payload.decodeSyncBoolean(iosExtendedDynamicRangeEnabledKey)?.let(::saveIosExtendedDynamicRangeEnabled)
|
||||
payload.decodeSyncBoolean(iosTargetColorspaceHintEnabledKey)?.let(::saveIosTargetColorspaceHintEnabled)
|
||||
payload.decodeSyncBoolean(iosHdrComputePeakEnabledKey)?.let(::saveIosHdrComputePeakEnabled)
|
||||
payload.decodeSyncBoolean(iosDebandEnabledKey)?.let(::saveIosDebandEnabled)
|
||||
payload.decodeSyncBoolean(iosInterpolationEnabledKey)?.let(::saveIosInterpolationEnabled)
|
||||
payload.decodeSyncInt(iosBrightnessKey)?.let(::saveIosBrightness)
|
||||
payload.decodeSyncInt(iosContrastKey)?.let(::saveIosContrast)
|
||||
payload.decodeSyncInt(iosSaturationKey)?.let(::saveIosSaturation)
|
||||
payload.decodeSyncInt(iosGammaKey)?.let(::saveIosGamma)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,37 @@ final class MPVPlayerBridgeImpl: NSObject, NuvioPlayerBridge {
|
|||
func seekTo(positionMs: Int64) { playerVC?.seekToMs(positionMs) }
|
||||
func seekBy(offsetMs: Int64) { playerVC?.seekByMs(offsetMs) }
|
||||
func retry() { playerVC?.retryPlayback() }
|
||||
func configureVideoOutput(
|
||||
hardwareDecoder: String,
|
||||
targetColorspaceHint: Bool,
|
||||
toneMapping: String,
|
||||
hdrComputePeak: Bool,
|
||||
targetPrimaries: String,
|
||||
targetTransfer: String,
|
||||
extendedDynamicRange: Bool,
|
||||
deband: Bool,
|
||||
interpolation: Bool,
|
||||
brightness: Int32,
|
||||
contrast: Int32,
|
||||
saturation: Int32,
|
||||
gamma: Int32
|
||||
) {
|
||||
playerVC?.configureVideoOutput(
|
||||
hardwareDecoder: hardwareDecoder,
|
||||
targetColorspaceHint: targetColorspaceHint,
|
||||
toneMapping: toneMapping,
|
||||
hdrComputePeak: hdrComputePeak,
|
||||
targetPrimaries: targetPrimaries,
|
||||
targetTransfer: targetTransfer,
|
||||
extendedDynamicRange: extendedDynamicRange,
|
||||
deband: deband,
|
||||
interpolation: interpolation,
|
||||
brightness: Int(brightness),
|
||||
contrast: Int(contrast),
|
||||
saturation: Int(saturation),
|
||||
gamma: Int(gamma)
|
||||
)
|
||||
}
|
||||
func setPlaybackSpeed(speed: Float) { playerVC?.setSpeed(speed) }
|
||||
func setResizeMode(mode: Int32) { playerVC?.setResize(Int(mode)) }
|
||||
|
||||
|
|
@ -204,6 +235,7 @@ final class MPVPlayerViewController: UIViewController {
|
|||
metalLayer.contentsScale = view.window?.screen.nativeScale ?? UIScreen.main.nativeScale
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.backgroundColor = UIColor.black.cgColor
|
||||
metalLayer.wantsExtendedDynamicRangeContent = true
|
||||
view.layer.addSublayer(metalLayer)
|
||||
layoutMetalLayer()
|
||||
|
||||
|
|
@ -286,7 +318,7 @@ final class MPVPlayerViewController: UIViewController {
|
|||
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "tone-mapping", "auto"))
|
||||
checkError(mpv_set_option_string(mpv, "hdr-compute-peak", "no"))
|
||||
checkError(mpv_set_option_string(mpv, "hdr-compute-peak", "yes"))
|
||||
|
||||
checkError(mpv_initialize(mpv))
|
||||
|
||||
|
|
@ -435,6 +467,38 @@ final class MPVPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func configureVideoOutput(
|
||||
hardwareDecoder: String,
|
||||
targetColorspaceHint: Bool,
|
||||
toneMapping: String,
|
||||
hdrComputePeak: Bool,
|
||||
targetPrimaries: String,
|
||||
targetTransfer: String,
|
||||
extendedDynamicRange: Bool,
|
||||
deband: Bool,
|
||||
interpolation: Bool,
|
||||
brightness: Int,
|
||||
contrast: Int,
|
||||
saturation: Int,
|
||||
gamma: Int
|
||||
) {
|
||||
metalLayer.wantsExtendedDynamicRangeContent = extendedDynamicRange
|
||||
guard mpv != nil else { return }
|
||||
|
||||
setStringProperty("hwdec", hardwareDecoder)
|
||||
setStringProperty("target-colorspace-hint", targetColorspaceHint ? "yes" : "no")
|
||||
setStringProperty("tone-mapping", toneMapping)
|
||||
setStringProperty("hdr-compute-peak", hdrComputePeak ? "yes" : "no")
|
||||
setStringProperty("target-prim", targetPrimaries)
|
||||
setStringProperty("target-trc", targetTransfer)
|
||||
setStringProperty("deband", deband ? "yes" : "no")
|
||||
setStringProperty("interpolation", interpolation ? "yes" : "no")
|
||||
setVideoEqualizer("brightness", brightness)
|
||||
setVideoEqualizer("contrast", contrast)
|
||||
setVideoEqualizer("saturation", saturation)
|
||||
setVideoEqualizer("gamma", gamma)
|
||||
}
|
||||
|
||||
func setSpeed(_ speed: Float) {
|
||||
guard mpv != nil else { return }
|
||||
var s = Double(speed)
|
||||
|
|
@ -831,6 +895,12 @@ final class MPVPlayerViewController: UIViewController {
|
|||
checkError(mpv_set_property_string(mpv, name, value))
|
||||
}
|
||||
|
||||
private func setVideoEqualizer(_ name: String, _ value: Int) {
|
||||
guard mpv != nil else { return }
|
||||
var clamped = Int64(max(-100, min(100, value)))
|
||||
checkError(mpv_set_property(mpv, name, MPV_FORMAT_INT64, &clamped))
|
||||
}
|
||||
|
||||
private func getInt(_ name: String) -> Int {
|
||||
guard mpv != nil else { return 0 }
|
||||
var data = Int64()
|
||||
|
|
|
|||
Loading…
Reference in a new issue