From 340b94f84989d1e69fe38a140fd92b7dfdcdf564 Mon Sep 17 00:00:00 2001 From: Miriam Zimmerman Date: Fri, 30 May 2025 14:02:19 -0400 Subject: [PATCH] Improve handling of missing camera during calls. --- .../securesms/ringrtc/Camera.java | 35 +++++++++++++++++-- .../ringrtc/CameraEventListener.java | 3 +- .../webrtc/DeviceAwareActionProcessor.java | 19 ++++++++++ .../service/webrtc/SignalCallManager.java | 13 +++++-- .../service/webrtc/WebRtcActionProcessor.java | 17 +++++++++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index 278d21d2e5..1c58fe9551 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -39,11 +39,14 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa private static final String TAG = Log.tag(Camera.class); @NonNull private final Context context; - @Nullable private final CameraVideoCapturer capturer; + @Nullable private CameraVideoCapturer capturer; @Nullable private CameraEventListener cameraEventListener; @NonNull private final EglBaseWrapper eglBase; private final int cameraCount; @NonNull private CameraState.Direction activeDirection; + private CameraState.Direction oldActiveDirection; + private CapturerObserver observer; + private boolean enabled; private boolean isInitialized; private int orientation; @@ -79,6 +82,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @Override public void initCapturer(@NonNull CapturerObserver observer) { if (capturer != null) { + this.observer = observer; // save in case we need to disposeAndFlipCamera eglBase.performWithValidEglBase(base -> { capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", base.getEglBaseContext()), context, @@ -99,6 +103,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa if (capturer == null || cameraCount < 2) { throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + " of them."); } + oldActiveDirection = activeDirection; activeDirection = PENDING; capturer.switchCamera(this); } @@ -146,6 +151,28 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa } } + public void disposeAndFlipCamera() { + if (capturer != null) { + capturer.dispose(); + boolean wasInitialized = isInitialized; + isInitialized = false; + CameraState.Direction candidateDirection = oldActiveDirection.switchDirection(); + CameraVideoCapturer captureCandidate = createVideoCapturer(getCameraEnumerator(context), candidateDirection); + if (captureCandidate != null) { + capturer = captureCandidate; + activeDirection = candidateDirection; + if (wasInitialized) { + initCapturer(this.observer); + } + if (enabled) { + setEnabled(true); + } + } else { + activeDirection = NONE; + } + } + } + int getCount() { return cameraCount; } @@ -204,7 +231,9 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @Override public void onCameraSwitchError(String errorMessage) { Log.e(TAG, "onCameraSwitchError: " + errorMessage); - if (cameraEventListener != null) cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount())); + if (cameraEventListener != null) { + cameraEventListener.onCameraSwitchFailure(new CameraState(getActiveDirection(), getCount())); + } } private static class FilteredCamera2Enumerator extends Camera2Enumerator { @@ -314,7 +343,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa public void onCapturerStarted(boolean success) { observer.onCapturerStarted(success); if (success && cameraEventListener != null) { - cameraEventListener.onFullyInitialized(); + cameraEventListener.onFullyInitialized(getCameraState()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java index 441040d4f6..9ae3946350 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CameraEventListener.java @@ -8,7 +8,8 @@ import androidx.annotation.NonNull; * onCameraSwitchCompleted is triggered by {@link org.webrtc.CameraVideoCapturer.CameraSwitchHandler} */ public interface CameraEventListener { - void onFullyInitialized(); + void onFullyInitialized(@NonNull CameraState newCameraState); void onCameraSwitchCompleted(@NonNull CameraState newCameraState); + void onCameraSwitchFailure(@NonNull CameraState newCameraState); void onCameraStopped(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java index c02845fb75..8b0e60558c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java @@ -80,4 +80,23 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor { .cameraState(newCameraState) .build(); } + + @Override + public @NonNull WebRtcServiceState handleCameraSwitchFailure(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) { + Log.i(tag, "handleCameraSwitchFailure():"); + + BroadcastVideoSink localSink = currentState.getVideoState().getLocalSink(); + if (localSink != null) { + localSink.setRotateToRightSide(false); + } + if (currentState.getVideoState().getCamera() != null) { + // Retry by recreating with the opposite preferred camera + currentState.getVideoState().getCamera().disposeAndFlipCamera(); + } + + return currentState.builder() + .changeLocalDeviceState() + .cameraState(newCameraState) + .build(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index f0a90c3389..890885def9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -68,7 +68,6 @@ import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.RecipientAccessList; -import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.rx.RxStore; @@ -988,8 +987,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. } @Override - public void onFullyInitialized() { - process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().isLandscapeEnabled(), s.getLocalDeviceState().getDeviceOrientation().getDegrees())); + public void onFullyInitialized(@NonNull final CameraState newCameraState) { + process((s, p) -> { + WebRtcServiceState s1 = p.handleSetCameraDirection(s, newCameraState); + return p.handleOrientationChanged(s1, s.getLocalDeviceState().isLandscapeEnabled(), s.getLocalDeviceState().getDeviceOrientation().getDegrees()); + }); } @Override @@ -997,6 +999,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState)); } + @Override + public void onCameraSwitchFailure(@NonNull final CameraState newCameraState) { + process((s, p) -> p.handleCameraSwitchFailure(s, newCameraState)); + } + @Override public void onCameraStopped() { Log.i(TAG, "Camera error. Muting video."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 8087459588..7805e28017 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -569,6 +569,11 @@ public abstract class WebRtcActionProcessor { return currentState; } + public @NonNull WebRtcServiceState handleCameraSwitchFailure(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) { + Log.i(tag, "handleCameraSwitchFailure not processed"); + return currentState; + } + public @NonNull WebRtcServiceState handleNetworkChanged(@NonNull WebRtcServiceState currentState, boolean available) { Log.i(tag, "handleNetworkChanged not processed"); return currentState; @@ -630,6 +635,18 @@ public abstract class WebRtcActionProcessor { .build(); } + protected @NonNull WebRtcServiceState handleSetCameraDirection(@NonNull WebRtcServiceState currentState, CameraState state) { + BroadcastVideoSink sink = currentState.getVideoState().getLocalSink(); + if (sink != null) { + sink.setRotateToRightSide(state.getActiveDirection() == CameraState.Direction.BACK); + } + + return currentState.builder() + .changeLocalDeviceState() + .cameraState(state) + .build(); + } + //endregion Local device //region End call