Fix Android stream audio backpressure without blocking video
This commit is contained in:
1 parent
91d1a7994e
commit
0fdc6e435b
3 files changed
+290
-65
No files matched your search
@@ -0,0 +1,133 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.util.ArrayDeque
|
||||
|
||||
class StreamAudioOutputTest {
|
||||
private class Device(override val threshold: Int = 1) : StreamAudioOutput.Sink {
|
||||
override var head = 0L
|
||||
override val description: String get() = "fake threshold=$threshold"
|
||||
val samples = ArrayList<Short>()
|
||||
var limit = 960
|
||||
var calls = 0
|
||||
var paused = false
|
||||
var closed = false
|
||||
override fun write(pcm: ShortArray, offset: Int, count: Int): Int {
|
||||
check(!paused && !closed)
|
||||
calls++
|
||||
if (limit < 0) return limit
|
||||
val accepted = minOf(limit, count)
|
||||
for (i in offset until offset + accepted) samples.add(pcm[i])
|
||||
return accepted
|
||||
}
|
||||
override fun pause() { paused = true }
|
||||
override fun play() { paused = false }
|
||||
override fun close() { closed = true }
|
||||
}
|
||||
private class Player(val device: Device = Device()) {
|
||||
val input = ArrayDeque<ShortArray>()
|
||||
val logs = ArrayList<String>()
|
||||
val output = StreamAudioOutput({ device }, { logs.add(it) })
|
||||
var finished = false
|
||||
var pulls = 0
|
||||
fun step(ms: Long, hold: Boolean = false) = output.step(ms * 1_000_000, hold,
|
||||
{ pulls++; input.pollFirst() }, { finished && input.isEmpty() })
|
||||
}
|
||||
|
||||
@Test fun slowStartupDoesNotBlockOtherPlayerWork() {
|
||||
val p = Player(); p.input.add(ShortArray(960) { it.toShort() }); p.device.limit = 0
|
||||
var videoSteps = 0
|
||||
for (ms in 0L..300L step 5) { p.step(ms); videoSteps++ }
|
||||
assertEquals(61, videoSteps); assertEquals(61, p.device.calls); assertEquals(1, p.pulls)
|
||||
assertEquals(480, p.output.pendingFrames); assertEquals(0L, p.output.played)
|
||||
p.device.limit = 960; p.step(305); p.device.head = 480; p.step(310)
|
||||
assertTrue(p.output.drained); assertTrue(p.logs.any { it.contains("audio started") })
|
||||
}
|
||||
|
||||
@Test fun partialWritesPreserveEverySampleAndOnlyPullOnce() {
|
||||
val p = Player(); val pcm = ShortArray(960) { it.toShort() }; p.input.add(pcm)
|
||||
p.device.limit = 120
|
||||
repeat(8) { p.step(it * 5L) }
|
||||
assertEquals(pcm.toList(), p.device.samples); assertEquals(1, p.pulls)
|
||||
assertEquals(480L, p.output.written); assertEquals(0L, p.output.played)
|
||||
assertEquals(0, p.output.pendingFrames); assertFalse(p.output.drained)
|
||||
}
|
||||
|
||||
@Test fun waitingForInitialNetworkDataIsNotDeviceStall() {
|
||||
val p = Player(Device(3848)); p.input.add(ShortArray(200)); p.step(0); p.step(10000)
|
||||
assertEquals(100L, p.output.written); assertEquals(0L, p.output.played)
|
||||
}
|
||||
|
||||
@Test fun clockDoesNotAdvanceWithWallTimeAndStalledDeviceFails() {
|
||||
val p = Player(); p.input.add(ShortArray(960)); p.step(0); p.step(1999)
|
||||
assertEquals(0L, p.output.played)
|
||||
val error = assertThrows(IllegalStateException::class.java) { p.step(2000) }
|
||||
assertTrue(error.message!!.contains("no playback progress"))
|
||||
assertTrue(error.message!!.contains("written=480 played=0"))
|
||||
}
|
||||
|
||||
@Test fun actualHeadProgressRestartsWatchdog() {
|
||||
val p = Player(); p.input.add(ShortArray(960)); p.step(0)
|
||||
p.device.head = 100; p.step(1900); p.step(3800)
|
||||
assertEquals(100L, p.output.played)
|
||||
p.device.head = 480; p.step(3900); p.step(10000)
|
||||
assertTrue(p.output.drained)
|
||||
}
|
||||
|
||||
@Test fun intentionalPausePreservesPartialPcmAndSuspendsWatchdog() {
|
||||
val p = Player(); val pcm = ShortArray(960) { it.toShort() }; p.input.add(pcm); p.device.limit = 120
|
||||
p.step(0); p.step(5, true); p.step(10000, true)
|
||||
assertTrue(p.device.paused); assertEquals(1, p.device.calls); assertEquals(420, p.output.pendingFrames)
|
||||
p.device.limit = 960; p.step(10005)
|
||||
assertFalse(p.device.paused); assertEquals(pcm.toList(), p.device.samples)
|
||||
}
|
||||
|
||||
@Test fun eofWaitsForPendingPcmAndPhysicalPlayback() {
|
||||
val p = Player(); p.input.add(ShortArray(960) { 7 }); p.finished = true; p.device.limit = 480
|
||||
p.step(0); p.device.head = 240; p.step(5)
|
||||
assertFalse(p.output.drained); assertEquals(480L, p.output.written)
|
||||
p.device.head = 480; p.step(10)
|
||||
assertTrue(p.output.drained); assertEquals(List(960) { 7.toShort() }, p.device.samples)
|
||||
}
|
||||
|
||||
@Test fun shortEofPrimesOldDeviceWithoutAdvancingMediaClock() {
|
||||
val p = Player(Device(3848)); p.input.add(ShortArray(200) { 7 }); p.finished = true
|
||||
repeat(12) { p.step(it * 5L) }
|
||||
assertEquals(100L, p.output.written); assertEquals(0L, p.output.played)
|
||||
assertEquals(200 + 3848 * 2, p.device.samples.size)
|
||||
assertTrue(p.device.samples.drop(200).all { it == 0.toShort() })
|
||||
p.device.head = 200; p.step(65)
|
||||
assertEquals(100L, p.output.played); assertTrue(p.output.drained)
|
||||
}
|
||||
|
||||
@Test fun eofAfterUnderrunAlsoPrimesOldDevice() {
|
||||
val p = Player(Device(3848)); p.input.add(ShortArray(960)); p.step(0)
|
||||
p.device.head = 480; p.step(5)
|
||||
p.input.add(ShortArray(100) { 9 }); p.finished = true
|
||||
repeat(12) { p.step(10 + it * 5L) }
|
||||
assertEquals(530L, p.output.written)
|
||||
assertTrue(p.device.samples.size / 2 - p.device.head >= p.device.threshold)
|
||||
p.device.head = 530; p.step(75); assertTrue(p.output.drained)
|
||||
}
|
||||
|
||||
@Test fun resetDiscardsPendingAndNewOutputStartsAtZero() {
|
||||
val p = Player(); p.input.add(ShortArray(960) { 8 }); p.device.limit = 120; p.step(0); p.output.close()
|
||||
assertTrue(p.device.closed); assertEquals(0, p.output.pendingFrames)
|
||||
val next = Player(); next.input.add(ShortArray(960) { 9 }); next.step(10)
|
||||
assertEquals(480L, next.output.written); assertEquals(0L, next.output.played)
|
||||
assertTrue(next.device.samples.all { it == 9.toShort() })
|
||||
}
|
||||
|
||||
@Test fun writeErrorContainsDeviceAndQueueState() {
|
||||
val p = Player(); p.input.add(ShortArray(960)); p.device.limit = -6
|
||||
val error = assertThrows(IllegalStateException::class.java) { p.step(0) }
|
||||
assertTrue(error.message!!.contains("write=-6")); assertTrue(error.message!!.contains("pending=480"))
|
||||
}
|
||||
|
||||
@Test fun unexpectedHeadResetIsNotInterpretedAsFourBillionPlayedFrames() {
|
||||
val p = Player(); p.input.add(ShortArray(960)); p.step(0); p.device.head = 100; p.step(5)
|
||||
p.device.head = 0
|
||||
assertThrows(IllegalStateException::class.java) { p.step(10) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user