mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 17:52:06 +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 nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||||
private const val useLibassKey = "use_libass"
|
private const val useLibassKey = "use_libass"
|
||||||
private const val libassRenderTypeKey = "libass_render_type"
|
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(
|
private val syncKeys = listOf(
|
||||||
showLoadingOverlayKey,
|
showLoadingOverlayKey,
|
||||||
resizeModeKey,
|
resizeModeKey,
|
||||||
|
|
@ -92,6 +106,20 @@ actual object PlayerSettingsStorage {
|
||||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||||
useLibassKey,
|
useLibassKey,
|
||||||
libassRenderTypeKey,
|
libassRenderTypeKey,
|
||||||
|
iosVideoOutputPresetKey,
|
||||||
|
iosToneMappingModeKey,
|
||||||
|
iosTargetPrimariesKey,
|
||||||
|
iosTargetTransferKey,
|
||||||
|
iosHardwareDecoderModeKey,
|
||||||
|
iosExtendedDynamicRangeEnabledKey,
|
||||||
|
iosTargetColorspaceHintEnabledKey,
|
||||||
|
iosHdrComputePeakEnabledKey,
|
||||||
|
iosDebandEnabledKey,
|
||||||
|
iosInterpolationEnabledKey,
|
||||||
|
iosBrightnessKey,
|
||||||
|
iosContrastKey,
|
||||||
|
iosSaturationKey,
|
||||||
|
iosGammaKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var preferences: SharedPreferences? = null
|
private var preferences: SharedPreferences? = null
|
||||||
|
|
@ -652,6 +680,120 @@ actual object PlayerSettingsStorage {
|
||||||
?.apply()
|
?.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 {
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
||||||
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
||||||
|
|
@ -688,6 +830,20 @@ actual object PlayerSettingsStorage {
|
||||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||||
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
||||||
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(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) {
|
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||||
|
|
@ -732,5 +888,19 @@ actual object PlayerSettingsStorage {
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||||
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
||||||
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
|
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.Replay10
|
||||||
import androidx.compose.material.icons.rounded.Speed
|
import androidx.compose.material.icons.rounded.Speed
|
||||||
import androidx.compose.material.icons.rounded.SwapHoriz
|
import androidx.compose.material.icons.rounded.SwapHoriz
|
||||||
|
import androidx.compose.material.icons.rounded.Tune
|
||||||
import androidx.compose.material.icons.rounded.VideoLibrary
|
import androidx.compose.material.icons.rounded.VideoLibrary
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -83,6 +84,7 @@ internal fun PlayerControlsShell(
|
||||||
onSpeedClick: () -> Unit,
|
onSpeedClick: () -> Unit,
|
||||||
onSubtitleClick: () -> Unit,
|
onSubtitleClick: () -> Unit,
|
||||||
onAudioClick: () -> Unit,
|
onAudioClick: () -> Unit,
|
||||||
|
onVideoSettingsClick: (() -> Unit)? = null,
|
||||||
onSourcesClick: (() -> Unit)? = null,
|
onSourcesClick: (() -> Unit)? = null,
|
||||||
onEpisodesClick: (() -> Unit)? = null,
|
onEpisodesClick: (() -> Unit)? = null,
|
||||||
onSubmitIntroClick: (() -> Unit)? = null,
|
onSubmitIntroClick: (() -> Unit)? = null,
|
||||||
|
|
@ -182,6 +184,7 @@ internal fun PlayerControlsShell(
|
||||||
onSpeedClick = onSpeedClick,
|
onSpeedClick = onSpeedClick,
|
||||||
onSubtitleClick = onSubtitleClick,
|
onSubtitleClick = onSubtitleClick,
|
||||||
onAudioClick = onAudioClick,
|
onAudioClick = onAudioClick,
|
||||||
|
onVideoSettingsClick = onVideoSettingsClick,
|
||||||
onSourcesClick = onSourcesClick,
|
onSourcesClick = onSourcesClick,
|
||||||
onEpisodesClick = onEpisodesClick,
|
onEpisodesClick = onEpisodesClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -469,6 +472,7 @@ private fun ProgressControls(
|
||||||
onSpeedClick: () -> Unit,
|
onSpeedClick: () -> Unit,
|
||||||
onSubtitleClick: () -> Unit,
|
onSubtitleClick: () -> Unit,
|
||||||
onAudioClick: () -> Unit,
|
onAudioClick: () -> Unit,
|
||||||
|
onVideoSettingsClick: (() -> Unit)? = null,
|
||||||
onSourcesClick: (() -> Unit)? = null,
|
onSourcesClick: (() -> Unit)? = null,
|
||||||
onEpisodesClick: (() -> Unit)? = null,
|
onEpisodesClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -538,6 +542,13 @@ private fun ProgressControls(
|
||||||
painter = audioPainter,
|
painter = audioPainter,
|
||||||
onClick = onAudioClick,
|
onClick = onAudioClick,
|
||||||
)
|
)
|
||||||
|
if (onVideoSettingsClick != null) {
|
||||||
|
PlayerActionPillButton(
|
||||||
|
label = "Video",
|
||||||
|
icon = Icons.Rounded.Tune,
|
||||||
|
onClick = onVideoSettingsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (onSourcesClick != null) {
|
if (onSourcesClick != null) {
|
||||||
PlayerActionPillButton(
|
PlayerActionPillButton(
|
||||||
label = stringResource(Res.string.compose_player_sources),
|
label = stringResource(Res.string.compose_player_sources),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ interface PlayerEngineController {
|
||||||
fun clearExternalSubtitle()
|
fun clearExternalSubtitle()
|
||||||
fun clearExternalSubtitleAndSelect(trackIndex: Int)
|
fun clearExternalSubtitleAndSelect(trackIndex: Int)
|
||||||
fun applySubtitleStyle(style: SubtitleStyleState) {}
|
fun applySubtitleStyle(style: SubtitleStyleState) {}
|
||||||
|
fun configureIosVideoOutput(settings: PlayerSettingsUiState) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun sanitizePlaybackHeaders(headers: Map<String, String>?): Map<String, String> {
|
internal fun sanitizePlaybackHeaders(headers: Map<String, String>?): Map<String, String> {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,73 @@ enum class PlayerResizeMode {
|
||||||
Zoom,
|
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(
|
data class PlayerPlaybackSnapshot(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val isPlaying: Boolean = false,
|
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.WatchProgressPlaybackSession
|
||||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||||
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
import com.nuvio.app.features.watchprogress.buildPlaybackVideoId
|
||||||
|
import com.nuvio.app.isIos
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
@ -472,6 +473,7 @@ fun PlayerScreen(
|
||||||
|
|
||||||
var showAudioModal by remember { mutableStateOf(false) }
|
var showAudioModal by remember { mutableStateOf(false) }
|
||||||
var showSubtitleModal 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 audioTracks by remember { mutableStateOf<List<AudioTrack>>(emptyList()) }
|
||||||
var subtitleTracks by remember { mutableStateOf<List<SubtitleTrack>>(emptyList()) }
|
var subtitleTracks by remember { mutableStateOf<List<SubtitleTrack>>(emptyList()) }
|
||||||
var selectedAudioIndex by remember { mutableStateOf(-1) }
|
var selectedAudioIndex by remember { mutableStateOf(-1) }
|
||||||
|
|
@ -609,6 +611,7 @@ fun PlayerScreen(
|
||||||
renderedGestureFeedback = null
|
renderedGestureFeedback = null
|
||||||
showAudioModal = false
|
showAudioModal = false
|
||||||
showSubtitleModal = false
|
showSubtitleModal = false
|
||||||
|
showVideoSettingsModal = false
|
||||||
showSourcesPanel = false
|
showSourcesPanel = false
|
||||||
showEpisodesPanel = false
|
showEpisodesPanel = false
|
||||||
episodeStreamsPanelState = EpisodeStreamsPanelState()
|
episodeStreamsPanelState = EpisodeStreamsPanelState()
|
||||||
|
|
@ -1805,6 +1808,14 @@ fun PlayerScreen(
|
||||||
refreshTracks()
|
refreshTracks()
|
||||||
showAudioModal = true
|
showAudioModal = true
|
||||||
},
|
},
|
||||||
|
onVideoSettingsClick = if (isIos) {
|
||||||
|
{
|
||||||
|
showVideoSettingsModal = true
|
||||||
|
controlsVisible = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
onSourcesClick = if (activeVideoId != null) { { openSourcesPanel() } } else null,
|
onSourcesClick = if (activeVideoId != null) { { openSourcesPanel() } } else null,
|
||||||
onEpisodesClick = if (isSeries) { { openEpisodesPanel() } } else null,
|
onEpisodesClick = if (isSeries) { { openEpisodesPanel() } } else null,
|
||||||
onSubmitIntroClick = if (isSeries && playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introDbApiKey.isNotBlank()) { { showSubmitIntroModal = true } } else null,
|
onSubmitIntroClick = if (isSeries && playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introDbApiKey.isNotBlank()) { { showSubmitIntroModal = true } } else null,
|
||||||
|
|
@ -1973,6 +1984,15 @@ fun PlayerScreen(
|
||||||
onDismiss = { showSubtitleModal = false },
|
onDismiss = { showSubtitleModal = false },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IosVideoSettingsModal(
|
||||||
|
visible = showVideoSettingsModal,
|
||||||
|
settings = playerSettingsUiState,
|
||||||
|
onSettingsChanged = {
|
||||||
|
playerController?.configureIosVideoOutput(PlayerSettingsRepository.uiState.value)
|
||||||
|
},
|
||||||
|
onDismiss = { showVideoSettingsModal = false },
|
||||||
|
)
|
||||||
|
|
||||||
// Sources Panel
|
// Sources Panel
|
||||||
PlayerSourcesPanel(
|
PlayerSourcesPanel(
|
||||||
visible = showSourcesPanel,
|
visible = showSourcesPanel,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,20 @@ data class PlayerSettingsUiState(
|
||||||
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
|
||||||
val useLibass: Boolean = false,
|
val useLibass: Boolean = false,
|
||||||
val libassRenderType: String = "CUES",
|
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 {
|
object PlayerSettingsRepository {
|
||||||
|
|
@ -84,6 +98,20 @@ object PlayerSettingsRepository {
|
||||||
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||||
private var useLibass = false
|
private var useLibass = false
|
||||||
private var libassRenderType = "CUES"
|
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() {
|
fun ensureLoaded() {
|
||||||
if (hasLoaded) return
|
if (hasLoaded) return
|
||||||
|
|
@ -130,6 +158,20 @@ object PlayerSettingsRepository {
|
||||||
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
nextEpisodeThresholdMinutesBeforeEnd = 2f
|
||||||
useLibass = false
|
useLibass = false
|
||||||
libassRenderType = "CUES"
|
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()
|
publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +246,30 @@ object PlayerSettingsRepository {
|
||||||
nextEpisodeThresholdMinutesBeforeEnd = PlayerSettingsStorage.loadNextEpisodeThresholdMinutesBeforeEnd() ?: 2f
|
nextEpisodeThresholdMinutesBeforeEnd = PlayerSettingsStorage.loadNextEpisodeThresholdMinutesBeforeEnd() ?: 2f
|
||||||
useLibass = PlayerSettingsStorage.loadUseLibass() ?: false
|
useLibass = PlayerSettingsStorage.loadUseLibass() ?: false
|
||||||
libassRenderType = PlayerSettingsStorage.loadLibassRenderType() ?: "CUES"
|
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()
|
publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,6 +564,164 @@ object PlayerSettingsRepository {
|
||||||
PlayerSettingsStorage.saveLibassRenderType(renderType)
|
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() {
|
private fun publish() {
|
||||||
_uiState.value = PlayerSettingsUiState(
|
_uiState.value = PlayerSettingsUiState(
|
||||||
showLoadingOverlay = showLoadingOverlay,
|
showLoadingOverlay = showLoadingOverlay,
|
||||||
|
|
@ -534,6 +758,20 @@ object PlayerSettingsRepository {
|
||||||
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
|
||||||
useLibass = useLibass,
|
useLibass = useLibass,
|
||||||
libassRenderType = libassRenderType,
|
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 saveUseLibass(enabled: Boolean)
|
||||||
fun loadLibassRenderType(): String?
|
fun loadLibassRenderType(): String?
|
||||||
fun saveLibassRenderType(renderType: 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 exportToSyncPayload(): JsonObject
|
||||||
fun replaceFromSyncPayload(payload: 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.AvailableLanguageOptions
|
||||||
import com.nuvio.app.features.player.ExternalPlayerApp
|
import com.nuvio.app.features.player.ExternalPlayerApp
|
||||||
import com.nuvio.app.features.player.ExternalPlayerPlatform
|
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.PlayerSettingsRepository
|
||||||
import com.nuvio.app.features.player.SubtitleLanguageOption
|
import com.nuvio.app.features.player.SubtitleLanguageOption
|
||||||
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
|
||||||
|
|
@ -175,6 +178,9 @@ private fun PlaybackSettingsSection(
|
||||||
var showReuseCacheDurationDialog by remember { mutableStateOf(false) }
|
var showReuseCacheDurationDialog by remember { mutableStateOf(false) }
|
||||||
var showDecoderPriorityDialog by remember { mutableStateOf(false) }
|
var showDecoderPriorityDialog by remember { mutableStateOf(false) }
|
||||||
var showHoldToSpeedValueDialog 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 showLibassRenderTypeDialog by remember { mutableStateOf(false) }
|
||||||
var showAutoPlayModeDialog by remember { mutableStateOf(false) }
|
var showAutoPlayModeDialog by remember { mutableStateOf(false) }
|
||||||
var showAutoPlaySourceDialog 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) {
|
if (!isIos) {
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title = stringResource(Res.string.settings_playback_section_subtitle_rendering),
|
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) {
|
if (showLibassRenderTypeDialog) {
|
||||||
LibassRenderTypeDialog(
|
LibassRenderTypeDialog(
|
||||||
selectedRenderType = libassRenderType,
|
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
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun HoldToSpeedValueDialog(
|
private fun HoldToSpeedValueDialog(
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,21 @@ interface NuvioPlayerBridge {
|
||||||
fun seekTo(positionMs: Long)
|
fun seekTo(positionMs: Long)
|
||||||
fun seekBy(offsetMs: Long)
|
fun seekBy(offsetMs: Long)
|
||||||
fun retry()
|
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 setPlaybackSpeed(speed: Float)
|
||||||
fun setResizeMode(mode: Int) // 0=Fit, 1=Fill, 2=Zoom
|
fun setResizeMode(mode: Int) // 0=Fit, 1=Fill, 2=Zoom
|
||||||
fun getAudioTrackCount(): Int
|
fun getAudioTrackCount(): Int
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package com.nuvio.app.features.player
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.interop.UIKitViewController
|
import androidx.compose.ui.interop.UIKitViewController
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -37,6 +39,9 @@ actual fun PlatformPlayerSurface(
|
||||||
val latestOnControllerReady = rememberUpdatedState(onControllerReady)
|
val latestOnControllerReady = rememberUpdatedState(onControllerReady)
|
||||||
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
||||||
val latestOnError = rememberUpdatedState(onError)
|
val latestOnError = rememberUpdatedState(onError)
|
||||||
|
PlayerSettingsRepository.ensureLoaded()
|
||||||
|
val playerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
val latestPlayerSettings = rememberUpdatedState(playerSettings)
|
||||||
|
|
||||||
val bridge = remember {
|
val bridge = remember {
|
||||||
NuvioPlayerBridgeFactory.create()
|
NuvioPlayerBridgeFactory.create()
|
||||||
|
|
@ -71,6 +76,10 @@ actual fun PlatformPlayerSurface(
|
||||||
bridge.retry()
|
bridge.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun configureIosVideoOutput(settings: PlayerSettingsUiState) {
|
||||||
|
bridge.applyIosVideoOutputSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setPlaybackSpeed(speed: Float) {
|
override fun setPlaybackSpeed(speed: Float) {
|
||||||
bridge.setPlaybackSpeed(speed)
|
bridge.setPlaybackSpeed(speed)
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +223,7 @@ actual fun PlatformPlayerSurface(
|
||||||
|
|
||||||
// Load file and set initial state
|
// Load file and set initial state
|
||||||
LaunchedEffect(bridge, sourceUrl, sourceAudioUrl, sourceHeaders) {
|
LaunchedEffect(bridge, sourceUrl, sourceAudioUrl, sourceHeaders) {
|
||||||
|
bridge.applyIosVideoOutputSettings(latestPlayerSettings.value)
|
||||||
bridge.loadFileWithAudio(
|
bridge.loadFileWithAudio(
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
sourceAudioUrl,
|
sourceAudioUrl,
|
||||||
|
|
@ -242,6 +252,10 @@ actual fun PlatformPlayerSurface(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(bridge, playerSettings) {
|
||||||
|
bridge.applyIosVideoOutputSettings(playerSettings)
|
||||||
|
}
|
||||||
|
|
||||||
// Polling for snapshots
|
// Polling for snapshots
|
||||||
LaunchedEffect(bridge) {
|
LaunchedEffect(bridge) {
|
||||||
var lastReportedError: String? = null
|
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 {
|
private fun Color.toMpvColorString(): String {
|
||||||
val redInt = (red * 255f).toInt().coerceIn(0, 255)
|
val redInt = (red * 255f).toInt().coerceIn(0, 255)
|
||||||
val greenInt = (green * 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 nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
|
||||||
private const val useLibassKey = "use_libass"
|
private const val useLibassKey = "use_libass"
|
||||||
private const val libassRenderTypeKey = "libass_render_type"
|
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(
|
private val syncKeys = listOf(
|
||||||
showLoadingOverlayKey,
|
showLoadingOverlayKey,
|
||||||
resizeModeKey,
|
resizeModeKey,
|
||||||
|
|
@ -90,8 +104,42 @@ actual object PlayerSettingsStorage {
|
||||||
nextEpisodeThresholdMinutesBeforeEndKey,
|
nextEpisodeThresholdMinutesBeforeEndKey,
|
||||||
useLibassKey,
|
useLibassKey,
|
||||||
libassRenderTypeKey,
|
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? {
|
actual fun loadShowLoadingOverlay(): Boolean? {
|
||||||
val defaults = NSUserDefaults.standardUserDefaults
|
val defaults = NSUserDefaults.standardUserDefaults
|
||||||
val key = ProfileScopedKey.of(showLoadingOverlayKey)
|
val key = ProfileScopedKey.of(showLoadingOverlayKey)
|
||||||
|
|
@ -552,6 +600,100 @@ actual object PlayerSettingsStorage {
|
||||||
|
|
||||||
actual fun saveLibassRenderType(renderType: String) {}
|
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 {
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
|
||||||
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
loadResizeMode()?.let { put(resizeModeKey, encodeSyncString(it)) }
|
||||||
|
|
@ -588,6 +730,20 @@ actual object PlayerSettingsStorage {
|
||||||
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
|
||||||
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
|
||||||
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(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) {
|
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||||
|
|
@ -631,5 +787,19 @@ actual object PlayerSettingsStorage {
|
||||||
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
|
||||||
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
|
||||||
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
|
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 seekTo(positionMs: Int64) { playerVC?.seekToMs(positionMs) }
|
||||||
func seekBy(offsetMs: Int64) { playerVC?.seekByMs(offsetMs) }
|
func seekBy(offsetMs: Int64) { playerVC?.seekByMs(offsetMs) }
|
||||||
func retry() { playerVC?.retryPlayback() }
|
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 setPlaybackSpeed(speed: Float) { playerVC?.setSpeed(speed) }
|
||||||
func setResizeMode(mode: Int32) { playerVC?.setResize(Int(mode)) }
|
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.contentsScale = view.window?.screen.nativeScale ?? UIScreen.main.nativeScale
|
||||||
metalLayer.framebufferOnly = true
|
metalLayer.framebufferOnly = true
|
||||||
metalLayer.backgroundColor = UIColor.black.cgColor
|
metalLayer.backgroundColor = UIColor.black.cgColor
|
||||||
|
metalLayer.wantsExtendedDynamicRangeContent = true
|
||||||
view.layer.addSublayer(metalLayer)
|
view.layer.addSublayer(metalLayer)
|
||||||
layoutMetalLayer()
|
layoutMetalLayer()
|
||||||
|
|
||||||
|
|
@ -286,7 +318,7 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
|
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, "target-colorspace-hint", "yes"))
|
||||||
checkError(mpv_set_option_string(mpv, "tone-mapping", "auto"))
|
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))
|
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) {
|
func setSpeed(_ speed: Float) {
|
||||||
guard mpv != nil else { return }
|
guard mpv != nil else { return }
|
||||||
var s = Double(speed)
|
var s = Double(speed)
|
||||||
|
|
@ -831,6 +895,12 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
checkError(mpv_set_property_string(mpv, name, value))
|
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 {
|
private func getInt(_ name: String) -> Int {
|
||||||
guard mpv != nil else { return 0 }
|
guard mpv != nil else { return 0 }
|
||||||
var data = Int64()
|
var data = Int64()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue