1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4
5import com.google.common.base.Function;
6import com.google.common.base.Preconditions;
7import com.google.common.base.Strings;
8import com.google.common.collect.Collections2;
9import com.google.common.collect.ImmutableMap;
10import com.google.common.collect.Iterables;
11import com.google.common.collect.Maps;
12import com.google.common.collect.Sets;
13
14import org.checkerframework.checker.nullness.compatqual.NullableDecl;
15
16import java.util.Map;
17import java.util.Set;
18
19import eu.siacs.conversations.Config;
20import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
21import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
22import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
23import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
24import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
25import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
26import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
27import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
28
29public class RtpContentMap {
30
31 public final Group group;
32 public final Map<String, DescriptionTransport> contents;
33
34 private RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
35 this.group = group;
36 this.contents = contents;
37 }
38
39 public static RtpContentMap of(final JinglePacket jinglePacket) {
40 return new RtpContentMap(jinglePacket.getGroup(), DescriptionTransport.of(jinglePacket.getJingleContents()));
41 }
42
43 public static RtpContentMap of(final SessionDescription sessionDescription) {
44 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
45 for (SessionDescription.Media media : sessionDescription.media) {
46 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
47 Preconditions.checkNotNull(id, "media has no mid");
48 contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
49 }
50 final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null);
51 final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
52 return new RtpContentMap(group, contentMapBuilder.build());
53 }
54
55 public Set<Media> getMedia() {
56 return Sets.newHashSet(Collections2.transform(contents.values(), input -> {
57 final RtpDescription rtpDescription = input == null ? null : input.description;
58 return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia();
59 }));
60 }
61
62 void requireContentDescriptions() {
63 if (this.contents.size() == 0) {
64 throw new IllegalStateException("No contents available");
65 }
66 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
67 if (entry.getValue().description == null) {
68 throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey()));
69 }
70 }
71 }
72
73 void requireDTLSFingerprint() {
74 if (this.contents.size() == 0) {
75 throw new IllegalStateException("No contents available");
76 }
77 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
78 final IceUdpTransportInfo transport = entry.getValue().transport;
79 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
80 if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
81 throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
82 }
83 if (Strings.isNullOrEmpty(fingerprint.getSetup())) {
84 throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
85 }
86 }
87 }
88
89 JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
90 final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
91 if (this.group != null) {
92 jinglePacket.addGroup(this.group);
93 }
94 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
95 final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
96 if (entry.getValue().description != null) {
97 content.addChild(entry.getValue().description);
98 }
99 content.addChild(entry.getValue().transport);
100 jinglePacket.addJingleContent(content);
101 }
102 return jinglePacket;
103 }
104
105 RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) {
106 final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
107 final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport;
108 if (transportInfo == null) {
109 throw new IllegalArgumentException("Unable to find transport info for content name " + contentName);
110 }
111 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
112 newTransportInfo.addChild(candidate);
113 return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
114
115 }
116
117 public static class DescriptionTransport {
118 public final RtpDescription description;
119 public final IceUdpTransportInfo transport;
120
121 DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
122 this.description = description;
123 this.transport = transport;
124 }
125
126 public static DescriptionTransport of(final Content content) {
127 final GenericDescription description = content.getDescription();
128 final GenericTransportInfo transportInfo = content.getTransport();
129 final RtpDescription rtpDescription;
130 final IceUdpTransportInfo iceUdpTransportInfo;
131 if (description == null) {
132 rtpDescription = null;
133 } else if (description instanceof RtpDescription) {
134 rtpDescription = (RtpDescription) description;
135 } else {
136 Log.d(Config.LOGTAG, "description was " + description);
137 throw new UnsupportedApplicationException("Content does not contain rtp description");
138 }
139 if (transportInfo instanceof IceUdpTransportInfo) {
140 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
141 } else {
142 throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
143 }
144 return new DescriptionTransport(rtpDescription, iceUdpTransportInfo);
145 }
146
147 public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
148 final RtpDescription rtpDescription = RtpDescription.of(media);
149 final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
150 return new DescriptionTransport(rtpDescription, transportInfo);
151 }
152
153 public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
154 return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
155 @NullableDecl
156 @Override
157 public DescriptionTransport apply(@NullableDecl Content content) {
158 return content == null ? null : of(content);
159 }
160 }));
161 }
162 }
163
164 public static class UnsupportedApplicationException extends IllegalArgumentException {
165 UnsupportedApplicationException(String message) {
166 super(message);
167 }
168 }
169
170 public static class UnsupportedTransportException extends IllegalArgumentException {
171 UnsupportedTransportException(String message) {
172 super(message);
173 }
174 }
175}