Improve screen share capture dimension calculation and use remote config.

This commit is contained in:
Cody Henthorne
2026-05-14 11:17:17 -04:00
committed by Michelle Tang
parent 4dd57460de
commit 9dcf68581d
13 changed files with 132 additions and 54 deletions
@@ -13,5 +13,4 @@ sealed interface LabsSettingsEvents {
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
data class ToggleScreenShare(val enabled: Boolean) : LabsSettingsEvents
}
@@ -151,15 +151,6 @@ private fun LabsSettingsContent(
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleStarredMessages(it)) }
)
}
item {
Rows.ToggleRow(
checked = state.screenShare,
text = "Screen Sharing",
label = "Share your screen during calls. Adds a screen share option to the overflow menu in 1:1 and group calls.",
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleScreenShare(it)) }
)
}
}
}
}
@@ -15,6 +15,5 @@ data class LabsSettingsState(
val groupSuggestionsForMembers: Boolean = false,
val betterSearch: Boolean = false,
val autoLowerHand: Boolean = false,
val starredMessages: Boolean = false,
val screenShare: Boolean = false
val starredMessages: Boolean = false
)
@@ -46,10 +46,6 @@ class LabsSettingsViewModel : ViewModel() {
SignalStore.labs.starredMessages = event.enabled
_state.value = _state.value.copy(starredMessages = event.enabled)
}
is LabsSettingsEvents.ToggleScreenShare -> {
SignalStore.labs.screenShare = event.enabled
_state.value = _state.value.copy(screenShare = event.enabled)
}
}
}
@@ -61,9 +57,7 @@ class LabsSettingsViewModel : ViewModel() {
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers,
betterSearch = SignalStore.labs.betterSearch,
autoLowerHand = SignalStore.labs.autoLowerHand,
starredMessages = SignalStore.labs.starredMessages,
screenShare = SignalStore.labs.screenShare
starredMessages = SignalStore.labs.starredMessages
)
}
}
@@ -167,7 +167,7 @@ public final class WebRtcControls {
public boolean displayOverflow() {
boolean connectedGroupCall = isGroupCall() && groupCallState == GroupCallState.CONNECTED && hasAtLeastOneRemote;
boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && SignalStore.labs().getScreenShare();
boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && RemoteConfig.screenSharing();
return isAtLeastOutgoing() && (connectedGroupCall || connected1to1Call);
}
@@ -284,7 +284,7 @@ public final class WebRtcControls {
return callState.isAtLeast(CallState.OUTGOING);
}
private boolean isGroupCall() {
public boolean isGroupCall() {
return groupCallState != GroupCallState.NONE;
}
@@ -48,6 +48,7 @@ data class AdditionalActionsState(
val isSelfHandRaised: Boolean = false,
val isScreenSharing: Boolean = false,
val displayScreenShareToggle: Boolean = false,
val isGroupCall: Boolean = true,
@Stable val listener: AdditionalActionsListener = AdditionalActionsListener.Empty
)
@@ -85,15 +86,18 @@ private fun AdditionalActionsPopupContent(
Column(
verticalArrangement = spacedBy(12.dp),
modifier = Modifier
.width(IntrinsicSize.Min)
.width(IntrinsicSize.Max)
.padding(12.dp)
) {
CallReactionScrubber(
reactions = state.reactions,
listener = state.listener
)
if (state.isGroupCall) {
CallReactionScrubber(
reactions = state.reactions,
listener = state.listener
)
}
CallScreenMenu(
isGroupCall = state.isGroupCall,
onRaiseHandClick = state.listener::onRaiseHandClick,
isSelfHandRaised = state.isSelfHandRaised,
isScreenSharing = state.isScreenSharing,
@@ -141,6 +145,7 @@ private fun CallReactionScrubber(
@Composable
private fun CallScreenMenu(
isGroupCall: Boolean,
isSelfHandRaised: Boolean,
onRaiseHandClick: (Boolean) -> Unit,
isScreenSharing: Boolean = false,
@@ -152,11 +157,13 @@ private fun CallScreenMenu(
.fillMaxWidth()
.background(SignalTheme.colors.colorSurface2, RoundedCornerShape(18.dp))
) {
CallScreenMenuOption(
imageVector = ImageVector.vectorResource(R.drawable.symbol_raise_hand_24),
title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand),
onClick = { onRaiseHandClick(!isSelfHandRaised) }
)
if (isGroupCall) {
CallScreenMenuOption(
imageVector = ImageVector.vectorResource(R.drawable.symbol_raise_hand_24),
title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand),
onClick = { onRaiseHandClick(!isSelfHandRaised) }
)
}
if (displayScreenShareToggle) {
CallScreenMenuOption(
@@ -242,3 +249,28 @@ private fun CallScreenAdditionalActionsPopupPreview() {
)
}
}
@NightPreview
@Composable
private fun CallScreenAdditionalActionsScreenSharingPreview() {
Previews.Preview {
AdditionalActionsPopupContent(
state = AdditionalActionsState(
isGroupCall = false,
displayScreenShareToggle = true,
isShown = false,
reactions = persistentListOf(
"\u2764\ufe0f",
"\ud83d\udc4d",
"\ud83d\udc4e",
"\ud83d\ude02",
"\ud83d\ude2e",
"\ud83d\ude22"
),
isSelfHandRaised = false,
listener = AdditionalActionsListener.Empty,
triggerAlignedPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState()
)
)
}
}
@@ -211,6 +211,7 @@ data class CallControlsState(
val displayGroupRingingToggle: Boolean = false,
val isGroupRingingEnabled: Boolean = false,
val isGroupRingingAllowed: Boolean = false,
val isGroupCall: Boolean = false,
val displayAdditionalActions: Boolean = false,
val displayStartCallButton: Boolean = false,
val startCallButtonText: Int = R.string.WebRtcCallView__start_call,
@@ -252,6 +253,7 @@ data class CallControlsState(
displayMicToggle = webRtcControls.displayMuteAudio(),
isMicEnabled = callParticipantsState.localParticipant.isMicrophoneEnabled,
displayGroupRingingToggle = webRtcControls.displayRingToggle(),
isGroupCall = webRtcControls.isGroupCall,
isGroupRingingEnabled = callParticipantsState.ringGroup,
isGroupRingingAllowed = groupMemberCount <= RemoteConfig.maxGroupCallRingSize,
displayAdditionalActions = webRtcControls.displayOverflow(),
@@ -77,11 +77,11 @@ import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent
import org.thoughtcrime.securesms.events.GroupCallReactionEvent
import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.CameraState
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
import org.thoughtcrime.securesms.util.RemoteConfig
import kotlin.math.max
import kotlin.math.round
import kotlin.time.Duration.Companion.milliseconds
@@ -181,7 +181,8 @@ fun CallScreen(
reactions = callScreenState.reactions,
isSelfHandRaised = localParticipant.isHandRaised,
isScreenSharing = callScreenState.isLocalScreenSharing,
displayScreenShareToggle = callControlsState.displayEndCallButton && SignalStore.labs.screenShare,
displayScreenShareToggle = callControlsState.displayEndCallButton && RemoteConfig.screenSharing,
isGroupCall = callControlsState.isGroupCall,
listener = additionalActionsListener,
triggerAlignedPopupState = additionalActionsPopupState
)
@@ -11,7 +11,6 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
const val BETTER_SEARCH: String = "labs.better_search"
const val AUTO_LOWER_HAND: String = "labs.auto_lower_hand"
const val STARRED_MESSAGES: String = "labs.starred_messages"
const val SCREEN_SHARE: String = "labs.screen_share"
}
public override fun onFirstEverAppLaunch() = Unit
@@ -32,8 +31,6 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
var starredMessages by booleanValue(STARRED_MESSAGES, true).falseForExternalUsers()
var screenShare by booleanValue(SCREEN_SHARE, true).falseForExternalUsers()
private fun SignalStoreValueDelegate<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
}
@@ -56,7 +56,6 @@ class OutgoingVideoSourceRouter(
override fun setOrientation(orientation: Int?) {
camera.setOrientation(orientation)
screenShareCapturer?.onConfigurationChanged()
}
val cameraState: CameraState
@@ -2,7 +2,12 @@ package org.thoughtcrime.securesms.ringrtc
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.media.projection.MediaProjection
import android.os.Build
import android.util.DisplayMetrics
import android.view.Display
import android.view.WindowManager
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.Log.tag
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper
@@ -22,6 +27,27 @@ class ScreenShareCapturer(
private val sink: CapturerObserver
) {
companion object {
private val TAG = tag(ScreenShareCapturer::class.java)
private const val MAX_DIMENSION = 1280
private const val FRAME_RATE = 15
}
private val displayManager: DisplayManager by lazy {
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
}
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayChanged(displayId: Int) {
if (displayId == Display.DEFAULT_DISPLAY) {
onDisplayChangedInternal()
}
}
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
}
private var screenCapturer: ScreenCapturerAndroid? = null
private var surfaceHelper: SurfaceTextureHelper? = null
private var captureWidth: Int = 0
@@ -45,10 +71,15 @@ class ScreenShareCapturer(
override fun onStop() {
Log.i(TAG, "MediaProjection stopped")
}
override fun onCapturedContentResize(width: Int, height: Int) {
Log.i(TAG, "onCapturedContentResize($width, $height)")
applyCaptureFormat(width, height)
}
}
)
val (width, height) = computeCaptureDimensions()
val (width, height) = scaleForEncoder(readDisplayBounds())
captureWidth = width
captureHeight = height
@@ -57,27 +88,37 @@ class ScreenShareCapturer(
surfaceHelper = SurfaceTextureHelper.create("WebRTC-ScreenShareHelper", base!!.getEglBaseContext())
screenCapturer!!.initialize(surfaceHelper, context, sink)
screenCapturer!!.startCapture(width, height, FRAME_RATE)
if (Build.VERSION.SDK_INT < 34) {
displayManager.registerDisplayListener(displayListener, surfaceHelper!!.handler)
}
}
}
fun onConfigurationChanged() {
private fun onDisplayChangedInternal() {
if (!isCapturing) return
applyCaptureFormat(readDisplayBounds())
}
val (width, height) = computeCaptureDimensions()
private fun applyCaptureFormat(rawDimensions: Pair<Int, Int>) {
applyCaptureFormat(rawDimensions.first, rawDimensions.second)
}
private fun applyCaptureFormat(rawWidth: Int, rawHeight: Int) {
val (width, height) = scaleForEncoder(rawWidth to rawHeight)
if (width == captureWidth && height == captureHeight) {
return
}
Log.i(TAG, "onConfigurationChanged(): capture dimensions " + width + "x" + height)
Log.i(TAG, "applyCaptureFormat(): capture dimensions " + width + "x" + height)
captureWidth = width
captureHeight = height
screenCapturer?.changeCaptureFormat(width, height, FRAME_RATE)
}
private fun computeCaptureDimensions(): Pair<Int, Int> {
val metrics = context.resources.displayMetrics
var width = metrics.widthPixels
var height = metrics.heightPixels
private fun scaleForEncoder(raw: Pair<Int, Int>): Pair<Int, Int> {
var width = raw.first
var height = raw.second
val maxDimension = max(width, height)
if (maxDimension > MAX_DIMENSION) {
@@ -93,6 +134,21 @@ class ScreenShareCapturer(
return width to height
}
@Suppress("DEPRECATION")
private fun readDisplayBounds(): Pair<Int, Int> {
return if (Build.VERSION.SDK_INT >= 30) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val bounds = windowManager.maximumWindowMetrics.bounds
bounds.width() to bounds.height()
} else {
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
val metrics = DisplayMetrics()
display.getRealMetrics(metrics)
metrics.widthPixels to metrics.heightPixels
}
}
fun stop() {
if (!isCapturing) {
return
@@ -100,6 +156,10 @@ class ScreenShareCapturer(
Log.i(TAG, "stop()")
if (Build.VERSION.SDK_INT < 34) {
displayManager.unregisterDisplayListener(displayListener)
}
if (screenCapturer != null) {
screenCapturer!!.stopCapture()
screenCapturer!!.dispose()
@@ -119,11 +179,4 @@ class ScreenShareCapturer(
fun dispose() {
stop()
}
companion object {
private val TAG = tag(ScreenShareCapturer::class.java)
private const val MAX_DIMENSION = 1280
private const val FRAME_RATE = 15
}
}
@@ -1376,5 +1376,16 @@ object RemoteConfig {
hotSwappable = true
)
/**
* Whether screen sharing is available during calls.
*/
@JvmStatic
@get:JvmName("screenSharing")
val screenSharing: Boolean by remoteBoolean(
key = "android.calling.screenSharing",
defaultValue = false,
hotSwappable = true
)
// endregion
}
+2 -2
View File
@@ -2706,9 +2706,9 @@
<!-- A negative button for a dialog confirming the user wants to lower their hand (withdraw a raised hand) -->
<string name="CallOverflowPopupWindow__cancel">Cancel</string>
<!-- A clickable button to share your screen in a call -->
<string name="CallOverflowPopupWindow__share_screen" translatable="false">Share screen (Labs)</string>
<string name="CallOverflowPopupWindow__share_screen">Share screen</string>
<!-- A clickable button to stop sharing your screen in a call -->
<string name="CallOverflowPopupWindow__stop_screen_share" translatable="false">Stop sharing (Labs)</string>
<string name="CallOverflowPopupWindow__stop_screen_share">Stop sharing</string>
<!-- A button to take you to a list of participants with raised hands -->
<string name="CallOverflowPopupWindow__view">View</string>