Improve handling of missing camera during calls.

This commit is contained in:
Miriam Zimmerman
2025-05-30 14:02:19 -04:00
committed by Cody Henthorne
parent faf0b630c1
commit 340b94f849
5 changed files with 80 additions and 7 deletions

View File

@@ -39,11 +39,14 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
private static final String TAG = Log.tag(Camera.class); private static final String TAG = Log.tag(Camera.class);
@NonNull private final Context context; @NonNull private final Context context;
@Nullable private final CameraVideoCapturer capturer; @Nullable private CameraVideoCapturer capturer;
@Nullable private CameraEventListener cameraEventListener; @Nullable private CameraEventListener cameraEventListener;
@NonNull private final EglBaseWrapper eglBase; @NonNull private final EglBaseWrapper eglBase;
private final int cameraCount; private final int cameraCount;
@NonNull private CameraState.Direction activeDirection; @NonNull private CameraState.Direction activeDirection;
private CameraState.Direction oldActiveDirection;
private CapturerObserver observer;
private boolean enabled; private boolean enabled;
private boolean isInitialized; private boolean isInitialized;
private int orientation; private int orientation;
@@ -79,6 +82,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
@Override @Override
public void initCapturer(@NonNull CapturerObserver observer) { public void initCapturer(@NonNull CapturerObserver observer) {
if (capturer != null) { if (capturer != null) {
this.observer = observer; // save in case we need to disposeAndFlipCamera
eglBase.performWithValidEglBase(base -> { eglBase.performWithValidEglBase(base -> {
capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", base.getEglBaseContext()), capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", base.getEglBaseContext()),
context, context,
@@ -99,6 +103,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
if (capturer == null || cameraCount < 2) { if (capturer == null || cameraCount < 2) {
throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + " of them."); throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + " of them.");
} }
oldActiveDirection = activeDirection;
activeDirection = PENDING; activeDirection = PENDING;
capturer.switchCamera(this); 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() { int getCount() {
return cameraCount; return cameraCount;
} }
@@ -204,7 +231,9 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
@Override @Override
public void onCameraSwitchError(String errorMessage) { public void onCameraSwitchError(String errorMessage) {
Log.e(TAG, "onCameraSwitchError: " + 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 { private static class FilteredCamera2Enumerator extends Camera2Enumerator {
@@ -314,7 +343,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
public void onCapturerStarted(boolean success) { public void onCapturerStarted(boolean success) {
observer.onCapturerStarted(success); observer.onCapturerStarted(success);
if (success && cameraEventListener != null) { if (success && cameraEventListener != null) {
cameraEventListener.onFullyInitialized(); cameraEventListener.onFullyInitialized(getCameraState());
} }
} }

View File

@@ -8,7 +8,8 @@ import androidx.annotation.NonNull;
* onCameraSwitchCompleted is triggered by {@link org.webrtc.CameraVideoCapturer.CameraSwitchHandler} * onCameraSwitchCompleted is triggered by {@link org.webrtc.CameraVideoCapturer.CameraSwitchHandler}
*/ */
public interface CameraEventListener { public interface CameraEventListener {
void onFullyInitialized(); void onFullyInitialized(@NonNull CameraState newCameraState);
void onCameraSwitchCompleted(@NonNull CameraState newCameraState); void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
void onCameraSwitchFailure(@NonNull CameraState newCameraState);
void onCameraStopped(); void onCameraStopped();
} }

View File

@@ -80,4 +80,23 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
.cameraState(newCameraState) .cameraState(newCameraState)
.build(); .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();
}
} }

View File

@@ -68,7 +68,6 @@ import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.rx.RxStore; import org.thoughtcrime.securesms.util.rx.RxStore;
@@ -988,8 +987,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
} }
@Override @Override
public void onFullyInitialized() { public void onFullyInitialized(@NonNull final CameraState newCameraState) {
process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().isLandscapeEnabled(), s.getLocalDeviceState().getDeviceOrientation().getDegrees())); process((s, p) -> {
WebRtcServiceState s1 = p.handleSetCameraDirection(s, newCameraState);
return p.handleOrientationChanged(s1, s.getLocalDeviceState().isLandscapeEnabled(), s.getLocalDeviceState().getDeviceOrientation().getDegrees());
});
} }
@Override @Override
@@ -997,6 +999,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState)); process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState));
} }
@Override
public void onCameraSwitchFailure(@NonNull final CameraState newCameraState) {
process((s, p) -> p.handleCameraSwitchFailure(s, newCameraState));
}
@Override @Override
public void onCameraStopped() { public void onCameraStopped() {
Log.i(TAG, "Camera error. Muting video."); Log.i(TAG, "Camera error. Muting video.");

View File

@@ -569,6 +569,11 @@ public abstract class WebRtcActionProcessor {
return currentState; 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) { public @NonNull WebRtcServiceState handleNetworkChanged(@NonNull WebRtcServiceState currentState, boolean available) {
Log.i(tag, "handleNetworkChanged not processed"); Log.i(tag, "handleNetworkChanged not processed");
return currentState; return currentState;
@@ -630,6 +635,18 @@ public abstract class WebRtcActionProcessor {
.build(); .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 //endregion Local device
//region End call //region End call