Register Android video presentation ACK before the frame draw

This commit is contained in:
evgeny committed 2026-10-20 00:38:05 +02:00
1 parent 2c828d481d
commit 65f7babb2a
2 files changed
+44 -7

No files matched your search

@@ -48,6 +48,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
var lastOutput = -1L; var lastProgress = System.nanoTime()
var reportedGeneration = 0L; var rejectedGeneration = 0L
var reportAt = System.nanoTime(); var pulled = 0; var queued = 0; var decoded = 0; var rendered = 0; var late = 0
var acknowledged = 0; var ackMaxUs = 0L
val clock = LongArray(8); val info = MediaCodec.BufferInfo()
fun close() {
try { decoder?.stop() } catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder stop: ${e.message}") }
@@ -71,6 +72,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
while (true) {
val consumed = presentations.poll() ?: break
if (consumed.surface === surface && consumed.pts == awaitingPts) {
acknowledged++; ackMaxUs = maxOf(ackMaxUs, (consumed.at * 1000 - lastRendered) / 1000)
CallVideoNative.playoutPresented(playout, consumed.pts, consumed.at)
awaitingPts = -1; awaiting = null; worked = true
}
@@ -172,8 +174,10 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
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} totalLocal=$localQueued awaitingSurfacePts=$awaitingPts " +
"surfaceWaitMs=${if (awaitingPts >= 0) (now - lastRendered) / 1_000_000 else 0} audioValid=$valid audioPts=${clock[4]}")
"surfaceWaitMs=${if (awaitingPts >= 0) (now - lastRendered) / 1_000_000 else 0} " +
"acknowledged=$acknowledged surfaceAckMaxUs=$ackMaxUs audioValid=$valid audioPts=${clock[4]}")
reportAt = now; pulled = 0; queued = 0; decoded = 0; rendered = 0; late = 0
acknowledged = 0; ackMaxUs = 0
}
} catch (e: Exception) {
LogManager.addLog("ERROR", "call", "video receive call=$id: ${e.message}")
@@ -6,6 +6,7 @@ import android.graphics.SurfaceTexture
import android.os.Build
import android.view.Surface
import android.view.TextureView
import android.view.ViewTreeObserver
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState
@@ -211,11 +212,17 @@ private fun CallVideoPlayback(video: CallVideoEngine, onTap: () -> Unit, modifie
}
}
private class CallVideoTexture(context: Context, private val video: CallVideoEngine) : TextureView(context) {
private class CallVideoTexture(context: Context, private val video: CallVideoEngine) : TextureView(context), ViewTreeObserver.OnPreDrawListener {
private var attached: Surface? = null
private var closed = false
private var format: CallVideoFormat? = null
private var large = false
private var observer: ViewTreeObserver? = null
private var reportAt = 0L
private var updates = 0
private var commits = 0
private var updateAt = 0L
private var commitMaxUs = 0L
fun update(next: CallVideoFormat?, expanded: Boolean) {
if (format == next && large == expanded) return
format = next; large = expanded; fit()
@@ -232,16 +239,42 @@ private class CallVideoTexture(context: Context, private val video: CallVideoEng
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { fit() }
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean { closeOutput(); return false }
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
val surface = attached ?: return
// TextureView callback предшествует dequeue RenderThread; timestamp читаем после commit.
if (Build.VERSION.SDK_INT >= 29) viewTreeObserver.registerFrameCommitCallback { consumed(texture, surface) }
else postOnAnimation { consumed(texture, surface) }
updateAt = System.nanoTime(); updates++
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
observer = viewTreeObserver.also { it.addOnPreDrawListener(this) }
LogManager.addLog("DEBUG", "call", "video presentation attached preDrawCommit=true hardware=$isHardwareAccelerated")
}
override fun onDetachedFromWindow() {
observer?.takeIf { it.isAlive }?.removeOnPreDrawListener(this); observer = null
super.onDetachedFromWindow()
}
override fun onPreDraw(): Boolean {
val texture = surfaceTexture; val surface = attached
if (!closed && texture != null && surface != null) {
// ViewRoot собирает commit callbacks ДО TextureView.draw/onSurfaceTextureUpdated.
// Регистрируем до draw, иначе ACK ждёт следующего постороннего обновления окна.
if (Build.VERSION.SDK_INT >= 29 && isHardwareAccelerated)
viewTreeObserver.registerFrameCommitCallback { consumed(texture, surface) }
else postOnAnimation { consumed(texture, surface) }
}
return true
}
private fun consumed(texture: SurfaceTexture, surface: Surface) {
if (closed || attached !== surface) return
if (!video.presented(surface, texture.timestamp / 1000)) postOnAnimation { consumed(texture, surface) }
val now = System.nanoTime(); commits++
if (updateAt != 0L) { commitMaxUs = maxOf(commitMaxUs, (now - updateAt) / 1000); updateAt = 0 }
// Если timestamp ещё старый, новое TextureView update само назначит draw/commit.
video.presented(surface, texture.timestamp / 1000)
if (reportAt == 0L) reportAt = now
if (now - reportAt >= 1_000_000_000) {
LogManager.addLog("DEBUG", "call", "video surface intervalMs=${(now - reportAt) / 1_000_000} " +
"updates=$updates commits=$commits updateToCommitMaxUs=$commitMaxUs pts=${texture.timestamp / 1000}")
reportAt = now; updates = 0; commits = 0; commitMaxUs = 0
}
}
private fun fit() {
val f = format ?: return