Load audio and network settings on IO and document persistent change ownership
This commit is contained in:
1 parent
30648086b4
commit
013ff93406
3 files changed
+40
-7
No files matched your search
@@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/** Очередь Application на main: сохранить выбор, затем применить runtime без зависимости от UI scope.
|
||||
/** Очередь Application на main: сохранить выбор, затем отправить runtime-обновление без зависимости от UI scope.
|
||||
* submit принимает неизменный снимок аргументов caller; отмена await не отменяет запись.
|
||||
* awaitIdle ставит барьер после принятых изменений перед формированием restart-конфига. */
|
||||
class SettingsOperations(private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
|
||||
@@ -28,7 +28,7 @@ class SettingsOperations(private val scope: CoroutineScope = CoroutineScope(Supe
|
||||
mutex.withLock {
|
||||
try {
|
||||
operation()
|
||||
log("INFO", "Setting applied name=$name")
|
||||
log("INFO", "Settings operation completed name=$name")
|
||||
} catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) { reportFailure(name, e); throw e }
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ private fun DeveloperSettingsTab(vm: ChatViewModel, onDirty: (Boolean) -> Unit,
|
||||
listenPort = provider.getInt("node.listen_port").toString()
|
||||
controlPort = provider.getInt("control.port").toString()
|
||||
controlBindIp = provider.getString("control.bind_ip") ?: "127.0.0.1"
|
||||
val allIface = listOf("lo" to "127.0.0.1") + provider.getAvailableInterfaces()
|
||||
val allIface = listOf("lo" to "127.0.0.1") + withContext(Dispatchers.IO) { provider.getAvailableInterfaces() }
|
||||
ctrlBindLabel = allIface.firstOrNull { it.second == controlBindIp }?.let { "${it.first} (${it.second})" } ?: "lo (127.0.0.1)"
|
||||
debugLevel = provider.getString("debug.console_level") ?: "info"
|
||||
logFile = provider.getString("debug.log_file") ?: "utun.log"
|
||||
@@ -1177,10 +1177,34 @@ private fun RadioSettingsTab() {
|
||||
|
||||
/* ── Audio Settings Tab ── */
|
||||
|
||||
private data class AudioSettingsSnapshot(val voice: VoiceSettings, val bitrate: CallVideoBitrateRange,
|
||||
val jitterMs: Int, val sound: Boolean)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AudioSettingsTab(vm: ChatViewModel) {
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
var snapshot by remember { mutableStateOf<AudioSettingsSnapshot?>(null) }
|
||||
var loadFailed by remember { mutableStateOf(false) }
|
||||
var loadAttempt by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(loadAttempt) {
|
||||
loadFailed = false
|
||||
try {
|
||||
snapshot = withContext(Dispatchers.IO) { AudioSettingsSnapshot(provider.getVoiceSettings(), provider.getCallVideoBitrateRange(),
|
||||
provider.getInt("chat.call_jitter_max_reserve_ms").coerceIn(500, 2000), provider.getSoundEnabled()) }
|
||||
} catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) { loadFailed = true; provider.changes.reportFailure("load audio settings", e) }
|
||||
}
|
||||
val settingsSnapshot = snapshot
|
||||
if (settingsSnapshot == null) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (loadFailed) {
|
||||
Text(stringResource(R.string.settings_update_failed), color = MaterialTheme.colorScheme.error)
|
||||
Button(onClick = { loadAttempt++ }) { Text(stringResource(R.string.retry)) }
|
||||
} else CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* Mic test state */
|
||||
var micTesting by remember { mutableStateOf(false) }
|
||||
@@ -1192,7 +1216,7 @@ private fun AudioSettingsTab(vm: ChatViewModel) {
|
||||
}
|
||||
|
||||
/* Notification sound */
|
||||
var soundEnabled by remember { mutableStateOf(provider.getSoundEnabled()) }
|
||||
var soundEnabled by remember { mutableStateOf(settingsSnapshot.sound) }
|
||||
|
||||
LaunchedEffect(micTesting) {
|
||||
try { while (micTesting && vm.audioRecorder.isTesting) { micLevel = NativeLib.voiceGetPeakLevel(); delay(25) } }
|
||||
@@ -1203,12 +1227,12 @@ private fun AudioSettingsTab(vm: ChatViewModel) {
|
||||
|
||||
/* Codec preset */
|
||||
val presets = listOf(stringResource(R.string.codec_low), stringResource(R.string.codec_standard), stringResource(R.string.codec_high))
|
||||
val savedVoiceSettings = remember { provider.getVoiceSettings() }
|
||||
val savedVoiceSettings = settingsSnapshot.voice
|
||||
var preset by remember { mutableStateOf(savedVoiceSettings.preset) }
|
||||
var presetExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
var jitterReserveMs by remember { mutableStateOf(provider.getInt("chat.call_jitter_max_reserve_ms")) }
|
||||
val savedVideoRange = remember { provider.getCallVideoBitrateRange() }
|
||||
var jitterReserveMs by remember { mutableStateOf(settingsSnapshot.jitterMs) }
|
||||
val savedVideoRange = settingsSnapshot.bitrate
|
||||
var videoMinimum by rememberSaveable { mutableIntStateOf(savedVideoRange.minimum) }
|
||||
var videoMaximum by rememberSaveable { mutableIntStateOf(savedVideoRange.maximum) }
|
||||
var videoRangeSaving by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -292,6 +292,15 @@ Metadata читаются в IO через отдельный владелец r
|
||||
к прежним значениям при выходе. VideoFilePlayerTest/VideoFileMetadataTest проверяют
|
||||
эти контракты на управляемых SDK-границах; аппаратное декодирование в JVM не выполняется.
|
||||
|
||||
ConfigProvider владеет SettingsOperations: изменения с неизменным снимком аргументов
|
||||
принимаются на main, последовательно сохраняются и затем отправляют runtime-команды.
|
||||
Закрытие/пересоздание Settings не отменяет уже принятые записи. Back перед restart и
|
||||
открытие вкладки ждут очередь; ошибка записи остаётся видимой и не отправляет live-обновление.
|
||||
Ошибка Flow чтения сохраняет последнее значение и повторяет подписку с backoff.
|
||||
Первоначальный отказ чтения не открывает редактор с defaults; тяжёлые audio/network чтения идут через IO.
|
||||
Тест микрофона принадлежит своей вкладке: dispose запрашивает асинхронный cleanup,
|
||||
поздняя выдача разрешения не запускает захват после ухода UI. Очистка теста не отменяет запись сообщения.
|
||||
|
||||
TelecomCallManager на main регистрирует self-managed account и connections по call ID; lookup разрешён workers.
|
||||
Поздний/дублирующий Connection не заменяет живого владельца и не возвращается framework как новый активный вызов.
|
||||
Ошибки markDisconnected не прерывают destroy. Неудавшийся destroy сохраняет отдельного retiring owner
|
||||
|
||||
Reference in new issue
Block a user