Verify stream viewer cleanup barriers with native and SDK faults
This commit is contained in:
1 parent
e072624c8d
commit
304b48a289
1 file changed
+106
+106
@@ -0,0 +1,106 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.media.AudioTrack
|
||||
import android.media.MediaCodec
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Настоящие viewer/video/mixer workers; управляемые границы JNI и Android SDK. */
|
||||
class StreamPlayerEngineLifecycleTest {
|
||||
private val viewers = mutableListOf<StreamPlayerEngine>()
|
||||
private val surfaces = mutableListOf<Surface>()
|
||||
private val main get() = Looper.getMainLooper()
|
||||
@Before fun prepare() {
|
||||
main.advanceBy(30_000); assertFalse(StreamPlayerEngine.viewing.value)
|
||||
HostStreams.reset(); CallController.blocked = false
|
||||
MediaCodec.created.clear(); MediaCodec.nextFailures = emptySet(); MediaCodec.onInput = {}
|
||||
AudioTrack.created.clear(); AudioTrack.nextFailures = emptySet(); MediaAudioController.failures = emptySet()
|
||||
}
|
||||
@After fun release() {
|
||||
HostStreams.failures = emptySet(); HostStreams.onPull = {}; HostStreams.onWait = {}
|
||||
MediaCodec.created.forEach { it.failures = emptySet() }; AudioTrack.created.forEach { it.failures = emptySet() }
|
||||
MediaAudioController.failures = emptySet(); viewers.forEach { it.stop() }
|
||||
await("viewer registry cleanup") { !StreamPlayerEngine.viewing.value }
|
||||
StreamAudioMixer.awaitIdle(); surfaces.forEach { it.release() }; main.advanceBy(30_000)
|
||||
assertTrue(HostStreams.ports.isEmpty()); assertTrue(HostStreams.signals.isEmpty()); assertTrue(HostStreams.playbacks.isEmpty())
|
||||
assertTrue(MediaCodec.created.all { it.released }); assertTrue(AudioTrack.created.all { it.released })
|
||||
assertTrue(MediaAudioController.outputs.isEmpty())
|
||||
}
|
||||
private fun await(message: String, condition: () -> Boolean) {
|
||||
val until = System.nanoTime() + TimeUnit.SECONDS.toNanos(4)
|
||||
while (!condition() && System.nanoTime() < until) { main.advanceBy(10); Thread.sleep(2) }
|
||||
main.advanceBy(0); assertTrue(message, condition())
|
||||
}
|
||||
private fun record(kind: Int, data: ByteArray = byteArrayOf()) = ByteBuffer.allocate(28 + data.size).order(ByteOrder.BIG_ENDIAN)
|
||||
.put(kind.toByte()).put(1).putShort(640).putShort(480).putShort(0).putLong(1000).putInt(data.size).putInt(1)
|
||||
.put(1).put(0).put(0).put(0).put(data).array()
|
||||
private fun viewer(key: Boolean = false, audio: Boolean = false): StreamPlayerEngine {
|
||||
HostStreams.status = true; HostStreams.audioEnabled = audio
|
||||
if (key) HostStreams.records.add(record(1, StreamVideoCodec.H264.keyFrame(byteArrayOf(0, 0, 0, 1, 5, 1),
|
||||
byteArrayOf(0, 0, 0, 1, 7, 1, 0, 0, 0, 1, 8, 1))))
|
||||
return StreamPlayerEngine(1, 2, 3).also {
|
||||
viewers.add(it); val surface = Surface().also { surfaces.add(it) }; it.start(surface)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun failedOpenStillCompletesCleanupAndTheReleaseCallback() {
|
||||
HostStreams.failures = setOf("open"); val viewer = viewer(); val released = AtomicInteger()
|
||||
viewer.stop { released.incrementAndGet() }; await("failed open callback") { released.get() == 1 }
|
||||
assertTrue(HostStreams.signals.isEmpty())
|
||||
}
|
||||
@Test fun nativeStopFailureCannotSkipIndependentPlaybackCleanupOrLoseTheSignal() {
|
||||
val viewer = viewer(); await("native playback opened") { HostStreams.playbacks.isNotEmpty() }
|
||||
HostStreams.failures = setOf("stop"); val released = AtomicInteger(); viewer.stop { released.incrementAndGet() }
|
||||
await("independent audio cleanup") { HostStreams.playbacks.isEmpty() }
|
||||
assertEquals(0, released.get()); assertTrue(HostStreams.signals.isNotEmpty()); assertTrue(StreamPlayerEngine.viewing.value)
|
||||
HostStreams.failures = emptySet(); await("native stop retry acknowledged") { released.get() == 1 }
|
||||
}
|
||||
@Test fun decoderReleaseFailureKeepsTheSurfaceCallbackAndNotificationOwned() {
|
||||
val viewer = viewer(key = true); await("decoder is active") { MediaCodec.created.any { it.started } }
|
||||
MediaCodec.created.single().failures = setOf("release")
|
||||
val released = AtomicInteger(); viewer.stop { released.incrementAndGet() }
|
||||
await("playback closes independently") { HostStreams.playbacks.isEmpty() }
|
||||
assertEquals(0, released.get()); assertTrue(HostStreams.signals.isNotEmpty())
|
||||
MediaCodec.created.single().failures = emptySet(); await("decoder retry acknowledged") { released.get() == 1 }
|
||||
}
|
||||
@Test fun audioResetFailureDoesNotDropTheOriginalNativeHandle() {
|
||||
val viewer = viewer(); await("playback created") { HostStreams.playbacks.size == 1 }
|
||||
val original = HostStreams.playbacks.single(); HostStreams.failures = setOf("playback close")
|
||||
HostStreams.records.add(record(3))
|
||||
await("failed reset reported") { viewer.status.value == com.utun.chat.R.string.stream_failed }
|
||||
assertEquals(setOf(original), HostStreams.playbacks.toSet())
|
||||
val released = AtomicInteger(); viewer.stop { released.incrementAndGet() }; assertEquals(0, released.get())
|
||||
HostStreams.failures = emptySet(); await("original handle release retries") { released.get() == 1 }
|
||||
}
|
||||
@Test fun signalCloseFailureDoesNotAcknowledgeTheSurfaceOrViewingBarrier() {
|
||||
val viewer = viewer(); await("viewer opened") { HostStreams.signals.size == 1 }
|
||||
HostStreams.failures = setOf("signal close"); val released = AtomicInteger(); viewer.stop { released.incrementAndGet() }
|
||||
await("other native owners closed") { HostStreams.playbacks.isEmpty() && HostStreams.ports.isEmpty() }
|
||||
assertEquals(0, released.get()); assertTrue(StreamPlayerEngine.viewing.value)
|
||||
HostStreams.failures = emptySet(); await("signal close retry") { released.get() == 1 }
|
||||
}
|
||||
@Test fun failedReleaseCallbackIsRetriedBeforeTheViewerIsAcknowledged() {
|
||||
val viewer = viewer(); await("viewer opened") { HostStreams.signals.size == 1 }
|
||||
val attempts = AtomicInteger(); val released = AtomicInteger()
|
||||
viewer.stop { if (attempts.incrementAndGet() == 1) error("Surface release failed"); released.incrementAndGet() }
|
||||
await("release callback retries") { released.get() == 1 }; assertEquals(2, attempts.get())
|
||||
}
|
||||
@Test fun failedAudioOutputReleaseBlocksAMixerReplacementUntilTheOldDeviceAcknowledges() {
|
||||
val first = viewer(key = true, audio = true); await("first shared output active") { AudioTrack.created.size == 1 }
|
||||
val old = AudioTrack.created.single(); old.failures = setOf("release")
|
||||
first.stop(); await("first viewer released") { !StreamPlayerEngine.viewing.value }
|
||||
assertThrows(IllegalStateException::class.java) { StreamAudioMixer.awaitIdle() }
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user