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