Browse Source

Use Android low-latency output and presentation timestamps for call clocks

master
evgeny 3 days ago
parent
commit
cb9852e8d5
  1. 61
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt
  2. 35
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTiming.kt
  3. 13
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt
  4. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt
  5. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt
  6. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  7. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt
  8. 1
      tools/chatgui-android/app/src/main/res/values-ru/strings.xml
  9. 1
      tools/chatgui-android/app/src/main/res/values/strings.xml
  10. 32
      tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallAudioTimingTest.kt
  11. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

61
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt

@ -25,7 +25,7 @@ import com.utun.chat.ChatApplication
* - play-поток: NativeLib.callAudioPull (C: джиттер-буфер → декод → SoundTouch) → AudioTrack.write
*
* Воспроизведение тянет PCM из единого голосового стека в C (call_audio): адаптивный
* джиттер-буфер (кодированные кадры) + time-stretch держат задержку ~60мс и догоняют
* джиттер-буфер (кодированные кадры) с минимальным запасом 20мс + CallDelay догоняют
* после сетевых бёрстов. На Kotlin-стороне своего джиттера больше нет.
*/
class CallAudioEngine {
@ -71,13 +71,19 @@ class CallAudioEngine {
@Volatile private var playFramesPlayed = 0L
@Volatile private var capFramesRead = 0L
private var playbackBuffer: CallPlaybackBuffer? = null // под trackLock
@Volatile private var routeGeneration = 0L
private data class OutputSample(val atUs: Long, val generation: Long, val latency: CallOutputLatency)
@Volatile private var outputSample: OutputSample? = null
@Volatile private var startElapsedMs = 0L
@Volatile private var captureElapsedMs = 0L
private val mainHandler = Handler(Looper.getMainLooper())
private val trackRouteListener = AudioRouting.OnRoutingChangedListener { output ->
synchronized(trackLock) {
val track = audioTrack
if (active && track != null && output === track) configurePlaybackBuffer(track, "route")
if (active && track != null && output === track) {
routeGeneration++
configurePlaybackBuffer(track, "route")
}
}
}
private val aecDelayRunnable = Runnable { measureAecDelay() }
@ -86,6 +92,12 @@ class CallAudioEngine {
fun isActive(): Boolean = active
fun outputLatency(id: Long): CallOutputLatency {
val sample = outputSample
return if (active && id == callId && sample != null && sample.generation == routeGeneration &&
System.nanoTime() / 1000 - sample.atUs in 0..500_000) sample.latency else CallOutputLatency()
}
/** Позднее создание Telecom Connection: сначала снимаем собственную заявку на audio mode. */
fun bindTelecom(connection: UtunConnection) {
if (!active || router.telecom === connection) return
@ -128,6 +140,7 @@ class CallAudioEngine {
val nativeMs = SystemClock.elapsedRealtime() - startElapsedMs
callId = id
routeGeneration = 0; outputSample = null
router.bindTelecom(TelecomCallManager.connectionFor(id))
muted = false
failed.set(false)
@ -194,12 +207,13 @@ class CallAudioEngine {
val minQueryMs = SystemClock.elapsedRealtime() - createdAt
val trackBuf = (trackMin * 2).coerceAtLeast(FRAME_SAMPLES * 4)
val constructorAt = SystemClock.elapsedRealtime()
val track = AudioTrack(
AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build(),
AudioFormat.Builder().setSampleRate(SAMPLE_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build(),
trackBuf, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)
val track = AudioTrack.Builder()
.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build())
.setAudioFormat(AudioFormat.Builder().setSampleRate(SAMPLE_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build())
.setBufferSizeInBytes(trackBuf).setTransferMode(AudioTrack.MODE_STREAM)
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY).build()
val constructorMs = SystemClock.elapsedRealtime() - constructorAt
audioTrack = track
if (!active) return@runWorker
@ -489,7 +503,7 @@ class CallAudioEngine {
val stamp = AudioTimestamp()
while (!Thread.currentThread().isInterrupted) {
if (active) adjustPlaybackBuffer(track)
if (trace.enabled && active) trace.device = samplePlaybackDevice(track, stamp)
if (active) trace.device = samplePlaybackDevice(track, stamp)
trace.flush()
Thread.sleep(200)
}
@ -518,6 +532,7 @@ class CallAudioEngine {
/** Только reporter: Binder/внутренние locks AudioTrack не должны вызываться здесь из playback. */
private fun samplePlaybackDevice(track: AudioTrack, stamp: AudioTimestamp): CallAudioTrace.DeviceSample {
val beginNs = System.nanoTime()
val generation = routeGeneration
val stampOk = track.getTimestamp(stamp)
val stampEndNs = System.nanoTime()
val route = track.routedDevice
@ -525,12 +540,13 @@ class CallAudioEngine {
val underruns = track.underrunCount.toLong()
val bufferFrames = track.bufferSizeInFrames.toLong()
val startFrames = if (Build.VERSION.SDK_INT >= 31) track.startThresholdInFrames.toLong() else bufferFrames
val performanceMode = track.performanceMode
val endNs = System.nanoTime()
return CallAudioTrace.DeviceSample(
route?.id?.toLong() ?: -1, route?.type?.toLong() ?: -1, underruns, bufferFrames, startFrames,
if (stampOk) stamp.framePosition else -1, if (stampOk) stamp.nanoTime else -1, stampOk,
beginNs / 1000, endNs / 1000, (stampEndNs - beginNs) / 1000,
(routeEndNs - stampEndNs) / 1000, (endNs - routeEndNs) / 1000)
(routeEndNs - stampEndNs) / 1000, (endNs - routeEndNs) / 1000, generation, performanceMode)
}
private fun playLoop(trace: CallAudioTrace) {
@ -546,6 +562,9 @@ class CallAudioEngine {
}
val epoch = snapshot[0]
val head = CallPlaybackHead()
val playbackClock = CallPlaybackClock()
var clockMode = ""
var outputUs = -1L
var clockFailed = false
val diagnostic = LongArray(8)
val row = LongArray(CallAudioTrace.WIDTH)
@ -590,6 +609,7 @@ class CallAudioEngine {
return
}
Thread.sleep(2)
continue
} else {
blockedAt = 0L
offset += written
@ -604,13 +624,26 @@ class CallAudioEngine {
observedPlayed = played
playFramesPlayed = played
operationNs = System.nanoTime()
NativeLib.callAudioProgress(callId, epoch, played, nowUs)
val device = trace.device
val anchor = playbackClock.update(playFramesWritten, played, nowUs, routeGeneration,
device.generation, device.stampFrame, device.stampNs / 1000)
outputUs = if (anchor?.measured == true) (anchor.atUs - nowUs).coerceAtLeast(0) else -1
outputSample = OutputSample(nowUs, routeGeneration, CallOutputLatency(
((playFramesWritten - played) * 1000 / SAMPLE_RATE).toInt(),
if (outputUs >= 0) (outputUs / 1000).toInt() else -1))
if (anchor != null) NativeLib.callAudioProgress(callId, epoch, anchor.position, anchor.atUs, nowUs, anchor.measured)
progressCallNs += System.nanoTime() - operationNs
val mode = if (anchor == null) "unavailable" else if (anchor.measured) "timestamp" else "head-estimate"
if (mode != clockMode) {
LogManager.addLog(if (mode == "unavailable") "WARN" else "INFO", "CallAudio",
"playback clock id=$callId mode=$mode routeGeneration=$routeGeneration outputUs=$outputUs")
clockMode = mode
}
} else if (!clockFailed) {
clockFailed = true
playFramesPlayed = -1
// Отрицательная позиция немедленно инвалидирует снимок в native.
NativeLib.callAudioProgress(callId, epoch, -1, nowUs)
NativeLib.callAudioProgress(callId, epoch, -1, nowUs, nowUs, false)
LogManager.addLog("ERROR", "CallAudio",
"playback clock invalid id=$callId epoch=$epoch written=$playFramesWritten; audio continues")
}
@ -627,6 +660,7 @@ class CallAudioEngine {
diagnostic.copyInto(row, 16); row[24] = offset.toLong()
row[25] = headCallNs / 1000; row[26] = progressCallNs / 1000; row[27] = writeSpanUs
row[28] = diagnosticUs; row[29] = nowUs - pullAtNs / 1000
row[35] = outputUs
trace.record(row)
}
@ -680,7 +714,8 @@ class CallAudioEngine {
val level = if (actual == requested && (change == null || change.underruns == 0)) "INFO" else "WARN"
LogManager.addLog(level, "CallAudio",
"playback buffering id=$callId reason=$reason requestedFrames=$requested actualFrames=$actual " +
"previousFrames=$previous startFrames=$start capacityFrames=$capacity underruns=${change?.underruns ?: 0} pacing=device")
"previousFrames=$previous startFrames=$start capacityFrames=$capacity underruns=${change?.underruns ?: 0} " +
"performanceRequested=${AudioTrack.PERFORMANCE_MODE_LOW_LATENCY} performanceActual=${track.performanceMode} pacing=device")
if (playReady && (previous != actual || reason == "route")) {
// Очередь сначала принимает новый размер; устаревшая задача не затронет следующий звонок.
mainHandler.post {

35
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTiming.kt

@ -39,3 +39,38 @@ internal class CallPlaybackHead {
return position
}
}
/** Якорь конца записанного PCM: позиция не регрессирует при переходе head → аппаратный timestamp. */
internal class CallPlaybackClock {
data class Anchor(val position: Long, val atUs: Long, val measured: Boolean)
private var generation = -1L
private var stampFrame = -1L
private var stampUs = -1L
private var measuredOnce = false
fun update(written: Long, head: Long, nowUs: Long, routeGeneration: Long,
sampleGeneration: Long, frame: Long, timeUs: Long): Anchor? {
if (generation != routeGeneration) {
generation = routeGeneration
stampFrame = -1; stampUs = -1
}
if (head < 0 || head > written) return null
// AudioTimestamp может содержать только младшие 32 бита; разворачиваем относительно written.
var extended = frame
if (frame in 0..0xffffffffL) {
extended = (written and -0x100000000L) or frame
if (extended > written) extended -= 0x100000000L
}
if (sampleGeneration == generation && extended in 0..written && timeUs > 0 &&
timeUs in (nowUs - 500_000)..(nowUs + 200_000) && written - extended <= 96_000) {
stampFrame = extended; stampUs = timeUs; measuredOnce = true
}
if (stampFrame >= 0 && stampUs in (nowUs - 500_000)..(nowUs + 200_000)) {
val at = stampUs + (written - stampFrame) * 1_000_000 / 48_000
if (at in (nowUs - 20_000)..(nowUs + 2_000_000)) return Anchor(written, at, true)
}
// Исчезнувший аппаратный timestamp не заменяем head: это сдвинуло бы видео вперёд на задержку HAL.
if (measuredOnce) return null
return Anchor(written, nowUs + (written - head) * 1_000_000 / 48_000, false)
}
}

13
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt

@ -3,13 +3,13 @@ package com.utun.chat.data
/** Только числа в playback-потоке; форматирование и вывод выполняет отдельный reporter. */
internal class CallAudioTrace(private val id: Long, private val emit: (String, String) -> Unit) {
companion object {
const val WIDTH = 35
const val WIDTH = 37
const val CAPACITY = 512
// monoUs, routeId, routeType, written, played, pullUs, writeCallUs, pullGapExcessUs, zeroWrites,
// deviceUnderruns, bufferFrames, startFrames, stampFrame, stampNs, stampReadUs, stampOk,
// encodedMs, processingMs, reserveMs, spreadMs, pcmSamples, tempo100, jbUnderruns, waiting, accepted
// headCallUs, progressCallUs, writeSpanUs, jbSnapshotUs, blockUs,
// deviceBeginUs, deviceEndUs, stampQueryUs, routeQueryUs, propertiesQueryUs
// deviceBeginUs, deviceEndUs, stampQueryUs, routeQueryUs, propertiesQueryUs, outputUs, performanceMode
private const val PRE_US = 2_000_000L
private const val POST_US = 3_000_000L
}
@ -20,7 +20,8 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S
val bufferFrames: Long = -1, val startFrames: Long = -1,
val stampFrame: Long = -1, val stampNs: Long = -1, val stampOk: Boolean = false,
val beginUs: Long = -1, val endUs: Long = -1,
val stampQueryUs: Long = 0, val routeQueryUs: Long = 0, val propertiesQueryUs: Long = 0
val stampQueryUs: Long = 0, val routeQueryUs: Long = 0, val propertiesQueryUs: Long = 0,
val generation: Long = -1, val performanceMode: Int = -1
) {
fun copyTo(row: LongArray) {
row[1] = routeId; row[2] = routeType; row[9] = underruns
@ -28,6 +29,7 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S
row[12] = stampFrame; row[13] = stampNs; row[14] = beginUs; row[15] = if (stampOk) 1 else 0
row[30] = beginUs; row[31] = endUs
row[32] = stampQueryUs; row[33] = routeQueryUs; row[34] = propertiesQueryUs
row[36] = performanceMode.toLong()
}
}
@Volatile var device = DeviceSample()
@ -139,7 +141,8 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S
"stamp=${last[12]}@${last[13]} readUs=${last[14]} ok=${last[15]} " +
"deviceAgeUs=${if (last[31] >= 0) last[0] - last[31] else -1} " +
"stampQueryUs=${range { it[32] }} routeQueryUs=${range { it[33] }} propertiesQueryUs=${range { it[34] }} " +
"buffer=${last[10]} start=${last[11]} fault=$faults suppressed=$suppressed")
"buffer=${last[10]} start=${last[11]} outputUs=${range { it[35] }} performanceMode=${last[36]} " +
"outputIncludesQueue=1 fault=$faults suppressed=$suppressed")
reportedDeviceUnderruns = last[9]; reportedJbUnderruns = last[22]
summary.clear()
}
@ -151,7 +154,7 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S
"bufferFrames,startFrames,stampFrame,stampNs,stampReadUs,stampOk," +
"encodedMs,processingMs,reserveMs,spreadMs,pcmSamples,tempo100,jbUnderruns,waiting,accepted," +
"headCallUs,progressCallUs,writeSpanUs,jbSnapshotUs,blockUs," +
"deviceBeginUs,deviceEndUs,stampQueryUs,routeQueryUs,propertiesQueryUs")
"deviceBeginUs,deviceEndUs,stampQueryUs,routeQueryUs,propertiesQueryUs,outputUs,performanceMode")
// До пяти секунд, по четыре снимка в строке, с явным временем каждого.
history.filter { it[0] in (eventAt - PRE_US)..(eventAt + POST_US) }.chunked(4).forEach { group ->
emit("DEBUG", "audio detail call=$id rows=" + group.joinToString(";") { it.joinToString(",") })

3
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt

@ -403,7 +403,8 @@ object CallController : TelecomCallManager.Listener {
39 -> if (st.phase == CallPhase.ACTIVE) {
val buf = ByteBuffer.wrap(data, 8, 16).order(ByteOrder.BIG_ENDIAN)
fun metric() = buf.short.toInt() and 0xFFFF
_state.value = st.copy(stats = CallStats(metric(), metric(), metric(), metric(), metric(), metric(), metric(), metric()))
_state.value = st.copy(stats = CallStats(metric(), metric(), metric(), metric(), metric(), metric(), metric(), metric(),
service?.audio?.outputLatency(id) ?: CallOutputLatency()))
}
42 -> _state.value = st.copy(bgpPath = String(data, 8, data.size - 8))
44 -> {

6
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallState.kt

@ -15,9 +15,13 @@ enum class CallConnectionType(val labelRes: Int) {
data class CallStats(
val rttMs: Int, val bufferMs: Int, val tempoX100: Int, val dropped: Int,
val underruns: Int, val minMs: Int, val maxMs: Int, val reserveMs: Int
val underruns: Int, val minMs: Int, val maxMs: Int, val reserveMs: Int,
val output: CallOutputLatency = CallOutputLatency()
)
/** outputMs включает queueMs; это задержка конца записанного PCM, не сумма двух буферов. */
data class CallOutputLatency(val queueMs: Int = -1, val outputMs: Int = -1)
data class CallState(
val callId: Long = 0,
val channelId: String = "",

5
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -268,7 +268,8 @@ object NativeLib {
fun callAudioStart(callId: Long): Boolean = nativeCallAudioStart(callId)
fun callAudioFeed(callId: Long, samples: ShortArray, capturePtsUs: Long): Boolean = nativeCallAudioFeed(callId, samples, capturePtsUs)
fun callAudioPull(callId: Long, out: ShortArray): Int = nativeCallAudioPull(callId, out)
fun callAudioProgress(callId: Long, epoch: Long, position: Long, nowUs: Long) = nativeCallAudioProgress(callId, epoch, position, nowUs)
fun callAudioProgress(callId: Long, epoch: Long, position: Long, atUs: Long, nowUs: Long, measured: Boolean) =
nativeCallAudioProgress(callId, epoch, position, atUs, nowUs, measured)
/** out: epoch, source_epoch, played, written, pts_us, valid, measured, held_back_us. */
fun callAudioClock(callId: Long, nowUs: Long, out: LongArray): Boolean = nativeCallAudioClock(callId, nowUs, out)
/** call=debug: encoded_ms, processing_ms, reserve_ms, spread_ms, pcm_samples, tempo_x100, underruns, waiting. */
@ -404,7 +405,7 @@ object NativeLib {
private external fun nativeCallAudioStart(callId: Long): Boolean
private external fun nativeCallAudioFeed(callId: Long, samples: ShortArray, capturePtsUs: Long): Boolean
private external fun nativeCallAudioPull(callId: Long, out: ShortArray): Int
private external fun nativeCallAudioProgress(callId: Long, epoch: Long, position: Long, nowUs: Long)
private external fun nativeCallAudioProgress(callId: Long, epoch: Long, position: Long, atUs: Long, nowUs: Long, measured: Boolean)
private external fun nativeCallAudioClock(callId: Long, nowUs: Long, out: LongArray): Boolean
private external fun nativeCallAudioDiagnostics(callId: Long, out: LongArray): Boolean
private external fun nativeCallAudioStop()

5
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt

@ -257,6 +257,11 @@ fun CallScreen(
stats.minMs, stats.maxMs, "%.2f".format(stats.tempoX100 / 100.0), stats.rttMs, stats.dropped, stats.underruns),
modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFAEBCC8),
fontSize = 12.sp, textAlign = TextAlign.Center)
Text(stringResource(R.string.call_output_stats,
stats.output.queueMs.takeIf { it >= 0 }?.toString() ?: "—",
stats.output.outputMs.takeIf { it >= 0 }?.toString() ?: "—"),
modifier = Modifier.padding(horizontal = 16.dp), color = Color(0xFFAEBCC8),
fontSize = 12.sp, textAlign = TextAlign.Center)
}
Spacer(Modifier.height(40.dp))
}

1
tools/chatgui-android/app/src/main/res/values-ru/strings.xml

@ -374,6 +374,7 @@
<string name="file_save_failed">"Не удалось сохранить файл"</string>
<string name="transfer_key_confirm">"Передать приватный ключ канала участнику «%1$s»?\n\nКлюч передаётся зашифрованным и подписанным. Получатель станет совладельцем и сможет управлять участниками."</string>
<string name="call_stats">"Буфер: %1$s мс · Запас: %2$s мс\nЗа 10 с: минимум %3$s · максимум %4$s мс\nСкорость: %5$s× · RTT: %6$s мс\nПотери буфера: %7$s · Недостачи: %8$s"</string>
<string name="call_output_stats">"Аудиовыход: %2$s мс (включая очередь приложения: %1$s мс)"</string>
<plurals name="bgp_nodes">
<item quantity="one">BGP: %1$d узел</item>
<item quantity="few">BGP: %1$d узла</item>

1
tools/chatgui-android/app/src/main/res/values/strings.xml

@ -375,6 +375,7 @@
<string name="file_save_failed">"Could not save file"</string>
<string name="transfer_key_confirm">"Send the channel private key to “%1$s”?\n\nThe key is transferred encrypted and signed. The recipient will become a co-owner and will be able to manage members."</string>
<string name="call_stats">"Buffer: %1$s ms · Reserve: %2$s ms\nOver 10 s: min %3$s · max %4$s ms\nSpeed: %5$s× · RTT: %6$s ms\nBuffer drops: %7$s · Underruns: %8$s"</string>
<string name="call_output_stats">"Audio output: %2$s ms (includes app queue: %1$s ms)"</string>
<plurals name="bgp_nodes">
<item quantity="one">BGP: %1$d node</item>
<item quantity="other">BGP: %1$d nodes</item>

32
tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallAudioTimingTest.kt

@ -4,6 +4,38 @@ import org.junit.Assert.*
import org.junit.Test
class CallAudioTimingTest {
@Test fun playbackUsesPresentationTimeAndMonotonicWrittenAnchor() {
val c = CallPlaybackClock()
val fallback = c.update(9600, 8640, 1_000_000, 1, 1, -1, -1)!!
assertFalse(fallback.measured)
assertEquals(1_020_000L, fallback.atUs)
val hardware = c.update(10560, 9600, 1_020_000, 1, 1, 4800, 1_000_000)!!
assertTrue(hardware.measured)
assertTrue(hardware.position >= fallback.position)
assertEquals(1_120_000L, hardware.atUs)
// Повторный timestamp и временный отказ запроса сохраняют тот же физический ход часов.
assertEquals(1_140_000L, c.update(11520, 10560, 1_040_000, 1, 1, -1, -1)!!.atUs)
assertNull(c.update(35520, 34560, 1_600_000, 1, 1, -1, -1))
}
@Test fun playbackRouteRejectsOldTimestampWithoutFallingBackToHead() {
val c = CallPlaybackClock()
assertTrue(c.update(9600, 8640, 1_000_000, 1, 1, 4800, 1_000_000)!!.measured)
assertNull(c.update(10560, 9600, 1_020_000, 2, 1, 5760, 1_020_000))
assertEquals(1_060_000L, c.update(11520, 10560, 1_040_000, 2, 2, 10560, 1_040_000)!!.atUs)
}
@Test fun playbackTimestampWrapAndInvalidValues() {
val c = CallPlaybackClock()
val written = 0x100000000L + 960
val a = c.update(written, written - 960, 1_000_000, 1, 1, 0, 1_000_000)!!
assertTrue(a.measured)
assertEquals(1_020_000L, a.atUs)
assertNull(c.update(written, -1, 1_000_000, 1, 1, 0, 1_000_000))
assertFalse(CallPlaybackClock().update(960, 0, 1_000_000, 1, 1, 1920, 1_000_000)!!.measured)
assertFalse(CallPlaybackClock().update(960, 0, 1_000_000, 1, 1, 0, 10_000_000)!!.measured)
}
@Test fun captureFallbackUsesSamplesInsteadOfReadCompletionJitter() {
val c = CallCaptureClock()
assertEquals(980_000L, c.firstSampleUs(0, 960, 1_000_000, null, null))

4
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -2688,9 +2688,9 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioStop(
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioProgress(
JNIEnv* env, jobject thiz, jlong callId, jlong epoch, jlong position, jlong nowUs) {
JNIEnv* env, jobject thiz, jlong callId, jlong epoch, jlong position, jlong atUs, jlong nowUs, jboolean measured) {
(void)env; (void)thiz;
call_audio_playback_progress(callId, epoch, position, nowUs, nowUs, 0);
call_audio_playback_progress(callId, epoch, position, atUs, nowUs, measured);
}
/* Предвыделенный снимок: epoch, source_epoch, played, written, pts, valid, measured, held_back_us. */

Loading…
Cancel
Save