Reject new capture while an IO teardown holds its lifecycle lock
This commit is contained in:
1 parent
c7356d2a5b
commit
48a20cda8c
2 files changed
+19
-9
No files matched your search
@@ -12,6 +12,8 @@ import com.utun.chat.ChatApplication
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Запись голосового сообщения/тест микрофона и отдельный плеер native Opus-файла.
|
||||
@@ -54,6 +56,7 @@ class AudioRecorderManager {
|
||||
@Volatile private var captureRunning = false
|
||||
@Volatile private var captureFailed = false
|
||||
private val closing = AtomicReference<CompletableFuture<Unit>?>(null)
|
||||
private val captureLock = ReentrantLock()
|
||||
private val playbackWorkers = ConcurrentHashMap.newKeySet<Thread>()
|
||||
val isTesting: Boolean get() = captureMode == CaptureMode.TEST && captureRunning
|
||||
|
||||
@@ -67,7 +70,8 @@ class AudioRecorderManager {
|
||||
* true — Android/native захват запущен; false — отказ с диагностикой и откатом принадлежащих ресурсов. */
|
||||
fun startRecording(channelId: String, replyTo: String = "", requestId: Long = 0, generation: Long = 0): Boolean {
|
||||
if (closing.get()?.isDone == false) { LogManager.addLog("WARN", "AudioRecorder", "record start rejected: cleanup pending"); return false }
|
||||
return synchronized(this) { startCapture(CaptureMode.RECORD, channelId, replyTo, requestId, generation) }
|
||||
if (!captureLock.tryLock()) { LogManager.addLog("WARN", "AudioRecorder", "record start rejected: capture lifecycle busy"); return false }
|
||||
try { return startCapture(CaptureMode.RECORD, channelId, replyTo, requestId, generation) } finally { captureLock.unlock() }
|
||||
}
|
||||
|
||||
private fun startCapture(mode: CaptureMode, target: String, reply: String = "", request: Long = 0, generation: Long = 0): Boolean {
|
||||
@@ -192,14 +196,16 @@ class AudioRecorderManager {
|
||||
|
||||
/** Длительность native в мс, -1 — отказ/запись отброшена после ошибки, 0 — нет записи.
|
||||
* Успех означает передачу подготовки в native, публикация сообщения подтверждается отдельно событиями. */
|
||||
@Synchronized fun stopRecording(): Int {
|
||||
if (captureMode == CaptureMode.RECORD) return finishCapture(true)
|
||||
fun stopRecording(): Int = captureLock.withLock {
|
||||
if (captureMode == CaptureMode.RECORD) return@withLock finishCapture(true)
|
||||
if (captureMode == null) releaseCaptureDevice()
|
||||
return 0
|
||||
0
|
||||
}
|
||||
fun cancelRecording() {
|
||||
if (closing.get()?.isDone == false) return
|
||||
synchronized(this) { if (captureMode == CaptureMode.RECORD) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() }
|
||||
if (!captureLock.tryLock()) { if (captureMode == CaptureMode.RECORD) requestClose(); return }
|
||||
try { if (captureMode == CaptureMode.RECORD) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() }
|
||||
finally { captureLock.unlock() }
|
||||
}
|
||||
fun isActive(): Boolean = captureMode == CaptureMode.RECORD && captureRunning
|
||||
|
||||
@@ -210,11 +216,14 @@ class AudioRecorderManager {
|
||||
LogManager.addLog("WARN", "AudioRecorder", "microphone test rejected: stream owns capture")
|
||||
return false
|
||||
}
|
||||
return synchronized(this) { if (isTesting) true else startCapture(CaptureMode.TEST, "__test__") }
|
||||
if (!captureLock.tryLock()) { LogManager.addLog("WARN", "AudioRecorder", "microphone test rejected: capture lifecycle busy"); return false }
|
||||
try { return if (isTesting) true else startCapture(CaptureMode.TEST, "__test__") } finally { captureLock.unlock() }
|
||||
}
|
||||
fun stopMicTest() {
|
||||
if (closing.get()?.isDone == false) return
|
||||
synchronized(this) { if (captureMode == CaptureMode.TEST) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() }
|
||||
if (!captureLock.tryLock()) { if (captureMode == CaptureMode.TEST) requestClose(); return }
|
||||
try { if (captureMode == CaptureMode.TEST) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() }
|
||||
finally { captureLock.unlock() }
|
||||
}
|
||||
|
||||
@Volatile private var playing = false
|
||||
@@ -222,7 +231,7 @@ class AudioRecorderManager {
|
||||
private var playThread: Thread? = null
|
||||
@Volatile private var playGeneration = 0
|
||||
|
||||
@Synchronized private fun forgetIfIdle() {
|
||||
private fun forgetIfIdle() = captureLock.withLock {
|
||||
if (captureMode == null && audioRecord == null && playbackWorkers.isEmpty()) owners.remove(this)
|
||||
}
|
||||
|
||||
@@ -241,7 +250,7 @@ class AudioRecorderManager {
|
||||
var retryMs = 250L
|
||||
try {
|
||||
while (true) {
|
||||
val released = synchronized(this) {
|
||||
val released = captureLock.withLock {
|
||||
if (captureMode != null) finishCapture(false) else releaseCaptureDevice()
|
||||
stopPlayback()
|
||||
captureMode == null && audioRecord == null && playbackWorkers.isEmpty()
|
||||
|
||||
+1
@@ -126,6 +126,7 @@ class AudioRecorderManagerTest {
|
||||
val worker = Thread(stopped, "voice-stop-race").apply { start() }
|
||||
try {
|
||||
assertTrue(device.stopEntered.await(1, TimeUnit.SECONDS))
|
||||
assertFalse(manager.startMicTest())
|
||||
leave.countDown()
|
||||
assertEquals(-1, stopped.get(1, TimeUnit.SECONDS))
|
||||
assertEquals(0, NativeLib.stops.get()); assertEquals(1, NativeLib.cancels.get())
|
||||
|
||||
Reference in new issue
Block a user