treat transport-info w/o candidates and changed credentials as offer
This commit is contained in:
parent
717c83753f
commit
5b80c62a63
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.xml;
|
package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -165,8 +167,9 @@ public class Element {
|
||||||
return this.attributes;
|
return this.attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder elementOutput = new StringBuilder();
|
final StringBuilder elementOutput = new StringBuilder();
|
||||||
if ((content == null) && (children.size() == 0)) {
|
if ((content == null) && (children.size() == 0)) {
|
||||||
Tag emptyTag = Tag.empty(name);
|
Tag emptyTag = Tag.empty(name);
|
||||||
emptyTag.setAtttributes(this.attributes);
|
emptyTag.setAtttributes(this.attributes);
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.webrtc.IceCandidate;
|
||||||
import org.webrtc.PeerConnection;
|
import org.webrtc.PeerConnection;
|
||||||
import org.webrtc.VideoTrack;
|
import org.webrtc.VideoTrack;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -142,7 +141,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
}
|
}
|
||||||
|
|
||||||
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
|
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
|
||||||
private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>();
|
//TODO convert to Queue<Map.Entry<String, Description>>?
|
||||||
|
private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>> pendingIceCandidates = new LinkedList<>();
|
||||||
private final OmemoVerification omemoVerification = new OmemoVerification();
|
private final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
private final Message message;
|
private final Message message;
|
||||||
private State state = State.NULL;
|
private State state = State.NULL;
|
||||||
|
@ -193,7 +193,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
|
|
||||||
switch (jinglePacket.getAction()) {
|
switch (jinglePacket.getAction()) {
|
||||||
case SESSION_INITIATE:
|
case SESSION_INITIATE:
|
||||||
receiveSessionInitiate(jinglePacket);
|
receiveSessionInitiate(jinglePacket);
|
||||||
|
@ -254,23 +253,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
private void receiveTransportInfo(final JinglePacket jinglePacket) {
|
private void receiveTransportInfo(final JinglePacket jinglePacket) {
|
||||||
//Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received
|
//Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received
|
||||||
if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
|
if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
|
||||||
respondOk(jinglePacket);
|
|
||||||
final RtpContentMap contentMap;
|
final RtpContentMap contentMap;
|
||||||
try {
|
try {
|
||||||
contentMap = RtpContentMap.of(jinglePacket);
|
contentMap = RtpContentMap.of(jinglePacket);
|
||||||
} catch (IllegalArgumentException | NullPointerException e) {
|
} catch (final IllegalArgumentException | NullPointerException e) {
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
|
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
|
||||||
|
respondOk(jinglePacket);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
||||||
if (this.state == State.SESSION_ACCEPTED) {
|
if (this.state == State.SESSION_ACCEPTED) {
|
||||||
|
//zero candidates + modified credentials are an ICE restart offer
|
||||||
|
if (checkForIceRestart(contentMap, jinglePacket)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
respondOk(jinglePacket);
|
||||||
try {
|
try {
|
||||||
processCandidates(candidates);
|
processCandidates(candidates);
|
||||||
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
|
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
|
||||||
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored");
|
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pendingIceCandidates.push(candidates);
|
respondOk(jinglePacket);
|
||||||
|
pendingIceCandidates.addAll(candidates);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isTerminated()) {
|
if (isTerminated()) {
|
||||||
|
@ -283,37 +288,106 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkForIceRestart(final RtpContentMap rtpContentMap, final JinglePacket jinglePacket) {
|
||||||
|
final RtpContentMap existing = getRemoteContentMap();
|
||||||
|
final Map<String, IceUdpTransportInfo.Credentials> existingCredentials = existing.getCredentials();
|
||||||
|
final Map<String, IceUdpTransportInfo.Credentials> newCredentials = rtpContentMap.getCredentials();
|
||||||
|
if (!existingCredentials.keySet().equals(newCredentials.keySet())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (existingCredentials.equals(newCredentials)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final boolean isOffer = rtpContentMap.emptyCandidates();
|
||||||
|
Log.d(Config.LOGTAG, "detected ICE restart. offer=" + isOffer + " " + jinglePacket);
|
||||||
|
//TODO reset to 'actpass'?
|
||||||
|
final RtpContentMap restartContentMap = existing.modifiedCredentials(newCredentials);
|
||||||
|
try {
|
||||||
|
if (applyIceRestart(isOffer, restartContentMap)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "responding with tie break");
|
||||||
|
//TODO respond with conflict
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d(Config.LOGTAG, "failure to apply ICE restart. sending error", e);
|
||||||
|
//TODO send some kind of error
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyIceRestart(final boolean isOffer, final RtpContentMap restartContentMap) throws ExecutionException, InterruptedException {
|
||||||
|
final SessionDescription sessionDescription = SessionDescription.of(restartContentMap);
|
||||||
|
final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER;
|
||||||
|
org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString());
|
||||||
|
if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) {
|
||||||
|
if (isInitiator()) {
|
||||||
|
//We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//rollback our own local description. should happen automatically but doesn't
|
||||||
|
webRTCWrapper.rollbackLocalDescription().get();
|
||||||
|
}
|
||||||
|
webRTCWrapper.setRemoteDescription(sdp).get();
|
||||||
|
if (isInitiator()) {
|
||||||
|
this.responderRtpContentMap = restartContentMap;
|
||||||
|
} else {
|
||||||
|
this.initiatorRtpContentMap = restartContentMap;
|
||||||
|
}
|
||||||
|
if (isOffer) {
|
||||||
|
webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
|
||||||
|
final SessionDescription localSessionDescription = setLocalSessionDescription();
|
||||||
|
setLocalContentMap(RtpContentMap.of(localSessionDescription));
|
||||||
|
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void processCandidates(final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
private void processCandidates(final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
||||||
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
|
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
||||||
|
processCandidate(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCandidate(final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
|
||||||
|
final RtpContentMap rtpContentMap = getRemoteContentMap();
|
||||||
|
final List<String> indices = toIdentificationTags(rtpContentMap);
|
||||||
|
final String sdpMid = content.getKey(); //aka content name
|
||||||
|
final IceUdpTransportInfo transport = content.getValue().transport;
|
||||||
|
final IceUdpTransportInfo.Credentials credentials = transport.getCredentials();
|
||||||
|
|
||||||
|
//TODO check that credentials remained the same
|
||||||
|
|
||||||
|
for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) {
|
||||||
|
final String sdp;
|
||||||
|
try {
|
||||||
|
sdp = candidate.toSdpAttribute(credentials.ufrag);
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int mLineIndex = indices.indexOf(sdpMid);
|
||||||
|
if (mLineIndex < 0) {
|
||||||
|
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
|
||||||
|
}
|
||||||
|
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
|
||||||
|
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
|
||||||
|
this.webRTCWrapper.addIceCandidate(iceCandidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RtpContentMap getRemoteContentMap() {
|
||||||
|
return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> toIdentificationTags(final RtpContentMap rtpContentMap) {
|
||||||
final Group originalGroup = rtpContentMap.group;
|
final Group originalGroup = rtpContentMap.group;
|
||||||
final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags();
|
final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags();
|
||||||
if (identificationTags.size() == 0) {
|
if (identificationTags.size() == 0) {
|
||||||
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
|
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
|
||||||
}
|
}
|
||||||
processCandidates(identificationTags, contents);
|
return identificationTags;
|
||||||
}
|
|
||||||
|
|
||||||
private void processCandidates(final List<String> indices, final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
|
||||||
final String ufrag = content.getValue().transport.getAttribute("ufrag");
|
|
||||||
for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) {
|
|
||||||
final String sdp;
|
|
||||||
try {
|
|
||||||
sdp = candidate.toSdpAttribute(ufrag);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String sdpMid = content.getKey();
|
|
||||||
final int mLineIndex = indices.indexOf(sdpMid);
|
|
||||||
if (mLineIndex < 0) {
|
|
||||||
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
|
|
||||||
}
|
|
||||||
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
|
|
||||||
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
|
|
||||||
this.webRTCWrapper.addIceCandidate(iceCandidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
|
private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
|
||||||
|
@ -401,11 +475,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
}
|
}
|
||||||
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
|
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
|
||||||
respondOk(jinglePacket);
|
respondOk(jinglePacket);
|
||||||
|
pendingIceCandidates.addAll(contentMap.contents.entrySet());
|
||||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
|
||||||
if (candidates.size() > 0) {
|
|
||||||
pendingIceCandidates.push(candidates);
|
|
||||||
}
|
|
||||||
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
|
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
|
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
|
||||||
sendSessionAccept();
|
sendSessionAccept();
|
||||||
|
@ -495,8 +565,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
sendSessionTerminate(Reason.FAILED_APPLICATION);
|
sendSessionTerminate(Reason.FAILED_APPLICATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final List<String> identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags();
|
processCandidates(contentMap.contents.entrySet());
|
||||||
processCandidates(identificationTags, contentMap.contents.entrySet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSessionAccept() {
|
private void sendSessionAccept() {
|
||||||
|
@ -558,9 +627,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addIceCandidatesFromBlackLog() {
|
private void addIceCandidatesFromBlackLog() {
|
||||||
while (!this.pendingIceCandidates.isEmpty()) {
|
Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
|
||||||
processCandidates(this.pendingIceCandidates.poll());
|
while ((foo = this.pendingIceCandidates.poll()) != null) {
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log");
|
processCandidate(foo);
|
||||||
|
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1335,7 +1405,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onIceCandidate(final IceCandidate iceCandidate) {
|
public void onIceCandidate(final IceCandidate iceCandidate) {
|
||||||
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp);
|
final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
|
||||||
|
final Collection<String> currentUfrags = Collections2.transform(rtpContentMap.getCredentials().values(), c -> c.ufrag);
|
||||||
|
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, currentUfrags);
|
||||||
|
if (candidate == null) {
|
||||||
|
Log.d(Config.LOGTAG,"ignoring (not sending) candidate: "+iceCandidate.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString());
|
Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString());
|
||||||
sendTransportInfo(iceCandidate.sdpMid, candidate);
|
sendTransportInfo(iceCandidate.sdpMid, candidate);
|
||||||
}
|
}
|
||||||
|
@ -1373,23 +1449,42 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
@Override
|
@Override
|
||||||
public void onRenegotiationNeeded() {
|
public void onRenegotiationNeeded() {
|
||||||
Log.d(Config.LOGTAG, "onRenegotiationNeeded()");
|
Log.d(Config.LOGTAG, "onRenegotiationNeeded()");
|
||||||
this.webRTCWrapper.execute(this::renegotiate);
|
this.webRTCWrapper.execute(this::initiateIceRestart);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renegotiate() {
|
private void initiateIceRestart() {
|
||||||
|
PeerConnection.SignalingState signalingState = webRTCWrapper.getSignalingState();
|
||||||
|
Log.d(Config.LOGTAG, "initiateIceRestart() - " + signalingState);
|
||||||
|
if (signalingState != PeerConnection.SignalingState.STABLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
|
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
|
||||||
|
final SessionDescription sessionDescription;
|
||||||
try {
|
try {
|
||||||
final SessionDescription sessionDescription = setLocalSessionDescription();
|
sessionDescription = setLocalSessionDescription();
|
||||||
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
|
|
||||||
setRenegotiatedContentMap(rtpContentMap);
|
|
||||||
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.d(Config.LOGTAG, "failed to renegotiate", e);
|
Log.d(Config.LOGTAG, "failed to renegotiate", e);
|
||||||
//TODO send some sort of failure (comparable to when initiating)
|
//TODO send some sort of failure (comparable to when initiating)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
|
||||||
|
final RtpContentMap transportInfo = rtpContentMap.transportInfo();
|
||||||
|
final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
|
||||||
|
Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
|
||||||
|
jinglePacket.setTo(id.with);
|
||||||
|
xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> {
|
||||||
|
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||||
|
Log.d(Config.LOGTAG, "received success to our ice restart");
|
||||||
|
setLocalContentMap(rtpContentMap);
|
||||||
|
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "received failure to our ice restart");
|
||||||
|
//TODO handle tie-break. Rollback?
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRenegotiatedContentMap(final RtpContentMap rtpContentMap) {
|
private void setLocalContentMap(final RtpContentMap rtpContentMap) {
|
||||||
if (isInitiator()) {
|
if (isInitiator()) {
|
||||||
this.initiatorRtpContentMap = rtpContentMap;
|
this.initiatorRtpContentMap = rtpContentMap;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
@ -17,9 +15,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||||
|
@ -137,7 +135,37 @@ public class RtpContentMap {
|
||||||
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
|
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
|
||||||
newTransportInfo.addChild(candidate);
|
newTransportInfo.addChild(candidate);
|
||||||
return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
|
return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
RtpContentMap transportInfo() {
|
||||||
|
return new RtpContentMap(
|
||||||
|
null,
|
||||||
|
Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, IceUdpTransportInfo.Credentials> getCredentials() {
|
||||||
|
return Maps.transformValues(contents, dt -> dt.transport.getCredentials());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean emptyCandidates() {
|
||||||
|
int count = 0;
|
||||||
|
for (DescriptionTransport descriptionTransport : contents.values()) {
|
||||||
|
count += descriptionTransport.transport.getCandidates().size();
|
||||||
|
}
|
||||||
|
return count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RtpContentMap modifiedCredentials(Map<String, IceUdpTransportInfo.Credentials> credentialsMap) {
|
||||||
|
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
|
||||||
|
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
||||||
|
final RtpDescription rtpDescription = content.getValue().description;
|
||||||
|
IceUdpTransportInfo transportInfo = content.getValue().transport;
|
||||||
|
final IceUdpTransportInfo.Credentials credentials = Objects.requireNonNull(credentialsMap.get(content.getKey()));
|
||||||
|
final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials);
|
||||||
|
contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
|
||||||
|
}
|
||||||
|
return new RtpContentMap(this.group, contentMapBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DescriptionTransport {
|
public static class DescriptionTransport {
|
||||||
|
|
|
@ -17,7 +17,6 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
import org.webrtc.AudioSource;
|
import org.webrtc.AudioSource;
|
||||||
import org.webrtc.AudioTrack;
|
import org.webrtc.AudioTrack;
|
||||||
import org.webrtc.Camera1Enumerator;
|
|
||||||
import org.webrtc.Camera2Enumerator;
|
import org.webrtc.Camera2Enumerator;
|
||||||
import org.webrtc.CameraEnumerationAndroid;
|
import org.webrtc.CameraEnumerationAndroid;
|
||||||
import org.webrtc.CameraEnumerator;
|
import org.webrtc.CameraEnumerator;
|
||||||
|
@ -87,6 +86,7 @@ public class WebRTCWrapper {
|
||||||
|
|
||||||
private final EventCallback eventCallback;
|
private final EventCallback eventCallback;
|
||||||
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
|
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean ignoreOnRenegotiationNeeded = new AtomicBoolean(false);
|
||||||
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
|
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
|
||||||
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
|
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,6 +163,10 @@ public class WebRTCWrapper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRenegotiationNeeded() {
|
public void onRenegotiationNeeded() {
|
||||||
|
if (ignoreOnRenegotiationNeeded.get()) {
|
||||||
|
Log.d(EXTENDED_LOGGING_TAG, "ignoring onRenegotiationNeeded()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
|
Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
|
||||||
final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState();
|
final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState();
|
||||||
if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) {
|
if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) {
|
||||||
|
@ -307,12 +311,12 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
void restartIce() {
|
void restartIce() {
|
||||||
executorService.execute(()-> requirePeerConnection().restartIce());
|
executorService.execute(() -> requirePeerConnection().restartIce());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
|
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
|
||||||
readyToReceivedIceCandidates.set(ready);
|
readyToReceivedIceCandidates.set(ready);
|
||||||
while(ready && iceCandidates.peek() != null) {
|
while (ready && iceCandidates.peek() != null) {
|
||||||
eventCallback.onIceCandidate(iceCandidates.poll());
|
eventCallback.onIceCandidate(iceCandidates.poll());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,6 +456,26 @@ public class WebRTCWrapper {
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> rollbackLocalDescription() {
|
||||||
|
final SettableFuture<Void> future = SettableFuture.create();
|
||||||
|
final SessionDescription rollback = new SessionDescription(SessionDescription.Type.ROLLBACK, "");
|
||||||
|
ignoreOnRenegotiationNeeded.set(true);
|
||||||
|
requirePeerConnection().setLocalDescription(new SetSdpObserver() {
|
||||||
|
@Override
|
||||||
|
public void onSetSuccess() {
|
||||||
|
future.set(null);
|
||||||
|
ignoreOnRenegotiationNeeded.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSetFailure(final String message) {
|
||||||
|
future.setException(new FailureToSetDescriptionException(message));
|
||||||
|
}
|
||||||
|
}, rollback);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void logDescription(final SessionDescription sessionDescription) {
|
private static void logDescription(final SessionDescription sessionDescription) {
|
||||||
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
|
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
|
||||||
Log.d(EXTENDED_LOGGING_TAG, line);
|
Log.d(EXTENDED_LOGGING_TAG, line);
|
||||||
|
@ -552,6 +576,10 @@ public class WebRTCWrapper {
|
||||||
executorService.execute(command);
|
executorService.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PeerConnection.SignalingState getSignalingState() {
|
||||||
|
return requirePeerConnection().signalingState();
|
||||||
|
}
|
||||||
|
|
||||||
public interface EventCallback {
|
public interface EventCallback {
|
||||||
void onIceCandidate(IceCandidate iceCandidate);
|
void onIceCandidate(IceCandidate iceCandidate);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
@ -8,6 +9,7 @@ import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
@ -58,6 +60,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
|
return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Credentials getCredentials() {
|
||||||
|
final String ufrag = this.getAttribute("ufrag");
|
||||||
|
final String password = this.getAttribute("pwd");
|
||||||
|
return new Credentials(ufrag, password);
|
||||||
|
}
|
||||||
|
|
||||||
public List<Candidate> getCandidates() {
|
public List<Candidate> getCandidates() {
|
||||||
final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
|
||||||
for (final Element child : getChildren()) {
|
for (final Element child : getChildren()) {
|
||||||
|
@ -74,6 +82,37 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
return transportInfo;
|
return transportInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IceUdpTransportInfo modifyCredentials(Credentials credentials) {
|
||||||
|
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
|
||||||
|
transportInfo.setAttribute("ufrag", credentials.ufrag);
|
||||||
|
transportInfo.setAttribute("pwd", credentials.password);
|
||||||
|
transportInfo.setChildren(getChildren());
|
||||||
|
return transportInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Credentials {
|
||||||
|
public final String ufrag;
|
||||||
|
public final String password;
|
||||||
|
|
||||||
|
public Credentials(String ufrag, String password) {
|
||||||
|
this.ufrag = ufrag;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Credentials that = (Credentials) o;
|
||||||
|
return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(ufrag, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class Candidate extends Element {
|
public static class Candidate extends Element {
|
||||||
|
|
||||||
private Candidate() {
|
private Candidate() {
|
||||||
|
@ -89,7 +128,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
|
// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
|
||||||
public static Candidate fromSdpAttribute(final String attribute) {
|
public static Candidate fromSdpAttribute(final String attribute, Collection<String> currentUfrags) {
|
||||||
final String[] pair = attribute.split(":", 2);
|
final String[] pair = attribute.split(":", 2);
|
||||||
if (pair.length == 2 && "candidate".equals(pair[0])) {
|
if (pair.length == 2 && "candidate".equals(pair[0])) {
|
||||||
final String[] segments = pair[1].split(" ");
|
final String[] segments = pair[1].split(" ");
|
||||||
|
@ -105,6 +144,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
for (int i = 6; i < segments.length - 1; i = i + 2) {
|
for (int i = 6; i < segments.length - 1; i = i + 2) {
|
||||||
additional.put(segments[i], segments[i + 1]);
|
additional.put(segments[i], segments[i + 1]);
|
||||||
}
|
}
|
||||||
|
final String ufrag = additional.get("ufrag");
|
||||||
|
if (ufrag != null && !currentUfrags.contains(ufrag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final Candidate candidate = new Candidate();
|
final Candidate candidate = new Candidate();
|
||||||
candidate.setAttribute("component", component);
|
candidate.setAttribute("component", component);
|
||||||
candidate.setAttribute("foundation", foundation);
|
candidate.setAttribute("foundation", foundation);
|
||||||
|
|
Loading…
Reference in a new issue