diff --git a/res/drawable/webrtc_camera_flip_button.xml b/res/drawable/webrtc_camera_flip_button.xml new file mode 100644 index 0000000000..e422c8295a --- /dev/null +++ b/res/drawable/webrtc_camera_flip_button.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/layout/webrtc_call_controls.xml b/res/layout/webrtc_call_controls.xml index 9e513c370b..fa0e5a06fb 100644 --- a/res/layout/webrtc_call_controls.xml +++ b/res/layout/webrtc_call_controls.xml @@ -34,6 +34,13 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 1c62de864e..ab10363c59 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -995,6 +995,7 @@ Signal Call Mute + Use rear camera Signal Call diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index 39f0833d97..9c9b0989ed 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -139,6 +139,7 @@ public class WebRtcCallActivity extends Activity { callScreen.setIncomingCallActionListener(new IncomingCallActionListener()); callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener()); callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener()); + callScreen.setCameraFlipButtonListener(new CameraFlipButtonListener()); callScreen.setSpeakerButtonListener(new SpeakerButtonListener()); callScreen.setBluetoothButtonListener(new BluetoothButtonListener()); @@ -159,6 +160,13 @@ public class WebRtcCallActivity extends Activity { startService(intent); } + private void handleSetCameraFlip(boolean isRear) { + Intent intent = new Intent(this, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_SET_CAMERA_FLIP); + intent.putExtra(WebRtcCallService.EXTRA_CAMERA_FLIP_REAR, isRear); + startService(intent); + } + private void handleAnswerCall() { WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class); @@ -348,6 +356,11 @@ public class WebRtcCallActivity extends Activity { } } + private class CameraFlipButtonListener implements WebRtcCallControls.CameraFlipButtonListener { + @Override + public void onToggle(boolean isRear) { WebRtcCallActivity.this.handleSetCameraFlip(isRear); } + } + private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener { @Override public void onSpeakerChange(boolean isSpeaker) { diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java index 6c5d07c8b7..a7d8005279 100644 --- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java +++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java @@ -27,6 +27,7 @@ public class WebRtcCallControls extends LinearLayout { private AccessibleToggleButton audioMuteButton; private AccessibleToggleButton videoMuteButton; + private AccessibleToggleButton cameraFlipButton; private AccessibleToggleButton speakerButton; private AccessibleToggleButton bluetoothButton; @@ -60,6 +61,8 @@ public class WebRtcCallControls extends LinearLayout { this.bluetoothButton = ViewUtil.findById(this, R.id.bluetoothButton); this.audioMuteButton = ViewUtil.findById(this, R.id.muteButton); this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button); + this.cameraFlipButton = ViewUtil.findById(this, R.id.camera_flip_button); + this.cameraFlipButton.setVisibility(View.INVISIBLE); // shown once video is enabled } public void setAudioMuteButtonListener(final MuteButtonListener listener) { @@ -75,7 +78,18 @@ public class WebRtcCallControls extends LinearLayout { videoMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - listener.onToggle(!isChecked); + boolean videoMuted = !isChecked; + listener.onToggle(videoMuted); + cameraFlipButton.setVisibility(videoMuted ? View.INVISIBLE : View.VISIBLE); + } + }); + } + + public void setCameraFlipButtonListener(final CameraFlipButtonListener listener) { + cameraFlipButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + listener.onToggle(isChecked); } }); } @@ -138,21 +152,25 @@ public class WebRtcCallControls extends LinearLayout { speakerButton.setAlpha(1.0f); bluetoothButton.setAlpha(1.0f); videoMuteButton.setAlpha(1.0f); + cameraFlipButton.setAlpha(1.0f); audioMuteButton.setAlpha(1.0f); speakerButton.setEnabled(true); bluetoothButton.setEnabled(true); videoMuteButton.setEnabled(true); + cameraFlipButton.setEnabled(true); audioMuteButton.setEnabled(true); } else if (!enabled && Build.VERSION.SDK_INT >= 11) { speakerButton.setAlpha(0.3f); bluetoothButton.setAlpha(0.3f); videoMuteButton.setAlpha(0.3f); + cameraFlipButton.setAlpha(0.3f); audioMuteButton.setAlpha(0.3f); speakerButton.setEnabled(false); bluetoothButton.setEnabled(false); videoMuteButton.setEnabled(false); + cameraFlipButton.setEnabled(false); audioMuteButton.setEnabled(false); } } @@ -179,6 +197,10 @@ public class WebRtcCallControls extends LinearLayout { public void onToggle(boolean isMuted); } + public static interface CameraFlipButtonListener { + public void onToggle(boolean isRear); + } + public static interface SpeakerButtonListener { public void onSpeakerChange(boolean isSpeaker); } diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java index 7aa0500cc8..30d1765630 100644 --- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java +++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java @@ -154,6 +154,10 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi this.controls.setVideoMuteButtonListener(listener); } + public void setCameraFlipButtonListener(WebRtcCallControls.CameraFlipButtonListener listener) { + this.controls.setCameraFlipButtonListener(listener); + } + public void setSpeakerButtonListener(WebRtcCallControls.SpeakerButtonListener listener) { this.controls.setSpeakerButtonListener(listener); } diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java index 2734cb586d..58aba5f928 100644 --- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -116,6 +116,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo public static final String EXTRA_REMOTE_ADDRESS = "remote_address"; public static final String EXTRA_MUTE = "mute_value"; + public static final String EXTRA_CAMERA_FLIP_REAR = "camera_flip_rear_value"; public static final String EXTRA_AVAILABLE = "enabled_value"; public static final String EXTRA_REMOTE_DESCRIPTION = "remote_description"; public static final String EXTRA_TIMESTAMP = "timestamp"; @@ -132,6 +133,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"; public static final String ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO"; public static final String ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"; + public static final String ACTION_SET_CAMERA_FLIP = "SET_CAMERA_FLIP"; public static final String ACTION_BLUETOOTH_CHANGE = "BLUETOOTH_CHANGE"; public static final String ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"; public static final String ACTION_SCREEN_OFF = "SCREEN_OFF"; @@ -208,6 +210,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo else if (intent.getAction().equals(ACTION_REMOTE_HANGUP)) handleRemoteHangup(intent); else if (intent.getAction().equals(ACTION_SET_MUTE_AUDIO)) handleSetMuteAudio(intent); else if (intent.getAction().equals(ACTION_SET_MUTE_VIDEO)) handleSetMuteVideo(intent); + else if (intent.getAction().equals(ACTION_SET_CAMERA_FLIP)) handleSetCameraFlip(intent); else if (intent.getAction().equals(ACTION_BLUETOOTH_CHANGE)) handleBluetoothChange(intent); else if (intent.getAction().equals(ACTION_WIRED_HEADSET_CHANGE)) handleWiredHeadsetChange(intent); else if (intent.getAction().equals((ACTION_SCREEN_OFF))) handleScreenOffChange(intent); @@ -804,6 +807,17 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo sendMessage(viewModelStateFor(callState), this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } + private void handleSetCameraFlip(Intent intent) { + boolean isRear = intent.getBooleanExtra(EXTRA_CAMERA_FLIP_REAR, false); + Log.w(TAG, "handleSetCameraFlip(isRear=" + isRear + ")..."); + + if (this.localVideoEnabled) { + if (this.peerConnection != null) { + this.peerConnection.flipCameras(isRear); + } + } + } + private void handleBluetoothChange(Intent intent) { this.bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false); diff --git a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java index 1b65b531d0..0e63b28c1d 100644 --- a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java +++ b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java @@ -40,8 +40,16 @@ public class PeerConnectionWrapper { @NonNull private final AudioSource audioSource; @Nullable private final VideoCapturer videoCapturer; + @Nullable private final VideoCapturer videoCapturerRear; @Nullable private final VideoSource videoSource; + @Nullable private final VideoSource videoSourceRear; @Nullable private final VideoTrack videoTrack; + @Nullable private final VideoTrack videoTrackRear; + + @Nullable private VideoCapturer videoCapturerActive; + @Nullable private VideoTrack videoTrackActive; + + @Nullable private final MediaStream mediaStream; public PeerConnectionWrapper(@NonNull Context context, @NonNull PeerConnectionFactory factory, @@ -72,44 +80,71 @@ public class PeerConnectionWrapper { this.peerConnection.setAudioPlayout(false); this.peerConnection.setAudioRecording(false); - this.videoCapturer = createVideoCapturer(context); + this.videoCapturer = createVideoCapturer(context, false); + this.videoCapturerRear = createVideoCapturer(context, true); - MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS"); + this.videoCapturerActive = videoCapturer; + + this.mediaStream = factory.createLocalMediaStream("ARDAMS"); this.audioSource = factory.createAudioSource(audioConstraints); this.audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource); this.audioTrack.setEnabled(false); - mediaStream.addTrack(audioTrack); + this.mediaStream.addTrack(audioTrack); if (videoCapturer != null) { this.videoSource = factory.createVideoSource(videoCapturer); this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource); + this.videoTrackActive = videoTrack; + this.videoTrack.addRenderer(new VideoRenderer(localRenderer)); this.videoTrack.setEnabled(false); - mediaStream.addTrack(videoTrack); + this.mediaStream.addTrack(videoTrack); } else { this.videoSource = null; this.videoTrack = null; + this.videoTrackActive = null; + } + + if (videoCapturerRear != null) { + this.videoSourceRear = factory.createVideoSource(videoCapturerRear); + this.videoTrackRear = factory.createVideoTrack("ARDAMSv0", videoSourceRear); + this.videoTrackRear.addRenderer(new VideoRenderer(localRenderer)); + this.videoTrackRear.setEnabled(false); + } else { + this.videoSourceRear = null; + this.videoTrackRear = null; } this.peerConnection.addStream(mediaStream); } public void setVideoEnabled(boolean enabled) { - if (this.videoTrack != null) { - this.videoTrack.setEnabled(enabled); + if (this.videoTrackActive != null) { + this.videoTrackActive.setEnabled(enabled); } - if (this.videoCapturer != null) { + if (this.videoCapturerActive != null) { try { - if (enabled) this.videoCapturer.startCapture(1280, 720, 30); - else this.videoCapturer.stopCapture(); + if (enabled) this.videoCapturerActive.startCapture(1280, 720, 30); + else this.videoCapturerActive.stopCapture(); } catch (InterruptedException e) { Log.w(TAG, e); } } } + public void flipCameras(boolean isRear) { + if (videoCapturerRear != null) { + setVideoEnabled(false); + mediaStream.removeTrack(videoTrackActive); + this.videoTrackActive = isRear ? videoTrackRear : videoTrack; + this.videoCapturerActive = isRear ? videoCapturerRear : videoCapturer; + mediaStream.addTrack(videoTrackActive); + setVideoEnabled(true); + } + } + public void setCommunicationMode() { this.peerConnection.setAudioPlayout(true); this.peerConnection.setAudioRecording(true); @@ -268,10 +303,23 @@ public class PeerConnectionWrapper { this.videoCapturer.dispose(); } + if (this.videoCapturerRear != null) { + try { + this.videoCapturerRear.stopCapture(); + } catch (InterruptedException e) { + Log.w(TAG, e); + } + this.videoCapturerRear.dispose(); + } + if (this.videoSource != null) { this.videoSource.dispose(); } + if (this.videoSourceRear != null) { + this.videoSourceRear.dispose(); + } + this.audioSource.dispose(); this.peerConnection.close(); this.peerConnection.dispose(); @@ -281,7 +329,7 @@ public class PeerConnectionWrapper { return this.peerConnection.addIceCandidate(candidate); } - private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull Context context) { + private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull Context context, boolean rear) { boolean camera2EnumeratorIsSupported = false; try { camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context); @@ -298,12 +346,16 @@ public class PeerConnectionWrapper { String[] deviceNames = enumerator.getDeviceNames(); for (String deviceName : deviceNames) { - if (enumerator.isFrontFacing(deviceName)) { - Log.w(TAG, "Creating front facing camera capturer."); + boolean isDesiredDirection = + rear ? enumerator.isBackFacing(deviceName) + : enumerator.isFrontFacing(deviceName); + if (isDesiredDirection) { + String direction = rear ? "rear" : "front"; + Log.w(TAG, "Creating " + direction + " facing camera capturer."); final CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); if (videoCapturer != null) { - Log.w(TAG, "Found front facing capturer: " + deviceName); + Log.w(TAG, "Found " + direction + " facing capturer: " + deviceName); return videoCapturer; } @@ -311,7 +363,10 @@ public class PeerConnectionWrapper { } for (String deviceName : deviceNames) { - if (!enumerator.isFrontFacing(deviceName)) { + boolean isDesiredDirection = + rear ? enumerator.isBackFacing(deviceName) + : enumerator.isFrontFacing(deviceName); + if (!isDesiredDirection) { Log.w(TAG, "Creating other camera capturer."); final CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);