1package eu.siacs.conversations.xmpp.jingle.stanzas;
2
3import android.util.Log;
4import android.util.Pair;
5
6import com.google.common.base.Preconditions;
7import com.google.common.collect.ArrayListMultimap;
8import com.google.common.collect.ImmutableList;
9
10import java.util.HashMap;
11import java.util.List;
12import java.util.Locale;
13import java.util.Map;
14
15import eu.siacs.conversations.Config;
16import eu.siacs.conversations.xml.Element;
17import eu.siacs.conversations.xml.Namespace;
18import eu.siacs.conversations.xmpp.jingle.SessionDescription;
19
20public class RtpDescription extends GenericDescription {
21
22
23 private RtpDescription() {
24 super("description", Namespace.JINGLE_APPS_RTP);
25 }
26
27 public Media getMedia() {
28 return Media.of(this.getAttribute("media"));
29 }
30
31 public List<PayloadType> getPayloadTypes() {
32 final ImmutableList.Builder<PayloadType> builder = new ImmutableList.Builder<>();
33 for (Element child : getChildren()) {
34 if ("payload-type".equals(child.getName())) {
35 builder.add(PayloadType.of(child));
36 }
37 }
38 return builder.build();
39 }
40
41 public List<FeedbackNegotiation> getFeedbackNegotiations() {
42 return FeedbackNegotiation.fromChildren(this.getChildren());
43 }
44
45 public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
46 return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
47 }
48
49 public List<RtpHeaderExtension> getHeaderExtensions() {
50 final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
51 for (final Element child : getChildren()) {
52 if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) {
53 builder.add(RtpHeaderExtension.upgrade(child));
54 }
55 }
56 return builder.build();
57 }
58
59 public static RtpDescription upgrade(final Element element) {
60 Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
61 Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace");
62 final RtpDescription description = new RtpDescription();
63 description.setAttributes(element.getAttributes());
64 description.setChildren(element.getChildren());
65 return description;
66 }
67
68 public static class FeedbackNegotiation extends Element {
69 private FeedbackNegotiation() {
70 super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
71 }
72
73 public FeedbackNegotiation(String type, String subType) {
74 super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
75 this.setAttribute("type", type);
76 if (subType != null) {
77 this.setAttribute("subtype", subType);
78 }
79 }
80
81 public String getType() {
82 return this.getAttribute("type");
83 }
84
85 public String getSubType() {
86 return this.getAttribute("subtype");
87 }
88
89 private static FeedbackNegotiation upgrade(final Element element) {
90 Preconditions.checkArgument("rtcp-fb".equals(element.getName()));
91 Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
92 final FeedbackNegotiation feedback = new FeedbackNegotiation();
93 feedback.setAttributes(element.getAttributes());
94 feedback.setChildren(element.getChildren());
95 return feedback;
96 }
97
98 public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
99 ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
100 for (final Element child : children) {
101 if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
102 builder.add(upgrade(child));
103 }
104 }
105 return builder.build();
106 }
107
108 }
109
110 public static class FeedbackNegotiationTrrInt extends Element {
111
112 private FeedbackNegotiationTrrInt(int value) {
113 super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
114 this.setAttribute("value", value);
115 }
116
117
118 private FeedbackNegotiationTrrInt() {
119 super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
120 }
121
122 public int getValue() {
123 final String value = getAttribute("value");
124 if (value == null) {
125 return 0;
126 }
127 return SessionDescription.ignorantIntParser(value);
128
129 }
130
131 private static FeedbackNegotiationTrrInt upgrade(final Element element) {
132 Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
133 Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
134 final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
135 trr.setAttributes(element.getAttributes());
136 trr.setChildren(element.getChildren());
137 return trr;
138 }
139
140 public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
141 ImmutableList.Builder<FeedbackNegotiationTrrInt> builder = new ImmutableList.Builder<>();
142 for (final Element child : children) {
143 if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
144 builder.add(upgrade(child));
145 }
146 }
147 return builder.build();
148 }
149 }
150
151
152 //XEP-0294: Jingle RTP Header Extensions Negotiation
153 //maps to `extmap:$id $uri`
154 public static class RtpHeaderExtension extends Element {
155
156 private RtpHeaderExtension() {
157 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
158 }
159
160 public RtpHeaderExtension(String id, String uri) {
161 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
162 this.setAttribute("id", id);
163 this.setAttribute("uri", uri);
164 }
165
166 public String getId() {
167 return this.getAttribute("id");
168 }
169
170 public String getUri() {
171 return this.getAttribute("uri");
172 }
173
174 public static RtpHeaderExtension upgrade(final Element element) {
175 Preconditions.checkArgument("rtp-hdrext".equals(element.getName()));
176 Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace()));
177 final RtpHeaderExtension extension = new RtpHeaderExtension();
178 extension.setAttributes(element.getAttributes());
179 extension.setChildren(element.getChildren());
180 return extension;
181 }
182
183 public static RtpHeaderExtension ofSdpString(final String sdp) {
184 final String[] pair = sdp.split(" ", 2);
185 if (pair.length == 2) {
186 final String id = pair[0];
187 final String uri = pair[1];
188 return new RtpHeaderExtension(id, uri);
189 } else {
190 return null;
191 }
192 }
193 }
194
195 //maps to `rtpmap $id $name/$clockrate/$channels`
196 public static class PayloadType extends Element {
197
198 private PayloadType() {
199 super("payload-type", Namespace.JINGLE_APPS_RTP);
200 }
201
202 public PayloadType(String id, String name, int clockRate, int channels) {
203 super("payload-type", Namespace.JINGLE_APPS_RTP);
204 this.setAttribute("id", id);
205 this.setAttribute("name", name);
206 this.setAttribute("clockrate", clockRate);
207 if (channels != 1) {
208 this.setAttribute("channels", channels);
209 }
210 }
211
212 public String getId() {
213 return this.getAttribute("id");
214 }
215
216 public String getPayloadTypeName() {
217 return this.getAttribute("name");
218 }
219
220 public int getClockRate() {
221 final String clockRate = this.getAttribute("clockrate");
222 if (clockRate == null) {
223 return 0;
224 }
225 try {
226 return Integer.parseInt(clockRate);
227 } catch (NumberFormatException e) {
228 return 0;
229 }
230 }
231
232 public int getChannels() {
233 final String channels = this.getAttribute("channels");
234 if (channels == null) {
235 return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel
236 }
237 try {
238 return Integer.parseInt(channels);
239 } catch (NumberFormatException e) {
240 return 1;
241 }
242 }
243
244 public List<Parameter> getParameters() {
245 final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
246 for (Element child : getChildren()) {
247 if ("parameter".equals(child.getName())) {
248 builder.add(Parameter.of(child));
249 }
250 }
251 return builder.build();
252 }
253
254 public List<FeedbackNegotiation> getFeedbackNegotiations() {
255 return FeedbackNegotiation.fromChildren(this.getChildren());
256 }
257
258 public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
259 return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
260 }
261
262 public static PayloadType of(final Element element) {
263 Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type");
264 PayloadType payloadType = new PayloadType();
265 payloadType.setAttributes(element.getAttributes());
266 payloadType.setChildren(element.getChildren());
267 return payloadType;
268 }
269
270 public static PayloadType ofSdpString(final String sdp) {
271 final String[] pair = sdp.split(" ", 2);
272 if (pair.length == 2) {
273 final String id = pair[0];
274 final String[] parts = pair[1].split("/");
275 if (parts.length >= 2) {
276 final String name = parts[0];
277 final int clockRate = SessionDescription.ignorantIntParser(parts[1]);
278 final int channels;
279 if (parts.length >= 3) {
280 channels = SessionDescription.ignorantIntParser(parts[2]);
281 } else {
282 channels = 1;
283 }
284 return new PayloadType(id, name, clockRate, channels);
285 }
286 }
287 return null;
288 }
289
290 public void addChildren(final List<Element> children) {
291 if (children != null) {
292 this.children.addAll(children);
293 }
294 }
295
296 public void addParameters(List<Parameter> parameters) {
297 if (parameters != null) {
298 this.children.addAll(parameters);
299 }
300 }
301 }
302
303 //map to `fmtp $id key=value;key=value
304 //where id is the id of the parent payload-type
305 public static class Parameter extends Element {
306
307 private Parameter() {
308 super("parameter", Namespace.JINGLE_APPS_RTP);
309 }
310
311 public Parameter(String name, String value) {
312 super("parameter", Namespace.JINGLE_APPS_RTP);
313 this.setAttribute("name", name);
314 this.setAttribute("value", value);
315 }
316
317 public String getParameterName() {
318 return this.getAttribute("name");
319 }
320
321 public String getParameterValue() {
322 return this.getAttribute("value");
323 }
324
325 public static Parameter of(final Element element) {
326 Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter");
327 Parameter parameter = new Parameter();
328 parameter.setAttributes(element.getAttributes());
329 parameter.setChildren(element.getChildren());
330 return parameter;
331 }
332
333 public static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
334 final String[] pair = sdp.split(" ");
335 if (pair.length == 2) {
336 final String id = pair[0];
337 ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
338 for (final String parameter : pair[1].split(";")) {
339 final String[] parts = parameter.split("=", 2);
340 if (parts.length == 2) {
341 builder.add(new Parameter(parts[0], parts[1]));
342 }
343 }
344 return new Pair<>(id, builder.build());
345 } else {
346 return null;
347 }
348 }
349 }
350
351 public enum Media {
352 VIDEO, AUDIO, UNKNOWN;
353
354 @Override
355 public String toString() {
356 return super.toString().toLowerCase(Locale.ROOT);
357 }
358
359 public static Media of(String value) {
360 try {
361 return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT));
362 } catch (IllegalArgumentException e) {
363 return UNKNOWN;
364 }
365 }
366 }
367
368 public static RtpDescription of(final SessionDescription.Media media) {
369 final RtpDescription rtpDescription = new RtpDescription();
370 final Map<String, List<Parameter>> parameterMap = new HashMap<>();
371 ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
372 for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
373 final String[] parts = rtcpFb.split(" ");
374 if (parts.length >= 2) {
375 final String id = parts[0];
376 final String type = parts[1];
377 final String subType = parts.length >= 3 ? parts[2] : null;
378 if ("trr-int".equals(type)) {
379 if (subType != null) {
380 feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
381 }
382 } else {
383 feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
384 }
385 }
386 }
387 for (final String fmtp : media.attributes.get("fmtp")) {
388 final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
389 if (pair != null) {
390 parameterMap.put(pair.first, pair.second);
391 }
392 }
393 rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
394 for (final String rtpmap : media.attributes.get("rtpmap")) {
395 final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
396 if (payloadType != null) {
397 payloadType.addParameters(parameterMap.get(payloadType.getId()));
398 payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
399 rtpDescription.addChild(payloadType);
400 }
401 }
402 for (final String extmap : media.attributes.get("extmap")) {
403 final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
404 if (extension != null) {
405 rtpDescription.addChild(extension);
406 }
407 }
408 return rtpDescription;
409 }
410
411 private void addChildren(List<Element> elements) {
412 if (elements != null) {
413 this.children.addAll(elements);
414 }
415 }
416}