|
|
|
|
@ -12,6 +12,9 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
@Volatile private var running = true |
|
|
|
|
private data class Output(val surface: Surface?, val release: () -> Unit, val detach: Surface? = null) |
|
|
|
|
private val outputs = ConcurrentLinkedQueue<Output>() |
|
|
|
|
private data class Presentation(val surface: Surface, val pts: Long) |
|
|
|
|
private val presentations = ConcurrentLinkedQueue<Presentation>() |
|
|
|
|
fun presented(surface: Surface, pts: Long) { if (running) presentations.add(Presentation(surface, pts)) } |
|
|
|
|
private val displayed = MutableStateFlow<CallVideoFormat?>(null) |
|
|
|
|
val display: StateFlow<CallVideoFormat?> = displayed |
|
|
|
|
@Volatile private var verified = emptyMap<Int, CallVideoCodec.Companion.Decoder>() |
|
|
|
|
@ -32,6 +35,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
if (playout == 0L) { failure("Video playout allocation failed"); return } |
|
|
|
|
var decoder: MediaCodec? = null; var surface: Surface? = null; var format: CallVideoFormat? = null |
|
|
|
|
var output: Output? = null; var lastRendered = 0L |
|
|
|
|
var awaitingPts = -1L |
|
|
|
|
var pending: ByteArray? = null; val meta = LongArray(13) |
|
|
|
|
val pictures = ArrayDeque<Picture>(); val inputs = ArrayDeque<Timing>() |
|
|
|
|
var draining = false; var endSeen = false |
|
|
|
|
@ -43,6 +47,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
try { decoder?.stop() } catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder stop: ${e.message}") } |
|
|
|
|
try { decoder?.release() } catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder release: ${e.message}") } |
|
|
|
|
decoder = null; format = null; pictures.clear(); inputs.clear() |
|
|
|
|
awaitingPts = -1; presentations.clear() |
|
|
|
|
draining = false; endSeen = false; lastOutput = -1 |
|
|
|
|
} |
|
|
|
|
try { |
|
|
|
|
@ -57,6 +62,10 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
pending = null; displayed.value = null; CallVideoNative.requestKey(id) |
|
|
|
|
LogManager.addLog("INFO", "call", "video output attached=${surface != null} call=$id") |
|
|
|
|
} |
|
|
|
|
while (true) { |
|
|
|
|
val consumed = presentations.poll() ?: break |
|
|
|
|
if (consumed.surface === surface && consumed.pts == awaitingPts) { awaitingPts = -1; worked = true } |
|
|
|
|
} |
|
|
|
|
// Малый output lookahead; основной долг остаётся в encoded FIFO. |
|
|
|
|
while (decoder != null && pictures.size < 2 && !endSeen) { |
|
|
|
|
val out = decoder!!.dequeueOutputBuffer(info, 0) |
|
|
|
|
@ -80,7 +89,8 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
} |
|
|
|
|
val now = System.nanoTime() |
|
|
|
|
val valid = NativeLib.callAudioClock(id, now / 1000, clock) && clock[5] != 0L |
|
|
|
|
val first = pictures.peekFirst(); val nextPicture = if (pictures.size > 1) pictures.peekLast() else null |
|
|
|
|
val first = if (awaitingPts < 0) pictures.peekFirst() else null |
|
|
|
|
val nextPicture = if (first != null && pictures.size > 1) pictures.peekLast() else null |
|
|
|
|
val decision = CallVideoNative.playoutStep(playout, first?.timing?.pts ?: -1, first?.timing?.received ?: 0, |
|
|
|
|
nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000) |
|
|
|
|
if (decision > 0) wakeUs = now / 1000 + decision |
|
|
|
|
@ -88,13 +98,14 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
val render = decision == 0L |
|
|
|
|
decoder!!.releaseOutputBuffer(first.index, render); pictures.removeFirst(); worked = true |
|
|
|
|
if (render) { |
|
|
|
|
awaitingPts = first.timing.pts |
|
|
|
|
rendered++ |
|
|
|
|
if (displayed.value != format) LogManager.addLog("INFO", "call", "video visible call=$id format=$format") |
|
|
|
|
displayed.value = format; lastRendered = now |
|
|
|
|
} else late++ |
|
|
|
|
lastProgress = now |
|
|
|
|
} |
|
|
|
|
if (endSeen && pictures.isEmpty()) { |
|
|
|
|
if (endSeen && pictures.isEmpty() && awaitingPts < 0) { |
|
|
|
|
check(inputs.isEmpty()) { "Video EOS before all output frames" } |
|
|
|
|
LogManager.addLog("DEBUG", "call", "video decoder drained call=$id generation=${format?.generation}") |
|
|
|
|
close() |
|
|
|
|
@ -137,16 +148,18 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else if (data != null && surface == null) { pending = null; CallVideoNative.requestKey(id) } |
|
|
|
|
if (decoder != null && pictures.isEmpty() && inputs.isNotEmpty() && now - lastProgress > 2_000_000_000) |
|
|
|
|
if (decoder != null && awaitingPts < 0 && pictures.isEmpty() && inputs.isNotEmpty() |
|
|
|
|
&& now - lastProgress > 2_000_000_000) |
|
|
|
|
error("Video decoder stalled") |
|
|
|
|
if (lastRendered != 0L && now - lastRendered >= 5_000_000_000) { |
|
|
|
|
if (awaitingPts < 0 && lastRendered != 0L && now - lastRendered >= 5_000_000_000) { |
|
|
|
|
displayed.value = null; lastRendered = 0 |
|
|
|
|
LogManager.addLog("INFO", "call", "video hidden: no rendered frames for 5s call=$id") |
|
|
|
|
} |
|
|
|
|
if (now - reportAt >= 1_000_000_000) { |
|
|
|
|
LogManager.addLog("DEBUG", "call", "video playback call=$id intervalMs=${(now - reportAt) / 1_000_000} " + |
|
|
|
|
"pulled=$pulled queued=$queued decoded=$decoded rendered=$rendered skipped=$late inflight=${inputs.size} " + |
|
|
|
|
"pending=${pending != null} ready=${pictures.size} audioValid=$valid audioPts=${clock[4]}") |
|
|
|
|
"pending=${pending != null} ready=${pictures.size} awaitingSurfacePts=$awaitingPts " + |
|
|
|
|
"surfaceWaitMs=${if (awaitingPts >= 0) (now - lastRendered) / 1_000_000 else 0} audioValid=$valid audioPts=${clock[4]}") |
|
|
|
|
reportAt = now; pulled = 0; queued = 0; decoded = 0; rendered = 0; late = 0 |
|
|
|
|
} |
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|