Restore video call modes and remember the selected bitrate range

This commit is contained in:
evgeny committed 2026-10-28 12:08:07 +01:00
1 parent 76f81bc14e
commit c9f8f17858
2 files changed
+84 -11

No files matched your search

@@ -17,6 +17,7 @@ import android.util.Size
import android.view.Surface
import android.view.OrientationEventListener
import com.utun.chat.R
import com.utun.chat.ChatApplication
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.nio.ByteBuffer
@@ -26,7 +27,22 @@ data class CallVideoMode(val width: Int, val height: Int, val fps: Int)
data class CallVideoCamera(val id: String, val name: String, val modes: List<CallVideoMode>, val sensor: Int, val front: Boolean,
val focal35: Float)
data class CallVideoSettings(val enabled: Boolean = false, val camera: String = "", val width: Int = 640,
val height: Int = 480, val codec: Int = 0, val fps: Int = 30)
val height: Int = 480, val codec: Int = 0, val fps: Int = 30) {
/** Восстановить поддерживаемый режим; включение передачи относится только к текущему звонку. */
fun restore(state: CallVideoState): CallVideoSettings {
val format = codec.takeIf { it == 0 || (it in state.codecs && state.cameras.any { camera ->
state.modes(camera.id, it).isNotEmpty()
}) } ?: 0
val selected = state.cameras.firstOrNull { it.id == camera && state.modes(it.id, format).isNotEmpty() }
?: state.cameras.firstOrNull { state.modes(it.id, format).isNotEmpty() }
val modes = selected?.let { state.modes(it.id, format) }.orEmpty()
val mode = modes.firstOrNull { it.width == width && it.height == height && it.fps == fps }
?: modes.filter { it.width == width && it.height == height && it.fps <= fps }.maxByOrNull { it.fps }
?: modes.minWithOrNull(compareBy<CallVideoMode> { it.width * it.height }.thenBy { it.fps })
return copy(enabled = false, camera = selected?.id.orEmpty(), codec = format,
width = mode?.width ?: 640, height = mode?.height ?: 480, fps = mode?.fps ?: 30)
}
}
data class CallVideoState(val ready: Boolean = false, val settings: CallVideoSettings = CallVideoSettings(),
val cameras: List<CallVideoCamera> = emptyList(), val codecs: List<Int> = emptyList(), val bitrate: Int = 0,
val range: CallVideoBitrateRange = CallVideoBitrateRange(), val status: String = "", val transmitting: Boolean = false,
@@ -186,13 +202,22 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
c.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0,
c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT, lens.eqFocal)
}
val firstCamera = cameras.firstOrNull()
val firstMode = firstCamera?.modes?.firstOrNull { it.width == wanted.width && it.height == wanted.height && it.fps == wanted.fps }
?: firstCamera?.modes?.minWithOrNull(compareBy<CallVideoMode> { it.width * it.height }.thenBy { it.fps })
wanted = wanted.copy(camera = firstCamera?.id.orEmpty(), width = firstMode?.width ?: 640,
height = firstMode?.height ?: 480, fps = firstMode?.fps ?: 30)
mutable.value = mutable.value.copy(ready = true, cameras = cameras, codecs = encoders.keys.toList(), settings = wanted,
val available = mutable.value.copy(ready = true, cameras = cameras, codecs = encoders.keys.toList(),
codecModes = encoders.mapValues { entry -> entry.value.modes.map { CallVideoMode(it.size.width, it.size.height, it.fps) } })
val preferences = ChatApplication.instance.configProvider
val saved = try { preferences.getCallVideoCaptureSettings() }
catch (e: Exception) { LogManager.addLog("ERROR", "call", "Video settings load failed id=$id: $e"); CallVideoSettings() }
wanted = saved.restore(available)
if (saved.camera.isNotEmpty() && saved != wanted)
LogManager.addLog("WARN", "call", "Saved video settings adjusted id=$id saved=$saved restored=$wanted")
mutable.value = available.copy(settings = wanted)
val savedRange = try { preferences.getCallVideoBitrateRange() }
catch (e: Exception) { LogManager.addLog("ERROR", "call", "Video bitrate settings load failed id=$id: $e"); CallVideoBitrateRange() }
if (stopped) { LogManager.addLog("INFO", "call", "Video settings restore cancelled id=$id"); return@post }
if (CallVideoNative.range(id, savedRange.minimum * 1000, savedRange.maximum * 1000))
mutable.value = mutable.value.copy(range = savedRange)
else LogManager.addLog("WARN", "call", "Saved video bitrate range rejected id=$id range=$savedRange")
LogManager.addLog("INFO", "call", "Video settings restored id=$id settings=$wanted range=${mutable.value.range}")
report(if (encoders.isEmpty()) "Receive-only video" else "Video ready")
tick()
} catch (e: Exception) {
@@ -222,13 +247,21 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
wanted = selected; excluded = emptySet(); observeOrientation(selected.enabled)
mutable.value = current.copy(settings = selected, transmitting = selected.enabled && current.transmitting,
activeCodec = if (selected.enabled) current.activeCodec else 0, notice = null)
ChatApplication.instance.configProvider.saveCallVideoCaptureSettings(selected)
LogManager.addLog("INFO", "call", "video selection accepted id=$id requested=$settings resolved=$selected ready=${current.ready}")
}) LogManager.addLog("WARN", "call", "video selection rejected by stopped worker id=$id settings=$settings")
}
fun range(value: CallVideoBitrateRange) { handler.post {
if (CallVideoNative.range(id, value.minimum * 1000, value.maximum * 1000)) mutable.value = mutable.value.copy(range = value)
else report("Bitrate range rejected")
} }
fun range(value: CallVideoBitrateRange) {
if (!handler.post {
if (stopped) { LogManager.addLog("WARN", "call", "Bitrate selection after stop id=$id range=$value"); return@post }
if (CallVideoNative.range(id, value.minimum * 1000, value.maximum * 1000)) {
mutable.value = mutable.value.copy(range = value)
ChatApplication.instance.configProvider.saveCallVideoBitrateRange(value)
NativeLib.setChatSetting("call_video_bitrate_range_kbps", value.encode())
LogManager.addLog("INFO", "call", "Video bitrate selected id=$id range=$value")
} else LogManager.addLog("WARN", "call", "Bitrate range rejected id=$id range=$value")
}) LogManager.addLog("WARN", "call", "Bitrate selection rejected by stopped worker id=$id range=$value")
}
private fun report(message: String) {
mutable.value = mutable.value.copy(status = message)
LogManager.addLog("INFO", "call", "video call=$id $message")
@@ -0,0 +1,40 @@
package com.utun.chat.data
import org.junit.Assert.*
import org.junit.Test
class CallVideoSettingsTest {
private val sd = CallVideoMode(640, 480, 30)
private val hd = CallVideoMode(1280, 720, 60)
private val fullHd = CallVideoMode(1920, 1080, 30)
private val state = CallVideoState(ready = true, cameras = listOf(
CallVideoCamera("front", "Front", listOf(sd, fullHd), 90, true, 25f),
CallVideoCamera("back", "Back", listOf(sd, hd, fullHd), 90, false, 24f)),
codecs = listOf(1, 2), codecModes = mapOf(1 to listOf(sd, fullHd), 2 to listOf(sd, hd)))
@Test fun savedCameraCodecAndCadenceRestoreWithoutEnablingTransmission() {
val saved = CallVideoSettings(true, "back", 1280, 720, 2, 60)
assertEquals(saved.copy(enabled = false), saved.restore(state))
}
@Test fun unsupportedCadenceKeepsResolutionWithAvailableLowerRate() {
val saved = CallVideoSettings(camera = "front", width = 1920, height = 1080, codec = 1, fps = 60)
assertEquals(saved.copy(fps = 30), saved.restore(state))
}
@Test fun missingCameraAndCodecChooseUsableAutoMode() {
val saved = CallVideoSettings(camera = "gone", width = 1920, height = 1080, codec = 5)
assertEquals(saved.copy(camera = "front", codec = 0), saved.restore(state))
}
@Test fun listedCodecWithoutCameraModesFallsBackToAuto() {
val saved = CallVideoSettings(camera = "back", codec = 2)
val available = state.copy(codecModes = mapOf(1 to listOf(sd, fullHd), 2 to emptyList()))
assertEquals(saved.copy(codec = 0), saved.restore(available))
}
@Test fun missingModesProduceDisabledReceiveOnlySettings() {
val restored = CallVideoSettings(true, "gone", 1920, 1080, 2, 60).restore(CallVideoState())
assertEquals(CallVideoSettings(), restored)
}
}