diff --git a/res/drawable/webrtc_camera_front_button.xml b/res/drawable/webrtc_camera_front_button.xml new file mode 100644 index 000000000..cda55648d --- /dev/null +++ b/res/drawable/webrtc_camera_front_button.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/res/drawable/webrtc_camera_flip_button.xml b/res/drawable/webrtc_camera_rear_button.xml similarity index 78% rename from res/drawable/webrtc_camera_flip_button.xml rename to res/drawable/webrtc_camera_rear_button.xml index e422c8295..697a1b4e5 100644 --- a/res/drawable/webrtc_camera_flip_button.xml +++ b/res/drawable/webrtc_camera_rear_button.xml @@ -1,6 +1,5 @@ - + android:background="@drawable/webrtc_camera_rear_button" + android:visibility="gone"/> diff --git a/res/values/strings.xml b/res/values/strings.xml index ab10363c5..c298f1454 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -995,7 +995,7 @@ Signal Call Mute - Use rear camera + Switch Cameras Signal Call diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index 9c9b0989e..096b0ff4c 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.webrtc.CameraState; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.SignalProtocolAddress; @@ -160,10 +161,9 @@ public class WebRtcCallActivity extends Activity { startService(intent); } - private void handleSetCameraFlip(boolean isRear) { + private void handleFlipCamera() { Intent intent = new Intent(this, WebRtcCallService.class); - intent.setAction(WebRtcCallService.ACTION_SET_CAMERA_FLIP); - intent.putExtra(WebRtcCallService.EXTRA_CAMERA_FLIP_REAR, isRear); + intent.setAction(WebRtcCallService.ACTION_FLIP_CAMERA); startService(intent); } @@ -330,10 +330,10 @@ public class WebRtcCallActivity extends Activity { case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break; } - callScreen.setLocalVideoEnabled(event.isLocalVideoEnabled()); callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled()); callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled()); callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING); + callScreen.setLocalVideoState(event.getLocalCameraState()); } private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener { @@ -358,7 +358,9 @@ public class WebRtcCallActivity extends Activity { private class CameraFlipButtonListener implements WebRtcCallControls.CameraFlipButtonListener { @Override - public void onToggle(boolean isRear) { WebRtcCallActivity.this.handleSetCameraFlip(isRear); } + public void onToggle() { + WebRtcCallActivity.this.handleFlipCamera(); + } } private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener { diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java index a7d800527..7b091662b 100644 --- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java +++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallControls.java @@ -5,8 +5,8 @@ import android.annotation.TargetApi; import android.content.Context; import android.media.AudioManager; import android.os.Build; +import android.support.annotation.NonNull; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AccessibleToggleButton; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.webrtc.CameraState; public class WebRtcCallControls extends LinearLayout { @@ -27,9 +28,10 @@ public class WebRtcCallControls extends LinearLayout { private AccessibleToggleButton audioMuteButton; private AccessibleToggleButton videoMuteButton; - private AccessibleToggleButton cameraFlipButton; private AccessibleToggleButton speakerButton; private AccessibleToggleButton bluetoothButton; + private AccessibleToggleButton cameraFlipButton; + private boolean cameraFlipAvailable; @TargetApi(Build.VERSION_CODES.LOLLIPOP) public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -62,7 +64,6 @@ public class WebRtcCallControls extends LinearLayout { 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) { @@ -80,7 +81,7 @@ public class WebRtcCallControls extends LinearLayout { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { boolean videoMuted = !isChecked; listener.onToggle(videoMuted); - cameraFlipButton.setVisibility(videoMuted ? View.INVISIBLE : View.VISIBLE); + cameraFlipButton.setVisibility(!videoMuted && cameraFlipAvailable ? View.VISIBLE : View.GONE); } }); } @@ -89,7 +90,10 @@ public class WebRtcCallControls extends LinearLayout { cameraFlipButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - listener.onToggle(isChecked); + listener.onToggle(); + cameraFlipButton.setBackgroundResource(isChecked ? R.drawable.webrtc_camera_front_button + : R.drawable.webrtc_camera_rear_button); + cameraFlipButton.setEnabled(false); } }); } @@ -143,40 +147,46 @@ public class WebRtcCallControls extends LinearLayout { videoMuteButton.setChecked(enabled, false); } + public void setVideoAvailable(boolean available) { + videoMuteButton.setVisibility(available ? VISIBLE : GONE); + } + + public void setCameraFlipButtonEnabled(boolean enabled) { + cameraFlipButton.setChecked(enabled, false); + } + + public void setCameraFlipAvailable(boolean available) { + cameraFlipAvailable = available; + } + + public void setCameraFlipClickable(boolean clickable) { + setControlEnabled(cameraFlipButton, clickable); + } + public void setMicrophoneEnabled(boolean enabled) { audioMuteButton.setChecked(!enabled, false); } public void setControlsEnabled(boolean enabled) { - if (enabled && Build.VERSION.SDK_INT >= 11) { - speakerButton.setAlpha(1.0f); - bluetoothButton.setAlpha(1.0f); - videoMuteButton.setAlpha(1.0f); - cameraFlipButton.setAlpha(1.0f); - audioMuteButton.setAlpha(1.0f); + setControlEnabled(speakerButton, enabled); + setControlEnabled(bluetoothButton, enabled); + setControlEnabled(videoMuteButton, enabled); + setControlEnabled(cameraFlipButton, enabled); + setControlEnabled(audioMuteButton, enabled); + } - 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); + private void setControlEnabled(@NonNull View view, boolean enabled) { + if (enabled) { + view.setAlpha(1.0f); + view.setEnabled(true); + } else { + view.setAlpha(0.3f); + view.setEnabled(false); } } public void displayVideoTooltip(ViewGroup viewGroup) { - if (Build.VERSION.SDK_INT > 15) { + if (Build.VERSION.SDK_INT > 15 && videoMuteButton.getVisibility() == VISIBLE) { final ToolTipsManager toolTipsManager = new ToolTipsManager(); ToolTip toolTip = new ToolTip.Builder(getContext(), videoMuteButton, viewGroup, @@ -184,12 +194,7 @@ public class WebRtcCallControls extends LinearLayout { ToolTip.POSITION_BELOW).build(); toolTipsManager.show(toolTip); - videoMuteButton.postDelayed(new Runnable() { - @Override - public void run() { - toolTipsManager.findAndDismiss(videoMuteButton); - } - }, 4000); + videoMuteButton.postDelayed(() -> toolTipsManager.findAndDismiss(videoMuteButton), 4000); } } @@ -198,7 +203,7 @@ public class WebRtcCallControls extends LinearLayout { } public static interface CameraFlipButtonListener { - public void onToggle(boolean isRear); + public void onToggle(); } public static interface SpeakerButtonListener { diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java index 30d176563..7db63b4a8 100644 --- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java +++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java @@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VerifySpan; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.webrtc.CameraState; import org.webrtc.SurfaceViewRenderer; import org.whispersystems.libsignal.IdentityKey; @@ -62,6 +63,7 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi private static final String TAG = WebRtcCallScreen.class.getSimpleName(); private ImageView photo; + private SurfaceViewRenderer localRenderer; private PercentFrameLayout localRenderLayout; private PercentFrameLayout remoteRenderLayout; private TextView name; @@ -187,14 +189,19 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi this.controls.setControlsEnabled(enabled); } - public void setLocalVideoEnabled(boolean enabled) { - if (enabled && this.localRenderLayout.isHidden()) { - this.controls.setVideoEnabled(true); - this.localRenderLayout.setHidden(false); - this.localRenderLayout.requestLayout(); - } else if (!enabled && !this.localRenderLayout.isHidden()){ - this.controls.setVideoEnabled(false); - this.localRenderLayout.setHidden(true); + public void setLocalVideoState(@NonNull CameraState cameraState) { + this.controls.setVideoAvailable(cameraState.getCameraCount() > 0); + this.controls.setVideoEnabled(cameraState.isEnabled()); + this.controls.setCameraFlipAvailable(cameraState.getCameraCount() > 1); + this.controls.setCameraFlipClickable(cameraState.getActiveDirection() != CameraState.Direction.PENDING); + this.controls.setCameraFlipButtonEnabled(cameraState.getActiveDirection() == CameraState.Direction.BACK); + + if (this.localRenderer != null) { + this.localRenderer.setMirror(cameraState.getActiveDirection() == CameraState.Direction.FRONT); + } + + if (this.localRenderLayout.isHidden() == cameraState.isEnabled()) { + this.localRenderLayout.setHidden(!cameraState.isEnabled()); this.localRenderLayout.requestLayout(); } } @@ -276,6 +283,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi localRenderLayout.addView(localRenderer); remoteRenderLayout.addView(remoteRenderer); + + this.localRenderer = localRenderer; } } diff --git a/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java index 325a6730e..aeeb78c74 100644 --- a/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java +++ b/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.webrtc.CameraState; import org.whispersystems.libsignal.IdentityKey; public class WebRtcViewModel { @@ -30,29 +31,40 @@ public class WebRtcViewModel { private final @Nullable IdentityKey identityKey; private final boolean remoteVideoEnabled; - private final boolean localVideoEnabled; private final boolean isBluetoothAvailable; private final boolean isMicrophoneEnabled; - public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient, - boolean localVideoEnabled, boolean remoteVideoEnabled, - boolean isBluetoothAvailable, boolean isMicrophoneEnabled) + private final CameraState localCameraState; + + public WebRtcViewModel(@NonNull State state, + @NonNull Recipient recipient, + @NonNull CameraState localCameraState, + boolean remoteVideoEnabled, + boolean isBluetoothAvailable, + boolean isMicrophoneEnabled) { - this(state, recipient, null, - localVideoEnabled, remoteVideoEnabled, - isBluetoothAvailable, isMicrophoneEnabled); + this(state, + recipient, + null, + localCameraState, + remoteVideoEnabled, + isBluetoothAvailable, + isMicrophoneEnabled); } - public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient, + public WebRtcViewModel(@NonNull State state, + @NonNull Recipient recipient, @Nullable IdentityKey identityKey, - boolean localVideoEnabled, boolean remoteVideoEnabled, - boolean isBluetoothAvailable, boolean isMicrophoneEnabled) + @NonNull CameraState localCameraState, + boolean remoteVideoEnabled, + boolean isBluetoothAvailable, + boolean isMicrophoneEnabled) { this.state = state; this.recipient = recipient; + this.localCameraState = localCameraState; this.identityKey = identityKey; - this.localVideoEnabled = localVideoEnabled; this.remoteVideoEnabled = remoteVideoEnabled; this.isBluetoothAvailable = isBluetoothAvailable; this.isMicrophoneEnabled = isMicrophoneEnabled; @@ -66,8 +78,11 @@ public class WebRtcViewModel { return recipient; } - @Nullable - public IdentityKey getIdentityKey() { + public @NonNull CameraState getLocalCameraState() { + return localCameraState; + } + + public @Nullable IdentityKey getIdentityKey() { return identityKey; } @@ -75,10 +90,6 @@ public class WebRtcViewModel { return remoteVideoEnabled; } - public boolean isLocalVideoEnabled() { - return localVideoEnabled; - } - public boolean isBluetoothAvailable() { return isBluetoothAvailable; } @@ -87,7 +98,8 @@ public class WebRtcViewModel { return isMicrophoneEnabled; } + public String toString() { - return "[State: " + state + ", recipient: " + recipient.getAddress() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localVideoEnabled + "]"; + return "[State: " + state + ", recipient: " + recipient.getAddress() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localCameraState.isEnabled() + "]"; } } diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java index 58aba5f92..81c428412 100644 --- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.WebRtcCallActivity; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.events.WebRtcViewModel; @@ -43,6 +42,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; +import org.thoughtcrime.securesms.webrtc.CameraState; import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver; import org.thoughtcrime.securesms.webrtc.PeerConnectionFactoryOptions; import org.thoughtcrime.securesms.webrtc.PeerConnectionWrapper; @@ -104,7 +104,12 @@ import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INC import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING; import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING; -public class WebRtcCallService extends Service implements InjectableType, PeerConnection.Observer, DataChannel.Observer, BluetoothStateManager.BluetoothStateListener { +public class WebRtcCallService extends Service implements InjectableType, + PeerConnection.Observer, + DataChannel.Observer, + BluetoothStateManager.BluetoothStateListener, + PeerConnectionWrapper.CameraEventListener +{ private static final String TAG = WebRtcCallService.class.getSimpleName(); @@ -116,7 +121,6 @@ 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"; @@ -133,7 +137,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_FLIP_CAMERA = "FLIP_CAMERA"; 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"; @@ -149,11 +153,11 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo public static final String ACTION_REMOTE_VIDEO_MUTE = "REMOTE_VIDEO_MUTE"; public static final String ACTION_ICE_CONNECTED = "ICE_CONNECTED"; - private CallState callState = CallState.STATE_IDLE; - private boolean microphoneEnabled = true; - private boolean localVideoEnabled = false; - private boolean remoteVideoEnabled = false; - private boolean bluetoothAvailable = false; + private CallState callState = CallState.STATE_IDLE; + private CameraState localCameraState = CameraState.UNKNOWN; + private boolean microphoneEnabled = true; + private boolean remoteVideoEnabled = false; + private boolean bluetoothAvailable = false; @Inject public SignalServiceMessageSender messageSender; @Inject public SignalServiceAccountManager accountManager; @@ -210,7 +214,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_FLIP_CAMERA)) 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); @@ -265,6 +269,15 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo startService(intent); } + @Override + public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) { + this.localCameraState = newCameraState; + if (recipient != null) { + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + } + } + + // Initializers private void initializeResources() { @@ -358,7 +371,8 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, !isSystemContact || isAlwaysTurn); + WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, WebRtcCallService.this, !isSystemContact || isAlwaysTurn); + WebRtcCallService.this.localCameraState = WebRtcCallService.this.peerConnection.getCameraState(); WebRtcCallService.this.peerConnection.setRemoteDescription(new SessionDescription(SessionDescription.Type.OFFER, offer)); WebRtcCallService.this.lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); @@ -379,6 +393,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo terminate(); } }); + + if (recipient != null) { + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + } } catch (PeerConnectionException e) { Log.w(TAG, e); terminate(); @@ -400,7 +418,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo initializeVideo(); - sendMessage(WebRtcViewModel.State.CALL_OUTGOING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_OUTGOING, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); audioManager.initializeAudioForCall(); audioManager.startOutgoingRinger(OutgoingRinger.Type.SONAR); @@ -417,7 +435,8 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo try { boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, isAlwaysTurn); + WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(WebRtcCallService.this, peerConnectionFactory, WebRtcCallService.this, localRenderer, result, WebRtcCallService.this, isAlwaysTurn); + WebRtcCallService.this.localCameraState = WebRtcCallService.this.peerConnection.getCameraState(); WebRtcCallService.this.dataChannel = WebRtcCallService.this.peerConnection.createDataChannel(DATA_CHANNEL_NAME); WebRtcCallService.this.dataChannel.registerObserver(WebRtcCallService.this); @@ -434,16 +453,20 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo Log.w(TAG, error); if (error instanceof UntrustedIdentityException) { - sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, recipient, ((UntrustedIdentityException)error).getIdentityKey(), localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, recipient, ((UntrustedIdentityException)error).getIdentityKey(), localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } else if (error instanceof UnregisteredUserException) { - sendMessage(WebRtcViewModel.State.NO_SUCH_USER, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.NO_SUCH_USER, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } else if (error instanceof IOException) { - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } terminate(); } }); + + if (recipient != null) { + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + } } catch (PeerConnectionException e) { Log.w(TAG, e); terminate(); @@ -475,7 +498,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo @Override public void onFailureContinue(Throwable error) { Log.w(TAG, error); - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); terminate(); } @@ -529,7 +552,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo @Override public void onFailureContinue(Throwable error) { Log.w(TAG, error); - sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); terminate(); } @@ -543,7 +566,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.callState = CallState.STATE_LOCAL_RINGING; this.lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); - sendMessage(WebRtcViewModel.State.CALL_INCOMING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_INCOMING, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); startCallCardActivity(); audioManager.initializeAudioForCall(); @@ -565,7 +588,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.callState = CallState.STATE_REMOTE_RINGING; this.audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING); - sendMessage(WebRtcViewModel.State.CALL_RINGING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_RINGING, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } } @@ -589,10 +612,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo callState = CallState.STATE_CONNECTED; - if (localVideoEnabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); - else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); + if (localCameraState.isEnabled()) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); + else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_CONNECTED, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); unregisterPowerButtonReceiver(); @@ -600,12 +623,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.peerConnection.setCommunicationMode(); this.peerConnection.setAudioEnabled(microphoneEnabled); - this.peerConnection.setVideoEnabled(localVideoEnabled); + this.peerConnection.setVideoEnabled(localCameraState.isEnabled()); this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder() .setVideoStreamingStatus(WebRtcDataProtos.VideoStreamingStatus.newBuilder() .setId(this.callId) - .setEnabled(localVideoEnabled)) + .setEnabled(localCameraState.isEnabled())) .build().toByteArray()), false)); } @@ -644,7 +667,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo return; } - sendMessage(WebRtcViewModel.State.CALL_BUSY, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_BUSY, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); audioManager.startOutgoingRinger(OutgoingRinger.Type.BUSY); Util.runOnMainDelayed(new Runnable() { @@ -666,7 +689,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.callState != CallState.STATE_CONNECTED) { Log.w(TAG, "Timing out call: " + this.callId); - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); if (this.callState == CallState.STATE_ANSWERING || this.callState == CallState.STATE_LOCAL_RINGING) { insertMissedCall(this.recipient, true); @@ -735,7 +758,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder().setHangup(Hangup.newBuilder().setId(this.callId)).build().toByteArray()), false)); sendMessage(this.recipient, SignalServiceCallMessage.forHangup(new HangupMessage(this.callId))); - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } terminate(); @@ -752,9 +775,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo } if (this.callState == CallState.STATE_DIALING || this.callState == CallState.STATE_REMOTE_RINGING) { - sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } else { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } if (this.callState == CallState.STATE_ANSWERING || this.callState == CallState.STATE_LOCAL_RINGING) { @@ -777,26 +800,25 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo AudioManager audioManager = ServiceUtil.getAudioManager(this); boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false); - this.localVideoEnabled = !muted; - if (this.peerConnection != null) { - this.peerConnection.setVideoEnabled(this.localVideoEnabled); + this.peerConnection.setVideoEnabled(!muted); + this.localCameraState = this.peerConnection.getCameraState(); } if (this.callId != null && this.dataChannel != null) { this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder() .setVideoStreamingStatus(WebRtcDataProtos.VideoStreamingStatus.newBuilder() .setId(this.callId) - .setEnabled(localVideoEnabled)) + .setEnabled(!muted)) .build().toByteArray()), false)); } if (callState == CallState.STATE_CONNECTED) { - if (localVideoEnabled) this.lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); - else this.lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); + if (localCameraState.isEnabled()) this.lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); + else this.lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); } - if (localVideoEnabled && + if (localCameraState.isEnabled() && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn() && !audioManager.isWiredHeadsetOn()) @@ -804,16 +826,17 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo audioManager.setSpeakerphoneOn(true); } - sendMessage(viewModelStateFor(callState), this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(viewModelStateFor(callState), this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } private void handleSetCameraFlip(Intent intent) { - boolean isRear = intent.getBooleanExtra(EXTRA_CAMERA_FLIP_REAR, false); - Log.w(TAG, "handleSetCameraFlip(isRear=" + isRear + ")..."); + Log.w(TAG, "handleSetCameraFlip()..."); - if (this.localVideoEnabled) { - if (this.peerConnection != null) { - this.peerConnection.flipCameras(isRear); + if (localCameraState.isEnabled() && peerConnection != null) { + peerConnection.flipCamera(); + localCameraState = peerConnection.getCameraState(); + if (recipient != null) { + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } } } @@ -822,7 +845,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo this.bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false); if (recipient != null) { - sendMessage(viewModelStateFor(callState), recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } } @@ -839,12 +862,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo if (present && audioManager.isSpeakerphoneOn()) { audioManager.setSpeakerphoneOn(false); audioManager.setBluetoothScoOn(false); - } else if (!present && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn() && localVideoEnabled) { + } else if (!present && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn() && localCameraState.isEnabled()) { audioManager.setSpeakerphoneOn(true); } if (recipient != null) { - sendMessage(viewModelStateFor(callState), recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(viewModelStateFor(callState), recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } } } @@ -868,7 +891,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo } this.remoteVideoEnabled = !muted; - sendMessage(WebRtcViewModel.State.CALL_CONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); + sendMessage(WebRtcViewModel.State.CALL_CONNECTED, this.recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled); } /// Helper Methods @@ -932,10 +955,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo } this.callState = CallState.STATE_IDLE; + this.localCameraState = CameraState.UNKNOWN; this.recipient = null; this.callId = null; this.microphoneEnabled = true; - this.localVideoEnabled = false; this.remoteVideoEnabled = false; this.pendingOutgoingIceUpdates = null; this.pendingIncomingIceUpdates = null; @@ -944,20 +967,24 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo private void sendMessage(@NonNull WebRtcViewModel.State state, - @NonNull Recipient recipient, - boolean localVideoEnabled, boolean remoteVideoEnabled, - boolean bluetoothAvailable, boolean microphoneEnabled) + @NonNull Recipient recipient, + @NonNull CameraState localCameraState, + boolean remoteVideoEnabled, + boolean bluetoothAvailable, + boolean microphoneEnabled) { - EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled)); + EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled)); } private void sendMessage(@NonNull WebRtcViewModel.State state, - @NonNull Recipient recipient, - @NonNull IdentityKey identityKey, - boolean localVideoEnabled, boolean remoteVideoEnabled, - boolean bluetoothAvailable, boolean microphoneEnabled) + @NonNull Recipient recipient, + @NonNull IdentityKey identityKey, + @NonNull CameraState localCameraState, + boolean remoteVideoEnabled, + boolean bluetoothAvailable, + boolean microphoneEnabled) { - EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, identityKey, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled)); + EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, identityKey, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled)); } private ListenableFutureTask sendMessage(@NonNull final Recipient recipient, diff --git a/src/org/thoughtcrime/securesms/webrtc/CameraState.java b/src/org/thoughtcrime/securesms/webrtc/CameraState.java new file mode 100644 index 000000000..c5b0e2572 --- /dev/null +++ b/src/org/thoughtcrime/securesms/webrtc/CameraState.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.webrtc; + +import android.support.annotation.NonNull; + +public class CameraState { + + public static final CameraState UNKNOWN = new CameraState(Direction.NONE, 0); + + private final Direction activeDirection; + private final int cameraCount; + + public CameraState(@NonNull Direction activeDirection, int cameraCount) { + this.activeDirection = activeDirection; + this.cameraCount = cameraCount; + } + + public int getCameraCount() { + return cameraCount; + } + + public Direction getActiveDirection() { + return activeDirection; + } + + public boolean isEnabled() { + return this.activeDirection != Direction.NONE; + } + + public enum Direction { + FRONT, BACK, NONE, PENDING + } +} diff --git a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java index 0e63b28c1..f48c39990 100644 --- a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java +++ b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java @@ -21,7 +21,6 @@ import org.webrtc.PeerConnection; import org.webrtc.PeerConnectionFactory; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; -import org.webrtc.VideoCapturer; import org.webrtc.VideoRenderer; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; @@ -30,6 +29,11 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; +import static org.thoughtcrime.securesms.webrtc.CameraState.Direction.BACK; +import static org.thoughtcrime.securesms.webrtc.CameraState.Direction.FRONT; +import static org.thoughtcrime.securesms.webrtc.CameraState.Direction.NONE; +import static org.thoughtcrime.securesms.webrtc.CameraState.Direction.PENDING; + public class PeerConnectionWrapper { private static final String TAG = PeerConnectionWrapper.class.getSimpleName(); @@ -38,25 +42,17 @@ public class PeerConnectionWrapper { @NonNull private final PeerConnection peerConnection; @NonNull private final AudioTrack audioTrack; @NonNull private final AudioSource audioSource; - - @Nullable private final VideoCapturer videoCapturer; - @Nullable private final VideoCapturer videoCapturerRear; + @NonNull private final Camera camera; @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, - @NonNull PeerConnection.Observer observer, - @NonNull VideoRenderer.Callbacks localRenderer, + public PeerConnectionWrapper(@NonNull Context context, + @NonNull PeerConnectionFactory factory, + @NonNull PeerConnection.Observer observer, + @NonNull VideoRenderer.Callbacks localRenderer, @NonNull List turnServers, - boolean hideIp) + @NonNull CameraEventListener cameraEventListener, + boolean hideIp) { List iceServers = new LinkedList<>(); iceServers.add(STUN_SERVER); @@ -80,69 +76,42 @@ public class PeerConnectionWrapper { this.peerConnection.setAudioPlayout(false); this.peerConnection.setAudioRecording(false); - this.videoCapturer = createVideoCapturer(context, false); - this.videoCapturerRear = createVideoCapturer(context, true); - - this.videoCapturerActive = videoCapturer; - - this.mediaStream = factory.createLocalMediaStream("ARDAMS"); + MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS"); this.audioSource = factory.createAudioSource(audioConstraints); this.audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource); this.audioTrack.setEnabled(false); - this.mediaStream.addTrack(audioTrack); + mediaStream.addTrack(audioTrack); - if (videoCapturer != null) { - this.videoSource = factory.createVideoSource(videoCapturer); + this.camera = new Camera(context, cameraEventListener); + + if (camera.capturer != null) { + this.videoSource = factory.createVideoSource(camera.capturer); this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource); - this.videoTrackActive = videoTrack; - this.videoTrack.addRenderer(new VideoRenderer(localRenderer)); this.videoTrack.setEnabled(false); - this.mediaStream.addTrack(videoTrack); + 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.videoTrackActive != null) { - this.videoTrackActive.setEnabled(enabled); - } - - if (this.videoCapturerActive != null) { - try { - if (enabled) this.videoCapturerActive.startCapture(1280, 720, 30); - else this.videoCapturerActive.stopCapture(); - } catch (InterruptedException e) { - Log.w(TAG, e); - } + if (this.videoTrack != null) { + this.videoTrack.setEnabled(enabled); } + camera.setEnabled(enabled); } - 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 flipCamera() { + camera.flip(); + } + + public CameraState getCameraState() { + return new CameraState(camera.getActiveDirection(), camera.getCount()); } public void setCommunicationMode() { @@ -294,32 +263,12 @@ public class PeerConnectionWrapper { } public void dispose() { - if (this.videoCapturer != null) { - try { - this.videoCapturer.stopCapture(); - } catch (InterruptedException e) { - Log.w(TAG, e); - } - this.videoCapturer.dispose(); - } - - if (this.videoCapturerRear != null) { - try { - this.videoCapturerRear.stopCapture(); - } catch (InterruptedException e) { - Log.w(TAG, e); - } - this.videoCapturerRear.dispose(); - } + this.camera.dispose(); if (this.videoSource != null) { this.videoSource.dispose(); } - if (this.videoSourceRear != null) { - this.videoSourceRear.dispose(); - } - this.audioSource.dispose(); this.peerConnection.close(); this.peerConnection.dispose(); @@ -329,57 +278,6 @@ public class PeerConnectionWrapper { return this.peerConnection.addIceCandidate(candidate); } - private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull Context context, boolean rear) { - boolean camera2EnumeratorIsSupported = false; - try { - camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context); - } catch (final Throwable throwable) { - Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable); - } - - Log.w(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported); - CameraEnumerator enumerator; - - if (camera2EnumeratorIsSupported) enumerator = new Camera2Enumerator(context); - else enumerator = new Camera1Enumerator(true); - - String[] deviceNames = enumerator.getDeviceNames(); - - for (String deviceName : deviceNames) { - 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 " + direction + " facing capturer: " + deviceName); - - return videoCapturer; - } - } - } - - for (String deviceName : deviceNames) { - 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); - - if (videoCapturer != null) { - Log.w(TAG, "Found other facing capturer: " + deviceName); - return videoCapturer; - } - } - } - - Log.w(TAG, "Video capture not supported!"); - return null; - } private SessionDescription correctSessionDescription(SessionDescription sessionDescription) { String updatedSdp = sessionDescription.description.replaceAll("(a=fmtp:111 ((?!cbr=).)*)\r?\n", "$1;cbr=1\r\n"); @@ -397,4 +295,125 @@ public class PeerConnectionWrapper { super(throwable); } } + + private static class Camera implements CameraVideoCapturer.CameraSwitchHandler { + + @Nullable + private final CameraVideoCapturer capturer; + private final CameraEventListener cameraEventListener; + private final int cameraCount; + + private CameraState.Direction activeDirection; + private boolean enabled; + + Camera(@NonNull Context context, @NonNull CameraEventListener cameraEventListener) + { + this.cameraEventListener = cameraEventListener; + CameraEnumerator enumerator = getCameraEnumerator(context); + cameraCount = enumerator.getDeviceNames().length; + + CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT); + if (capturerCandidate != null) { + activeDirection = FRONT; + } else { + capturerCandidate = createVideoCapturer(enumerator, BACK); + if (capturerCandidate != null) { + activeDirection = BACK; + } else { + activeDirection = NONE; + } + } + capturer = capturerCandidate; + } + + void flip() { + if (capturer == null || cameraCount < 2) { + Log.w(TAG, "Tried to flip the camera, but we only have " + cameraCount + " of them."); + return; + } + activeDirection = PENDING; + capturer.switchCamera(this); + } + + void setEnabled(boolean enabled) { + this.enabled = enabled; + + if (capturer == null) { + return; + } + + try { + if (enabled) { + capturer.startCapture(1280, 720, 30); + } else { + capturer.stopCapture(); + } + } catch (InterruptedException e) { + Log.w(TAG, "Got interrupted while trying to stop video capture", e); + } + } + + void dispose() { + if (capturer != null) { + capturer.dispose(); + } + } + + int getCount() { + return cameraCount; + } + + @NonNull CameraState.Direction getActiveDirection() { + return enabled ? activeDirection : NONE; + } + + @Nullable CameraVideoCapturer getCapturer() { + return capturer; + } + + private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator, + @NonNull CameraState.Direction direction) + { + String[] deviceNames = enumerator.getDeviceNames(); + for (String deviceName : deviceNames) { + if ((direction == FRONT && enumerator.isFrontFacing(deviceName)) || + (direction == BACK && enumerator.isBackFacing(deviceName))) + { + return enumerator.createCapturer(deviceName, null); + } + } + + return null; + } + + private @NonNull CameraEnumerator getCameraEnumerator(@NonNull Context context) { + boolean camera2EnumeratorIsSupported = false; + try { + camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context); + } catch (final Throwable throwable) { + Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable); + } + + Log.w(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported); + + return camera2EnumeratorIsSupported ? new Camera2Enumerator(context) + : new Camera1Enumerator(true); + } + + @Override + public void onCameraSwitchDone(boolean isFrontFacing) { + activeDirection = isFrontFacing ? FRONT : BACK; + cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount())); + } + + @Override + public void onCameraSwitchError(String errorMessage) { + Log.e(TAG, "onCameraSwitchError: " + errorMessage); + cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount())); + } + } + + public interface CameraEventListener { + void onCameraSwitchCompleted(@NonNull CameraState newCameraState); + } }