mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-20 07:10:15 +01:00
Add screen share to 1:1 and group calling.
This commit is contained in:
committed by
Michelle Tang
parent
d1e2fc0423
commit
39529af4e9
@@ -38,6 +38,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
@@ -1403,7 +1404,7 @@
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync|microphone|camera|phoneCall" />
|
||||
android:foregroundServiceType="dataSync|microphone|camera|phoneCall|mediaProjection" />
|
||||
|
||||
<service
|
||||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||
|
||||
+1
-1
@@ -12,6 +12,6 @@ sealed interface LabsSettingsEvents {
|
||||
data class ToggleGroupSuggestionsForMembers(val enabled: Boolean) : 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
|
||||
}
|
||||
|
||||
+9
@@ -151,6 +151,15 @@ 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)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -15,6 +15,6 @@ data class LabsSettingsState(
|
||||
val groupSuggestionsForMembers: Boolean = false,
|
||||
val betterSearch: Boolean = false,
|
||||
val autoLowerHand: Boolean = false,
|
||||
|
||||
val starredMessages: Boolean = false
|
||||
val starredMessages: Boolean = false,
|
||||
val screenShare: Boolean = false
|
||||
)
|
||||
|
||||
+6
-1
@@ -46,6 +46,10 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +62,8 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
betterSearch = SignalStore.labs.betterSearch,
|
||||
autoLowerHand = SignalStore.labs.autoLowerHand,
|
||||
|
||||
starredMessages = SignalStore.labs.starredMessages
|
||||
starredMessages = SignalStore.labs.starredMessages,
|
||||
screenShare = SignalStore.labs.screenShare
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.annotation.Px;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -164,7 +166,9 @@ public final class WebRtcControls {
|
||||
}
|
||||
|
||||
public boolean displayOverflow() {
|
||||
return isAtLeastOutgoing() && hasAtLeastOneRemote && isGroupCall() && groupCallState == GroupCallState.CONNECTED;
|
||||
boolean connectedGroupCall = isGroupCall() && groupCallState == GroupCallState.CONNECTED && hasAtLeastOneRemote;
|
||||
boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && SignalStore.labs().getScreenShare();
|
||||
return isAtLeastOutgoing() && (connectedGroupCall || connected1to1Call);
|
||||
}
|
||||
|
||||
public boolean displayMuteAudio() {
|
||||
|
||||
+20
-2
@@ -46,6 +46,8 @@ data class AdditionalActionsState(
|
||||
val isShown: Boolean = false,
|
||||
val reactions: PersistentList<String> = persistentListOf(),
|
||||
val isSelfHandRaised: Boolean = false,
|
||||
val isScreenSharing: Boolean = false,
|
||||
val displayScreenShareToggle: Boolean = false,
|
||||
@Stable val listener: AdditionalActionsListener = AdditionalActionsListener.Empty
|
||||
)
|
||||
|
||||
@@ -53,11 +55,13 @@ interface AdditionalActionsListener {
|
||||
fun onReactClick(reaction: String)
|
||||
fun onReactWithAnyClick()
|
||||
fun onRaiseHandClick(raised: Boolean)
|
||||
fun onScreenShareClick(sharing: Boolean)
|
||||
|
||||
object Empty : AdditionalActionsListener {
|
||||
override fun onReactClick(reaction: String) = Unit
|
||||
override fun onReactWithAnyClick() = Unit
|
||||
override fun onRaiseHandClick(raised: Boolean) = Unit
|
||||
override fun onScreenShareClick(sharing: Boolean) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +95,10 @@ private fun AdditionalActionsPopupContent(
|
||||
|
||||
CallScreenMenu(
|
||||
onRaiseHandClick = state.listener::onRaiseHandClick,
|
||||
isSelfHandRaised = state.isSelfHandRaised
|
||||
isSelfHandRaised = state.isSelfHandRaised,
|
||||
isScreenSharing = state.isScreenSharing,
|
||||
displayScreenShareToggle = state.displayScreenShareToggle,
|
||||
onScreenShareClick = state.listener::onScreenShareClick
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +142,10 @@ private fun CallReactionScrubber(
|
||||
@Composable
|
||||
private fun CallScreenMenu(
|
||||
isSelfHandRaised: Boolean,
|
||||
onRaiseHandClick: (Boolean) -> Unit
|
||||
onRaiseHandClick: (Boolean) -> Unit,
|
||||
isScreenSharing: Boolean = false,
|
||||
displayScreenShareToggle: Boolean = false,
|
||||
onScreenShareClick: (Boolean) -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -147,6 +157,14 @@ private fun CallScreenMenu(
|
||||
title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand),
|
||||
onClick = { onRaiseHandClick(!isSelfHandRaised) }
|
||||
)
|
||||
|
||||
if (displayScreenShareToggle) {
|
||||
CallScreenMenuOption(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_screen_share_24),
|
||||
title = if (isScreenSharing) stringResource(R.string.CallOverflowPopupWindow__stop_screen_share) else stringResource(R.string.CallOverflowPopupWindow__share_screen),
|
||||
onClick = { onScreenShareClick(!isScreenSharing) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ fun CallControls(
|
||||
}
|
||||
|
||||
val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
if (callControlsState.displayVideoToggle) {
|
||||
if (callControlsState.displayVideoToggle && !callControlsState.isLocalScreenSharing) {
|
||||
CallScreenTooltipBox(
|
||||
text = stringResource(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video),
|
||||
displayTooltip = displayVideoTooltip,
|
||||
@@ -214,7 +214,8 @@ data class CallControlsState(
|
||||
val displayAdditionalActions: Boolean = false,
|
||||
val displayStartCallButton: Boolean = false,
|
||||
val startCallButtonText: Int = R.string.WebRtcCallView__start_call,
|
||||
val displayEndCallButton: Boolean = false
|
||||
val displayEndCallButton: Boolean = false,
|
||||
val isLocalScreenSharing: Boolean = false
|
||||
) {
|
||||
|
||||
val hasAnyControls: Boolean
|
||||
@@ -235,7 +236,8 @@ data class CallControlsState(
|
||||
callParticipantsState: CallParticipantsState,
|
||||
webRtcControls: WebRtcControls,
|
||||
groupMemberCount: Int,
|
||||
isAudioDeviceChangePending: Boolean = false
|
||||
isAudioDeviceChangePending: Boolean = false,
|
||||
isLocalScreenSharing: Boolean = false
|
||||
): CallControlsState {
|
||||
return CallControlsState(
|
||||
isEarpieceAvailable = webRtcControls.isEarpieceAvailableForAudioToggle,
|
||||
@@ -255,7 +257,8 @@ data class CallControlsState(
|
||||
displayAdditionalActions = webRtcControls.displayOverflow(),
|
||||
displayStartCallButton = webRtcControls.displayStartCallControls(),
|
||||
startCallButtonText = webRtcControls.startCallButtonText,
|
||||
displayEndCallButton = webRtcControls.displayEndCall()
|
||||
displayEndCallButton = webRtcControls.displayEndCall(),
|
||||
isLocalScreenSharing = isLocalScreenSharing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ sealed interface CallEvent {
|
||||
data class StartCall(val isVideoCall: Boolean) : CallEvent
|
||||
data class ShowGroupCallSafetyNumberChange(val identityRecords: List<IdentityRecord>) : CallEvent
|
||||
data object SwitchToSpeaker : CallEvent
|
||||
data object ShowSwipeToSpeakerHint : CallEvent
|
||||
data object ShowSwipeToScreenShareHint : CallEvent
|
||||
data object ShowLargeGroupAutoMuteToast : CallEvent
|
||||
data class ShowRemoteMuteToast(private val muted: Recipient, private val mutedBy: Recipient) : CallEvent {
|
||||
fun getDescription(context: Context): String {
|
||||
|
||||
@@ -77,6 +77,7 @@ 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
|
||||
@@ -171,11 +172,15 @@ fun CallScreen(
|
||||
val additionalActionsPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState()
|
||||
val additionalActionsState = remember(
|
||||
callScreenState.reactions,
|
||||
localParticipant.isHandRaised
|
||||
localParticipant.isHandRaised,
|
||||
callScreenState.isLocalScreenSharing,
|
||||
callControlsState.displayEndCallButton
|
||||
) {
|
||||
AdditionalActionsState(
|
||||
reactions = callScreenState.reactions,
|
||||
isSelfHandRaised = localParticipant.isHandRaised,
|
||||
isScreenSharing = callScreenState.isLocalScreenSharing,
|
||||
displayScreenShareToggle = callControlsState.displayEndCallButton && SignalStore.labs.screenShare,
|
||||
listener = additionalActionsListener,
|
||||
triggerAlignedPopupState = additionalActionsPopupState
|
||||
)
|
||||
@@ -505,7 +510,7 @@ fun CallScreen(
|
||||
)
|
||||
|
||||
SwipeToSpeakerHintPopup(
|
||||
visible = callScreenState.displaySwipeToSpeakerHint,
|
||||
hintType = callScreenState.swipeHint,
|
||||
onDismiss = onSwipeToSpeakerHintDismissed,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
|
||||
+2
@@ -36,6 +36,7 @@ interface CallScreenControlsListener {
|
||||
fun onNavigateUpClicked()
|
||||
fun toggleControls()
|
||||
fun onAudioPermissionsRequested(onGranted: Runnable?)
|
||||
fun onScreenShareChanged(sharing: Boolean)
|
||||
|
||||
object Empty : CallScreenControlsListener {
|
||||
override fun onStartCall(isVideoCall: Boolean) = Unit
|
||||
@@ -58,5 +59,6 @@ interface CallScreenControlsListener {
|
||||
override fun onNavigateUpClicked() = Unit
|
||||
override fun toggleControls() = Unit
|
||||
override fun onAudioPermissionsRequested(onGranted: Runnable?) = Unit
|
||||
override fun onScreenShareChanged(sharing: Boolean) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ interface CallScreenMediator {
|
||||
fun enableRingGroup(canRing: Boolean)
|
||||
fun showSpeakerViewHint()
|
||||
fun hideSpeakerViewHint()
|
||||
fun showScreenShareHint()
|
||||
fun showVideoTooltip(): Dismissible
|
||||
fun showCameraTooltip(): Dismissible
|
||||
fun onCallStateUpdate(callControlsChange: CallControlsChange)
|
||||
|
||||
+9
-2
@@ -25,7 +25,7 @@ data class CallScreenState(
|
||||
val isDisplayingAudioToggleSheet: Boolean = false,
|
||||
val displaySwitchCameraTooltip: Boolean = false,
|
||||
val displayVideoTooltip: Boolean = false,
|
||||
val displaySwipeToSpeakerHint: Boolean = false,
|
||||
val swipeHint: SwipeHintType = SwipeHintType.NONE,
|
||||
val displayWifiToCellularPopup: Boolean = false,
|
||||
val remoteMuteToastMessage: String? = null,
|
||||
val displayAdditionalActionsDialog: Boolean = false,
|
||||
@@ -34,7 +34,14 @@ data class CallScreenState(
|
||||
val isParticipantUpdatePopupEnabled: Boolean = true,
|
||||
val isCallStateUpdatePopupEnabled: Boolean = false,
|
||||
val isWaitingToBeLetIn: Boolean = false,
|
||||
val reactions: PersistentList<String> = persistentListOf()
|
||||
val reactions: PersistentList<String> = persistentListOf(),
|
||||
val isLocalScreenSharing: Boolean = false
|
||||
) {
|
||||
fun isDisplayingControlMenu(): Boolean = isDisplayingAudioToggleSheet || displayAdditionalActionsDialog
|
||||
}
|
||||
|
||||
enum class SwipeHintType {
|
||||
NONE,
|
||||
SPEAKER_VIEW,
|
||||
SCREEN_SHARE
|
||||
}
|
||||
|
||||
+30
-3
@@ -14,6 +14,7 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -110,6 +111,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
val recipient by viewModel.getRecipientFlow().collectAsStateWithLifecycle(Recipient.UNKNOWN)
|
||||
val webRtcCallState by callScreenViewModel.callState.collectAsStateWithLifecycle()
|
||||
val callScreenState by callScreenViewModel.callScreenState.collectAsStateWithLifecycle()
|
||||
val isLocalScreenSharing by viewModel.isLocalScreenSharing.collectAsStateWithLifecycle()
|
||||
val callControlsState by viewModel.getCallControlsState().collectAsStateWithLifecycle(CallControlsState())
|
||||
val callParticipantsViewState by callScreenViewModel.callParticipantsViewState.collectAsStateWithLifecycle()
|
||||
val callParticipantsState = remember(callParticipantsViewState) { callParticipantsViewState.callParticipantsState }
|
||||
@@ -173,6 +175,22 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isLocalScreenSharing) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(isLocalScreenSharing = isLocalScreenSharing) }
|
||||
}
|
||||
|
||||
LaunchedEffect(callScreenController, callScreenControlsListener) {
|
||||
snapshotFlow { callScreenController.callParticipantsVerticalPagerState.settledPage }
|
||||
.collect { page ->
|
||||
val selected = if (page == 1) {
|
||||
CallParticipantsState.SelectedPage.FOCUSED
|
||||
} else {
|
||||
CallParticipantsState.SelectedPage.GRID
|
||||
}
|
||||
callScreenControlsListener.onPageChanged(selected)
|
||||
}
|
||||
}
|
||||
|
||||
val controlAndInfoState by controlsAndInfoViewModel.state
|
||||
|
||||
SignalTheme(isDarkMode = true) {
|
||||
@@ -217,7 +235,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
onControlsToggled = onControlsToggled,
|
||||
onCallScreenDialogDismissed = { callScreenViewModel.dialog.update { CallScreenDialogType.NONE } },
|
||||
onWifiToCellularPopupDismissed = { callScreenViewModel.callScreenState.update { it.copy(displayWifiToCellularPopup = false) } },
|
||||
onSwipeToSpeakerHintDismissed = { callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) } },
|
||||
onSwipeToSpeakerHintDismissed = { callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.NONE) } },
|
||||
onRemoteMuteToastDismissed = { callScreenViewModel.callScreenState.update { it.copy(remoteMuteToastMessage = null) } },
|
||||
callParticipantUpdatePopupController = callParticipantUpdatePopupController,
|
||||
isSelfAdmin = controlAndInfoState.isSelfAdmin(),
|
||||
@@ -322,11 +340,15 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
}
|
||||
|
||||
override fun showSpeakerViewHint() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = true) }
|
||||
callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.SPEAKER_VIEW) }
|
||||
}
|
||||
|
||||
override fun hideSpeakerViewHint() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) }
|
||||
callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.NONE) }
|
||||
}
|
||||
|
||||
override fun showScreenShareHint() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.SCREEN_SHARE) }
|
||||
}
|
||||
|
||||
override fun showVideoTooltip(): Dismissible {
|
||||
@@ -393,6 +415,11 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayAdditionalActionsDialog = false) }
|
||||
}
|
||||
|
||||
override fun onScreenShareClick(sharing: Boolean) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayAdditionalActionsDialog = false) }
|
||||
controlsListener.value.onScreenShareChanged(sharing)
|
||||
}
|
||||
|
||||
private fun handleFailure() {
|
||||
Toast.makeText(activity, R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
+23
-8
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -25,16 +26,23 @@ import kotlin.time.Duration.Companion.seconds
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Popup shown to hint the user that they can swipe to view screen share.
|
||||
* Popup shown to hint the user that they should swipe between the grid view and
|
||||
* the focused page for speaker/screen share when available.
|
||||
*/
|
||||
@Composable
|
||||
fun SwipeToSpeakerHintPopup(
|
||||
visible: Boolean,
|
||||
hintType: SwipeHintType,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val textResId = when (hintType) {
|
||||
SwipeHintType.SCREEN_SHARE -> R.string.CallToastPopupWindow__swipe_to_view_screen_share
|
||||
SwipeHintType.SPEAKER_VIEW,
|
||||
SwipeHintType.NONE -> R.string.CallToastPopupWindow__swipe_to_view_speaker
|
||||
}
|
||||
|
||||
CallScreenPopup(
|
||||
visible = visible,
|
||||
visible = hintType != SwipeHintType.NONE,
|
||||
onDismiss = onDismiss,
|
||||
displayDuration = 3.seconds,
|
||||
modifier = modifier
|
||||
@@ -51,7 +59,7 @@ fun SwipeToSpeakerHintPopup(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.CallToastPopupWindow__swipe_to_view_screen_share),
|
||||
text = stringResource(textResId),
|
||||
color = colorResource(CoreUiR.color.signal_light_colorOnSecondaryContainer),
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
@@ -63,9 +71,16 @@ fun SwipeToSpeakerHintPopup(
|
||||
@Composable
|
||||
private fun SwipeToSpeakerHintPopupPreview() {
|
||||
Previews.Preview {
|
||||
SwipeToSpeakerHintPopup(
|
||||
visible = true,
|
||||
onDismiss = {}
|
||||
)
|
||||
Column {
|
||||
SwipeToSpeakerHintPopup(
|
||||
hintType = SwipeHintType.SPEAKER_VIEW,
|
||||
onDismiss = {}
|
||||
)
|
||||
|
||||
SwipeToSpeakerHintPopup(
|
||||
hintType = SwipeHintType.SCREEN_SHARE,
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-2
@@ -14,6 +14,8 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
import android.media.projection.MediaProjectionConfig
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Rational
|
||||
@@ -22,6 +24,7 @@ import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@@ -38,6 +41,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -123,6 +127,11 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private var ephemeralStateDisposable = Disposable.empty()
|
||||
private val callPermissionsDialogController = CallPermissionsDialogController()
|
||||
private val eventBusSubscriber = EventBusSubscriber()
|
||||
private val mediaProjectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
AppDependencies.signalCallManager.startScreenShare(result.data!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
@@ -210,6 +219,17 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
callScreen.setMicEnabled(viewModel.microphoneEnabled.value)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel
|
||||
.isLocalScreenSharing
|
||||
.drop(1)
|
||||
.collect { sharing ->
|
||||
if (!sharing && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
AppDependencies.signalCallManager.setEnableVideo(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
@@ -312,7 +332,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
requestNewSizesThrottle.clear()
|
||||
}
|
||||
|
||||
if (!isChangingConfigurations && !isInMultiWindowModeCompat()) {
|
||||
if (!isChangingConfigurations && !isInMultiWindowModeCompat() && !viewModel.isLocalScreenSharing.value) {
|
||||
AppDependencies.signalCallManager.setEnableVideo(false)
|
||||
}
|
||||
|
||||
@@ -985,7 +1005,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
is CallEvent.StartCall -> startCall(event.isVideoCall)
|
||||
is CallEvent.ShowGroupCallSafetyNumberChange -> SafetyNumberBottomSheet.forGroupCall(event.identityRecords).show(supportFragmentManager)
|
||||
is CallEvent.SwitchToSpeaker -> callScreen.switchToSpeakerView()
|
||||
is CallEvent.ShowSwipeToSpeakerHint -> callScreen.showSpeakerViewHint()
|
||||
is CallEvent.ShowSwipeToScreenShareHint -> callScreen.showScreenShareHint()
|
||||
is CallEvent.ShowRemoteMuteToast -> callScreen.showRemoteMuteToast(event.getDescription(this))
|
||||
is CallEvent.ShowLargeGroupAutoMuteToast -> {
|
||||
callScreen.onCallStateUpdate(CallControlsChange.MIC_OFF)
|
||||
@@ -1390,6 +1410,20 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
override fun onAudioPermissionsRequested(onGranted: Runnable?) {
|
||||
askAudioPermissions { onGranted?.run() }
|
||||
}
|
||||
|
||||
override fun onScreenShareChanged(sharing: Boolean) {
|
||||
if (sharing) {
|
||||
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val intent = if (Build.VERSION.SDK_INT >= 34) {
|
||||
mediaProjectionManager.createScreenCaptureIntent(MediaProjectionConfig.createConfigForDefaultDisplay())
|
||||
} else {
|
||||
mediaProjectionManager.createScreenCaptureIntent()
|
||||
}
|
||||
mediaProjectionLauncher.launch(intent)
|
||||
} else {
|
||||
AppDependencies.signalCallManager.stopScreenShare()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PendingParticipantsViewListener : PendingParticipantsListener {
|
||||
|
||||
+12
-5
@@ -70,6 +70,9 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
private val ephemeralState = MutableStateFlow<WebRtcEphemeralState?>(null)
|
||||
private val remoteMutesReported = MutableStateFlow(HashSet<CallParticipantId>())
|
||||
|
||||
private val _isLocalScreenSharing = MutableStateFlow(false)
|
||||
val isLocalScreenSharing: StateFlow<Boolean> = _isLocalScreenSharing
|
||||
|
||||
private val controlsWithFoldableState: Flow<WebRtcControls> = combine(foldableState, webRtcControls, this::updateControlsFoldableState)
|
||||
private val realWebRtcControls: StateFlow<WebRtcControls> = combine(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, WebRtcControls.NONE)
|
||||
@@ -100,6 +103,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
private var callConnectedTime = -1L
|
||||
private var answerWithVideoAvailable = false
|
||||
private var previousParticipantList = Collections.emptyList<CallParticipant>()
|
||||
private var hasSeededParticipantList = false
|
||||
private var switchOnFirstScreenShare = true
|
||||
private var showScreenShareTip = true
|
||||
private var hasShownAutoMuteToast = false
|
||||
@@ -181,9 +185,10 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
callParticipantsState,
|
||||
getWebRtcControls(),
|
||||
groupSize,
|
||||
isAudioDeviceChangePending
|
||||
) { participantsState, controls, groupMemberCount, audioChangePending ->
|
||||
CallControlsState.fromViewModelData(participantsState, controls, groupMemberCount, audioChangePending)
|
||||
isAudioDeviceChangePending,
|
||||
_isLocalScreenSharing
|
||||
) { participantsState, controls, groupMemberCount, audioChangePending, isLocalScreenSharing ->
|
||||
CallControlsState.fromViewModelData(participantsState, controls, groupMemberCount, audioChangePending, isLocalScreenSharing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +268,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
) {
|
||||
showScreenShareTip = false
|
||||
viewModelScope.launch {
|
||||
events.emit(CallEvent.ShowSwipeToSpeakerHint)
|
||||
events.emit(CallEvent.ShowSwipeToScreenShareHint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +324,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
val wasMicrophoneEnabled = internalMicrophoneEnabled.value
|
||||
internalMicrophoneEnabled.value = localParticipant.isMicrophoneEnabled
|
||||
isAudioDeviceChangePending.value = webRtcViewModel.isAudioDeviceChangePending
|
||||
_isLocalScreenSharing.value = webRtcViewModel.isLocalScreenSharing
|
||||
|
||||
if (internalMicrophoneEnabled.value) {
|
||||
remoteMutedBy.update { null }
|
||||
@@ -347,12 +353,13 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
if (webRtcViewModel.groupState.isConnected) {
|
||||
if (!containsPlaceholders(previousParticipantList)) {
|
||||
if (!containsPlaceholders(previousParticipantList) && hasSeededParticipantList) {
|
||||
val update = CallParticipantListUpdate.computeDeltaUpdate(previousParticipantList, webRtcViewModel.remoteParticipants)
|
||||
viewModelScope.launch {
|
||||
callParticipantListUpdate.emit(update)
|
||||
}
|
||||
}
|
||||
hasSeededParticipantList = true
|
||||
|
||||
for (remote in webRtcViewModel.remoteParticipants) {
|
||||
if (remote.remotelyMutedBy == null) {
|
||||
|
||||
@@ -124,12 +124,14 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
||||
val isAudioDeviceChangePending: Boolean = state.localDeviceState.isAudioDeviceChangePending
|
||||
|
||||
val localParticipant: CallParticipant = createLocal(
|
||||
state.localDeviceState.cameraState,
|
||||
(if (state.videoState.localSink != null) state.videoState.localSink else BroadcastVideoSink())!!,
|
||||
state.localDeviceState.isMicrophoneEnabled,
|
||||
state.localDeviceState.handRaisedTimestamp
|
||||
cameraState = state.localDeviceState.cameraState,
|
||||
renderer = state.videoState.localSink ?: BroadcastVideoSink(),
|
||||
microphoneEnabled = state.localDeviceState.isMicrophoneEnabled,
|
||||
handRaisedTimestamp = state.localDeviceState.handRaisedTimestamp
|
||||
)
|
||||
|
||||
val isLocalScreenSharing: Boolean = state.localDeviceState.isScreenSharing
|
||||
|
||||
val remoteMutedBy: CallParticipant? = state.localDeviceState.remoteMutedBy
|
||||
|
||||
val isCellularConnection: Boolean = when (state.localDeviceState.networkConnectionType) {
|
||||
|
||||
@@ -10,8 +10,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
|
||||
const val GROUP_SUGGESTIONS_FOR_MEMBERS: String = "labs.group_suggestions_for_members"
|
||||
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,6 +32,8 @@ 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 }
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
|
||||
private static final String TAG = Log.tag(Camera.class);
|
||||
|
||||
private static final int CAPTURE_WIDTH = 1280;
|
||||
private static final int CAPTURE_HEIGHT = 720;
|
||||
private static final int CAPTURE_FPS = 30;
|
||||
|
||||
@NonNull private final Context context;
|
||||
@Nullable private final CameraVideoCapturer capturer;
|
||||
@Nullable private CameraEventListener cameraEventListener;
|
||||
@@ -47,6 +51,8 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
@NonNull private CameraState.Direction activeDirection;
|
||||
private boolean enabled;
|
||||
private boolean isInitialized;
|
||||
private boolean capturing;
|
||||
private boolean paused;
|
||||
private int orientation;
|
||||
@Nullable private volatile VideoSink vanitySink;
|
||||
|
||||
@@ -120,24 +126,42 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
Log.i(TAG, "setEnabled(): " + enabled);
|
||||
|
||||
this.enabled = enabled;
|
||||
this.paused = false;
|
||||
|
||||
if (capturer == null || !isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
Log.i(TAG, "setEnabled(): starting capture");
|
||||
capturer.startCapture(1280, 720, 30);
|
||||
} else {
|
||||
Log.i(TAG, "setEnabled(): stopping capture");
|
||||
capturer.stopCapture();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Got interrupted while trying to stop video capture", e);
|
||||
if (enabled) {
|
||||
startCapture();
|
||||
} else {
|
||||
stopCapture();
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseCapture() {
|
||||
if (capturer != null && capturing) {
|
||||
paused = true;
|
||||
stopCapture();
|
||||
}
|
||||
}
|
||||
|
||||
public void resumeCapture() {
|
||||
paused = false;
|
||||
if (capturer != null && isInitialized && enabled && !capturing) {
|
||||
startCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the camera should be capturing based on user state, regardless of the current pause status. */
|
||||
public boolean shouldBeCapturing() {
|
||||
return capturer != null && isInitialized && enabled;
|
||||
}
|
||||
|
||||
public boolean isCapturing() {
|
||||
return capturing;
|
||||
}
|
||||
|
||||
public void setCameraEventListener(@Nullable CameraEventListener cameraEventListener) {
|
||||
this.cameraEventListener = cameraEventListener;
|
||||
}
|
||||
@@ -151,10 +175,18 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
this.vanitySink = vanitySink;
|
||||
}
|
||||
|
||||
void deliverToVanitySink(@NonNull VideoFrame videoFrame) {
|
||||
VideoSink sink = vanitySink;
|
||||
if (sink != null) {
|
||||
sink.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (capturer != null) {
|
||||
capturer.dispose();
|
||||
isInitialized = false;
|
||||
capturing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,14 +202,30 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
return new CameraState(getActiveDirection(), getCount());
|
||||
}
|
||||
|
||||
@Nullable CameraVideoCapturer getCapturer() {
|
||||
return capturer;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return isInitialized;
|
||||
}
|
||||
|
||||
private void startCapture() {
|
||||
Log.i(TAG, "startCapture()");
|
||||
try {
|
||||
capturer.startCapture(CAPTURE_WIDTH, CAPTURE_HEIGHT, CAPTURE_FPS);
|
||||
capturing = true;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error starting capture", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopCapture() {
|
||||
Log.i(TAG, "stopCapture()");
|
||||
try {
|
||||
capturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted stopping capture", e);
|
||||
}
|
||||
capturing = false;
|
||||
}
|
||||
|
||||
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
|
||||
@NonNull CameraState.Direction direction)
|
||||
{
|
||||
@@ -332,6 +380,9 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
|
||||
@Override
|
||||
public void onCapturerStopped() {
|
||||
if (paused) {
|
||||
return;
|
||||
}
|
||||
observer.onCapturerStopped();
|
||||
if (cameraEventListener != null) cameraEventListener.onCameraStopped();
|
||||
}
|
||||
@@ -339,10 +390,6 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
||||
@Override
|
||||
public void onFrameCaptured(VideoFrame videoFrame) {
|
||||
observer.onFrameCaptured(videoFrame);
|
||||
VideoSink sink = vanitySink;
|
||||
if (sink != null) {
|
||||
sink.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package org.thoughtcrime.securesms.ringrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.Log.tag
|
||||
import org.signal.ringrtc.CameraControl
|
||||
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper
|
||||
import org.webrtc.CapturerObserver
|
||||
import org.webrtc.VideoFrame
|
||||
import org.webrtc.VideoSink
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* Owns the outgoing video pipeline: a [Camera] and a lazily-created
|
||||
* [ScreenShareCapturer], and the routing logic that decides which one's
|
||||
* frames flow into the WebRTC [CapturerObserver] that RingRTC supplies.
|
||||
*/
|
||||
class OutgoingVideoSourceRouter(
|
||||
private val context: Context,
|
||||
private val eglBase: EglBaseWrapper,
|
||||
cameraEventListener: CameraEventListener,
|
||||
desiredCameraDirection: CameraState.Direction
|
||||
) : CameraControl {
|
||||
|
||||
companion object {
|
||||
private val TAG = tag(OutgoingVideoSourceRouter::class.java)
|
||||
}
|
||||
|
||||
private val camera: Camera = Camera(context, cameraEventListener, eglBase, desiredCameraDirection)
|
||||
|
||||
private var screenShareCapturer: ScreenShareCapturer? = null
|
||||
private var downstream: CapturerObserver? = null
|
||||
|
||||
@Volatile
|
||||
var isScreenSharing: Boolean = false
|
||||
private set
|
||||
|
||||
override fun hasCapturer(): Boolean {
|
||||
return camera.hasCapturer()
|
||||
}
|
||||
|
||||
override fun initCapturer(observer: CapturerObserver) {
|
||||
Log.i(TAG, "initCapturer()")
|
||||
this.downstream = observer
|
||||
camera.initCapturer(CameraSideObserver())
|
||||
}
|
||||
|
||||
override fun setEnabled(enable: Boolean) {
|
||||
camera.setEnabled(enable)
|
||||
}
|
||||
|
||||
override fun flip() {
|
||||
camera.flip()
|
||||
}
|
||||
|
||||
override fun setOrientation(orientation: Int?) {
|
||||
camera.setOrientation(orientation)
|
||||
screenShareCapturer?.onConfigurationChanged()
|
||||
}
|
||||
|
||||
val cameraState: CameraState
|
||||
get() = camera.cameraState
|
||||
|
||||
val isInitialized: Boolean
|
||||
get() = camera.isInitialized
|
||||
|
||||
fun setCameraEventListener(cameraEventListener: CameraEventListener?) {
|
||||
camera.setCameraEventListener(cameraEventListener)
|
||||
}
|
||||
|
||||
fun setVanitySink(vanitySink: VideoSink?) {
|
||||
camera.setVanitySink(vanitySink)
|
||||
}
|
||||
|
||||
fun startScreenShare(mediaProjectionData: Intent) {
|
||||
if (isScreenSharing) {
|
||||
Log.w(TAG, "Already screen sharing")
|
||||
return
|
||||
}
|
||||
|
||||
if (downstream == null) {
|
||||
Log.w(TAG, "Cannot start screen share before initCapturer()")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "startScreenShare()")
|
||||
|
||||
isScreenSharing = true
|
||||
|
||||
if (camera.isCapturing) {
|
||||
camera.pauseCapture()
|
||||
}
|
||||
|
||||
if (screenShareCapturer == null) {
|
||||
screenShareCapturer = ScreenShareCapturer(context, eglBase, ScreenSideObserver())
|
||||
}
|
||||
|
||||
screenShareCapturer!!.start(mediaProjectionData)
|
||||
}
|
||||
|
||||
fun stopScreenShare() {
|
||||
if (!isScreenSharing) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "stopScreenShare()")
|
||||
|
||||
screenShareCapturer?.stop()
|
||||
|
||||
isScreenSharing = false
|
||||
|
||||
if (camera.shouldBeCapturing()) {
|
||||
camera.resumeCapture()
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
screenShareCapturer?.dispose()
|
||||
screenShareCapturer = null
|
||||
isScreenSharing = false
|
||||
camera.dispose()
|
||||
}
|
||||
|
||||
private inner class CameraSideObserver : CapturerObserver {
|
||||
override fun onCapturerStarted(success: Boolean) {
|
||||
if (!isScreenSharing) {
|
||||
downstream?.onCapturerStarted(success)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCapturerStopped() {
|
||||
if (!isScreenSharing) {
|
||||
downstream?.onCapturerStopped()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFrameCaptured(videoFrame: VideoFrame) {
|
||||
if (!isScreenSharing) {
|
||||
downstream?.onFrameCaptured(videoFrame)
|
||||
}
|
||||
camera.deliverToVanitySink(videoFrame)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ScreenSideObserver : CapturerObserver {
|
||||
override fun onCapturerStarted(success: Boolean) {
|
||||
if (isScreenSharing) {
|
||||
downstream?.onCapturerStarted(success)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCapturerStopped() {
|
||||
if (isScreenSharing) {
|
||||
downstream?.onCapturerStopped()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFrameCaptured(videoFrame: VideoFrame?) {
|
||||
if (isScreenSharing) {
|
||||
downstream?.onFrameCaptured(videoFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.ringrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.projection.MediaProjection
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.Log.tag
|
||||
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper
|
||||
import org.webrtc.CapturerObserver
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.ScreenCapturerAndroid
|
||||
import org.webrtc.SurfaceTextureHelper
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Captures the device screen via [MediaProjection] and forwards frames to a
|
||||
* [CapturerObserver] sink.
|
||||
*/
|
||||
class ScreenShareCapturer(
|
||||
private val context: Context,
|
||||
private val eglBase: EglBaseWrapper,
|
||||
private val sink: CapturerObserver
|
||||
) {
|
||||
|
||||
private var screenCapturer: ScreenCapturerAndroid? = null
|
||||
private var surfaceHelper: SurfaceTextureHelper? = null
|
||||
private var captureWidth: Int = 0
|
||||
private var captureHeight: Int = 0
|
||||
var isCapturing: Boolean = false
|
||||
private set
|
||||
|
||||
fun start(mediaProjectionData: Intent) {
|
||||
if (isCapturing) {
|
||||
Log.w(TAG, "Already capturing")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "start()")
|
||||
isCapturing = true
|
||||
|
||||
eglBase.performWithValidEglBase { base: EglBase? ->
|
||||
screenCapturer = ScreenCapturerAndroid(
|
||||
mediaProjectionData,
|
||||
object : MediaProjection.Callback() {
|
||||
override fun onStop() {
|
||||
Log.i(TAG, "MediaProjection stopped")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val (width, height) = computeCaptureDimensions()
|
||||
captureWidth = width
|
||||
captureHeight = height
|
||||
|
||||
Log.i(TAG, "start(): capture dimensions " + width + "x" + height)
|
||||
|
||||
surfaceHelper = SurfaceTextureHelper.create("WebRTC-ScreenShareHelper", base!!.getEglBaseContext())
|
||||
screenCapturer!!.initialize(surfaceHelper, context, sink)
|
||||
screenCapturer!!.startCapture(width, height, FRAME_RATE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfigurationChanged() {
|
||||
if (!isCapturing) return
|
||||
|
||||
val (width, height) = computeCaptureDimensions()
|
||||
if (width == captureWidth && height == captureHeight) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "onConfigurationChanged(): 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
|
||||
|
||||
val maxDimension = max(width, height)
|
||||
if (maxDimension > MAX_DIMENSION) {
|
||||
val scale = MAX_DIMENSION.toFloat() / maxDimension
|
||||
width = (width * scale).toInt()
|
||||
height = (height * scale).toInt()
|
||||
}
|
||||
|
||||
// Encoders require even dimensions
|
||||
width = width and 1.inv()
|
||||
height = height and 1.inv()
|
||||
|
||||
return width to height
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!isCapturing) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "stop()")
|
||||
|
||||
if (screenCapturer != null) {
|
||||
screenCapturer!!.stopCapture()
|
||||
screenCapturer!!.dispose()
|
||||
screenCapturer = null
|
||||
}
|
||||
|
||||
if (surfaceHelper != null) {
|
||||
surfaceHelper!!.dispose()
|
||||
surfaceHelper = null
|
||||
}
|
||||
|
||||
captureWidth = 0
|
||||
captureHeight = 0
|
||||
isCapturing = false
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stop()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = tag(ScreenShareCapturer::class.java)
|
||||
|
||||
private const val MAX_DIMENSION = 1280
|
||||
private const val FRAME_RATE = 15
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
@@ -181,6 +182,7 @@ abstract class SafeForegroundService : Service() {
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
checkNotNull(intent) { "Must have an intent!" }
|
||||
|
||||
@@ -188,8 +190,8 @@ abstract class SafeForegroundService : Service() {
|
||||
|
||||
if (intent.action == ACTION_TIMEOUT) {
|
||||
Log.i(TAG, "Time limit for foreground services has been met. Skipping starting a foreground.")
|
||||
} else if (Build.VERSION.SDK_INT >= 30 && serviceType != 0) {
|
||||
startForeground(notificationId, getForegroundNotification(intent), serviceType)
|
||||
} else if (Build.VERSION.SDK_INT >= 30 && serviceType(intent) != 0) {
|
||||
startForeground(notificationId, getForegroundNotification(intent), serviceType(intent))
|
||||
} else {
|
||||
startForeground(notificationId, getForegroundNotification(intent))
|
||||
}
|
||||
@@ -248,7 +250,7 @@ abstract class SafeForegroundService : Service() {
|
||||
|
||||
/** Special service type to use when calling start service if needed */
|
||||
@RequiresApi(30)
|
||||
open val serviceType: Int = 0
|
||||
open fun serviceType(intent: Intent): Int = 0
|
||||
|
||||
/** Notification to post as our foreground notification. */
|
||||
abstract fun getForegroundNotification(intent: Intent): Notification
|
||||
|
||||
@@ -288,12 +288,16 @@ class ActiveCallManager(
|
||||
private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID"
|
||||
private const val EXTRA_IS_VIDEO_CALL = "IS_VIDEO_CALL"
|
||||
private const val EXTRA_TYPE = "TYPE"
|
||||
private const val EXTRA_SCREEN_SHARING = "SCREEN_SHARING"
|
||||
|
||||
fun update(context: Context, @CallNotificationBuilder.CallNotificationType type: Int, recipientId: RecipientId, isVideoCall: Boolean) {
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun update(context: Context, @CallNotificationBuilder.CallNotificationType type: Int, recipientId: RecipientId, isVideoCall: Boolean, isScreenSharing: Boolean = false) {
|
||||
val extras = bundleOf(
|
||||
EXTRA_TYPE to type,
|
||||
EXTRA_RECIPIENT_ID to recipientId,
|
||||
EXTRA_IS_VIDEO_CALL to isVideoCall
|
||||
EXTRA_IS_VIDEO_CALL to isVideoCall,
|
||||
EXTRA_SCREEN_SHARING to isScreenSharing
|
||||
)
|
||||
|
||||
if (!SafeForegroundService.update(context, ActiveCallForegroundService::class.java, extras)) {
|
||||
@@ -308,34 +312,14 @@ class ActiveCallManager(
|
||||
}
|
||||
}
|
||||
|
||||
private var isScreenSharing: Boolean = false
|
||||
|
||||
override val tag: String
|
||||
get() = TAG
|
||||
|
||||
override val notificationId: Int
|
||||
get() = CallNotificationBuilder.WEBRTC_NOTIFICATION
|
||||
|
||||
@get:RequiresApi(30)
|
||||
override val serviceType: Int
|
||||
get() {
|
||||
val telecom = Build.VERSION.SDK_INT >= 36 && AndroidTelecomUtil.hasActiveController()
|
||||
|
||||
var type = if (telecom) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.CAMERA)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private var hangUpRtcOnDeviceCallAnswered: PhoneStateListener? = null
|
||||
private var notificationDisposable: Disposable = Disposable.disposed()
|
||||
@@ -385,6 +369,31 @@ class ActiveCallManager(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(30)
|
||||
override fun serviceType(intent: Intent): Int {
|
||||
val telecom = Build.VERSION.SDK_INT >= 36 && AndroidTelecomUtil.hasActiveController()
|
||||
|
||||
var type = if (telecom) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.CAMERA)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_SCREEN_SHARING, false)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
override fun getForegroundNotification(intent: Intent): Notification {
|
||||
notificationDisposable.dispose()
|
||||
|
||||
@@ -423,6 +432,15 @@ class ActiveCallManager(
|
||||
return createNotification(type, recipient, isVideoCall, skipAvatarLoad = requiresAsyncNotificationLoad)
|
||||
}
|
||||
|
||||
override fun onServiceUpdateCommandReceived(intent: Intent) {
|
||||
val wasScreenSharing = isScreenSharing
|
||||
isScreenSharing = intent.getBooleanExtra(EXTRA_SCREEN_SHARING, false)
|
||||
|
||||
if (isScreenSharing && !wasScreenSharing) {
|
||||
AppDependencies.signalCallManager.onScreenSharingServiceReady()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(type: Int, recipient: Recipient, isVideoCall: Boolean, skipAvatarLoad: Boolean): Notification {
|
||||
return CallNotificationBuilder.getCallInProgressNotification(
|
||||
this,
|
||||
|
||||
+6
-6
@@ -7,7 +7,7 @@ import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -93,19 +93,19 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(tag, "handleSetEnableVideo(): enable: " + enable);
|
||||
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
if (camera.isInitialized()) {
|
||||
camera.setEnabled(enable);
|
||||
if (router.isInitialized()) {
|
||||
router.setEnabled(enable);
|
||||
}
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
|
||||
//noinspection SimplifiableBooleanExpression
|
||||
if ((enable && camera.isInitialized()) || !enable) {
|
||||
if ((enable && router.isInitialized()) || !enable) {
|
||||
try {
|
||||
CallManager callManager = webRtcInteractor.getCallManager();
|
||||
callManager.setVideoEnable(enable, false);
|
||||
|
||||
+74
-1
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.ResultReceiver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -11,7 +12,11 @@ import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.LocalDeviceState;
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
|
||||
@@ -50,7 +55,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
|
||||
.cameraState(currentState.getVideoState().requireRouter().getCameraState())
|
||||
.build();
|
||||
|
||||
boolean localVideoEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled();
|
||||
@@ -122,6 +127,74 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
return activeCallDelegate.handleScreenSharingEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable Intent mediaProjectionData) {
|
||||
Log.i(TAG, "handleSetLocalScreenShare(): enable: " + enable);
|
||||
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
RecipientId recipientId = currentState.getCallInfoState().requireActivePeer().getRecipient().getId();
|
||||
|
||||
if (enable && mediaProjectionData != null) {
|
||||
Log.i(tag, "Updating service for media projection, then can screen share");
|
||||
ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, true);
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.setMediaProjectionIntent(mediaProjectionData)
|
||||
.build();
|
||||
} else {
|
||||
router.stopScreenShare();
|
||||
|
||||
boolean cameraWasEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled();
|
||||
ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, cameraWasEnabled, false);
|
||||
|
||||
try {
|
||||
webRtcInteractor.getCallManager().setVideoEnable(cameraWasEnabled, false);
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "setVideoEnable() after screen share failed: ", e);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.isScreenSharing(false)
|
||||
.setMediaProjectionIntent(null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleScreenSharingServiceReady()");
|
||||
|
||||
LocalDeviceState localDeviceState = currentState.getLocalDeviceState();
|
||||
Intent mediaProjectionIntent = localDeviceState.getMediaProjectionIntent();
|
||||
|
||||
if (localDeviceState.isScreenSharing()) {
|
||||
Log.w(tag, "handleScreenSharingServiceReady(): already screensharing!");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (mediaProjectionIntent == null) {
|
||||
Log.w(tag, "handleScreenSharingServiceReady(): Media intent is null, bailing");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
try {
|
||||
webRtcInteractor.getCallManager().setVideoEnable(true, true);
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "setVideoEnable() for screen share failed: ", e);
|
||||
}
|
||||
router.startScreenShare(mediaProjectionIntent);
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.isScreenSharing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
|
||||
return activeCallDelegate.handleLocalHangup(currentState);
|
||||
|
||||
+3
-3
@@ -61,11 +61,11 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleSetCameraFlip():");
|
||||
|
||||
if (currentState.getLocalDeviceState().getCameraState().isEnabled() && currentState.getVideoState().getCamera() != null) {
|
||||
currentState.getVideoState().getCamera().flip();
|
||||
if (currentState.getLocalDeviceState().getCameraState().isEnabled() && currentState.getVideoState().getRouter() != null) {
|
||||
currentState.getVideoState().getRouter().flip();
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(currentState.getVideoState().getCamera().getCameraState())
|
||||
.cameraState(currentState.getVideoState().getRouter().getCameraState())
|
||||
.build();
|
||||
}
|
||||
return currentState;
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
||||
.actionProcessor(actionProcessorFactory.createNetworkUnavailableActionProcessor(webRtcInteractor))
|
||||
.changeVideoState()
|
||||
.eglBase(videoState.getLockableEglBase())
|
||||
.camera(videoState.getCamera())
|
||||
.router(videoState.getRouter())
|
||||
.localSink(videoState.getLocalSink())
|
||||
.commit()
|
||||
.changeCallInfoState()
|
||||
|
||||
+82
-7
@@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.LongSparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
@@ -19,11 +18,14 @@ import org.thoughtcrime.securesms.events.GroupCallSpeechEvent;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.LocalDeviceState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -31,6 +33,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Process actions for when the call has at least once been connected and joined.
|
||||
@@ -87,19 +90,19 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(tag, "handleSetEnableVideo():");
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoMuted(!enable, false);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable set video muted", e);
|
||||
}
|
||||
camera.setEnabled(enable);
|
||||
router.setEnabled(enable);
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
|
||||
boolean localVideoEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled();
|
||||
@@ -111,6 +114,78 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable Intent mediaProjectionData) {
|
||||
Log.i(tag, "handleSetLocalScreenShare(): enable: " + enable);
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
RecipientId recipientId = currentState.getCallInfoState().getCallRecipient().getId();
|
||||
|
||||
if (enable && mediaProjectionData != null) {
|
||||
Log.i(tag, "Updating service for media projection, then can screen share");
|
||||
ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, true);
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.setMediaProjectionIntent(mediaProjectionData)
|
||||
.build();
|
||||
} else {
|
||||
router.stopScreenShare();
|
||||
|
||||
boolean cameraWasEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled();
|
||||
ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, false);
|
||||
|
||||
try {
|
||||
groupCall.setPresenting(false);
|
||||
groupCall.setOutgoingVideoMuted(!cameraWasEnabled, false);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to restore video mute after screen share", e);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.isScreenSharing(false)
|
||||
.setMediaProjectionIntent(null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleScreenSharingServiceReady()");
|
||||
|
||||
LocalDeviceState localDeviceState = currentState.getLocalDeviceState();
|
||||
Intent mediaProjectionIntent = localDeviceState.getMediaProjectionIntent();
|
||||
|
||||
if (localDeviceState.isScreenSharing()) {
|
||||
Log.w(tag, "handleScreenSharingServiceReady(): already screensharing!");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (mediaProjectionIntent == null) {
|
||||
Log.w(tag, "handleScreenSharingServiceReady(): Media intent is null, bailing");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoMuted(false, true);
|
||||
groupCall.setPresenting(true);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to unmute video for screen share", e);
|
||||
}
|
||||
router.startScreenShare(mediaProjectionIntent);
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.isScreenSharing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) {
|
||||
try {
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
@@ -142,19 +142,19 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoMuted(!enable, false);
|
||||
} catch (CallException e) {
|
||||
return groupCallFailure(currentState, "Unable to set video muted", e);
|
||||
}
|
||||
camera.setEnabled(enable);
|
||||
router.setEnabled(enable);
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
|
||||
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState);
|
||||
|
||||
+3
-3
@@ -196,7 +196,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
||||
webRtcInteractor.initializeAudioForCall(true);
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireRouter());
|
||||
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled(), false);
|
||||
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
|
||||
groupCall.setDataMode(NetworkUtil.getCallingDataMode(context, groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType()));
|
||||
@@ -220,13 +220,13 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(tag, "handleSetEnableVideo(): Changing for pre-join group call. enable: " + enable);
|
||||
|
||||
currentState.getVideoState().requireCamera().setEnabled(enable);
|
||||
currentState.getVideoState().requireRouter().setEnabled(enable);
|
||||
return currentState.builder()
|
||||
.changeCallSetupState(RemotePeer.GROUP_CALL_ID)
|
||||
.enableVideoOnCreate(enable)
|
||||
.commit()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
|
||||
.cameraState(currentState.getVideoState().requireRouter().getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CallState;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.CallSetupState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
|
||||
@@ -109,7 +109,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
RingRtcDynamicConfiguration.getAudioConfig(),
|
||||
videoState.requireLocalSink(),
|
||||
callParticipant.getVideoSink(),
|
||||
videoState.requireCamera(),
|
||||
videoState.requireRouter(),
|
||||
callSetupState.getIceServers(),
|
||||
hideIp,
|
||||
NetworkUtil.getCallingDataMode(context),
|
||||
@@ -137,14 +137,14 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId());
|
||||
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
camera.setVanitySink(null);
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
router.setVanitySink(null);
|
||||
|
||||
if (!answerWithVideo && currentState.getLocalDeviceState().getCameraState().isEnabled()) {
|
||||
camera.setEnabled(false);
|
||||
router.setEnabled(false);
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -172,9 +172,9 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
Log.i(TAG, "handleDenyCall():");
|
||||
|
||||
Camera camera = currentState.getVideoState().getCamera();
|
||||
if (camera != null) {
|
||||
camera.setVanitySink(null);
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter();
|
||||
if (router != null) {
|
||||
router.setVanitySink(null);
|
||||
}
|
||||
|
||||
webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer,
|
||||
@@ -209,21 +209,21 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
if (enabled) {
|
||||
Log.i(TAG, "handleSetIncomingRingingVanity(): enabling vanity camera");
|
||||
camera.setVanitySink(currentState.getVideoState().requireLocalSink());
|
||||
camera.setEnabled(true);
|
||||
router.setVanitySink(currentState.getVideoState().requireLocalSink());
|
||||
router.setEnabled(true);
|
||||
} else {
|
||||
Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera");
|
||||
camera.setVanitySink(null);
|
||||
camera.setEnabled(false);
|
||||
router.setVanitySink(null);
|
||||
router.setEnabled(false);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -196,22 +196,22 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
|
||||
if (enabled && !camera.isInitialized()) {
|
||||
if (enabled && !router.isInitialized()) {
|
||||
Log.i(TAG, "handleSetIncomingRingingVanity(): initializing vanity camera");
|
||||
return WebRtcVideoUtil.initializeVanityCamera(currentState);
|
||||
} else if (enabled) {
|
||||
Log.i(TAG, "handleSetIncomingRingingVanity(): enabling vanity camera");
|
||||
camera.setEnabled(true);
|
||||
router.setEnabled(true);
|
||||
} else {
|
||||
Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera");
|
||||
camera.setEnabled(false);
|
||||
router.setEnabled(false);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
|
||||
webRtcInteractor.initializeAudioForCall(true);
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireRouter());
|
||||
groupCall.setOutgoingVideoMuted(!answerWithVideo, false);
|
||||
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
|
||||
groupCall.setDataMode(NetworkUtil.getCallingDataMode(context, groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType()));
|
||||
@@ -270,11 +270,11 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
|
||||
}
|
||||
|
||||
if (answerWithVideo) {
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
camera.setEnabled(true);
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
router.setEnabled(true);
|
||||
currentState = currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -157,7 +157,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
RingRtcDynamicConfiguration.getAudioConfig(),
|
||||
videoState.requireLocalSink(),
|
||||
callParticipant.getVideoSink(),
|
||||
videoState.requireCamera(),
|
||||
videoState.requireRouter(),
|
||||
callSetupState.getIceServers(),
|
||||
hideIp,
|
||||
NetworkUtil.getCallingDataMode(context),
|
||||
@@ -170,7 +170,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(currentState.getVideoState().requireCamera().getCameraState())
|
||||
.cameraState(currentState.getVideoState().requireRouter().getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -63,10 +63,10 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(TAG, "handleSetEnableVideo(): Changing for pre-join call.");
|
||||
|
||||
currentState.getVideoState().getCamera().setEnabled(enable);
|
||||
currentState.getVideoState().getRouter().setEnabled(enable);
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(currentState.getVideoState().getCamera().getCameraState())
|
||||
.cameraState(currentState.getVideoState().getRouter().getCameraState())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,18 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
||||
process((s, p) -> p.handleSetEnableVideo(s, enabled));
|
||||
}
|
||||
|
||||
public void startScreenShare(@NonNull android.content.Intent mediaProjectionData) {
|
||||
process((s, p) -> p.handleSetLocalScreenShare(s, true, mediaProjectionData));
|
||||
}
|
||||
|
||||
public void stopScreenShare() {
|
||||
process((s, p) -> p.handleSetLocalScreenShare(s, false, null));
|
||||
}
|
||||
|
||||
public void onScreenSharingServiceReady() {
|
||||
process((s, p) -> p.handleScreenSharingServiceReady(s));
|
||||
}
|
||||
|
||||
public void setIncomingRingingVanity(boolean enabled) {
|
||||
process((s, p) -> p.handleSetIncomingRingingVanity(s, enabled));
|
||||
}
|
||||
|
||||
+14
-4
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.CallState;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
|
||||
@@ -414,6 +414,16 @@ public abstract class WebRtcActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable android.content.Intent mediaProjectionData) {
|
||||
Log.i(tag, "handleSetLocalScreenShare not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleScreenSharingServiceReady not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleReceivedHangup(@NonNull WebRtcServiceState currentState,
|
||||
@NonNull CallMetadata callMetadata,
|
||||
@NonNull HangupMetadata hangupMetadata)
|
||||
@@ -638,9 +648,9 @@ public abstract class WebRtcActionProcessor {
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, boolean isLandscapeEnabled, int orientationDegrees) {
|
||||
Camera camera = currentState.getVideoState().getCamera();
|
||||
if (camera != null) {
|
||||
camera.setOrientation(orientationDegrees);
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter();
|
||||
if (router != null) {
|
||||
router.setOrientation(orientationDegrees);
|
||||
}
|
||||
|
||||
int sinkRotationDegrees = isLandscapeEnabled ? BroadcastVideoSink.DEVICE_ROTATION_IGNORE : orientationDegrees;
|
||||
|
||||
@@ -7,9 +7,9 @@ import androidx.annotation.NonNull;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
|
||||
import org.webrtc.CapturerObserver;
|
||||
@@ -17,8 +17,8 @@ import org.webrtc.VideoFrame;
|
||||
import org.webrtc.VideoSink;
|
||||
|
||||
/**
|
||||
* Helper for initializing, reinitializing, and deinitializing the camera and it's related
|
||||
* infrastructure.
|
||||
* Helper for initializing, reinitializing, and deinitializing the outgoing video
|
||||
* pipeline (camera + screen share, owned by the {@link OutgoingVideoSourceRouter}).
|
||||
*/
|
||||
public final class WebRtcVideoUtil {
|
||||
|
||||
@@ -32,22 +32,25 @@ public final class WebRtcVideoUtil {
|
||||
final WebRtcServiceStateBuilder builder = currentState.builder();
|
||||
|
||||
ThreadUtil.runOnMainSync(() -> {
|
||||
EglBaseWrapper eglBase = EglBaseWrapper.acquireEglBase(eglBaseHolder);
|
||||
BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase,
|
||||
true,
|
||||
false,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
Camera camera = new Camera(context, cameraEventListener, eglBase, CameraState.Direction.FRONT);
|
||||
EglBaseWrapper eglBase = EglBaseWrapper.acquireEglBase(eglBaseHolder);
|
||||
BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase,
|
||||
true,
|
||||
false,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
OutgoingVideoSourceRouter router = new OutgoingVideoSourceRouter(context,
|
||||
eglBase,
|
||||
cameraEventListener,
|
||||
CameraState.Direction.FRONT);
|
||||
|
||||
camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
router.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
|
||||
builder.changeVideoState()
|
||||
.eglBase(eglBase)
|
||||
.localSink(localSink)
|
||||
.camera(camera)
|
||||
.router(router)
|
||||
.commit()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.commit();
|
||||
});
|
||||
|
||||
@@ -61,23 +64,23 @@ public final class WebRtcVideoUtil {
|
||||
final WebRtcServiceStateBuilder builder = currentState.builder();
|
||||
|
||||
ThreadUtil.runOnMainSync(() -> {
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
camera.setCameraEventListener(null);
|
||||
camera.setEnabled(false);
|
||||
camera.dispose();
|
||||
OutgoingVideoSourceRouter oldRouter = currentState.getVideoState().requireRouter();
|
||||
oldRouter.setCameraEventListener(null);
|
||||
oldRouter.setEnabled(false);
|
||||
oldRouter.dispose();
|
||||
|
||||
camera = new Camera(context,
|
||||
cameraEventListener,
|
||||
currentState.getVideoState().getLockableEglBase(),
|
||||
currentState.getLocalDeviceState().getCameraState().getActiveDirection());
|
||||
OutgoingVideoSourceRouter router = new OutgoingVideoSourceRouter(context,
|
||||
currentState.getVideoState().getLockableEglBase(),
|
||||
cameraEventListener,
|
||||
currentState.getLocalDeviceState().getCameraState().getActiveDirection());
|
||||
|
||||
camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
router.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
|
||||
builder.changeVideoState()
|
||||
.camera(camera)
|
||||
.router(router)
|
||||
.commit()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.commit();
|
||||
});
|
||||
|
||||
@@ -85,15 +88,15 @@ public final class WebRtcVideoUtil {
|
||||
}
|
||||
|
||||
public static @NonNull WebRtcServiceState deinitializeVideo(@NonNull WebRtcServiceState currentState) {
|
||||
Camera camera = currentState.getVideoState().getCamera();
|
||||
if (camera != null) {
|
||||
camera.dispose();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter();
|
||||
if (router != null) {
|
||||
router.dispose();
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeVideoState()
|
||||
.eglBase(null)
|
||||
.camera(null)
|
||||
.router(null)
|
||||
.localSink(null)
|
||||
.commit()
|
||||
.changeLocalDeviceState()
|
||||
@@ -102,11 +105,11 @@ public final class WebRtcVideoUtil {
|
||||
}
|
||||
|
||||
public static @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) {
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
VideoSink sink = currentState.getVideoState().requireLocalSink();
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
VideoSink sink = currentState.getVideoState().requireLocalSink();
|
||||
|
||||
if (camera.hasCapturer()) {
|
||||
camera.initCapturer(new CapturerObserver() {
|
||||
if (router.hasCapturer()) {
|
||||
router.initCapturer(new CapturerObserver() {
|
||||
@Override
|
||||
public void onFrameCaptured(VideoFrame videoFrame) {
|
||||
sink.onFrame(videoFrame);
|
||||
@@ -118,12 +121,12 @@ public final class WebRtcVideoUtil {
|
||||
@Override
|
||||
public void onCapturerStopped() {}
|
||||
});
|
||||
camera.setEnabled(true);
|
||||
router.setEnabled(true);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(camera.getCameraState())
|
||||
.cameraState(router.getCameraState())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc.state
|
||||
|
||||
import android.content.Intent
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
@@ -21,7 +22,9 @@ data class LocalDeviceState(
|
||||
var isAudioDeviceChangePending: Boolean = false,
|
||||
var networkConnectionType: PeerConnection.AdapterType = PeerConnection.AdapterType.UNKNOWN,
|
||||
var handRaisedTimestamp: Long = CallParticipant.HAND_LOWERED,
|
||||
var remoteMutedBy: CallParticipant? = null
|
||||
var remoteMutedBy: CallParticipant? = null,
|
||||
var isScreenSharing: Boolean = false,
|
||||
var mediaProjectionIntent: Intent? = null
|
||||
) {
|
||||
|
||||
fun duplicate(): LocalDeviceState {
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -13,22 +13,25 @@ import java.util.Objects;
|
||||
* Local device video state and infrastructure.
|
||||
*/
|
||||
public final class VideoState {
|
||||
EglBaseWrapper eglBase;
|
||||
BroadcastVideoSink localSink;
|
||||
Camera camera;
|
||||
EglBaseWrapper eglBase;
|
||||
BroadcastVideoSink localSink;
|
||||
OutgoingVideoSourceRouter router;
|
||||
|
||||
VideoState() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
VideoState(@NonNull VideoState toCopy) {
|
||||
this(toCopy.eglBase, toCopy.localSink, toCopy.camera);
|
||||
this(toCopy.eglBase, toCopy.localSink, toCopy.router);
|
||||
}
|
||||
|
||||
VideoState(@NonNull EglBaseWrapper eglBase, @Nullable BroadcastVideoSink localSink, @Nullable Camera camera) {
|
||||
VideoState(@Nullable EglBaseWrapper eglBase,
|
||||
@Nullable BroadcastVideoSink localSink,
|
||||
@Nullable OutgoingVideoSourceRouter router)
|
||||
{
|
||||
this.eglBase = eglBase;
|
||||
this.localSink = localSink;
|
||||
this.camera = camera;
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
public @NonNull EglBaseWrapper getLockableEglBase() {
|
||||
@@ -43,11 +46,11 @@ public final class VideoState {
|
||||
return Objects.requireNonNull(localSink);
|
||||
}
|
||||
|
||||
public @Nullable Camera getCamera() {
|
||||
return camera;
|
||||
public @Nullable OutgoingVideoSourceRouter getRouter() {
|
||||
return router;
|
||||
}
|
||||
|
||||
public @NonNull Camera requireCamera() {
|
||||
return Objects.requireNonNull(camera);
|
||||
public @NonNull OutgoingVideoSourceRouter requireRouter() {
|
||||
return Objects.requireNonNull(router);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc.state;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -15,8 +17,8 @@ import org.thoughtcrime.securesms.events.GroupCallSpeechEvent;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason;
|
||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
|
||||
@@ -158,6 +160,16 @@ public class WebRtcServiceStateBuilder {
|
||||
toBuild.setMicrophoneEnabled(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull LocalDeviceStateBuilder isScreenSharing(boolean isScreenSharing) {
|
||||
toBuild.setScreenSharing(isScreenSharing);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull LocalDeviceStateBuilder setMediaProjectionIntent(@Nullable Intent mediaProjectionIntent) {
|
||||
toBuild.setMediaProjectionIntent(mediaProjectionIntent);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class CallSetupStateBuilder {
|
||||
@@ -263,8 +275,8 @@ public class WebRtcServiceStateBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull VideoStateBuilder camera(@Nullable Camera camera) {
|
||||
toBuild.camera = camera;
|
||||
public @NonNull VideoStateBuilder router(@Nullable OutgoingVideoSourceRouter router) {
|
||||
toBuild.router = router;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_light_colorOnPrimary"
|
||||
android:pathData="M4 4h16c1.1 0 2 0.9 2 2v10c0 1.1-0.9 2-2 2h-5v1h2c0.55 0 1 0.45 1 1s-0.45 1-1 1H7c-0.55 0-1-0.45-1-1s0.45-1 1-1h2v-1H4c-1.1 0-2-0.9-2-2V6c0-1.1 0.9-2 2-2ZM4 6v10h16V6H4Z"/>
|
||||
<path
|
||||
android:fillColor="@color/signal_light_colorOnPrimary"
|
||||
android:pathData="M12 8l3 3h-2v3h-2v-3H9l3-3Z"/>
|
||||
</vector>
|
||||
@@ -2705,6 +2705,10 @@
|
||||
<string name="CallOverflowPopupWindow__lower_hand">Lower</string>
|
||||
<!-- 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>
|
||||
<!-- A clickable button to stop sharing your screen in a call -->
|
||||
<string name="CallOverflowPopupWindow__stop_screen_share" translatable="false">Stop sharing (Labs)</string>
|
||||
<!-- A button to take you to a list of participants with raised hands -->
|
||||
<string name="CallOverflowPopupWindow__view">View</string>
|
||||
|
||||
@@ -2829,6 +2833,8 @@
|
||||
|
||||
<!-- CallToastPopupWindow -->
|
||||
<string name="CallToastPopupWindow__swipe_to_view_screen_share">Swipe to view screen share</string>
|
||||
<!-- Hint shown to teach the user they can swipe between the grid view and the active speaker view -->
|
||||
<string name="CallToastPopupWindow__swipe_to_view_speaker">Swipe to view speaker</string>
|
||||
|
||||
<!-- ProxyBottomSheetFragment -->
|
||||
<string name="ProxyBottomSheetFragment_proxy_server">Proxy server</string>
|
||||
|
||||
Reference in New Issue
Block a user