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