Acknowledge Android texture consumption before presenting the next frame
This commit is contained in:
1 parent
13f09cfc20
commit
486d3314ef
5 files changed
+26
-8
No files matched your search
@@ -243,7 +243,8 @@ int call_video_port_send(const struct call_video_frame *frames, unsigned count)
|
|||||||
uint32_t bytes = 0;
|
uint32_t bytes = 0;
|
||||||
for (unsigned i = 0; i < count; ++i) {
|
for (unsigned i = 0; i < count; ++i) {
|
||||||
const struct call_video_frame *f = &frames[i];
|
const struct call_video_frame *f = &frames[i];
|
||||||
if (!f->data || !f->size || f->size > CALL_VIDEO_MAX_FRAME - bytes || f->offset || f->length != f->size
|
if (!f->data || !f->size || f->size > CALL_VIDEO_MAX_FRAME || f->size > CALL_VIDEO_PORT_TX_BYTES - bytes
|
||||||
|
|| f->offset || f->length != f->size
|
||||||
|| f->call_id != frames[0].call_id || !call_video_same_format(&f->format, &frames[0].format)
|
|| f->call_id != frames[0].call_id || !call_video_same_format(&f->format, &frames[0].format)
|
||||||
|| (i && (f->frame_id <= frames[i - 1].frame_id || f->pts_us <= frames[i - 1].pts_us))) {
|
|| (i && (f->frame_id <= frames[i - 1].frame_id || f->pts_us <= frames[i - 1].pts_us))) {
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video port invalid TX batch frame=%u count=%u bytes=%u", i, count, bytes); return -1;
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video port invalid TX batch frame=%u count=%u bytes=%u", i, count, bytes); return -1;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API
|
/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API
|
||||||
* потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst.
|
* потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst.
|
||||||
* Один TX mailbox (готовая пачка до восьми кадров <=2MiB) + восемь RX кадров <=4MiB;
|
* Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB;
|
||||||
* владение pull.data передаётся через u_free. TX не ждёт накопления пачки.
|
* владение pull.data передаётся через u_free. TX не ждёт накопления пачки.
|
||||||
* Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */
|
* Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */
|
||||||
#ifndef CALL_VIDEO_PORT_H
|
#ifndef CALL_VIDEO_PORT_H
|
||||||
@@ -28,6 +28,7 @@ int call_video_port_status(uint64_t id, struct call_video_port_status *out);
|
|||||||
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
|
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
|
||||||
int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps);
|
int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps);
|
||||||
#define CALL_VIDEO_PORT_TX_FRAMES 8u
|
#define CALL_VIDEO_PORT_TX_FRAMES 8u
|
||||||
|
#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME)
|
||||||
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
|
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
|
||||||
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */
|
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */
|
||||||
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
|
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
|
|
||||||
fun surface(surface: Surface?, release: () -> Unit = {}) = player.surface(surface, release)
|
fun surface(surface: Surface?, release: () -> Unit = {}) = player.surface(surface, release)
|
||||||
fun detachSurface(surface: Surface) = player.detach(surface)
|
fun detachSurface(surface: Surface) = player.detach(surface)
|
||||||
|
fun presented(surface: Surface, pts: Long) = player.presented(surface, pts)
|
||||||
fun select(settings: CallVideoSettings) { handler.post {
|
fun select(settings: CallVideoSettings) { handler.post {
|
||||||
if (!stopped) { wanted = settings; excluded = emptySet(); mutable.value = mutable.value.copy(settings = settings) }
|
if (!stopped) { wanted = settings; excluded = emptySet(); mutable.value = mutable.value.copy(settings = settings) }
|
||||||
} }
|
} }
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|||||||
@Volatile private var running = true
|
@Volatile private var running = true
|
||||||
private data class Output(val surface: Surface?, val release: () -> Unit, val detach: Surface? = null)
|
private data class Output(val surface: Surface?, val release: () -> Unit, val detach: Surface? = null)
|
||||||
private val outputs = ConcurrentLinkedQueue<Output>()
|
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)
|
private val displayed = MutableStateFlow<CallVideoFormat?>(null)
|
||||||
val display: StateFlow<CallVideoFormat?> = displayed
|
val display: StateFlow<CallVideoFormat?> = displayed
|
||||||
@Volatile private var verified = emptyMap<Int, CallVideoCodec.Companion.Decoder>()
|
@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 }
|
if (playout == 0L) { failure("Video playout allocation failed"); return }
|
||||||
var decoder: MediaCodec? = null; var surface: Surface? = null; var format: CallVideoFormat? = null
|
var decoder: MediaCodec? = null; var surface: Surface? = null; var format: CallVideoFormat? = null
|
||||||
var output: Output? = null; var lastRendered = 0L
|
var output: Output? = null; var lastRendered = 0L
|
||||||
|
var awaitingPts = -1L
|
||||||
var pending: ByteArray? = null; val meta = LongArray(13)
|
var pending: ByteArray? = null; val meta = LongArray(13)
|
||||||
val pictures = ArrayDeque<Picture>(); val inputs = ArrayDeque<Timing>()
|
val pictures = ArrayDeque<Picture>(); val inputs = ArrayDeque<Timing>()
|
||||||
var draining = false; var endSeen = false
|
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?.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}") }
|
try { decoder?.release() } catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder release: ${e.message}") }
|
||||||
decoder = null; format = null; pictures.clear(); inputs.clear()
|
decoder = null; format = null; pictures.clear(); inputs.clear()
|
||||||
|
awaitingPts = -1; presentations.clear()
|
||||||
draining = false; endSeen = false; lastOutput = -1
|
draining = false; endSeen = false; lastOutput = -1
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +62,10 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|||||||
pending = null; displayed.value = null; CallVideoNative.requestKey(id)
|
pending = null; displayed.value = null; CallVideoNative.requestKey(id)
|
||||||
LogManager.addLog("INFO", "call", "video output attached=${surface != null} call=$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.
|
// Малый output lookahead; основной долг остаётся в encoded FIFO.
|
||||||
while (decoder != null && pictures.size < 2 && !endSeen) {
|
while (decoder != null && pictures.size < 2 && !endSeen) {
|
||||||
val out = decoder!!.dequeueOutputBuffer(info, 0)
|
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 now = System.nanoTime()
|
||||||
val valid = NativeLib.callAudioClock(id, now / 1000, clock) && clock[5] != 0L
|
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,
|
val decision = CallVideoNative.playoutStep(playout, first?.timing?.pts ?: -1, first?.timing?.received ?: 0,
|
||||||
nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000)
|
nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000)
|
||||||
if (decision > 0) wakeUs = now / 1000 + decision
|
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
|
val render = decision == 0L
|
||||||
decoder!!.releaseOutputBuffer(first.index, render); pictures.removeFirst(); worked = true
|
decoder!!.releaseOutputBuffer(first.index, render); pictures.removeFirst(); worked = true
|
||||||
if (render) {
|
if (render) {
|
||||||
|
awaitingPts = first.timing.pts
|
||||||
rendered++
|
rendered++
|
||||||
if (displayed.value != format) LogManager.addLog("INFO", "call", "video visible call=$id format=$format")
|
if (displayed.value != format) LogManager.addLog("INFO", "call", "video visible call=$id format=$format")
|
||||||
displayed.value = format; lastRendered = now
|
displayed.value = format; lastRendered = now
|
||||||
} else late++
|
} else late++
|
||||||
lastProgress = now
|
lastProgress = now
|
||||||
}
|
}
|
||||||
if (endSeen && pictures.isEmpty()) {
|
if (endSeen && pictures.isEmpty() && awaitingPts < 0) {
|
||||||
check(inputs.isEmpty()) { "Video EOS before all output frames" }
|
check(inputs.isEmpty()) { "Video EOS before all output frames" }
|
||||||
LogManager.addLog("DEBUG", "call", "video decoder drained call=$id generation=${format?.generation}")
|
LogManager.addLog("DEBUG", "call", "video decoder drained call=$id generation=${format?.generation}")
|
||||||
close()
|
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) }
|
} 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")
|
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
|
displayed.value = null; lastRendered = 0
|
||||||
LogManager.addLog("INFO", "call", "video hidden: no rendered frames for 5s call=$id")
|
LogManager.addLog("INFO", "call", "video hidden: no rendered frames for 5s call=$id")
|
||||||
}
|
}
|
||||||
if (now - reportAt >= 1_000_000_000) {
|
if (now - reportAt >= 1_000_000_000) {
|
||||||
LogManager.addLog("DEBUG", "call", "video playback call=$id intervalMs=${(now - reportAt) / 1_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} " +
|
"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
|
reportAt = now; pulled = 0; queued = 0; decoded = 0; rendered = 0; late = 0
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ private class CallVideoTexture(context: Context, private val video: CallVideoEng
|
|||||||
}
|
}
|
||||||
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { fit() }
|
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { fit() }
|
||||||
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean { closeOutput(); return false }
|
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean { closeOutput(); return false }
|
||||||
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
|
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
|
||||||
|
attached?.let { video.presented(it, texture.timestamp / 1000) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun fit() {
|
private fun fit() {
|
||||||
|
|||||||
Reference in new issue
Block a user