mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Merge pull request #4333 from deltachat/wch423/call-mirror-self
Mirror self video during call
This commit is contained in:
@@ -504,6 +504,8 @@ public class CallActivity extends AppCompatActivity {
|
||||
viewModel.getVideoEnabled(), v -> videoConfigChanged.setValue(true));
|
||||
videoConfigChanged.addSource(
|
||||
viewModel.getRemoteVideoEnabled(), v -> videoConfigChanged.setValue(true));
|
||||
videoConfigChanged.addSource(
|
||||
viewModel.getIsFrontCamera(), v -> videoConfigChanged.setValue(true));
|
||||
|
||||
// Video layout
|
||||
videoConfigChanged.observe(
|
||||
@@ -705,17 +707,21 @@ public class CallActivity extends AppCompatActivity {
|
||||
VideoTrack localTrack = viewModel.getLocalVideoTrack().getValue();
|
||||
VideoTrack remoteTrack = viewModel.getRemoteVideoTrack().getValue();
|
||||
|
||||
boolean isFront = Boolean.TRUE.equals(viewModel.getIsFrontCamera().getValue());
|
||||
|
||||
boolean showFullScreen = false;
|
||||
|
||||
if (state == CallViewModel.CallState.CONNECTED
|
||||
&& remoteTrack != null
|
||||
&& Boolean.TRUE.equals(remoteVideoEnabled)) {
|
||||
remoteVideoView.setMirror(false);
|
||||
remoteTrack.addSink(remoteVideoView);
|
||||
showFullScreen = true;
|
||||
} else if (!coordinator.isIncomingCall()
|
||||
&& (state == CallViewModel.CallState.RINGING || state == CallViewModel.CallState.CONNECTING)
|
||||
&& localTrack != null
|
||||
&& Boolean.TRUE.equals(videoEnabled)) {
|
||||
remoteVideoView.setMirror(isFront);
|
||||
localTrack.addSink(remoteVideoView);
|
||||
showFullScreen = true;
|
||||
}
|
||||
@@ -729,6 +735,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
&& !isInPictureInPictureMode();
|
||||
|
||||
if (showCorner) {
|
||||
localVideoView.setMirror(isFront);
|
||||
localTrack.addSink(localVideoView);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
private final MutableLiveData<String> displayName = new MutableLiveData<>();
|
||||
private final MutableLiveData<Icon> displayIcon = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> outgoingCallPlaced = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> isFrontCamera = new MutableLiveData<>(true);
|
||||
|
||||
// Audio Routing Support
|
||||
private final MediatorLiveData<CallEndpointCompat> currentAudioEndpoint =
|
||||
@@ -325,6 +326,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return availableAudioEndpoints;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
// State Update Methods (CallService)
|
||||
|
||||
public void updateConnectionState(PeerConnection.PeerConnectionState state) {
|
||||
@@ -359,6 +364,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
isRelayUsed.postValue(isRelay);
|
||||
}
|
||||
|
||||
public void updateFrontCamera(boolean front) {
|
||||
Log.d(TAG, "updateFrontCamera: " + front);
|
||||
isFrontCamera.postValue(front);
|
||||
}
|
||||
|
||||
public void reportError(String error) {
|
||||
Log.e(TAG, "reportError: " + error);
|
||||
errorMessage.postValue(error);
|
||||
@@ -366,7 +376,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
// Delayed Media Initialization Support
|
||||
|
||||
public void startMediaCapture() {
|
||||
public synchronized void startMediaCapture() {
|
||||
Log.d(TAG, "startMediaCapture");
|
||||
if (callService != null) {
|
||||
callService.startMediaCapture();
|
||||
@@ -375,7 +385,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCallControlScopeAnswer() {
|
||||
public synchronized void handleCallControlScopeAnswer() {
|
||||
Log.d(TAG, "handleCallControlScopeAnswer");
|
||||
|
||||
if (!isIncomingCall) {
|
||||
@@ -628,7 +638,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
cleanupCall(activeAccId, activeCallId);
|
||||
}
|
||||
|
||||
public void setAudioEnabled(boolean enabled) {
|
||||
public synchronized void setAudioEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setAudioEnabled: " + enabled);
|
||||
|
||||
localAudioEnabled.postValue(enabled);
|
||||
@@ -640,7 +650,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void setVideoEnabled(boolean enabled) {
|
||||
public synchronized void setVideoEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setVideoEnabled: " + enabled);
|
||||
|
||||
localVideoEnabled.postValue(enabled);
|
||||
@@ -652,14 +662,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
public synchronized void switchCamera() {
|
||||
Log.d(TAG, "switchCamera");
|
||||
if (callService != null) {
|
||||
callService.switchCamera();
|
||||
}
|
||||
}
|
||||
|
||||
public void startOutgoingCall() {
|
||||
public synchronized void startOutgoingCall() {
|
||||
Log.d(TAG, "startOutgoingCall");
|
||||
if (callService != null) {
|
||||
callService.startOutgoingCall();
|
||||
|
||||
@@ -164,6 +164,8 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
|
||||
webRTCClient.setLocalMediaStream(stream);
|
||||
|
||||
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
|
||||
|
||||
callCoordinator.setVideoEnabled(startsWithVideo);
|
||||
|
||||
if (!stream.videoTracks.isEmpty()) {
|
||||
@@ -410,7 +412,18 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
Log.d(TAG, "switchCamera");
|
||||
|
||||
if (mediaStreamManager != null) {
|
||||
mediaStreamManager.switchCamera();
|
||||
mediaStreamManager.switchCamera(
|
||||
new MediaStreamManager.CameraSwitchCallback() {
|
||||
@Override
|
||||
public void onCameraSwitch(boolean isFrontCamera) {
|
||||
callCoordinator.updateFrontCamera(isFrontCamera);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Camera switch failed: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
@@ -38,6 +39,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
private final LiveData<Boolean> outgoingCallPlaced;
|
||||
private final LiveData<CallEndpointCompat> currentAudioEndpoint;
|
||||
private final LiveData<List<CallEndpointCompat>> availableAudioEndpoints;
|
||||
private final LiveData<Boolean> isFrontCamera;
|
||||
|
||||
// Translated from coordinator's connectionState
|
||||
private final MediatorLiveData<CallState> callState;
|
||||
@@ -46,7 +48,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
private Observer<VideoTrack> answerCallObserver;
|
||||
private Observer<VideoTrack> startOutgoingCallObserver;
|
||||
|
||||
private boolean hasCallEnded = false;
|
||||
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
|
||||
|
||||
// User-facing call states
|
||||
public enum CallState {
|
||||
@@ -79,6 +81,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
this.outgoingCallPlaced = callCoordinator.getOutgoingCallPlaced();
|
||||
this.currentAudioEndpoint = callCoordinator.getCurrentAudioEndpoint();
|
||||
this.availableAudioEndpoints = callCoordinator.getAvailableAudioEndpoints();
|
||||
this.isFrontCamera = callCoordinator.getIsFrontCamera();
|
||||
|
||||
this.callState = new MediatorLiveData<>(CallState.INITIALIZING);
|
||||
|
||||
@@ -113,9 +116,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
|
||||
if (state == PeerConnection.PeerConnectionState.FAILED
|
||||
|| state == PeerConnection.PeerConnectionState.CLOSED) {
|
||||
if (!hasCallEnded) {
|
||||
hasCallEnded = true;
|
||||
}
|
||||
hasCallEnded.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -257,11 +258,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void declineCall() {
|
||||
Log.d(TAG, "declineCall");
|
||||
|
||||
if (hasCallEnded) {
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
hasCallEnded = true;
|
||||
|
||||
callCoordinator.declineCall();
|
||||
}
|
||||
@@ -269,11 +269,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void hangUp() {
|
||||
Log.d(TAG, "hangUp");
|
||||
|
||||
if (hasCallEnded) {
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
hasCallEnded = true;
|
||||
|
||||
callCoordinator.hangUp();
|
||||
}
|
||||
@@ -354,8 +353,7 @@ public class CallViewModel extends AndroidViewModel {
|
||||
public void onCallDisconnected(DisconnectCause disconnectCause) {
|
||||
Log.d(TAG, "onCallDisconnected callback from CallControlScope, cause: " + disconnectCause);
|
||||
|
||||
if (!hasCallEnded) {
|
||||
hasCallEnded = true;
|
||||
if (hasCallEnded.compareAndSet(false, true)) {
|
||||
callState.postValue(CallState.ENDED);
|
||||
}
|
||||
}
|
||||
@@ -414,6 +412,10 @@ public class CallViewModel extends AndroidViewModel {
|
||||
return availableAudioEndpoints;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
// Notification Action Handlers
|
||||
|
||||
public void handleNotificationAnswer() {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import org.thoughtcrime.securesms.EglUtils;
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
@@ -31,6 +33,7 @@ public class MediaStreamManager {
|
||||
private VideoSource videoSource;
|
||||
private AudioSource audioSource;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private volatile boolean isFrontCamera = true;
|
||||
|
||||
public interface Callback {
|
||||
void onMediaStreamReady(MediaStream stream);
|
||||
@@ -38,6 +41,12 @@ public class MediaStreamManager {
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public interface CameraSwitchCallback {
|
||||
void onCameraSwitch(boolean isFrontCamera);
|
||||
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public MediaStreamManager(@NonNull Context context, PeerConnectionFactory peerConnectionFactory) {
|
||||
this.context = context.getApplicationContext();
|
||||
|
||||
@@ -45,6 +54,7 @@ public class MediaStreamManager {
|
||||
}
|
||||
|
||||
/** Create media stream with audio and optionally video */
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void createMediaStream(Callback callback) {
|
||||
try {
|
||||
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
|
||||
@@ -91,6 +101,7 @@ public class MediaStreamManager {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = true;
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +111,7 @@ public class MediaStreamManager {
|
||||
for (String deviceName : deviceNames) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = enumerator.isFrontFacing(deviceName);
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -107,12 +119,61 @@ public class MediaStreamManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
if (videoCapturer instanceof CameraVideoCapturer) {
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
cameraVideoCapturer.switchCamera(null);
|
||||
Log.d(TAG, "Camera switched");
|
||||
public void switchCamera(@Nullable CameraSwitchCallback callback) {
|
||||
if (!(videoCapturer instanceof CameraVideoCapturer)) {
|
||||
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
|
||||
return;
|
||||
}
|
||||
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
|
||||
// Find the opposite-facing camera
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
String targetCameraName = null;
|
||||
for (String deviceName : deviceNames) {
|
||||
boolean isTargetFront = !isFrontCamera;
|
||||
boolean deviceIsFront = enumerator.isFrontFacing(deviceName);
|
||||
|
||||
if (deviceIsFront == isTargetFront) {
|
||||
targetCameraName = deviceName;
|
||||
break; // Take the first match
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCameraName == null) {
|
||||
Log.e(TAG, "No camera found with opposite facing direction");
|
||||
if (callback != null) {
|
||||
callback.onError("No opposite camera available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalTargetCameraName = targetCameraName;
|
||||
Log.d(TAG, "Switching to camera: " + finalTargetCameraName);
|
||||
|
||||
// Call with explicit camera name
|
||||
cameraVideoCapturer.switchCamera(
|
||||
new CameraVideoCapturer.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFront) {
|
||||
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
|
||||
isFrontCamera = isFront;
|
||||
if (callback != null) callback.onCameraSwitch(isFront);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSwitchError(String errorDescription) {
|
||||
Log.e(TAG, "switchCamera FAILED: " + errorDescription);
|
||||
if (callback != null) callback.onError(errorDescription);
|
||||
}
|
||||
},
|
||||
finalTargetCameraName);
|
||||
}
|
||||
|
||||
public boolean isFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
/** Cleanup resources */
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -57,34 +58,34 @@ public class WebRTCClient {
|
||||
private final Context context;
|
||||
private final Handler mainHandler;
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
private List<PeerConnection.IceServer> iceServers;
|
||||
private volatile PeerConnection peerConnection;
|
||||
private volatile List<PeerConnection.IceServer> iceServers;
|
||||
|
||||
private DataChannel iceTricklingDataChannel;
|
||||
private DataChannel mutedStateDataChannel;
|
||||
|
||||
// ICE candidate
|
||||
private final List<IceCandidate> iceCandidateBuffer;
|
||||
private boolean iceTricklingChannelOpen;
|
||||
private boolean mutedStateChannelOpen;
|
||||
private boolean enableIceTrickling;
|
||||
private boolean isEnded = false;
|
||||
private volatile boolean iceTricklingChannelOpen;
|
||||
private volatile boolean mutedStateChannelOpen;
|
||||
private volatile boolean enableIceTrickling;
|
||||
private final AtomicBoolean isEnded = new AtomicBoolean(false);
|
||||
|
||||
// ICE gathering
|
||||
private volatile boolean isIceGatheringComplete;
|
||||
private volatile boolean hasRelayCandidate;
|
||||
private volatile boolean hasSrflxCandidate;
|
||||
private volatile boolean hasHostCandidate;
|
||||
private CountDownLatch iceGatheringLatch;
|
||||
private CountDownLatch relayCandidateLatch;
|
||||
private CountDownLatch srflxCandidateLatch;
|
||||
private volatile CountDownLatch iceGatheringLatch;
|
||||
private volatile CountDownLatch relayCandidateLatch;
|
||||
private volatile CountDownLatch srflxCandidateLatch;
|
||||
|
||||
// Media
|
||||
private MediaStream localStream;
|
||||
private VideoTrack localVideoTrack;
|
||||
private AudioTrack localAudioTrack;
|
||||
private VideoTrack remoteVideoTrack;
|
||||
private AudioTrack remoteAudioTrack;
|
||||
private volatile VideoTrack localVideoTrack;
|
||||
private volatile AudioTrack localAudioTrack;
|
||||
private volatile VideoTrack remoteVideoTrack;
|
||||
private volatile AudioTrack remoteAudioTrack;
|
||||
|
||||
// Callbacks to ViewModel
|
||||
private final Callbacks callbacks;
|
||||
@@ -644,7 +645,7 @@ public class WebRTCClient {
|
||||
boolean gotIce = waitForEnoughIce();
|
||||
|
||||
synchronized (WebRTCClient.this) {
|
||||
if (isEnded || peerConnection == null) {
|
||||
if (isEnded.get() || peerConnection == null) {
|
||||
Log.d(TAG, "Call ended during ICE gathering, aborting");
|
||||
return;
|
||||
}
|
||||
@@ -803,7 +804,7 @@ public class WebRTCClient {
|
||||
boolean gotIce = waitForEnoughIce();
|
||||
|
||||
synchronized (WebRTCClient.this) {
|
||||
if (isEnded || peerConnection == null) {
|
||||
if (isEnded.get() || peerConnection == null) {
|
||||
Log.d(TAG, "Call ended during ICE gathering, aborting");
|
||||
return;
|
||||
}
|
||||
@@ -939,12 +940,10 @@ public class WebRTCClient {
|
||||
// Cleanup
|
||||
|
||||
public synchronized void endCall() {
|
||||
if (isEnded) {
|
||||
if (!isEnded.compareAndSet(false, true)) {
|
||||
Log.d(TAG, "endCall() already called, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isEnded = true;
|
||||
Log.d(TAG, "Ending call");
|
||||
|
||||
// Unblock any thread waiting in waitForEnoughIce()
|
||||
|
||||
Reference in New Issue
Block a user