take senders attr into account when converting to and from sdp

This commit is contained in:
Daniel Gultsch 2022-11-20 17:00:40 +01:00
parent 59ea66ca78
commit 304205b2e3
6 changed files with 274 additions and 107 deletions

View file

@ -1272,7 +1272,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} }
descriptionTransportBuilder.put( descriptionTransportBuilder.put(
content.getKey(), content.getKey(),
new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
); );
} }
return Futures.immediateFuture( return Futures.immediateFuture(
@ -1306,7 +1306,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
omemoVerification.setOrEnsureEqual(decryptedTransport); omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put( descriptionTransportBuilder.put(
content.getKey(), content.getKey(),
new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload) new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
); );
} }
processPostponed(); processPostponed();

View file

@ -577,8 +577,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendInitRequest() { private void sendInitRequest() {
final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
final Content content = new Content(this.contentCreator, this.contentName); final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
@ -656,8 +655,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
gatherAndConnectDirectCandidates(); gatherAndConnectDirectCandidates();
this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
final Content content = new Content(contentCreator, contentName); final Content content = new Content(contentCreator, contentSenders, contentName);
content.setSenders(this.contentSenders);
content.setDescription(this.description); content.setDescription(this.description);
if (success && candidate != null && !equalCandidateExists(candidate)) { if (success && candidate != null && !equalCandidateExists(candidate)) {
final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
@ -696,8 +694,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendAcceptIbb() { private void sendAcceptIbb() {
this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
final Content content = new Content(contentCreator, contentName); final Content content = new Content(contentCreator, contentSenders, contentName);
content.setSenders(this.contentSenders);
content.setDescription(this.description); content.setDescription(this.description);
content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
packet.addJingleContent(content); packet.addJingleContent(content);
@ -910,8 +907,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendFallbackToIbb() { private void sendFallbackToIbb() {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
final Content content = new Content(this.contentCreator, this.contentName); final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
this.transportId = JingleConnectionManager.nextRandomId(); this.transportId = JingleConnectionManager.nextRandomId();
content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
packet.addJingleContent(content); packet.addJingleContent(content);
@ -944,8 +940,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
final Content content = new Content(contentCreator, contentName); final Content content = new Content(contentCreator, contentSenders, contentName);
content.setSenders(this.contentSenders);
content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
answer.addJingleContent(content); answer.addJingleContent(content);
@ -1124,8 +1119,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendProxyActivated(String cid) { private void sendProxyActivated(String cid) {
final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
final Content content = new Content(this.contentCreator, this.contentName); final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
packet.addJingleContent(content); packet.addJingleContent(content);
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
@ -1133,8 +1127,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendProxyError() { private void sendProxyError() {
final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
final Content content = new Content(this.contentCreator, this.contentName); final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
packet.addJingleContent(content); packet.addJingleContent(content);
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
@ -1142,8 +1135,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendCandidateUsed(final String cid) { private void sendCandidateUsed(final String cid) {
JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
final Content content = new Content(this.contentCreator, this.contentName); final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
packet.addJingleContent(content); packet.addJingleContent(content);
this.sentCandidate = true; this.sentCandidate = true;
@ -1156,8 +1148,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
private void sendCandidateError() { private void sendCandidateError() {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
Content content = new Content(this.contentCreator, this.contentName); Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
content.setSenders(this.contentSenders);
content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
packet.addJingleContent(content); packet.addJingleContent(content);
this.sentCandidate = true; this.sentCandidate = true;

View file

@ -425,7 +425,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap restartContentMap, final RtpContentMap restartContentMap,
final boolean isOffer) final boolean isOffer)
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator());
final org.webrtc.SessionDescription.Type type = final org.webrtc.SessionDescription.Type type =
isOffer isOffer
? org.webrtc.SessionDescription.Type.OFFER ? org.webrtc.SessionDescription.Type.OFFER
@ -444,7 +444,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
if (isOffer) { if (isOffer) {
webRTCWrapper.setIsReadyToReceiveIceCandidates(false); webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
final SessionDescription localSessionDescription = setLocalSessionDescription(); final SessionDescription localSessionDescription = setLocalSessionDescription();
setLocalContentMap(RtpContentMap.of(localSessionDescription)); setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator()));
// We need to respond OK before sending any candidates // We need to respond OK before sending any candidates
respondOk(jinglePacket); respondOk(jinglePacket);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true); webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@ -726,7 +726,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.storePeerDtlsSetup(contentMap.getDtlsSetup()); this.storePeerDtlsSetup(contentMap.getDtlsSetup());
final SessionDescription sessionDescription; final SessionDescription sessionDescription;
try { try {
sessionDescription = SessionDescription.of(contentMap); sessionDescription = SessionDescription.of(contentMap, false);
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -763,7 +763,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
final SessionDescription offer; final SessionDescription offer;
try { try {
offer = SessionDescription.of(rtpContentMap); offer = SessionDescription.of(rtpContentMap, true);
} catch (final IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -838,7 +838,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final org.webrtc.SessionDescription webRTCSessionDescription) { final org.webrtc.SessionDescription webRTCSessionDescription) {
final SessionDescription sessionDescription = final SessionDescription sessionDescription =
SessionDescription.parse(webRTCSessionDescription.description); SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false);
this.responderRtpContentMap = respondingRtpContentMap; this.responderRtpContentMap = respondingRtpContentMap;
storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
webRTCWrapper.setIsReadyToReceiveIceCandidates(true); webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@ -1289,7 +1289,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
final SessionDescription sessionDescription = final SessionDescription sessionDescription =
SessionDescription.parse(webRTCSessionDescription.description); SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true);
this.initiatorRtpContentMap = rtpContentMap; this.initiatorRtpContentMap = rtpContentMap;
//TODO delay ready to receive ice until after session-init //TODO delay ready to receive ice until after session-init
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
@ -1922,7 +1922,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return; return;
} }
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator());
final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final RtpContentMap transportInfo = rtpContentMap.transportInfo();
final JinglePacket jinglePacket = final JinglePacket jinglePacket =
transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import com.google.common.base.MoreObjects;
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.Collections2; import com.google.common.collect.Collections2;
@ -15,6 +16,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nonnull;
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;
@ -58,13 +61,15 @@ public class RtpContentMap {
return true; return true;
} }
public static RtpContentMap of(final SessionDescription sessionDescription) { public static RtpContentMap of(
final SessionDescription sessionDescription, final boolean isInitiator) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
new ImmutableMap.Builder<>(); new ImmutableMap.Builder<>();
for (SessionDescription.Media media : sessionDescription.media) { for (SessionDescription.Media media : sessionDescription.media) {
final String id = Iterables.getFirst(media.attributes.get("mid"), null); final String id = Iterables.getFirst(media.attributes.get("mid"), null);
Preconditions.checkNotNull(id, "media has no mid"); Preconditions.checkNotNull(id, "media has no mid");
contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); contentMapBuilder.put(
id, DescriptionTransport.of(sessionDescription, isInitiator, media));
} }
final String groupAttribute = final String groupAttribute =
Iterables.getFirst(sessionDescription.attributes.get("group"), null); Iterables.getFirst(sessionDescription.attributes.get("group"), null);
@ -140,11 +145,16 @@ public class RtpContentMap {
jinglePacket.addGroup(this.group); jinglePacket.addGroup(this.group);
} }
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) { for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); final DescriptionTransport descriptionTransport = entry.getValue();
if (entry.getValue().description != null) { final Content content =
content.addChild(entry.getValue().description); new Content(
Content.Creator.INITIATOR,
descriptionTransport.senders,
entry.getKey());
if (descriptionTransport.description != null) {
content.addChild(descriptionTransport.description);
} }
content.addChild(entry.getValue().transport); content.addChild(descriptionTransport.transport);
jinglePacket.addJingleContent(content); jinglePacket.addJingleContent(content);
} }
return jinglePacket; return jinglePacket;
@ -163,7 +173,10 @@ public class RtpContentMap {
newTransportInfo.addChild(candidate); newTransportInfo.addChild(candidate);
return new RtpContentMap( return new RtpContentMap(
null, null,
ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); ImmutableMap.of(
contentName,
new DescriptionTransport(
descriptionTransport.senders, null, newTransportInfo)));
} }
RtpContentMap transportInfo() { RtpContentMap transportInfo() {
@ -171,7 +184,9 @@ public class RtpContentMap {
null, null,
Maps.transformValues( Maps.transformValues(
contents, contents,
dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); dt ->
new DescriptionTransport(
dt.senders, null, dt.transport.cloneWrapper())));
} }
public IceUdpTransportInfo.Credentials getDistinctCredentials() { public IceUdpTransportInfo.Credentials getDistinctCredentials() {
@ -179,7 +194,8 @@ public class RtpContentMap {
final IceUdpTransportInfo.Credentials credentials = final IceUdpTransportInfo.Credentials credentials =
Iterables.getFirst(allCredentials, null); Iterables.getFirst(allCredentials, null);
if (allCredentials.size() == 1 && credentials != null) { if (allCredentials.size() == 1 && credentials != null) {
if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { if (Strings.isNullOrEmpty(credentials.password)
|| Strings.isNullOrEmpty(credentials.ufrag)) {
throw new IllegalStateException("Credentials are missing password or ufrag"); throw new IllegalStateException("Credentials are missing password or ufrag");
} }
return credentials; return credentials;
@ -233,23 +249,45 @@ public class RtpContentMap {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
new ImmutableMap.Builder<>(); new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) { for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
final RtpDescription rtpDescription = content.getValue().description; final DescriptionTransport descriptionTransport = content.getValue();
IceUdpTransportInfo transportInfo = content.getValue().transport; final RtpDescription rtpDescription = descriptionTransport.description;
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
final IceUdpTransportInfo modifiedTransportInfo = final IceUdpTransportInfo modifiedTransportInfo =
transportInfo.modifyCredentials(credentials, setup); transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put( contentMapBuilder.put(
content.getKey(), content.getKey(),
new DescriptionTransport(rtpDescription, modifiedTransportInfo)); new DescriptionTransport(
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
} }
return new RtpContentMap(this.group, contentMapBuilder.build()); return new RtpContentMap(this.group, contentMapBuilder.build());
} }
public Diff diff(final RtpContentMap rtpContentMap) {
final Set<String> existingContentIds = this.contents.keySet();
final Set<String> newContentIds = rtpContentMap.contents.keySet();
return new Diff(
Sets.difference(newContentIds, existingContentIds),
Sets.difference(existingContentIds, newContentIds));
}
public boolean iceRestart(final RtpContentMap rtpContentMap) {
try {
return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
} catch (final IllegalStateException e) {
return false;
}
}
public static class DescriptionTransport { public static class DescriptionTransport {
public final Content.Senders senders;
public final RtpDescription description; public final RtpDescription description;
public final IceUdpTransportInfo transport; public final IceUdpTransportInfo transport;
public DescriptionTransport( public DescriptionTransport(
final RtpDescription description, final IceUdpTransportInfo transport) { final Content.Senders senders,
final RtpDescription description,
final IceUdpTransportInfo transport) {
this.senders = senders;
this.description = description; this.description = description;
this.transport = transport; this.transport = transport;
} }
@ -257,6 +295,7 @@ public class RtpContentMap {
public static DescriptionTransport of(final Content content) { public static DescriptionTransport of(final Content content) {
final GenericDescription description = content.getDescription(); final GenericDescription description = content.getDescription();
final GenericTransportInfo transportInfo = content.getTransport(); final GenericTransportInfo transportInfo = content.getTransport();
final Content.Senders senders = content.getSenders();
final RtpDescription rtpDescription; final RtpDescription rtpDescription;
final IceUdpTransportInfo iceUdpTransportInfo; final IceUdpTransportInfo iceUdpTransportInfo;
if (description == null) { if (description == null) {
@ -274,22 +313,26 @@ public class RtpContentMap {
"Content does not contain ICE-UDP transport"); "Content does not contain ICE-UDP transport");
} }
return new DescriptionTransport( return new DescriptionTransport(
rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); senders,
rtpDescription,
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
} }
public static DescriptionTransport of( private static DescriptionTransport of(
final SessionDescription sessionDescription, final SessionDescription.Media media) { final SessionDescription sessionDescription,
final boolean isInitiator,
final SessionDescription.Media media) {
final Content.Senders senders = Content.Senders.of(media, isInitiator);
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
final IceUdpTransportInfo transportInfo = final IceUdpTransportInfo transportInfo =
IceUdpTransportInfo.of(sessionDescription, media); IceUdpTransportInfo.of(sessionDescription, media);
return new DescriptionTransport(rtpDescription, transportInfo); return new DescriptionTransport(senders, rtpDescription, transportInfo);
} }
public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) { public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
return ImmutableMap.copyOf( return ImmutableMap.copyOf(
Maps.transformValues( Maps.transformValues(
contents, contents, content -> content == null ? null : of(content)));
content -> content == null ? null : of(content)));
} }
} }
@ -304,4 +347,27 @@ public class RtpContentMap {
super(message); super(message);
} }
} }
public static final class Diff {
public final Set<String> added;
public final Set<String> removed;
private Diff(final Set<String> added, final Set<String> removed) {
this.added = added;
this.removed = removed;
}
public boolean hasModifications() {
return !this.added.isEmpty() || !this.removed.isEmpty();
}
@Override
@Nonnull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("added", added)
.add("removed", removed)
.toString();
}
}
} }

View file

@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@ -21,11 +23,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class SessionDescription { public class SessionDescription {
public final static String LINE_DIVIDER = "\r\n"; public static final String LINE_DIVIDER = "\r\n";
private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint private static final String HARDCODED_MEDIA_PROTOCOL =
private final static int HARDCODED_MEDIA_PORT = 9; "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
private final static String HARDCODED_ICE_OPTIONS = "trickle"; private static final int HARDCODED_MEDIA_PORT = 9;
private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; private static final String HARDCODED_ICE_OPTIONS = "trickle";
private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0";
public final int version; public final int version;
public final String name; public final String name;
@ -33,8 +36,12 @@ public class SessionDescription {
public final ArrayListMultimap<String, String> attributes; public final ArrayListMultimap<String, String> attributes;
public final List<Media> media; public final List<Media> media;
public SessionDescription(
public SessionDescription(int version, String name, String connectionData, ArrayListMultimap<String, String> attributes, List<Media> media) { int version,
String name,
String connectionData,
ArrayListMultimap<String, String> attributes,
List<Media> media) {
this.version = version; this.version = version;
this.name = name; this.name = name;
this.connectionData = connectionData; this.connectionData = connectionData;
@ -42,7 +49,8 @@ public class SessionDescription {
this.media = media; this.media = media;
} }
private static void appendAttributes(StringBuilder s, ArrayListMultimap<String, String> attributes) { private static void appendAttributes(
StringBuilder s, ArrayListMultimap<String, String> attributes) {
for (Map.Entry<String, String> attribute : attributes.entries()) { for (Map.Entry<String, String> attribute : attributes.entries()) {
final String key = attribute.getKey(); final String key = attribute.getKey();
final String value = attribute.getValue(); final String value = attribute.getValue();
@ -109,7 +117,6 @@ public class SessionDescription {
} }
break; break;
} }
} }
if (currentMediaBuilder != null) { if (currentMediaBuilder != null) {
currentMediaBuilder.setAttributes(attributeMap); currentMediaBuilder.setAttributes(attributeMap);
@ -121,7 +128,7 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription(); return sessionDescriptionBuilder.createSessionDescription();
} }
public static SessionDescription of(final RtpContentMap contentMap) { public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create(); final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>(); final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
@ -129,12 +136,17 @@ public class SessionDescription {
if (group != null) { if (group != null) {
final String semantics = group.getSemantics(); final String semantics = group.getSemantics();
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); attributeMap.put(
"group",
group.getSemantics()
+ " "
+ Joiner.on(' ').join(group.getIdentificationTags()));
} }
attributeMap.put("msid-semantic", " WMS my-media-stream"); attributeMap.put("msid-semantic", " WMS my-media-stream");
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry : contentMap.contents.entrySet()) { for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry :
contentMap.contents.entrySet()) {
final String name = entry.getKey(); final String name = entry.getKey();
RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
RtpDescription description = descriptionTransport.description; RtpDescription description = descriptionTransport.description;
@ -143,19 +155,22 @@ public class SessionDescription {
final String ufrag = transport.getAttribute("ufrag"); final String ufrag = transport.getAttribute("ufrag");
final String pwd = transport.getAttribute("pwd"); final String pwd = transport.getAttribute("pwd");
if (Strings.isNullOrEmpty(ufrag)) { if (Strings.isNullOrEmpty(ufrag)) {
throw new IllegalArgumentException("Transport element is missing required ufrag attribute"); throw new IllegalArgumentException(
"Transport element is missing required ufrag attribute");
} }
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
mediaAttributes.put("ice-ufrag", ufrag); mediaAttributes.put("ice-ufrag", ufrag);
if (Strings.isNullOrEmpty(pwd)) { if (Strings.isNullOrEmpty(pwd)) {
throw new IllegalArgumentException("Transport element is missing required pwd attribute"); throw new IllegalArgumentException(
"Transport element is missing required pwd attribute");
} }
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
mediaAttributes.put("ice-pwd", pwd); mediaAttributes.put("ice-pwd", pwd);
mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint != null) { if (fingerprint != null) {
mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); mediaAttributes.put(
"fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup != null) { if (setup != null) {
mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
@ -174,37 +189,56 @@ public class SessionDescription {
mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); mediaAttributes.put("rtpmap", payloadType.toSdpAttribute());
final List<RtpDescription.Parameter> parameters = payloadType.getParameters(); final List<RtpDescription.Parameter> parameters = payloadType.getParameters();
if (parameters.size() == 1) { if (parameters.size() == 1) {
mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); mediaAttributes.put(
"fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
} else if (parameters.size() > 0) { } else if (parameters.size() > 0) {
mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); mediaAttributes.put(
"fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
} }
for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { for (RtpDescription.FeedbackNegotiation feedbackNegotiation :
payloadType.getFeedbackNegotiations()) {
final String type = feedbackNegotiation.getType(); final String type = feedbackNegotiation.getType();
final String subtype = feedbackNegotiation.getSubType(); final String subtype = feedbackNegotiation.getSubType();
if (Strings.isNullOrEmpty(type)) { if (Strings.isNullOrEmpty(type)) {
throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); throw new IllegalArgumentException(
"a feedback for payload-type "
+ id
+ " negotiation is missing type");
} }
checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); checkNoWhitespace(
mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); type, "feedback negotiation type must not contain whitespace");
mediaAttributes.put(
"rtcp-fb",
id
+ " "
+ type
+ (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
} }
for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); payloadType.feedbackNegotiationTrrInts()) {
mediaAttributes.put(
"rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue());
} }
} }
for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { for (RtpDescription.FeedbackNegotiation feedbackNegotiation :
description.getFeedbackNegotiations()) {
final String type = feedbackNegotiation.getType(); final String type = feedbackNegotiation.getType();
final String subtype = feedbackNegotiation.getSubType(); final String subtype = feedbackNegotiation.getSubType();
if (Strings.isNullOrEmpty(type)) { if (Strings.isNullOrEmpty(type)) {
throw new IllegalArgumentException("a feedback negotiation is missing type"); throw new IllegalArgumentException("a feedback negotiation is missing type");
} }
checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); mediaAttributes.put(
"rtcp-fb",
"* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
} }
for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt :
description.feedbackNegotiationTrrInts()) {
mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue());
} }
for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { for (final RtpDescription.RtpHeaderExtension extension :
description.getHeaderExtensions()) {
final String id = extension.getId(); final String id = extension.getId();
final String uri = extension.getUri(); final String uri = extension.getUri();
if (Strings.isNullOrEmpty(id)) { if (Strings.isNullOrEmpty(id)) {
@ -218,7 +252,8 @@ public class SessionDescription {
mediaAttributes.put("extmap", id + " " + uri); mediaAttributes.put("extmap", id + " " + uri);
} }
if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { if (description.hasChild(
"extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) {
mediaAttributes.put("extmap-allow-mixed", ""); mediaAttributes.put("extmap-allow-mixed", "");
} }
@ -226,13 +261,16 @@ public class SessionDescription {
final String semantics = sourceGroup.getSemantics(); final String semantics = sourceGroup.getSemantics();
final List<String> groups = sourceGroup.getSsrcs(); final List<String> groups = sourceGroup.getSsrcs();
if (Strings.isNullOrEmpty(semantics)) { if (Strings.isNullOrEmpty(semantics)) {
throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); throw new IllegalArgumentException(
"A SSRC group is missing semantics attribute");
} }
checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); checkNoWhitespace(semantics, "source group semantics must not contain whitespace");
if (groups.size() == 0) { if (groups.size() == 0) {
throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
} }
mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); mediaAttributes.put(
"ssrc-group",
String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
} }
for (final RtpDescription.Source source : description.getSources()) { for (final RtpDescription.Source source : description.getSources()) {
for (final RtpDescription.Source.Parameter parameter : source.getParameters()) { for (final RtpDescription.Source.Parameter parameter : source.getParameters()) {
@ -240,14 +278,18 @@ public class SessionDescription {
final String parameterName = parameter.getParameterName(); final String parameterName = parameter.getParameterName();
final String parameterValue = parameter.getParameterValue(); final String parameterValue = parameter.getParameterValue();
if (Strings.isNullOrEmpty(id)) { if (Strings.isNullOrEmpty(id)) {
throw new IllegalArgumentException("A source specific media attribute is missing the id"); throw new IllegalArgumentException(
"A source specific media attribute is missing the id");
} }
checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces"); checkNoWhitespace(
id, "A source specific media attributes must not contain whitespaces");
if (Strings.isNullOrEmpty(parameterName)) { if (Strings.isNullOrEmpty(parameterName)) {
throw new IllegalArgumentException("A source specific media attribute is missing its name"); throw new IllegalArgumentException(
"A source specific media attribute is missing its name");
} }
if (Strings.isNullOrEmpty(parameterValue)) { if (Strings.isNullOrEmpty(parameterValue)) {
throw new IllegalArgumentException("A source specific media attribute is missing its value"); throw new IllegalArgumentException(
"A source specific media attribute is missing its value");
} }
mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue);
} }
@ -255,14 +297,14 @@ public class SessionDescription {
mediaAttributes.put("mid", name); mediaAttributes.put("mid", name);
//random additional attributes mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0");
mediaAttributes.put("sendrecv", "");
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) {
mediaAttributes.put("rtcp-mux", ""); mediaAttributes.put("rtcp-mux", "");
} }
// random additional attributes
mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0");
final MediaBuilder mediaBuilder = new MediaBuilder(); final MediaBuilder mediaBuilder = new MediaBuilder();
mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT));
mediaBuilder.setConnectionData(HARDCODED_CONNECTION); mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
@ -271,7 +313,6 @@ public class SessionDescription {
mediaBuilder.setAttributes(mediaAttributes); mediaBuilder.setAttributes(mediaAttributes);
mediaBuilder.setFormats(formatBuilder.build()); mediaBuilder.setFormats(formatBuilder.build());
mediaListBuilder.add(mediaBuilder.createMedia()); mediaListBuilder.add(mediaBuilder.createMedia());
} }
sessionDescriptionBuilder.setVersion(0); sessionDescriptionBuilder.setVersion(0);
sessionDescriptionBuilder.setName("-"); sessionDescriptionBuilder.setName("-");
@ -317,17 +358,33 @@ public class SessionDescription {
} }
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
final StringBuilder s = new StringBuilder() final StringBuilder s =
.append("v=").append(version).append(LINE_DIVIDER) new StringBuilder()
//TODO randomize or static .append("v=")
.append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means .append(version)
.append("s=").append(name).append(LINE_DIVIDER) .append(LINE_DIVIDER)
.append("t=0 0").append(LINE_DIVIDER); // TODO randomize or static
.append("o=- 8770656990916039506 2 IN IP4 127.0.0.1")
.append(LINE_DIVIDER) // what ever that means
.append("s=")
.append(name)
.append(LINE_DIVIDER)
.append("t=0 0")
.append(LINE_DIVIDER);
appendAttributes(s, attributes); appendAttributes(s, attributes);
for (Media media : this.media) { for (Media media : this.media) {
s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); s.append("m=")
.append(media.media)
.append(' ')
.append(media.port)
.append(' ')
.append(media.protocol)
.append(' ')
.append(Joiner.on(' ').join(media.formats))
.append(LINE_DIVIDER);
s.append("c=").append(media.connectionData).append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
appendAttributes(s, media.attributes); appendAttributes(s, media.attributes);
} }
@ -342,7 +399,13 @@ public class SessionDescription {
public final String connectionData; public final String connectionData;
public final ArrayListMultimap<String, String> attributes; public final ArrayListMultimap<String, String> attributes;
public Media(String media, int port, String protocol, List<Integer> formats, String connectionData, ArrayListMultimap<String, String> attributes) { public Media(
String media,
int port,
String protocol,
List<Integer> formats,
String connectionData,
ArrayListMultimap<String, String> attributes) {
this.media = media; this.media = media;
this.port = port; this.port = port;
this.protocol = protocol; this.protocol = protocol;
@ -351,5 +414,4 @@ public class SessionDescription {
this.attributes = attributes; this.attributes = attributes;
} }
} }
} }

