2 changed files with 67 additions and 0 deletions
@ -0,0 +1,36 @@ |
|||||||
|
package com.utun.chat.data |
||||||
|
|
||||||
|
/** Только playback worker. Ошибка ограничивает повтор в текущем маршруте, не в следующем. */ |
||||||
|
internal class CallOutputRecovery { |
||||||
|
var deviceId = Int.MIN_VALUE |
||||||
|
private set |
||||||
|
var failures = 0 |
||||||
|
private set |
||||||
|
|
||||||
|
fun route(id: Int): Boolean { |
||||||
|
if (id == deviceId) return false |
||||||
|
deviceId = id; failures = 0 |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
fun failed(): Boolean { failures++; return failures <= 2 } |
||||||
|
val tryFast: Boolean get() = failures == 0 |
||||||
|
} |
||||||
|
|
||||||
|
/** Локальный счётчик нового устройства начинается с нуля; общий PCM-счётчик не сбрасывается. */ |
||||||
|
internal class CallOutputPosition { |
||||||
|
var base = 0L |
||||||
|
private set |
||||||
|
private var head = CallPlaybackHead() |
||||||
|
|
||||||
|
fun reset() { base = 0; head = CallPlaybackHead() } |
||||||
|
|
||||||
|
fun reopen(written: Long) { |
||||||
|
require(written >= base) |
||||||
|
base = written |
||||||
|
head = CallPlaybackHead() |
||||||
|
} |
||||||
|
|
||||||
|
fun played(raw: Int, written: Long): Long? = head.update(raw, written - base)?.let { base + it } |
||||||
|
fun timestamp(local: Long): Long = if (local < 0) -1 else base + local |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
package com.utun.chat.data |
||||||
|
|
||||||
|
import org.junit.Assert.* |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class CallOutputRecoveryTest { |
||||||
|
@Test fun routeChangeRestoresFastAttemptButDuplicateConfirmationDoesNot() { |
||||||
|
val p = CallOutputRecovery() |
||||||
|
assertTrue(p.route(3)); assertTrue(p.tryFast) |
||||||
|
assertTrue(p.failed()); assertFalse(p.tryFast) |
||||||
|
assertFalse(p.route(3)); assertFalse(p.tryFast) |
||||||
|
assertTrue(p.failed()); assertFalse(p.failed()) |
||||||
|
assertTrue(p.route(2)); assertTrue(p.tryFast); assertEquals(0, p.failures) |
||||||
|
assertTrue(p.route(3)); assertTrue(p.tryFast) |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun replacementKeepsAcceptedPcmPositionAndPartialBlockRemainder() { |
||||||
|
val p = CallOutputPosition() |
||||||
|
assertEquals(480L, p.played(480, 960)) |
||||||
|
// 288 сэмплов следующего блока успели записаться; остаток 672 ещё принадлежит worker. |
||||||
|
val accepted = 960L + 288 |
||||||
|
p.reopen(accepted) |
||||||
|
assertEquals(accepted, p.played(0, accepted)) |
||||||
|
assertEquals(accepted + 96, p.played(96, accepted + 672)) |
||||||
|
assertEquals(accepted + 48, p.timestamp(48)) |
||||||
|
assertEquals(-1L, p.timestamp(-1)) |
||||||
|
assertNull(p.played(1000, accepted + 672)) |
||||||
|
p.reopen(1920) |
||||||
|
assertEquals(1920L, p.played(0, 1920)) |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue