diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt index 7c256d76..8bc4165b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt @@ -14,7 +14,13 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin private val outputs = ConcurrentLinkedQueue() private data class Presentation(val surface: Surface, val pts: Long) private val presentations = ConcurrentLinkedQueue() - fun presented(surface: Surface, pts: Long) { if (running) presentations.add(Presentation(surface, pts)) } + @Volatile private var awaiting: Presentation? = null + fun presented(surface: Surface, pts: Long): Boolean { + val expected = awaiting ?: return true + if (!running || expected.surface !== surface) return true + if (expected.pts != pts) return false + presentations.add(expected); return true + } private val displayed = MutableStateFlow(null) val display: StateFlow = displayed @Volatile private var verified = emptyMap() @@ -47,7 +53,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() + awaitingPts = -1; awaiting = null; presentations.clear() draining = false; endSeen = false; lastOutput = -1 } try { @@ -64,7 +70,9 @@ 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) { awaitingPts = -1; worked = true } + if (consumed.surface === surface && consumed.pts == awaitingPts) { + awaitingPts = -1; awaiting = null; worked = true + } } // Малый output lookahead; основной долг остаётся в encoded FIFO. while (decoder != null && pictures.size < 2 && !endSeen) { @@ -96,6 +104,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin if (decision > 0) wakeUs = now / 1000 + decision else if (first != null) { val render = decision == 0L + if (render) awaiting = Presentation(checkNotNull(surface), first.timing.pts) decoder!!.releaseOutputBuffer(first.index, render); pictures.removeFirst(); worked = true if (render) { awaitingPts = first.timing.pts diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallVideoPanel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallVideoPanel.kt index 524636f2..092622c5 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallVideoPanel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallVideoPanel.kt @@ -3,6 +3,7 @@ package com.utun.chat.ui.screens import android.content.Context import android.graphics.Matrix import android.graphics.SurfaceTexture +import android.os.Build import android.view.Surface import android.view.TextureView import androidx.compose.foundation.background @@ -123,10 +124,17 @@ 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) { - attached?.let { video.presented(it, texture.timestamp / 1000) } + 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) } } } } + private fun consumed(texture: SurfaceTexture, surface: Surface) { + if (closed || attached !== surface) return + if (!video.presented(surface, texture.timestamp / 1000)) postOnAnimation { consumed(texture, surface) } + } private fun fit() { val f = format ?: return if (width == 0 || height == 0) return