mirror of
				https://github.com/oxen-io/session-android.git
				synced 2025-10-30 23:09:20 +00:00 
			
		
		
		
	Add ringrtc support.
RingRTC provides Signal Messenger applications with a common interface for video and voice calling services built on top of WebRTC.
This commit is contained in:
		| @@ -88,7 +88,7 @@ dependencies { | ||||
|  | ||||
|     implementation 'org.whispersystems:signal-service-android:2.13.7' | ||||
|  | ||||
|     implementation 'org.whispersystems:webrtc-android:M75' | ||||
|     implementation 'org.signal:ringrtc-android:0.1.1' | ||||
|  | ||||
|     implementation "me.leolin:ShortcutBadger:1.1.16" | ||||
|     implementation 'se.emilsjolander:stickylistheaders:2.7.0' | ||||
| @@ -198,7 +198,7 @@ dependencyVerification { | ||||
|         'org.conscrypt:conscrypt-android:400ca559a49b860a82862b22cee0e3110764bdcf7ee7c79e7479895c25cdfc09', | ||||
|         'org.signal:aesgcmprovider:6eb4422e8a618b3b76cb2096a3619d251f9e27989dc68307a1e5414c3710f2d1', | ||||
|         'org.whispersystems:signal-service-android:5115aa434c52ca671c513995e6ae67d73f3abaaa605f9e6cf64c2e01da961c7e', | ||||
|         'org.whispersystems:webrtc-android:f8231bb57923afb243760213dc58924e85cce42f2f3cc8cb33a6d883672a921a', | ||||
|         'org.signal:ringrtc-android:91d4d89847c10e718bc8badfa0353f0095678855c35c2575509fea97f615de86', | ||||
|         'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774', | ||||
|         'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', | ||||
|         'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa', | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| -dontwarn org.webrtc.NetworkMonitorAutoDetect | ||||
| -dontwarn android.net.Network | ||||
| -keep class org.webrtc.** { *; } | ||||
| @@ -33,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller; | ||||
|  | ||||
| import org.conscrypt.Conscrypt; | ||||
| import org.signal.aesgcmprovider.AesGcmProvider; | ||||
| import org.signal.ringrtc.CallConnectionFactory; | ||||
| import org.thoughtcrime.securesms.components.TypingStatusRepository; | ||||
| import org.thoughtcrime.securesms.components.TypingStatusSender; | ||||
| import org.thoughtcrime.securesms.database.DatabaseFactory; | ||||
| @@ -70,8 +71,6 @@ import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; | ||||
| import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; | ||||
| import org.thoughtcrime.securesms.util.TextSecurePreferences; | ||||
| import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; | ||||
| import org.webrtc.PeerConnectionFactory; | ||||
| import org.webrtc.PeerConnectionFactory.InitializationOptions; | ||||
| import org.webrtc.voiceengine.WebRtcAudioManager; | ||||
| import org.webrtc.voiceengine.WebRtcAudioUtils; | ||||
| import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider; | ||||
| @@ -126,7 +125,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi | ||||
|     initializeSignedPreKeyCheck(); | ||||
|     initializePeriodicTasks(); | ||||
|     initializeCircumvention(); | ||||
|     initializeWebRtc(); | ||||
|     initializeRingRtc(); | ||||
|     initializePendingMessages(); | ||||
|     initializeUnidentifiedDeliveryAbilityRefresh(); | ||||
|     initializeBlobProvider(); | ||||
| @@ -282,7 +281,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private void initializeWebRtc() { | ||||
|   private void initializeRingRtc() { | ||||
|     try { | ||||
|       Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{ | ||||
|         add("Pixel"); | ||||
| @@ -311,7 +310,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi | ||||
|         WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true); | ||||
|       } | ||||
|  | ||||
|       PeerConnectionFactory.initialize(InitializationOptions.builder(this).createInitializationOptions()); | ||||
|       CallConnectionFactory.initialize(this); | ||||
|     } catch (UnsatisfiedLinkError e) { | ||||
|       Log.w(TAG, e); | ||||
|     } | ||||
|   | ||||
| @@ -43,10 +43,10 @@ import org.thoughtcrime.securesms.R; | ||||
| import org.thoughtcrime.securesms.mms.GlideApp; | ||||
| import org.thoughtcrime.securesms.recipients.Recipient; | ||||
| import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; | ||||
| import org.thoughtcrime.securesms.ringrtc.CameraState; | ||||
| 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; | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import org.thoughtcrime.securesms.recipients.Recipient; | ||||
| import org.thoughtcrime.securesms.webrtc.CameraState; | ||||
| import org.thoughtcrime.securesms.ringrtc.CameraState; | ||||
| import org.webrtc.SurfaceViewRenderer; | ||||
| import org.whispersystems.libsignal.IdentityKey; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,293 @@ | ||||
| package org.thoughtcrime.securesms.ringrtc; | ||||
|  | ||||
|  | ||||
| import android.content.Context; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import org.signal.ringrtc.CallConnection; | ||||
| import org.signal.ringrtc.CallConnectionFactory; | ||||
| import org.signal.ringrtc.CallException; | ||||
| import org.signal.ringrtc.SignalMessageRecipient; | ||||
|  | ||||
| import org.thoughtcrime.securesms.logging.Log; | ||||
|  | ||||
| import org.webrtc.AudioSource; | ||||
| import org.webrtc.AudioTrack; | ||||
| import org.webrtc.Camera1Enumerator; | ||||
| import org.webrtc.Camera2Enumerator; | ||||
| import org.webrtc.CameraEnumerator; | ||||
| import org.webrtc.CameraVideoCapturer; | ||||
| import org.webrtc.EglBase; | ||||
| import org.webrtc.IceCandidate; | ||||
| import org.webrtc.MediaConstraints; | ||||
| import org.webrtc.MediaStream; | ||||
| import org.webrtc.SurfaceTextureHelper; | ||||
| import org.webrtc.VideoSink; | ||||
| import org.webrtc.VideoSource; | ||||
| import org.webrtc.VideoTrack; | ||||
|  | ||||
| import org.whispersystems.signalservice.api.SignalServiceAccountManager; | ||||
| import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; | ||||
|  | ||||
| import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK; | ||||
| import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT; | ||||
| import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE; | ||||
| import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.PENDING; | ||||
|  | ||||
| public class CallConnectionWrapper { | ||||
|   private static final String TAG = Log.tag(CallConnectionWrapper.class); | ||||
|  | ||||
|   @NonNull  private final CallConnection callConnection; | ||||
|   @NonNull  private final AudioTrack     audioTrack; | ||||
|   @NonNull  private final AudioSource    audioSource; | ||||
|   @NonNull  private final Camera         camera; | ||||
|   @Nullable private final VideoSource    videoSource; | ||||
|   @Nullable private final VideoTrack     videoTrack; | ||||
|  | ||||
|   public CallConnectionWrapper(@NonNull Context                     context, | ||||
|                                @NonNull CallConnectionFactory       factory, | ||||
|                                @NonNull CallConnection.Observer     observer, | ||||
|                                @NonNull VideoSink                   localRenderer, | ||||
|                                @NonNull CameraEventListener         cameraEventListener, | ||||
|                                @NonNull EglBase                     eglBase, | ||||
|                                boolean                              hideIp, | ||||
|                                long                                 callId, | ||||
|                                boolean                              outBound, | ||||
|                                @NonNull SignalMessageRecipient      recipient, | ||||
|                                @NonNull SignalServiceAccountManager accountManager) | ||||
|     throws UnregisteredUserException, IOException, CallException | ||||
|   { | ||||
|  | ||||
|     CallConnection.Configuration configuration = new CallConnection.Configuration(callId, | ||||
|                                                                                   outBound, | ||||
|                                                                                   recipient, | ||||
|                                                                                   accountManager, | ||||
|                                                                                   hideIp); | ||||
|  | ||||
|     this.callConnection = factory.createCallConnection(configuration, observer); | ||||
|     this.callConnection.setAudioPlayout(false); | ||||
|     this.callConnection.setAudioRecording(false); | ||||
|  | ||||
|     MediaStream mediaStream           = factory.createLocalMediaStream("ARDAMS"); | ||||
|     MediaConstraints audioConstraints = new MediaConstraints(); | ||||
|  | ||||
|     audioConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); | ||||
|     this.audioSource = factory.createAudioSource(audioConstraints); | ||||
|     this.audioTrack  = factory.createAudioTrack("ARDAMSa0", audioSource); | ||||
|     this.audioTrack.setEnabled(false); | ||||
|     mediaStream.addTrack(audioTrack); | ||||
|  | ||||
|     this.camera = new Camera(context, cameraEventListener); | ||||
|  | ||||
|     if (camera.capturer != null) { | ||||
|       this.videoSource = factory.createVideoSource(false); | ||||
|       this.videoTrack  = factory.createVideoTrack("ARDAMSv0", videoSource); | ||||
|  | ||||
|       camera.capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()), context, videoSource.getCapturerObserver()); | ||||
|  | ||||
|       this.videoTrack.addSink(localRenderer); | ||||
|       this.videoTrack.setEnabled(false); | ||||
|       mediaStream.addTrack(videoTrack); | ||||
|     } else { | ||||
|       this.videoSource = null; | ||||
|       this.videoTrack  = null; | ||||
|     } | ||||
|  | ||||
|     this.callConnection.addStream(mediaStream); | ||||
|   } | ||||
|  | ||||
|   public boolean addIceCandidate(IceCandidate candidate) { | ||||
|     return callConnection.addIceCandidate(candidate); | ||||
|   } | ||||
|  | ||||
|   public void sendOffer() throws CallException { | ||||
|     callConnection.sendOffer(); | ||||
|   } | ||||
|  | ||||
|   public boolean validateResponse(SignalMessageRecipient recipient, Long inCallId) | ||||
|     throws CallException | ||||
|   { | ||||
|     return callConnection.validateResponse(recipient, inCallId); | ||||
|   } | ||||
|  | ||||
|   public void handleOfferAnswer(String sessionDescription) throws CallException { | ||||
|     callConnection.handleOfferAnswer(sessionDescription); | ||||
|   } | ||||
|  | ||||
|   public void acceptOffer(String offer) throws CallException { | ||||
|     callConnection.acceptOffer(offer); | ||||
|   } | ||||
|  | ||||
|   public void hangUp() throws CallException { | ||||
|     callConnection.hangUp(); | ||||
|   } | ||||
|  | ||||
|   public void answerCall() throws CallException { | ||||
|     callConnection.answerCall(); | ||||
|   } | ||||
|  | ||||
|   public void sendBusy(SignalMessageRecipient recipient, Long inCallId) throws CallException | ||||
|   { | ||||
|     callConnection.sendBusy(recipient, inCallId); | ||||
|   } | ||||
|  | ||||
|   public void setVideoEnabled(boolean enabled) throws CallException { | ||||
|     if (videoTrack != null) { | ||||
|       videoTrack.setEnabled(enabled); | ||||
|     } | ||||
|     camera.setEnabled(enabled); | ||||
|     callConnection.sendVideoStatus(enabled); | ||||
|   } | ||||
|  | ||||
|   public void flipCamera() { | ||||
|     camera.flip(); | ||||
|   } | ||||
|  | ||||
|   public CameraState getCameraState() { | ||||
|     return new CameraState(camera.getActiveDirection(), camera.getCount()); | ||||
|   } | ||||
|  | ||||
|   public void setCommunicationMode() { | ||||
|     callConnection.setAudioPlayout(true); | ||||
|     callConnection.setAudioRecording(true); | ||||
|   } | ||||
|  | ||||
|   public void setAudioEnabled(boolean enabled) { | ||||
|     audioTrack.setEnabled(enabled); | ||||
|   } | ||||
|  | ||||
|   public void dispose() { | ||||
|     camera.dispose(); | ||||
|  | ||||
|     if (videoSource != null) { | ||||
|       videoSource.dispose(); | ||||
|     } | ||||
|  | ||||
|     audioSource.dispose(); | ||||
|     callConnection.dispose(); | ||||
|   } | ||||
|  | ||||
|   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) { | ||||
|         throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + | ||||
|                                  " of them."); | ||||
|       } | ||||
|       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.i(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); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.thoughtcrime.securesms.webrtc; | ||||
| package org.thoughtcrime.securesms.ringrtc; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
							
								
								
									
										115
									
								
								src/org/thoughtcrime/securesms/ringrtc/MessageRecipient.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/org/thoughtcrime/securesms/ringrtc/MessageRecipient.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| package org.thoughtcrime.securesms.ringrtc; | ||||
|  | ||||
| import android.content.Context; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
|  | ||||
| import org.signal.ringrtc.SignalMessageRecipient; | ||||
|  | ||||
| import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; | ||||
| import org.thoughtcrime.securesms.database.Address; | ||||
| import org.thoughtcrime.securesms.logging.Log; | ||||
| import org.thoughtcrime.securesms.recipients.Recipient; | ||||
| import org.thoughtcrime.securesms.util.Util; | ||||
|  | ||||
| import org.whispersystems.signalservice.api.SignalServiceMessageSender; | ||||
| import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; | ||||
| import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; | ||||
| import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; | ||||
| import org.whispersystems.signalservice.api.messages.calls.BusyMessage; | ||||
| import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; | ||||
| import org.whispersystems.signalservice.api.messages.calls.HangupMessage; | ||||
| import org.whispersystems.signalservice.api.messages.calls.OfferMessage; | ||||
| import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; | ||||
| import org.whispersystems.signalservice.api.push.SignalServiceAddress; | ||||
|  | ||||
| public final class MessageRecipient implements SignalMessageRecipient { | ||||
|  | ||||
|   private static final String TAG = Log.tag(MessageRecipient.class); | ||||
|  | ||||
|   @NonNull private final Recipient recipient; | ||||
|   @NonNull private final SignalServiceMessageSender messageSender; | ||||
|  | ||||
|   public MessageRecipient(SignalServiceMessageSender messageSender, | ||||
|                           Recipient                  recipient) | ||||
|   { | ||||
|     this.recipient     = recipient; | ||||
|     this.messageSender = messageSender; | ||||
|   } | ||||
|  | ||||
|   public @NonNull Address getAddress() { | ||||
|     return recipient.getAddress(); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public boolean isEqual(@NonNull SignalMessageRecipient o) { | ||||
|     if (!(o instanceof MessageRecipient)) { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     MessageRecipient that = (MessageRecipient) o; | ||||
|  | ||||
|     return recipient.equals(that.recipient); | ||||
|   } | ||||
|  | ||||
|   void sendMessage(Context context, SignalServiceCallMessage callMessage) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     messageSender.sendCallMessage(new SignalServiceAddress(recipient.getAddress().toPhoneString()), | ||||
|                                   UnidentifiedAccessUtil.getAccessFor(context, recipient), | ||||
|                                   callMessage); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void sendOfferMessage(Context context, long callId, String description) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     Log.i(TAG, "MessageRecipient::sendOfferMessage(): callId: " + callId); | ||||
|  | ||||
|     OfferMessage offerMessage = new OfferMessage(callId, description); | ||||
|     sendMessage(context, SignalServiceCallMessage.forOffer(offerMessage)); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void sendAnswerMessage(Context context, long callId, String description) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     Log.i(TAG, "MessageRecipient::sendAnswerMessage(): callId: " + callId); | ||||
|  | ||||
|     AnswerMessage answerMessage = new AnswerMessage(callId, description); | ||||
|     sendMessage(context, SignalServiceCallMessage.forAnswer(answerMessage)); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void sendIceUpdates(Context context, List<IceUpdateMessage> iceUpdateMessages) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     Log.i(TAG, "MessageRecipient::sendIceUpdates(): iceUpdates: " + iceUpdateMessages.size()); | ||||
|  | ||||
|     sendMessage(context, SignalServiceCallMessage.forIceUpdates(iceUpdateMessages)); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void sendHangupMessage(Context context, long callId) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     Log.i(TAG, "MessageRecipient::sendHangupMessage(): callId: " + callId); | ||||
|  | ||||
|     HangupMessage hangupMessage = new HangupMessage(callId); | ||||
|     sendMessage(context, SignalServiceCallMessage.forHangup(hangupMessage)); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void sendBusyMessage(Context context, long callId) | ||||
|     throws IOException, UntrustedIdentityException, IOException | ||||
|   { | ||||
|     Log.i(TAG, "MessageRecipient::sendBusyMessage(): callId: " + callId); | ||||
|  | ||||
|     BusyMessage busyMessage = new BusyMessage(callId); | ||||
|     sendMessage(context, SignalServiceCallMessage.forBusy(busyMessage)); | ||||
|   } | ||||
|  | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,11 +0,0 @@ | ||||
| package org.thoughtcrime.securesms.webrtc; | ||||
|  | ||||
|  | ||||
| import org.webrtc.PeerConnectionFactory; | ||||
|  | ||||
| public class PeerConnectionFactoryOptions extends PeerConnectionFactory.Options { | ||||
|  | ||||
|   public PeerConnectionFactoryOptions() { | ||||
|     this.networkIgnoreMask = 1 << 4; | ||||
|   } | ||||
| } | ||||
| @@ -1,424 +0,0 @@ | ||||
| package org.thoughtcrime.securesms.webrtc; | ||||
|  | ||||
|  | ||||
| import android.content.Context; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import org.thoughtcrime.securesms.logging.Log; | ||||
|  | ||||
| import org.thoughtcrime.securesms.util.concurrent.SettableFuture; | ||||
| import org.webrtc.AudioSource; | ||||
| import org.webrtc.AudioTrack; | ||||
| import org.webrtc.Camera1Enumerator; | ||||
| import org.webrtc.Camera2Enumerator; | ||||
| import org.webrtc.CameraEnumerator; | ||||
| import org.webrtc.CameraVideoCapturer; | ||||
| import org.webrtc.DataChannel; | ||||
| import org.webrtc.EglBase; | ||||
| import org.webrtc.IceCandidate; | ||||
| import org.webrtc.MediaConstraints; | ||||
| import org.webrtc.MediaStream; | ||||
| import org.webrtc.PeerConnection; | ||||
| import org.webrtc.PeerConnectionFactory; | ||||
| import org.webrtc.SdpObserver; | ||||
| import org.webrtc.SessionDescription; | ||||
| import org.webrtc.SurfaceTextureHelper; | ||||
| import org.webrtc.VideoSink; | ||||
| import org.webrtc.VideoSource; | ||||
| import org.webrtc.VideoTrack; | ||||
|  | ||||
| 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(); | ||||
|  | ||||
|   private static final PeerConnection.IceServer STUN_SERVER = new PeerConnection.IceServer("stun:stun1.l.google.com:19302"); | ||||
|  | ||||
|   @NonNull  private final PeerConnection peerConnection; | ||||
|   @NonNull  private final AudioTrack     audioTrack; | ||||
|   @NonNull  private final AudioSource    audioSource; | ||||
|   @NonNull  private final Camera         camera; | ||||
|   @Nullable private final VideoSource    videoSource; | ||||
|   @Nullable private final VideoTrack     videoTrack; | ||||
|  | ||||
|   public PeerConnectionWrapper(@NonNull Context                        context, | ||||
|                                @NonNull PeerConnectionFactory          factory, | ||||
|                                @NonNull PeerConnection.Observer        observer, | ||||
|                                @NonNull VideoSink                      localRenderer, | ||||
|                                @NonNull List<PeerConnection.IceServer> turnServers, | ||||
|                                @NonNull CameraEventListener            cameraEventListener, | ||||
|                                @NonNull EglBase                        eglBase, | ||||
|                                boolean                                 hideIp) | ||||
|   { | ||||
|     List<PeerConnection.IceServer> iceServers = new LinkedList<>(); | ||||
|     iceServers.add(STUN_SERVER); | ||||
|     iceServers.addAll(turnServers); | ||||
|  | ||||
|     MediaConstraints                constraints      = new MediaConstraints(); | ||||
|     MediaConstraints                audioConstraints = new MediaConstraints(); | ||||
|     PeerConnection.RTCConfiguration configuration    = new PeerConnection.RTCConfiguration(iceServers); | ||||
|  | ||||
|     configuration.bundlePolicy  = PeerConnection.BundlePolicy.MAXBUNDLE; | ||||
|     configuration.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE; | ||||
|  | ||||
|     if (hideIp) { | ||||
|       configuration.iceTransportsType = PeerConnection.IceTransportsType.RELAY; | ||||
|     } | ||||
|  | ||||
|     constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); | ||||
|     audioConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); | ||||
|  | ||||
|     this.peerConnection = factory.createPeerConnection(configuration, constraints, observer); | ||||
|     this.peerConnection.setAudioPlayout(false); | ||||
|     this.peerConnection.setAudioRecording(false); | ||||
|  | ||||
|     MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS"); | ||||
|     this.audioSource = factory.createAudioSource(audioConstraints); | ||||
|     this.audioTrack  = factory.createAudioTrack("ARDAMSa0", audioSource); | ||||
|     this.audioTrack.setEnabled(false); | ||||
|     mediaStream.addTrack(audioTrack); | ||||
|  | ||||
|     this.camera = new Camera(context, cameraEventListener); | ||||
|  | ||||
|     if (camera.capturer != null) { | ||||
|       this.videoSource = factory.createVideoSource(false); | ||||
|       this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource); | ||||
|  | ||||
|       camera.capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()), context, videoSource.getCapturerObserver()); | ||||
|  | ||||
|       this.videoTrack.addSink(localRenderer); | ||||
|       this.videoTrack.setEnabled(false); | ||||
|       mediaStream.addTrack(videoTrack); | ||||
|     } else { | ||||
|       this.videoSource = null; | ||||
|       this.videoTrack  = null; | ||||
|     } | ||||
|  | ||||
|     this.peerConnection.addStream(mediaStream); | ||||
|   } | ||||
|  | ||||
|   public void setVideoEnabled(boolean enabled) { | ||||
|     if (this.videoTrack != null) { | ||||
|       this.videoTrack.setEnabled(enabled); | ||||
|     } | ||||
|     camera.setEnabled(enabled); | ||||
|   } | ||||
|  | ||||
|   public void flipCamera() { | ||||
|     camera.flip(); | ||||
|   } | ||||
|  | ||||
|   public CameraState getCameraState() { | ||||
|     return new CameraState(camera.getActiveDirection(), camera.getCount()); | ||||
|   } | ||||
|  | ||||
|   public void setCommunicationMode() { | ||||
|     this.peerConnection.setAudioPlayout(true); | ||||
|     this.peerConnection.setAudioRecording(true); | ||||
|   } | ||||
|  | ||||
|   public void setAudioEnabled(boolean enabled) { | ||||
|     this.audioTrack.setEnabled(enabled); | ||||
|   } | ||||
|  | ||||
|   public DataChannel createDataChannel(String name) { | ||||
|     DataChannel.Init dataChannelConfiguration = new DataChannel.Init(); | ||||
|     dataChannelConfiguration.ordered = true; | ||||
|  | ||||
|     return this.peerConnection.createDataChannel(name, dataChannelConfiguration); | ||||
|   } | ||||
|  | ||||
|   public SessionDescription createOffer(MediaConstraints mediaConstraints) throws PeerConnectionException { | ||||
|     final SettableFuture<SessionDescription> future = new SettableFuture<>(); | ||||
|  | ||||
|     peerConnection.createOffer(new SdpObserver() { | ||||
|       @Override | ||||
|       public void onCreateSuccess(SessionDescription sdp) { | ||||
|         future.set(sdp); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onCreateFailure(String error) { | ||||
|         future.setException(new PeerConnectionException(error)); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetSuccess() { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetFailure(String error) { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|     }, mediaConstraints); | ||||
|  | ||||
|     try { | ||||
|       return correctSessionDescription(future.get()); | ||||
|     } catch (InterruptedException e) { | ||||
|       throw new AssertionError(e); | ||||
|     } catch (ExecutionException e) { | ||||
|       throw new PeerConnectionException(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public SessionDescription createAnswer(MediaConstraints mediaConstraints) throws PeerConnectionException { | ||||
|     final SettableFuture<SessionDescription> future = new SettableFuture<>(); | ||||
|  | ||||
|     peerConnection.createAnswer(new SdpObserver() { | ||||
|       @Override | ||||
|       public void onCreateSuccess(SessionDescription sdp) { | ||||
|         future.set(sdp); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onCreateFailure(String error) { | ||||
|         future.setException(new PeerConnectionException(error)); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetSuccess() { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetFailure(String error) { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|     }, mediaConstraints); | ||||
|  | ||||
|     try { | ||||
|       return correctSessionDescription(future.get()); | ||||
|     } catch (InterruptedException e) { | ||||
|       throw new AssertionError(e); | ||||
|     } catch (ExecutionException e) { | ||||
|       throw new PeerConnectionException(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public void setRemoteDescription(SessionDescription sdp) throws PeerConnectionException { | ||||
|     final SettableFuture<Boolean> future = new SettableFuture<>(); | ||||
|  | ||||
|     peerConnection.setRemoteDescription(new SdpObserver() { | ||||
|       @Override | ||||
|       public void onCreateSuccess(SessionDescription sdp) {} | ||||
|  | ||||
|       @Override | ||||
|       public void onCreateFailure(String error) {} | ||||
|  | ||||
|       @Override | ||||
|       public void onSetSuccess() { | ||||
|         future.set(true); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetFailure(String error) { | ||||
|         future.setException(new PeerConnectionException(error)); | ||||
|       } | ||||
|     }, sdp); | ||||
|  | ||||
|     try { | ||||
|       future.get(); | ||||
|     } catch (InterruptedException e) { | ||||
|       throw new AssertionError(e); | ||||
|     } catch (ExecutionException e) { | ||||
|       throw new PeerConnectionException(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public void setLocalDescription(SessionDescription sdp) throws PeerConnectionException { | ||||
|     final SettableFuture<Boolean> future = new SettableFuture<>(); | ||||
|  | ||||
|     peerConnection.setLocalDescription(new SdpObserver() { | ||||
|       @Override | ||||
|       public void onCreateSuccess(SessionDescription sdp) { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onCreateFailure(String error) { | ||||
|         throw new AssertionError(); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetSuccess() { | ||||
|         future.set(true); | ||||
|       } | ||||
|  | ||||
|       @Override | ||||
|       public void onSetFailure(String error) { | ||||
|         future.setException(new PeerConnectionException(error)); | ||||
|       } | ||||
|     }, sdp); | ||||
|  | ||||
|     try { | ||||
|       future.get(); | ||||
|     } catch (InterruptedException e) { | ||||
|       throw new AssertionError(e); | ||||
|     } catch (ExecutionException e) { | ||||
|       throw new PeerConnectionException(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public void dispose() { | ||||
|     this.camera.dispose(); | ||||
|  | ||||
|     if (this.videoSource != null) { | ||||
|       this.videoSource.dispose(); | ||||
|     } | ||||
|  | ||||
|     this.audioSource.dispose(); | ||||
|     this.peerConnection.close(); | ||||
|     this.peerConnection.dispose(); | ||||
|   } | ||||
|  | ||||
|   public boolean addIceCandidate(IceCandidate candidate) { | ||||
|     return this.peerConnection.addIceCandidate(candidate); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   private SessionDescription correctSessionDescription(SessionDescription sessionDescription) { | ||||
|     String updatedSdp = sessionDescription.description.replaceAll("(a=fmtp:111 ((?!cbr=).)*)\r?\n", "$1;cbr=1\r\n"); | ||||
|     updatedSdp = updatedSdp.replaceAll(".+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\r?\n", ""); | ||||
|  | ||||
|     return new SessionDescription(sessionDescription.type, updatedSdp); | ||||
|   } | ||||
|  | ||||
|   public static class PeerConnectionException extends Exception { | ||||
|     public PeerConnectionException(String error) { | ||||
|       super(error); | ||||
|     } | ||||
|  | ||||
|     public PeerConnectionException(Throwable throwable) { | ||||
|       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.i(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); | ||||
|   } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Curt Brune
					Curt Brune