Add internal setting to set calling camera to a static fps.

This commit is contained in:
adel-signal
2026-09-09 16:44:25 -04:00
committed by Cody Henthorne
parent 296fdade07
commit 84c3fd6fd6
7 changed files with 119 additions and 0 deletions
@@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.payments.DataExportUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.data.QuickstartCredentialExporter
import org.thoughtcrime.securesms.ringrtc.CameraFpsRanges
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -100,6 +101,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
private lateinit var viewModel: InternalSettingsViewModel
private var searchMenuItem: MenuItem? = null
private val cameraFpsRangeDescription: String? by lazy { CameraFpsRanges.captureCameraRangesDescription(requireContext()) }
private var scrollToPosition: Int = 0
private val layoutManager: LinearLayoutManager?
get() = recyclerView?.layoutManager as? LinearLayoutManager
@@ -836,6 +839,25 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
clickPref(
title = DSLSettingsText.from("Minimum Capture FPS"),
summary = DSLSettingsText.from(
buildString {
append(if (state.callingMinimumCaptureFps > 0) "${state.callingMinimumCaptureFps} fps" else "Default")
cameraFpsRangeDescription?.let { append("\n$it") }
}
),
onClick = {
promptUserForInt(
title = "Minimum Capture FPS",
message = "Floor for the camera's capture framerate range. 0 for default logic. Applies on next call start.",
initialValue = state.callingMinimumCaptureFps.takeIf { it > 0 }
) { minimumFps ->
viewModel.setInternalCallingMinimumCaptureFps(minimumFps ?: 0)
}
}
)
dividerPref()
// TODO [alex] -- db access on main thread!
@@ -26,6 +26,7 @@ data class InternalSettingsState(
val callingUseSoftwareVp9Decode: Boolean,
val callingEnableSvc: Boolean,
val callingStatsIntervalSecs: Int,
val callingMinimumCaptureFps: Int,
val useBuiltInEmojiSet: Boolean,
val emojiVersion: EmojiFiles.Version?,
val removeSenderKeyMinimium: Boolean,
@@ -177,6 +177,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setInternalCallingMinimumCaptureFps(minimumFps: Int) {
preferenceDataStore.putInt(InternalValues.CALLING_MINIMUM_CAPTURE_FPS, minimumFps)
refresh()
}
fun setUseConversationItemV2Media(enabled: Boolean) {
SignalStore.internal.useConversationItemV2Media = enabled
refresh()
@@ -265,6 +270,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
callingUseSoftwareVp9Decode = SignalStore.internal.callingUseSoftwareVp9Decode,
callingEnableSvc = SignalStore.internal.callingEnableSvc,
callingStatsIntervalSecs = SignalStore.internal.callingStatsIntervalSecs,
callingMinimumCaptureFps = SignalStore.internal.callingMinimumCaptureFps,
useBuiltInEmojiSet = SignalStore.internal.forceBuiltInEmoji,
emojiVersion = null,
removeSenderKeyMinimium = SignalStore.internal.removeSenderKeyMinimum,
@@ -33,6 +33,7 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
const val CALLING_USE_SOFTWARE_VP9_DECODE: String = "internal.calling_use_software_vp9_decode"
const val CALLING_ENABLE_SVC: String = "internal.calling_enable_svc"
const val CALLING_STATS_INTERVAL_SECS: String = "internal.calling_stats_interval_secs"
const val CALLING_MINIMUM_CAPTURE_FPS: String = "internal.calling_minimum_capture_fps"
const val SHAKE_TO_REPORT: String = "internal.shake_to_report"
const val DISABLE_STORAGE_SERVICE: String = "internal.disable_storage_service"
const val LAST_SCROLL_POSITION: String = "internal.last_scroll_position"
@@ -196,6 +197,11 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
*/
var callingStatsIntervalSecs: Int by integerValue(CALLING_STATS_INTERVAL_SECS, 0).defaultForExternalUsers()
/**
* Minimum fps for the camera's capture framerate range. Zero means use default logic
*/
var callingMinimumCaptureFps: Int by integerValue(CALLING_MINIMUM_CAPTURE_FPS, 0).defaultForExternalUsers()
var lastScrollPosition: Int by integerValue(LAST_SCROLL_POSITION, 0).defaultForExternalUsers()
var useConversationItemV2Media by booleanValue(CONVERSATION_ITEM_V2_MEDIA, false).defaultForExternalUsers()
@@ -12,9 +12,11 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CameraControl;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
import org.thoughtcrime.securesms.service.webrtc.RingRtcDynamicConfiguration;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.CapturerObserver;
@@ -209,6 +211,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
private void startCapture() {
Log.i(TAG, "startCapture()");
try {
int minimumCaptureFps = RingRtcDynamicConfiguration.getMinimumCaptureFps();
CameraEnumerationAndroid.setMinimumFramerateFps(minimumCaptureFps);
Log.i(TAG, "startCapture(): minimum capture fps: " + (minimumCaptureFps > 0 ? String.valueOf(minimumCaptureFps) : "default"));
capturer.startCapture(CAPTURE_WIDTH, CAPTURE_HEIGHT, CAPTURE_FPS);
capturing = true;
} catch (Exception e) {
@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.ringrtc
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.util.Range
import androidx.core.content.ContextCompat
import org.signal.core.util.logging.Log
/**
* Exposes [captureCameraRangesDescription], which lists the fps ranges of the front and back camera
* that are suitabe for video calls.
*/
object CameraFpsRanges {
private val TAG = Log.tag(CameraFpsRanges::class)
/**
* Descriptions of capture ranges, one camera per line, e.g. "Front: [7,30] [15,15] [30,30]".
* Only returns fps ranges for the first non-monochrome camera sensor hardware for each direction
*/
fun captureCameraRangesDescription(context: Context): String? {
return forCaptureCameras(context)
.takeIf { it.isNotEmpty() }
?.joinToString("\n") { camera ->
"${camera.facing}: ${camera.ranges.joinToString(" ") { "[${it.lower},${it.upper}]" }}"
}
}
private fun forCaptureCameras(context: Context): List<CameraRanges> {
val cameraManager: CameraManager = ContextCompat.getSystemService(context, CameraManager::class.java) ?: return emptyList()
return try {
val candidates = cameraManager.cameraIdList.filterNot { id ->
cameraManager.characteristics(id)
?.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
?.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME)
?: false
}
listOf(
"Front" to CameraMetadata.LENS_FACING_FRONT,
"Back" to CameraMetadata.LENS_FACING_BACK
).mapNotNull { (label, facing) ->
candidates
.firstOrNull { cameraManager.characteristics(it)?.get(CameraCharacteristics.LENS_FACING) == facing }
?.let { id ->
val ranges = cameraManager.characteristics(id)
?.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
?.sortedWith(compareBy({ it.lower }, { it.upper }))
?: emptyList()
CameraRanges(label, id, ranges)
}
?.takeIf { it.ranges.isNotEmpty() }
}
} catch (e: Exception) {
Log.w(TAG, "Unable to read camera fps ranges", e)
emptyList()
}
}
private fun CameraManager.characteristics(cameraId: String): CameraCharacteristics? {
return try {
getCameraCharacteristics(cameraId)
} catch (e: Exception) {
Log.w(TAG, "Unable to read characteristics for camera $cameraId", e)
null
}
}
private data class CameraRanges(val facing: String, val cameraId: String, val ranges: List<Range<Int>>)
}
@@ -82,6 +82,11 @@ object RingRtcDynamicConfiguration {
return if (secs != 0) secs else null
}
@JvmStatic
fun getMinimumCaptureFps(): Int {
return SignalStore.internal.callingMinimumCaptureFps.coerceAtLeast(0)
}
private fun getSoCInfo(): Pair<String, String> {
// 1. Native API Method (Android 12 / API 31+)
if (Build.VERSION.SDK_INT >= 31) {