Guard viewer startup and cover the native teardown barrier
This commit is contained in:
1 parent
495d399691
commit
ee20c8b9b7
2 files changed
+33
-2
No files matched your search
@@ -68,6 +68,7 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
private val id = StreamController.nextId()
|
||||
private var requestedAtUs = 0L
|
||||
@Volatile private var running = false
|
||||
private var stopped = false // Main: stop до start также запрещает запуск этого одноразового viewer.
|
||||
@Volatile var drainTimeoutMs = 2000L
|
||||
private set
|
||||
private var worker: Thread? = null
|
||||
@@ -76,6 +77,8 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
private val releaseCallbacks = mutableListOf<() -> Unit>()
|
||||
|
||||
fun start(surface: Surface) {
|
||||
check(Looper.myLooper() == Looper.getMainLooper()) { "Viewer start requires main" }
|
||||
if (stopped) { LogManager.addLog("WARN", "stream", "viewer start after stop ignored id=$id"); return }
|
||||
if (worker != null) return
|
||||
if (CallController.blocksMedia()) {
|
||||
LogManager.addLog("WARN", "stream", "viewer blocked by call id=$id")
|
||||
@@ -108,10 +111,18 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
if (!main.post { players.remove(this); mutableViewing.value = players.isNotEmpty() })
|
||||
LogManager.addLog("ERROR", "stream", "viewer completion post rejected id=$id; registry retains owner")
|
||||
}
|
||||
}, "stream-audio-$id").apply { start() }
|
||||
}, "stream-audio-$id")
|
||||
try { worker!!.start() } catch (e: Exception) {
|
||||
running = false; stopped = true
|
||||
synchronized(releaseLock) { released = true }
|
||||
players.remove(this); mutableViewing.value = players.isNotEmpty(); mutableStatus.value = R.string.stream_failed
|
||||
LogManager.addLog("ERROR", "stream", "viewer worker start failed id=$id; no native owners started: $e")
|
||||
}
|
||||
}
|
||||
/** Surface/SurfaceTexture освобождаются callback только после остановки decoder и native DSP. */
|
||||
fun stop(onReleased: (() -> Unit)? = null) {
|
||||
check(Looper.myLooper() == Looper.getMainLooper()) { "Viewer stop requires main" }
|
||||
stopped = true
|
||||
running = false
|
||||
try { NativeLib.streamStop(id) } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "stream", "viewer stop request failed id=$id; worker retains cleanup: $e")
|
||||
@@ -120,7 +131,10 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
val immediate = synchronized(releaseLock) {
|
||||
if (worker == null || released) true else { releaseCallbacks.add(onReleased); false }
|
||||
}
|
||||
if (immediate) onReleased()
|
||||
if (immediate) try { onReleased() } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "stream", "late viewer release callback failed id=$id; caller retains ownership: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
@@ -53,6 +53,7 @@ class StreamPlayerEngineLifecycleTest {
|
||||
|
||||
@Test fun failedOpenStillCompletesCleanupAndTheReleaseCallback() {
|
||||
HostStreams.failures = setOf("open"); val viewer = viewer(); val released = AtomicInteger()
|
||||
await("open refusal reported") { viewer.status.value == com.utun.chat.R.string.stream_failed }
|
||||
viewer.stop { released.incrementAndGet() }; await("failed open callback") { released.get() == 1 }
|
||||
assertTrue(HostStreams.signals.isEmpty())
|
||||
}
|
||||
@@ -98,9 +99,25 @@ class StreamPlayerEngineLifecycleTest {
|
||||
val old = AudioTrack.created.single(); old.failures = setOf("release")
|
||||
first.stop(); await("first viewer released") { !StreamPlayerEngine.viewing.value }
|
||||
assertThrows(IllegalStateException::class.java) { StreamAudioMixer.awaitIdle() }
|
||||
assertThrows(IllegalStateException::class.java) { StreamPlayerEngine.stopForCore() }
|
||||
val second = viewer(key = true, audio = true); await("second playback ready") { HostStreams.playbacks.isNotEmpty() }
|
||||
assertEquals(1, AudioTrack.created.size); assertFalse(old.released)
|
||||
old.failures = emptySet(); await("second output opens after old release") { AudioTrack.created.size == 2 }
|
||||
assertTrue(old.released); second.stop()
|
||||
}
|
||||
|
||||
@Test fun coreTeardownCannotPassAViewerWithUnacknowledgedNativeResources() {
|
||||
val viewer = viewer(); await("viewer signal opened") { HostStreams.signals.size == 1 }
|
||||
HostStreams.failures = setOf("signal close")
|
||||
assertThrows(IllegalStateException::class.java) { StreamPlayerEngine.stopForCore() }
|
||||
await("other native owners close") { HostStreams.playbacks.isEmpty() && HostStreams.ports.isEmpty() }
|
||||
assertThrows(IllegalStateException::class.java) { StreamPlayerEngine.stopForCore() }
|
||||
HostStreams.failures = emptySet(); await("viewer barrier released") { !StreamPlayerEngine.viewing.value }
|
||||
StreamPlayerEngine.stopForCore()
|
||||
}
|
||||
@Test fun stopBeforeStartLeavesTheViewerPassive() {
|
||||
val viewer = StreamPlayerEngine(1, 2, 3).also { viewers.add(it) }
|
||||
viewer.stop(); val surface = Surface().also { surfaces.add(it) }; viewer.start(surface)
|
||||
assertFalse(StreamPlayerEngine.viewing.value); assertTrue(HostStreams.ports.isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user