View file

@ -1,20 +1,27 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
public class Content extends Element { public class Content extends Element {
public Content(final Creator creator, final String name) { public Content(final Creator creator, final Senders senders, final String name) {
super("content", Namespace.JINGLE); super("content", Namespace.JINGLE);
this.setAttribute("creator", creator.toString()); this.setAttribute("creator", creator.toString());
this.setAttribute("name", name); this.setAttribute("name", name);
this.setSenders(senders);
} }
private Content() { private Content() {
@ -38,12 +45,18 @@ public class Content extends Element {
} }
public Senders getSenders() { public Senders getSenders() {
final String attribute = getAttribute("senders");
if (Strings.isNullOrEmpty(attribute)) {
return Senders.BOTH;
}
return Senders.of(getAttribute("senders")); return Senders.of(getAttribute("senders"));
} }
public void setSenders(Senders senders) { public void setSenders(final Senders senders) {
if (senders != null && senders != Senders.BOTH) {
this.setAttribute("senders", senders.toString()); this.setAttribute("senders", senders.toString());
} }
}
public GenericDescription getDescription() { public GenericDescription getDescription() {
final Element description = this.findChild("description"); final Element description = this.findChild("description");
@ -51,9 +64,7 @@ public class Content extends Element {
return null; return null;
} }
final String namespace = description.getNamespace(); final String namespace = description.getNamespace();
if (FileTransferDescription.NAMESPACES.contains(namespace)) { if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return FileTransferDescription.upgrade(description);
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return RtpDescription.upgrade(description); return RtpDescription.upgrade(description);
} else { } else {
return GenericDescription.upgrade(description); return GenericDescription.upgrade(description);
@ -73,11 +84,7 @@ public class Content extends Element {
public GenericTransportInfo getTransport() { public GenericTransportInfo getTransport() {
final Element transport = this.findChild("transport"); final Element transport = this.findChild("transport");
final String namespace = transport == null ? null : transport.getNamespace(); final String namespace = transport == null ? null : transport.getNamespace();
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IbbTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
return S5BTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IceUdpTransportInfo.upgrade(transport); return IceUdpTransportInfo.upgrade(transport);
} else if (transport != null) { } else if (transport != null) {
return GenericTransportInfo.upgrade(transport); return GenericTransportInfo.upgrade(transport);
@ -91,7 +98,8 @@ public class Content extends Element {
} }
public enum Creator { public enum Creator {
INITIATOR, RESPONDER; INITIATOR,
RESPONDER;
public static Creator of(final String value) { public static Creator of(final String value) {
return Creator.valueOf(value.toUpperCase(Locale.ROOT)); return Creator.valueOf(value.toUpperCase(Locale.ROOT));
@ -105,16 +113,56 @@ public class Content extends Element {
} }
public enum Senders { public enum Senders {
BOTH, INITIATOR, NONE, RESPONDER; BOTH,
INITIATOR,
NONE,
RESPONDER;
public static Senders of(final String value) { public static Senders of(final String value) {
return Senders.valueOf(value.toUpperCase(Locale.ROOT)); return Senders.valueOf(value.toUpperCase(Locale.ROOT));
} }
public static Senders of(final SessionDescription.Media media, final boolean initiator) {
final Set<String> attributes = media.attributes.keySet();
if (attributes.contains("sendrecv")) {
return BOTH;
} else if (attributes.contains("inactive")) {
return NONE;
} else if (attributes.contains("sendonly")) {
return initiator ? INITIATOR : RESPONDER;
} else if (attributes.contains("recvonly")) {
return initiator ? RESPONDER : INITIATOR;
}
Log.w(Config.LOGTAG,"assuming default value for senders");
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
// present, "sendrecv" SHOULD be assumed as the default
// https://www.rfc-editor.org/rfc/rfc4566
return BOTH;
}
@Override @Override
@NonNull @NonNull
public String toString() { public String toString() {
return super.toString().toLowerCase(Locale.ROOT); return super.toString().toLowerCase(Locale.ROOT);
} }
public String asMediaAttribute(final boolean initiator) {
final boolean responder = !initiator;
if (this == Content.Senders.BOTH) {
return "sendrecv";
} else if (this == Content.Senders.NONE) {
return "inactive";
} else if ((initiator && this == Content.Senders.INITIATOR)
|| (responder && this == Content.Senders.RESPONDER)) {
return "sendonly";
} else if ((initiator && this == Content.Senders.RESPONDER)
|| (responder && this == Content.Senders.INITIATOR)) {
return "recvonly";
} else {
throw new IllegalStateException(
String.format(
"illegal combination of initiator=%s and %s", initiator, this));
}
}
} }
} }