Await radio and shared audio release before native core teardown
This commit is contained in:
1 parent
ea67b491f9
commit
044ff034a6
3 files changed
+29
-15
No files matched your search
@@ -2,7 +2,6 @@ package com.utun.chat.data
|
||||
|
||||
import com.utun.chat.ChatApplication
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ChatEventCallback {
|
||||
@@ -11,6 +10,8 @@ interface ChatEventCallback {
|
||||
|
||||
object NativeLib {
|
||||
@Volatile private var coreStopping = false
|
||||
@Volatile private var eventCallback: ChatEventCallback? = null
|
||||
fun isCoreStopping(): Boolean = coreStopping
|
||||
|
||||
init {
|
||||
System.loadLibrary("utun")
|
||||
@@ -18,6 +19,7 @@ object NativeLib {
|
||||
|
||||
/** Save Kotlin callbacks to C globals (log, config). Must be called before start(). */
|
||||
fun init(dbPath: String, controlPort: Int): Boolean {
|
||||
if (coreStopping) { LogManager.addLog("ERROR", "Core", "Initialization rejected: frontend cleanup pending"); return false }
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
val settings = provider.getVoiceSettings()
|
||||
val ok = nativeInit(
|
||||
@@ -29,6 +31,7 @@ object NativeLib {
|
||||
LogManager.addLog("ERROR", "AudioSettings", "Native initialization failed; voice settings not applied")
|
||||
return false
|
||||
}
|
||||
eventCallback?.let { nativeSetEventCallback(it) }
|
||||
nativeVoiceSetPreset(settings.preset)
|
||||
nativeVoiceSetCompressor(settings.compressorEnabled)
|
||||
nativeVoiceSetCompressorConfig(settings.maxGainDb, settings.lookbackMs, settings.lookaheadMs,
|
||||
@@ -46,39 +49,43 @@ object NativeLib {
|
||||
|
||||
/** Снимает frontend media на main, дожидается worker на IO даже до ready; ошибка запрещает следующий init. */
|
||||
suspend fun stop() {
|
||||
withContext(Dispatchers.Main.immediate) { prepareForCoreStop() }
|
||||
prepareForCoreStop()
|
||||
check(withContext(Dispatchers.IO) { nativeStop() }) { "native worker stop failed" }
|
||||
withContext(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() }
|
||||
coreStopping = false
|
||||
}
|
||||
|
||||
fun setEventCallback(cb: ChatEventCallback) {
|
||||
nativeSetEventCallback(object : ChatEventCallback {
|
||||
override fun onEvent(type: Int, data: ByteArray?) { cb.onEvent(type, data) }
|
||||
})
|
||||
nativeSetEventCallback(cb)
|
||||
eventCallback = cb
|
||||
}
|
||||
|
||||
/** Как stop, дополнительно освобождает JNI global refs только после подтверждённого join. */
|
||||
suspend fun destroy() {
|
||||
withContext(Dispatchers.Main.immediate) { prepareForCoreStop() }
|
||||
prepareForCoreStop()
|
||||
check(withContext(Dispatchers.IO) { nativeDestroy() }) { "native worker destroy failed" }
|
||||
withContext(Dispatchers.Main.immediate) { ChatApplication.instance.memberFiles.coreStopped() }
|
||||
coreStopping = false
|
||||
}
|
||||
|
||||
fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
|
||||
try { prepareForCoreStop(); return nativeRestart(configText) }
|
||||
suspend fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
|
||||
try { prepareForCoreStop(); return withContext(Dispatchers.IO) { nativeRestart(configText) } }
|
||||
finally { coreStopping = false }
|
||||
}
|
||||
|
||||
/** Снять frontend-владельцев перед stop/restart/сменой ключей. Неподтверждённый CallService либо ещё
|
||||
* активный mediaPreparation запрещает native stop с ошибкой; Main остаётся доступен для retry. */
|
||||
private fun prepareForCoreStop() {
|
||||
runBlocking(Dispatchers.Main.immediate) {
|
||||
private suspend fun prepareForCoreStop() {
|
||||
val radioStopped = withContext(Dispatchers.Main.immediate) {
|
||||
coreStopping = true
|
||||
val radio = RadioAudioEngine.instance().stop()
|
||||
AudioRecorderManager.stopAll()
|
||||
RadioCallController.reset()
|
||||
StreamController.reset(); CallController.reset(); ChatApplication.instance.avatars.stop(); ChatApplication.instance.memberFiles.cancel()
|
||||
radio
|
||||
}
|
||||
withContext(Dispatchers.IO) { radioStopped.get() }
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
CallService.requireReleased() // Не останавливать native при неподтверждённой очистке его media-владельца.
|
||||
StreamPlayerEngine.stopForCore()
|
||||
StreamController.requireReleased()
|
||||
@@ -88,6 +95,7 @@ object NativeLib {
|
||||
"Call media preparation still running"
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) { MediaAudioController.awaitIdle() }
|
||||
}
|
||||
|
||||
/** Лёгкое обновление сокетов (auto_sockets=android) без полного рестарта ядра */
|
||||
@@ -154,8 +162,8 @@ object NativeLib {
|
||||
fun subscribeNativeLogs(enabled: Boolean) { nativeSubscribeLogs(enabled) }
|
||||
fun setUdpLogTarget(ip: String, port: Int) { nativeSetUdpLogTarget(ip, port) }
|
||||
/** Принимает полный INI для пересоздания ядра с новой сохранённой идентичностью; true подтверждает приём запроса. */
|
||||
fun regenerateKeys(configText: String): Boolean {
|
||||
try { prepareForCoreStop(); return nativeRegenerateKeys(configText) }
|
||||
suspend fun regenerateKeys(configText: String): Boolean {
|
||||
try { prepareForCoreStop(); return withContext(Dispatchers.IO) { nativeRegenerateKeys(configText) } }
|
||||
finally { coreStopping = false }
|
||||
}
|
||||
|
||||
@@ -339,7 +347,7 @@ object NativeLib {
|
||||
private external fun nativeRadioCall(id: Long, group: Long, peer: Long, action: Int): Boolean
|
||||
|
||||
/* ── P2P аудио-звонок (медиа) ── */
|
||||
fun callAudioStart(callId: Long): Boolean = nativeCallAudioStart(callId)
|
||||
fun callAudioStart(callId: Long): Boolean = !coreStopping && nativeCallAudioStart(callId)
|
||||
/** Настоящий raw PCM и mute кадра: зануление выполняется в core после AEC/AGC. */
|
||||
fun callAudioFeed(callId: Long, samples: ShortArray, muted: Boolean, capturePtsUs: Long): Boolean =
|
||||
nativeCallAudioFeed(callId, samples, muted, capturePtsUs)
|
||||
@@ -359,11 +367,11 @@ object NativeLib {
|
||||
/* ── Рация (PTT walkie-talkie) ── */
|
||||
fun radioSetActive(groupId: Long, on: Boolean) { nativeRadioSetActive(groupId, on) }
|
||||
fun radioSubscribers(groupId: Long): Int = nativeRadioSubscribers(groupId)
|
||||
fun radioAudioStart(groupId: Long, channels: Int): Boolean = nativeRadioAudioStart(groupId, channels)
|
||||
fun radioAudioStart(groupId: Long, channels: Int): Boolean = !coreStopping && nativeRadioAudioStart(groupId, channels)
|
||||
fun radioAudioFeed(groupId: Long, samples: ShortArray): Boolean = nativeRadioAudioFeed(groupId, samples)
|
||||
fun radioAudioPull(groupId: Long, out: ShortArray, playedFrames: Long): Int = nativeRadioAudioPull(groupId, out, playedFrames)
|
||||
fun radioAudioStop() { nativeRadioAudioStop() }
|
||||
fun radioCaptureStart(groupId: Long, channels: Int, mode: Int): Boolean = nativeRadioCaptureStart(groupId, channels, mode)
|
||||
fun radioCaptureStart(groupId: Long, channels: Int, mode: Int): Boolean = !coreStopping && nativeRadioCaptureStart(groupId, channels, mode)
|
||||
/** true — копия снимка поставлена живому worker; проверка и применение ещё не подтверждены.
|
||||
* ConfigProvider.onRadioSettingsApplied вернёт тот же requestId и результат атомарной проверки/применения.
|
||||
* false — запрос отклонён (включая отсутствие callback). Stop/restart может отменить ожидающий снимок без ACK;
|
||||
|
||||
@@ -126,6 +126,10 @@ class RadioAudioEngine {
|
||||
* Ошибка очистки/запуска даёт false. Новый start/stop инвалидирует прежний результат, также возвращая false. */
|
||||
fun start(group: Long, onReady: ((Boolean) -> Unit)? = null) {
|
||||
checkMain("start")
|
||||
if (NativeLib.isCoreStopping()) {
|
||||
LogManager.addLog("WARN", "radio", "start rejected during core cleanup group=$group")
|
||||
onReady?.invoke(false); return
|
||||
}
|
||||
if (group == 0L) {
|
||||
LogManager.addLog("ERROR", "RadioAudio", "start rejected: zero group ID")
|
||||
onReady?.invoke(false)
|
||||
|
||||
+2
@@ -35,6 +35,8 @@ object LogManager {
|
||||
|
||||
/** Только host-тест: считает уведомления standby, не загружает JNI и не запускает ядро. */
|
||||
object NativeLib {
|
||||
@Volatile var coreStopping = false
|
||||
fun isCoreStopping() = coreStopping
|
||||
@Volatile var callAudioFailures = emptySet<String>()
|
||||
@Volatile var onCallFeed: () -> Unit = {}
|
||||
val callAudioCommands: MutableList<Pair<String, Long>> = Collections.synchronizedList(mutableListOf())
|
||||
|
||||
Reference in new issue
Block a user