1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Log;
4import android.util.Pair;
5
6import com.google.common.base.CharMatcher;
7import com.google.common.base.Joiner;
8import com.google.common.base.Strings;
9import com.google.common.collect.ArrayListMultimap;
10import com.google.common.collect.ImmutableList;
11
12import java.util.List;
13import java.util.Locale;
14import java.util.Map;
15
16import eu.siacs.conversations.Config;
17import eu.siacs.conversations.xml.Namespace;
18import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
19import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
20import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
21
22public class SessionDescription {
23
24 public final static String LINE_DIVIDER = "\r\n";
25 private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint
26 private final static int HARDCODED_MEDIA_PORT = 9;
27 private final static String HARDCODED_ICE_OPTIONS = "trickle";
28 private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0";
29
30 public final int version;
31 public final String name;
32 public final String connectionData;
33 public final ArrayListMultimap<String, String> attributes;
34 public final List<Media> media;
35
36
37 public SessionDescription(int version, String name, String connectionData, ArrayListMultimap<String, String> attributes, List<Media> media) {
38 this.version = version;
39 this.name = name;
40 this.connectionData = connectionData;
41 this.attributes = attributes;
42 this.media = media;
43 }
44
45 private static void appendAttributes(StringBuilder s, ArrayListMultimap<String, String> attributes) {
46 for (Map.Entry<String, String> attribute : attributes.entries()) {
47 final String key = attribute.getKey();
48 final String value = attribute.getValue();
49 s.append("a=").append(key);
50 if (!Strings.isNullOrEmpty(value)) {
51 s.append(':').append(value);
52 }
53 s.append(LINE_DIVIDER);
54 }
55 }
56
57 public static SessionDescription parse(final String input) {
58 final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
59 MediaBuilder currentMediaBuilder = null;
60 ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
61 ImmutableList.Builder<Media> mediaBuilder = new ImmutableList.Builder<>();
62 for (final String line : input.split(LINE_DIVIDER)) {
63 final String[] pair = line.trim().split("=", 2);
64 if (pair.length < 2 || pair[0].length() != 1) {
65 Log.d(Config.LOGTAG, "skipping sdp parsing on line " + line);
66 continue;
67 }
68 final char key = pair[0].charAt(0);
69 final String value = pair[1];
70 switch (key) {
71 case 'v':
72 sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
73 break;
74 case 'c':
75 if (currentMediaBuilder != null) {
76 currentMediaBuilder.setConnectionData(value);
77 } else {
78 sessionDescriptionBuilder.setConnectionData(value);
79 }
80 break;
81 case 's':
82 sessionDescriptionBuilder.setName(value);
83 break;
84 case 'a':
85 final Pair<String, String> attribute = parseAttribute(value);
86 attributeMap.put(attribute.first, attribute.second);
87 break;
88 case 'm':
89 if (currentMediaBuilder == null) {
90 sessionDescriptionBuilder.setAttributes(attributeMap);
91 } else {
92 currentMediaBuilder.setAttributes(attributeMap);
93 mediaBuilder.add(currentMediaBuilder.createMedia());
94 }
95 attributeMap = ArrayListMultimap.create();
96 currentMediaBuilder = new MediaBuilder();
97 final String[] parts = value.split(" ");
98 if (parts.length >= 3) {
99 currentMediaBuilder.setMedia(parts[0]);
100 currentMediaBuilder.setPort(ignorantIntParser(parts[1]));
101 currentMediaBuilder.setProtocol(parts[2]);
102 ImmutableList.Builder<Integer> formats = new ImmutableList.Builder<>();
103 for (int i = 3; i < parts.length; ++i) {
104 formats.add(ignorantIntParser(parts[i]));
105 }
106 currentMediaBuilder.setFormats(formats.build());
107 } else {
108 Log.d(Config.LOGTAG, "skipping media line " + line);
109 }
110 break;
111 }
112
113 }
114 if (currentMediaBuilder != null) {
115 currentMediaBuilder.setAttributes(attributeMap);
116 mediaBuilder.add(currentMediaBuilder.createMedia());
117 } else {
118 sessionDescriptionBuilder.setAttributes(attributeMap);
119 }
120 sessionDescriptionBuilder.setMedia(mediaBuilder.build());
121 return sessionDescriptionBuilder.createSessionDescription();
122 }
123
124 public static SessionDescription of(final RtpContentMap contentMap) {
125 final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
126 final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
127 final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
128 final Group group = contentMap.group;
129 if (group != null) {
130 final String semantics = group.getSemantics();
131 checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
132 attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags()));
133 }
134
135 attributeMap.put("msid-semantic", " WMS my-media-stream");
136
137 for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry : contentMap.contents.entrySet()) {
138 final String name = entry.getKey();
139 RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
140 RtpDescription description = descriptionTransport.description;
141 IceUdpTransportInfo transport = descriptionTransport.transport;
142 final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
143 final String ufrag = transport.getAttribute("ufrag");
144 final String pwd = transport.getAttribute("pwd");
145 if (Strings.isNullOrEmpty(ufrag)) {
146 throw new IllegalArgumentException("Transport element is missing required ufrag attribute");
147 }
148 checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
149 mediaAttributes.put("ice-ufrag", ufrag);
150 if (Strings.isNullOrEmpty(pwd)) {
151 throw new IllegalArgumentException("Transport element is missing required pwd attribute");
152 }
153 checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
154 mediaAttributes.put("ice-pwd", pwd);
155 mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
156 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
157 if (fingerprint != null) {
158 mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
159 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
160 if (setup != null) {
161 mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
162 }
163 }
164 final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
165 for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
166 final String id = payloadType.getId();
167 if (Strings.isNullOrEmpty(id)) {
168 throw new IllegalArgumentException("Payload type is missing id");
169 }
170 if (!isInt(id)) {
171 throw new IllegalArgumentException("Payload id is not numeric");
172 }
173 formatBuilder.add(payloadType.getIntId());
174 mediaAttributes.put("rtpmap", payloadType.toSdpAttribute());
175 final List<RtpDescription.Parameter> parameters = payloadType.getParameters();
176 if (parameters.size() == 1) {
177 mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
178 } else if (parameters.size() > 0) {
179 mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
180 }
181 for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) {
182 final String type = feedbackNegotiation.getType();
183 final String subtype = feedbackNegotiation.getSubType();
184 if (Strings.isNullOrEmpty(type)) {
185 throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type");
186 }
187 checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
188 mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
189 }
190 for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) {
191 mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue());
192 }
193 }
194
195 for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) {
196 final String type = feedbackNegotiation.getType();
197 final String subtype = feedbackNegotiation.getSubType();
198 if (Strings.isNullOrEmpty(type)) {
199 throw new IllegalArgumentException("a feedback negotiation is missing type");
200 }
201 checkNoWhitespace(type, "feedback negotiation type must not contain whitespace");
202 mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype));
203 }
204 for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) {
205 mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue());
206 }
207 for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) {
208 final String id = extension.getId();
209 final String uri = extension.getUri();
210 if (Strings.isNullOrEmpty(id)) {
211 throw new IllegalArgumentException("A header extension is missing id");
212 }
213 checkNoWhitespace(id, "header extension id must not contain whitespace");
214 if (Strings.isNullOrEmpty(uri)) {
215 throw new IllegalArgumentException("A header extension is missing uri");
216 }
217 checkNoWhitespace(uri, "feedback negotiation uri must not contain whitespace");
218 mediaAttributes.put("extmap", id + " " + uri);
219 }
220
221 if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) {
222 mediaAttributes.put("extmap-allow-mixed", "");
223 }
224
225 for (final RtpDescription.SourceGroup sourceGroup : description.getSourceGroups()) {
226 final String semantics = sourceGroup.getSemantics();
227 final List<String> groups = sourceGroup.getSsrcs();
228 if (Strings.isNullOrEmpty(semantics)) {
229 throw new IllegalArgumentException("A SSRC group is missing semantics attribute");
230 }
231 checkNoWhitespace(semantics, "source group semantics must not contain whitespace");
232 if (groups.size() == 0) {
233 throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
234 }
235 mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups)));
236 }
237 for (final RtpDescription.Source source : description.getSources()) {
238 for (final RtpDescription.Source.Parameter parameter : source.getParameters()) {
239 final String id = source.getSsrcId();
240 final String parameterName = parameter.getParameterName();
241 final String parameterValue = parameter.getParameterValue();
242 if (Strings.isNullOrEmpty(id)) {
243 throw new IllegalArgumentException("A source specific media attribute is missing the id");
244 }
245 checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces");
246 if (Strings.isNullOrEmpty(parameterName)) {
247 throw new IllegalArgumentException("A source specific media attribute is missing its name");
248 }
249 if (Strings.isNullOrEmpty(parameterValue)) {
250 throw new IllegalArgumentException("A source specific media attribute is missing its value");
251 }
252 mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue);
253 }
254 }
255
256 mediaAttributes.put("mid", name);
257
258 //random additional attributes
259 mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0");
260 mediaAttributes.put("sendrecv", "");
261
262 if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) {
263 mediaAttributes.put("rtcp-mux", "");
264 }
265
266 final MediaBuilder mediaBuilder = new MediaBuilder();
267 mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT));
268 mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
269 mediaBuilder.setPort(HARDCODED_MEDIA_PORT);
270 mediaBuilder.setProtocol(HARDCODED_MEDIA_PROTOCOL);
271 mediaBuilder.setAttributes(mediaAttributes);
272 mediaBuilder.setFormats(formatBuilder.build());
273 mediaListBuilder.add(mediaBuilder.createMedia());
274
275 }
276 sessionDescriptionBuilder.setVersion(0);
277 sessionDescriptionBuilder.setName("-");
278 sessionDescriptionBuilder.setMedia(mediaListBuilder.build());
279 sessionDescriptionBuilder.setAttributes(attributeMap);
280
281 return sessionDescriptionBuilder.createSessionDescription();
282 }
283
284 public static String checkNoWhitespace(final String input, final String message) {
285 if (CharMatcher.whitespace().matchesAnyOf(input)) {
286 throw new IllegalArgumentException(message);
287 }
288 return input;
289 }
290
291 public static int ignorantIntParser(final String input) {
292 try {
293 return Integer.parseInt(input);
294 } catch (NumberFormatException e) {
295 return 0;
296 }
297 }
298
299 public static boolean isInt(final String input) {
300 if (input == null) {
301 return false;
302 }
303 try {
304 Integer.parseInt(input);
305 return true;
306 } catch (NumberFormatException e) {
307 return false;
308 }
309 }
310
311 public static Pair<String, String> parseAttribute(final String input) {
312 final String[] pair = input.split(":", 2);
313 if (pair.length == 2) {
314 return new Pair<>(pair[0], pair[1]);
315 } else {
316 return new Pair<>(pair[0], "");
317 }
318 }
319
320 @Override
321 public String toString() {
322 final StringBuilder s = new StringBuilder()
323 .append("v=").append(version).append(LINE_DIVIDER)
324 //TODO randomize or static
325 .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means
326 .append("s=").append(name).append(LINE_DIVIDER)
327 .append("t=0 0").append(LINE_DIVIDER);
328 appendAttributes(s, attributes);
329 for (Media media : this.media) {
330 s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER);
331 s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
332 appendAttributes(s, media.attributes);
333 }
334 return s.toString();
335 }
336
337 public static class Media {
338 public final String media;
339 public final int port;
340 public final String protocol;
341 public final List<Integer> formats;
342 public final String connectionData;
343 public final ArrayListMultimap<String, String> attributes;
344
345 public Media(String media, int port, String protocol, List<Integer> formats, String connectionData, ArrayListMultimap<String, String> attributes) {
346 this.media = media;
347 this.port = port;
348 this.protocol = protocol;
349 this.formats = formats;
350 this.connectionData = connectionData;
351 this.attributes = attributes;
352 }
353 }
354
355}