3 changed files with 262 additions and 72 deletions
@ -0,0 +1,94 @@ |
|||||||
|
package com.utun.chat.data |
||||||
|
|
||||||
|
import java.util.ArrayDeque |
||||||
|
|
||||||
|
/** Один рабочий вход и один готовый выход. Codec владеет внутренним конвейером; |
||||||
|
* число поданных кадров и часы показа не ограничивают заполнение этого конвейера. |
||||||
|
* Все вызовы принадлежат одному player worker. Ошибки логирует владелец вместе с session ID. */ |
||||||
|
internal class StreamDecodePump<F : StreamDecodePump.Frame>( |
||||||
|
private val submit: (F) -> Boolean, |
||||||
|
private val receive: () -> Output?, |
||||||
|
private val release: (Output, Boolean) -> Unit |
||||||
|
) { |
||||||
|
interface Frame { val pts: Long } |
||||||
|
data class Output(val index: Int, val pts: Long, val eos: Boolean = false, val empty: Boolean = false) |
||||||
|
private val submitted = ArrayDeque<Long>() |
||||||
|
var input: F? = null |
||||||
|
private set |
||||||
|
var output: Output? = null |
||||||
|
private set |
||||||
|
var firstOutputPts: Long? = null |
||||||
|
private set |
||||||
|
var lastInputPts = -1L |
||||||
|
private set |
||||||
|
var inputCount = 0L |
||||||
|
private set |
||||||
|
var outputCount = 0L |
||||||
|
private set |
||||||
|
var shown = 0L |
||||||
|
private set |
||||||
|
var discarded = 0L |
||||||
|
private set |
||||||
|
var decoderLeadUs = 0L |
||||||
|
private set |
||||||
|
var framePeriodUs = 33333L |
||||||
|
private set |
||||||
|
var ended = false |
||||||
|
private set |
||||||
|
private var waitingSince = -1L |
||||||
|
private var lastInputAt = -1L |
||||||
|
val wantsInput: Boolean get() = input == null && output == null && !ended |
||||||
|
val oldestSubmittedPts: Long get() = submitted.firstOrNull() ?: -1L |
||||||
|
val inflight: Int get() = submitted.size |
||||||
|
|
||||||
|
fun offer(frame: F, now: Long) { |
||||||
|
// Уже запрошенный ответ может прийти после появления будущего output. |
||||||
|
check(input == null && !ended) { "Decoder already owns a working frame" } |
||||||
|
// Новые данные после сетевой паузы получают собственное окно ожидания decoder progress. |
||||||
|
if (lastInputAt < 0 || now - lastInputAt > 1_000_000) waitingSince = now |
||||||
|
input = frame |
||||||
|
} |
||||||
|
|
||||||
|
/** Выход проверяется перед каждым входом. Свободный input buffer берут лишь для готового кадра. */ |
||||||
|
fun step(position: Long, now: Long) { |
||||||
|
drain(position, now) |
||||||
|
if (output != null || ended) return |
||||||
|
val frame = input |
||||||
|
if (frame != null && submit(frame)) { |
||||||
|
check(lastInputPts < frame.pts) { "Decoder input PTS did not advance" } |
||||||
|
if (lastInputPts >= 0) framePeriodUs = (frame.pts - lastInputPts).coerceIn(1, 1_000_000) |
||||||
|
lastInputPts = frame.pts; lastInputAt = now; inputCount++; submitted.addLast(frame.pts); input = null |
||||||
|
drain(position, now) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drain(position: Long, now: Long) { |
||||||
|
repeat(8) { |
||||||
|
if (ended) return |
||||||
|
if (output == null) { |
||||||
|
val next = receive() ?: return |
||||||
|
output = next |
||||||
|
if (!next.empty) { |
||||||
|
if (firstOutputPts == null) firstOutputPts = next.pts |
||||||
|
outputCount++; decoderLeadUs = (lastInputPts - next.pts).coerceAtLeast(0) |
||||||
|
while (submitted.isNotEmpty() && submitted.first <= next.pts) submitted.removeFirst() |
||||||
|
} |
||||||
|
waitingSince = now |
||||||
|
} |
||||||
|
val next = output!! |
||||||
|
if (!next.empty && next.pts > position) return |
||||||
|
val render = !next.empty && next.pts >= position - 100000 |
||||||
|
release(next, render); output = null |
||||||
|
if (!next.empty) { if (render) shown++ else discarded++ } |
||||||
|
if (next.eos) ended = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Нехватка сети и ожидание PTS допустимы; зависший decoder при наличии данных — ошибка. */ |
||||||
|
fun checkProgress(now: Long, finishing: Boolean) { |
||||||
|
val hasWork = input != null || (lastInputAt >= 0 && now - lastInputAt < 1_000_000) || finishing |
||||||
|
check(ended || output != null || !hasWork || waitingSince < 0 || now - waitingSince <= 10_000_000) { |
||||||
|
"Decoder made no output progress: inputs=$inputCount outputs=$outputCount inflight=$inflight lastPTS=$lastInputPts" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
package com.utun.chat.data |
||||||
|
|
||||||
|
import org.junit.Assert.* |
||||||
|
import org.junit.Test |
||||||
|
import java.util.ArrayDeque |
||||||
|
|
||||||
|
class StreamDecodePumpTest { |
||||||
|
private data class Frame(override val pts: Long) : StreamDecodePump.Frame |
||||||
|
private class Codec(val depth: Int) { |
||||||
|
val queued = ArrayDeque<Frame>() |
||||||
|
var released = 0 |
||||||
|
var blocked = false |
||||||
|
var finishing = false |
||||||
|
var held = false |
||||||
|
var nextIndex = 0 |
||||||
|
val pump = StreamDecodePump<Frame>( |
||||||
|
submit = { if (blocked) false else { queued.addLast(it); true } }, |
||||||
|
receive = { |
||||||
|
if (held || (queued.size < depth && !finishing) || queued.isEmpty()) null |
||||||
|
else { held = true; StreamDecodePump.Output(nextIndex++, queued.removeFirst().pts) } |
||||||
|
}, |
||||||
|
release = { _, _ -> check(held); held = false; released++ } |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun fourAndEightFramePipelinesStartWithFrozenPresentationClock() { |
||||||
|
for (depth in listOf(4, 8)) { |
||||||
|
val codec = Codec(depth) |
||||||
|
repeat(depth) { i -> |
||||||
|
assertTrue(codec.pump.wantsInput) |
||||||
|
codec.pump.offer(Frame(i * 66666L), i * 66666L) |
||||||
|
codec.pump.step(0, i * 66666L) |
||||||
|
} |
||||||
|
assertEquals(depth.toLong(), codec.pump.inputCount) |
||||||
|
assertEquals(1L, codec.pump.outputCount) |
||||||
|
assertEquals(1, codec.released) |
||||||
|
assertEquals(0L, codec.pump.firstOutputPts) |
||||||
|
assertEquals((depth - 1) * 66666L, codec.pump.decoderLeadUs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun futureOutputStopsReadsAndNeverReleasesEarly() { |
||||||
|
val codec = Codec(1) |
||||||
|
codec.pump.offer(Frame(100000), 0); codec.pump.step(0, 0) |
||||||
|
assertFalse(codec.pump.wantsInput); assertNotNull(codec.pump.output); assertEquals(0, codec.released) |
||||||
|
codec.pump.step(99999, 15_000_000); codec.pump.checkProgress(15_000_000, false) |
||||||
|
assertEquals(0, codec.released) |
||||||
|
codec.pump.step(100000, 15_000_001) |
||||||
|
assertEquals(1, codec.released); assertTrue(codec.pump.wantsInput) |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun blockedCodecKeepsOneInputAndReportsStall() { |
||||||
|
val codec = Codec(4); codec.blocked = true |
||||||
|
codec.pump.offer(Frame(0), 0); codec.pump.step(0, 0) |
||||||
|
assertNotNull(codec.pump.input); assertFalse(codec.pump.wantsInput) |
||||||
|
try { codec.pump.checkProgress(10_000_001, false); fail("Stalled decoder must fail") } |
||||||
|
catch (expected: IllegalStateException) { assertTrue(expected.message!!.contains("no output progress")) } |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun networkPauseKeepsReferencesAndAllowsIncompletePipelineToResume() { |
||||||
|
val codec = Codec(4) |
||||||
|
repeat(2) { i -> codec.pump.offer(Frame(i * 40000L), i * 40000L); codec.pump.step(0, i * 40000L) } |
||||||
|
codec.pump.checkProgress(20_000_000, false) |
||||||
|
repeat(2) { i -> |
||||||
|
val now = 20_000_000 + i * 40000L |
||||||
|
codec.pump.offer(Frame((i + 2) * 40000L), now); codec.pump.step(0, now); codec.pump.checkProgress(now, false) |
||||||
|
} |
||||||
|
assertEquals(4L, codec.pump.inputCount); assertEquals(1, codec.released) |
||||||
|
} |
||||||
|
|
||||||
|
@Test fun finishDrainsShortStreamAndPreservesTheLastPicture() { |
||||||
|
val codec = Codec(8) |
||||||
|
repeat(3) { i -> codec.pump.offer(Frame(i * 40000L), i * 40000L); codec.pump.step(0, i * 40000L) } |
||||||
|
assertEquals(0, codec.released); codec.finishing = true |
||||||
|
codec.pump.step(0, 120000); assertEquals(1, codec.released); assertFalse(codec.pump.wantsInput) |
||||||
|
codec.pump.step(40000, 160000); assertEquals(2, codec.released) |
||||||
|
codec.pump.step(80000, 200000); assertEquals(3, codec.released) |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue