diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 384ec235fb0f4647611195926be5e9f51631e7a7..34a3f450c6be94e19f596f75c40f1b2e0f360475 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -26,6 +26,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import org.webrtc.DtmfSender; import org.webrtc.EglBase; @@ -184,6 +185,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; private final long created = System.currentTimeMillis() / 1000L; + private final SettableFuture> iceGatheringComplete = SettableFuture.create(); JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); @@ -1127,6 +1129,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } return; } + final Presence presence = id.getContact().getPresences().get(id.getWith().getResource()); + if (presence != null && presence.getServiceDiscoveryResult().getFeatures().contains("urn:ietf:rfc:3264")) webRTCWrapper.setRFC3264(true); final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); Futures.addCallback( future, @@ -1395,29 +1399,46 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void prepareSessionAccept( - final org.webrtc.SessionDescription webRTCSessionDescription) { - final SessionDescription sessionDescription = - SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); - this.responderRtpContentMap = respondingRtpContentMap; - storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); - final ListenableFuture outgoingContentMapFuture = - prepareOutgoingContentMap(respondingRtpContentMap); + final org.webrtc.SessionDescription initialWebRTCSessionDescription) { Futures.addCallback( - outgoingContentMapFuture, - new FutureCallback() { - @Override - public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionAccept(outgoingContentMap); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - } + webRTCWrapper.getRFC3264() ? Futures.withTimeout(iceGatheringComplete, 2, TimeUnit.SECONDS, JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE) : Futures.immediateFuture(null), + new FutureCallback>() { + @Override + public void onSuccess(final Collection iceCandidates) { + org.webrtc.SessionDescription webRTCSessionDescription = + JingleRtpConnection.this.webRTCWrapper.getLocalDescription(); + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); + JingleRtpConnection.this.responderRtpContentMap = respondingRtpContentMap; + storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); + final ListenableFuture outgoingContentMapFuture = + prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionAccept(outgoingContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } - @Override - public void onFailure(@NonNull Throwable throwable) { - failureToAcceptSession(throwable); - } - }, - MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull Throwable throwable) { + failureToAcceptSession(throwable); + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.e(Config.LOGTAG, "ICE gathering didn't finish clean: " + throwable); + onSuccess(null); + } + }, + MoreExecutors.directExecutor() + ); } private void sendSessionAccept(final RtpContentMap rtpContentMap) { @@ -1848,28 +1869,46 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void prepareSessionInitiate( - final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { - final SessionDescription sessionDescription = - SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); - this.initiatorRtpContentMap = rtpContentMap; - final ListenableFuture outgoingContentMapFuture = - encryptSessionInitiate(rtpContentMap); + final org.webrtc.SessionDescription initialWebRTCSessionDescription, final State targetState) { Futures.addCallback( - outgoingContentMapFuture, - new FutureCallback() { - @Override - public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, targetState); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - } + webRTCWrapper.getRFC3264() ? Futures.withTimeout(iceGatheringComplete, 2, TimeUnit.SECONDS, JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE) : Futures.immediateFuture(null), + new FutureCallback>() { + @Override + public void onSuccess(final Collection iceCandidates) { + org.webrtc.SessionDescription webRTCSessionDescription = + JingleRtpConnection.this.webRTCWrapper.getLocalDescription(); + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); + JingleRtpConnection.this.initiatorRtpContentMap = rtpContentMap; + final ListenableFuture outgoingContentMapFuture = + encryptSessionInitiate(rtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionInitiate(outgoingContentMap, targetState); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - failureToInitiateSession(throwable, targetState); - } - }, - MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToInitiateSession(throwable, targetState); + } + }, + MoreExecutors.directExecutor() + ); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.e(Config.LOGTAG, "ICE gathering didn't finish clean: " + throwable); + onSuccess(null); + } + }, + MoreExecutors.directExecutor() + ); } private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { @@ -2336,6 +2375,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); + final Presence presence = id.getContact().getPresences().get(id.getWith().getResource()); + if (presence != null && presence.getServiceDiscoveryResult().getFeatures().contains("urn:ietf:rfc:3264")) webRTCWrapper.setRFC3264(true); this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); this.webRTCWrapper.initializePeerConnection(media, iceServers); } @@ -2458,6 +2499,11 @@ public class JingleRtpConnection extends AbstractJingleConnection sendTransportInfo(iceCandidate.sdpMid, candidate); } + @Override + public void onIceGatheringComplete(Collection iceCandidates) { + iceGatheringComplete.set(iceCandidates); + } + @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d( diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f252fc8acb95c5827141eef02f02f2488c98fc08..276a737bc5052baabd40a649685f06f283f3d35f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -41,6 +41,7 @@ import org.webrtc.SessionDescription; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; +import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -102,6 +103,7 @@ public class WebRTCWrapper { private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); + private final AtomicBoolean rfc3264 = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @@ -152,11 +154,14 @@ public class WebRTCWrapper { public void onIceGatheringChange( PeerConnection.IceGatheringState iceGatheringState) { Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); + if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) { + eventCallback.onIceGatheringComplete(iceCandidates); + } } @Override public void onIceCandidate(IceCandidate iceCandidate) { - if (readyToReceivedIceCandidates.get()) { + if (readyToReceivedIceCandidates.get() && !rfc3264.get()) { eventCallback.onIceCandidate(iceCandidate); } else { iceCandidates.add(iceCandidate); @@ -305,7 +310,7 @@ public class WebRTCWrapper { .createAudioDeviceModule()) .createPeerConnectionFactory(); - final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); + final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, rfc3264.get()); final PeerConnection peerConnection = requirePeerConnectionFactory() .createPeerConnection(rtcConfig, peerConnectionObserver); @@ -420,13 +425,18 @@ public class WebRTCWrapper { } private static PeerConnection.RTCConfiguration buildConfiguration( - final List iceServers) { + final List iceServers, boolean rfc3264) { final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp - rtcConfig.continualGatheringPolicy = + if (rfc3264) { + rtcConfig.continualGatheringPolicy = + PeerConnection.ContinualGatheringPolicy.GATHER_ONCE; + } else { + rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + } rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; rtcConfig.enableImplicitRollback = true; @@ -434,7 +444,7 @@ public class WebRTCWrapper { } void reconfigurePeerConnection(final List iceServers) { - requirePeerConnection().setConfiguration(buildConfiguration(iceServers)); + requirePeerConnection().setConfiguration(buildConfiguration(iceServers, rfc3264.get())); } void restartIceAsync() { @@ -455,6 +465,7 @@ public class WebRTCWrapper { public void setIsReadyToReceiveIceCandidates(final boolean ready) { readyToReceivedIceCandidates.set(ready); + if (this.rfc3264.get()) return; final int was = iceCandidates.size(); while (ready && iceCandidates.peek() != null) { eventCallback.onIceCandidate(iceCandidates.poll()); @@ -465,6 +476,15 @@ public class WebRTCWrapper { "setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is); } + public void setRFC3264(final boolean rfc3264) { + // When this feature is enabled, do not trickle candidates + this.rfc3264.set(rfc3264); + } + + public boolean getRFC3264() { + return this.rfc3264.get(); + } + synchronized void close() { final PeerConnection peerConnection = this.peerConnection; final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; @@ -595,6 +615,10 @@ public class WebRTCWrapper { throw new IllegalStateException("Local video track does not exist"); } + synchronized SessionDescription getLocalDescription() { + return peerConnection.getLocalDescription(); + } + synchronized ListenableFuture setLocalDescription() { this.setIsReadyToReceiveIceCandidates(false); return Futures.transformAsync( @@ -769,6 +793,8 @@ public class WebRTCWrapper { Set availableAudioDevices); void onRenegotiationNeeded(); + + void onIceGatheringComplete(Collection iceCandidates); } private abstract static class SetSdpObserver implements SdpObserver { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index e3c041d6de41f2de51905d75c78be5dbf480d2fa..ea36d01ee56f089ec870f2ac2ae23cce93a4701e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -69,6 +69,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo { for (final String iceOption : IceOption.of(media)) { iceUdpTransportInfo.addChild(new IceOption(iceOption)); } + for (final String candidate : media.attributes.get("candidate")) { + iceUdpTransportInfo.addChild(Candidate.fromSdpAttributeValue(candidate, ufrag)); + } return iceUdpTransportInfo; }