Change in osmo-msc[master]: add sdp_msg API: SDP parsing/composition

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

neels gerrit-no-reply at lists.osmocom.org
Sun Nov 3 23:21:20 UTC 2019


neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-msc/+/15947 )


Change subject: add sdp_msg API: SDP parsing/composition
......................................................................

add sdp_msg API: SDP parsing/composition

Rationale: in order to add full SDP to the MNCC protocol (upcoming patch
I8c3b2de53ffae4ec3a66b9dabf308c290a2c999f), we need to parse and compose SDP
messages. Obviously, libosmo-mgcp-client already contains similar code, but
that is unfortunately heavily glued to the actual MGCP implementation. The
simplest solution is to create this separate implementation, copy-pasting from
the existing libosmo-mgcp-client code as is convenient.

Various foo_name() functions are implemented for the sdp_msg API.
Each is:
a) implemented as foo_name_buf(buf, len, val),
b) wrapped as foo_name_c(ctx, val)
c) and as foo_name(val), a convienience variant using OTC_SELECT.

(a) foo_name_buf() uses osmo_strbuf to write to a fixed-size caller provided
buffer: osmo_strbuf is most convenient to implement optional parts / loops.

(b) foo_name_c() uses a caller-provided talloc ctx to allocate such buffer;
implemented using a generalized NAME_C_IMPL(func_buf, arg) macro, which calls
func with an initial size buffer, and reallocates if more space is needed.

(c) foo_name() then calls foo_name_c() with the OTC_SELECT context (we know that
msc_main.c uses osmo_select_main_ctx()), implemented using a generalized
NAME_IMPL() macro, and returns "ERROR" on failure instead of NULL.

Change-Id: If3ce23cd5bab15e2ab4c52ef3e4c75979dffe931
---
M configure.ac
M include/osmocom/msc/Makefile.am
M include/osmocom/msc/debug.h
A include/osmocom/msc/sdp_msg.h
M src/libmsc/Makefile.am
A src/libmsc/sdp_msg.c
M tests/Makefile.am
A tests/sdp_msg/Makefile.am
A tests/sdp_msg/sdp_msg_test.c
A tests/sdp_msg/sdp_msg_test.err
A tests/sdp_msg/sdp_msg_test.ok
M tests/testsuite.at
12 files changed, 1,886 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-msc refs/changes/47/15947/1

diff --git a/configure.ac b/configure.ac
index 5aa652d..ee80900 100644
--- a/configure.ac
+++ b/configure.ac
@@ -254,6 +254,7 @@
     tests/db_sms/Makefile
     tests/sms_queue/Makefile
     tests/msc_vlr/Makefile
+    tests/sdp_msg/Makefile
     doc/Makefile
     doc/examples/Makefile
     doc/manuals/Makefile
diff --git a/include/osmocom/msc/Makefile.am b/include/osmocom/msc/Makefile.am
index 0d7d45c..faf1048 100644
--- a/include/osmocom/msc/Makefile.am
+++ b/include/osmocom/msc/Makefile.am
@@ -39,6 +39,7 @@
 	rrlp.h \
 	rtp_stream.h \
 	sccp_ran.h \
+	sdp_msg.h \
 	sgs_iface.h \
 	sgs_server.h \
 	sgs_vty.h \
diff --git a/include/osmocom/msc/debug.h b/include/osmocom/msc/debug.h
index 3347e20..1a335fd 100644
--- a/include/osmocom/msc/debug.h
+++ b/include/osmocom/msc/debug.h
@@ -25,3 +25,43 @@
 	DSS,
 	Debug_LastEntry,
 };
+
+/* Translate a buffer function to a talloc context function.
+ * This is the full function body of a char *foo_name_c(void *ctx, val...) function, implemented by an
+ * int foo_name_buf(buf, buflen, val...) function.
+ * Return a talloc'd string containing the result of the given foo_name_buf() function, or NULL on error.
+ * See for example sdp_msg_name_c() and sdp_msg_name_buf().
+ * FUNC_BUF: name of a function with signature foo_buf(char *buf, size_t buflen, ...).
+ * FUNC_BUF_ARGS: additional arguments to pass to FUNC_BUF after the buf and buflen.
+ */
+#define NAME_C_IMPL(FUNC_BUF, FUNC_BUF_ARGS...) \
+	size_t len = 64; \
+	int needed; \
+	char *str = (char*)talloc_named_const(ctx, len, #FUNC_BUF "_c()"); \
+	OSMO_ASSERT(str); \
+	needed = FUNC_BUF(str, len, ## FUNC_BUF_ARGS); \
+	if (needed < 0) \
+		return NULL; \
+	if (needed < len) \
+		return str; \
+	len = needed + 1; \
+	str = (char*)talloc_realloc_size(ctx, str, len); \
+	OSMO_ASSERT(str); \
+	needed = FUNC_BUF(str, len, ## FUNC_BUF_ARGS); \
+	if (needed < 0) \
+		return NULL; \
+	return str;
+
+/* Translate a NAME_C_IMPL function to a safe printf() arg function using OTC_SELECT.
+ * This is the full function body of a const char *foo_name(val...) function, implemented by a
+ * char *foo_name_c(ctx, val...) function.
+ * Return a string talloc'd from OTC_SELECT, or the constant "ERROR" on error; never return NULL.
+ * See for example sdp_msg_name() and sdp_msg_name_c().
+ * FUNC_C: name of a function with signature char *foo_name_c(void *ctx, ...).
+ * FUNC_C_ARGS: additional arguments to pass to FUNC_C after the ctx.
+ */
+#define NAME_IMPL(FUNC_C, FUNC_C_ARGS...) \
+	char *str = FUNC_C(OTC_SELECT, ## FUNC_C_ARGS); \
+	if (!str) \
+		return "ERROR"; \
+	return str;
diff --git a/include/osmocom/msc/sdp_msg.h b/include/osmocom/msc/sdp_msg.h
new file mode 100644
index 0000000..7ac6036
--- /dev/null
+++ b/include/osmocom/msc/sdp_msg.h
@@ -0,0 +1,68 @@
+/* Minimalistic SDP parse/compose API, focused on GSM audio codecs */
+#pragma once
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/sockaddr_str.h>
+
+extern const struct value_string sdp_msg_payload_type_names[];
+static inline const char *sdp_msg_payload_type_name(unsigned int payload_type)
+{ return get_value_string(sdp_msg_payload_type_names, payload_type); }
+int sdp_subtype_name_to_payload_type(const char *subtype_name);
+
+struct sdp_audio_codec {
+	unsigned int payload_type;
+	/* Like "GSM", "AMR", "EFR", ... */
+	char subtype_name[16];
+	unsigned int rate;
+	char fmtp[64];
+};
+
+struct sdp_audio_codecs {
+	unsigned int count;
+	struct sdp_audio_codec codec[16];
+};
+
+struct sdp_msg {
+	struct osmo_sockaddr_str rtp;
+	unsigned int ptime;
+	struct sdp_audio_codecs audio_codecs;
+};
+
+#define foreach_sdp_audio_codec(/* struct sdp_audio_codec* */ CODEC, \
+				/* struct sdp_audio_codecs* */ AC) \
+	for (CODEC = (AC)->codec; \
+	     (CODEC - (AC)->codec) < OSMO_MIN((AC)->count, ARRAY_SIZE((AC)->codec)); \
+	     CODEC++)
+
+const char *sdp_msg_line_end(const char *src);
+
+int sdp_audio_codec_cmp(const struct sdp_audio_codec *a, const struct sdp_audio_codec *b);
+
+struct sdp_audio_codec *sdp_audio_codec_add(struct sdp_audio_codecs *ac, unsigned int payload_type,
+					    const char *subtype_name, unsigned int rate, const char *fmtp);
+struct sdp_audio_codec *sdp_audio_codec_add_copy(struct sdp_audio_codecs *ac,
+						 const struct sdp_audio_codec *codec);
+int sdp_audio_codec_remove(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec);
+struct sdp_audio_codec *sdp_audio_codec_by_payload_type(struct sdp_audio_codecs *ac,
+							unsigned int payload_type, bool create);
+struct sdp_audio_codec *sdp_audio_codec_by_descr(struct sdp_audio_codecs *ac,
+						 const struct sdp_audio_codec *codec);
+
+void sdp_audio_codecs_intersection(struct sdp_audio_codecs *ac_dest, const struct sdp_audio_codecs *ac_other,
+				   bool translate_payload_type_numbers);
+void sdp_audio_codecs_select(struct sdp_audio_codecs *ac, struct sdp_audio_codec *codec);
+
+int sdp_msg_to_str(char *dst, size_t dst_size, const struct sdp_msg *sdp);
+int sdp_msg_from_str(struct sdp_msg *sdp, const char *src);
+
+int sdp_audio_codec_name_buf(char *buf, size_t buflen, const struct sdp_audio_codec *codec);
+char *sdp_audio_codec_name_c(void *ctx, const struct sdp_audio_codec *codec);
+const char *sdp_audio_codec_name(const struct sdp_audio_codec *codec);
+
+int sdp_audio_codecs_name_buf(char *buf, size_t buflen, const struct sdp_audio_codecs *ac);
+char *sdp_audio_codecs_name_c(void *ctx, const struct sdp_audio_codecs *ac);
+const char *sdp_audio_codecs_name(const struct sdp_audio_codecs *ac);
+
+int sdp_msg_name_buf(char *buf, size_t buflen, const struct sdp_msg *sdp);
+char *sdp_msg_name_c(void *ctx, const struct sdp_msg *sdp);
+const char *sdp_msg_name(const struct sdp_msg *sdp);
diff --git a/src/libmsc/Makefile.am b/src/libmsc/Makefile.am
index d834896..e6a2dc1 100644
--- a/src/libmsc/Makefile.am
+++ b/src/libmsc/Makefile.am
@@ -64,6 +64,7 @@
 	ran_peer.c \
 	rrlp.c \
 	rtp_stream.c \
+	sdp_msg.c \
 	silent_call.c \
 	sms_queue.c \
 	transaction.c \
diff --git a/src/libmsc/sdp_msg.c b/src/libmsc/sdp_msg.c
new file mode 100644
index 0000000..3386170
--- /dev/null
+++ b/src/libmsc/sdp_msg.c
@@ -0,0 +1,576 @@
+/* Minimalistic SDP parse/compose implementation, focused on GSM audio codecs */
+/*
+ * (C) 2019 by sysmocom - s.m.f.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0+
+ *
+ * Author: Neels Hofmeyr
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+
+#include <osmocom/msc/debug.h>
+#include <osmocom/msc/sdp_msg.h>
+
+/* Compare name, rate and fmtp, returning typical cmp result: 0 on match, and -1 / 1 on mismatch.
+ * Do *not* compare the payload_type number.
+ * The fmtp is only string-compared -- e.g. if AMR parameters appear in a different order, it amounts to a mismatch even
+ * though all parameters are the same. */
+int sdp_audio_codec_cmp(const struct sdp_audio_codec *a, const struct sdp_audio_codec *b)
+{
+	int rc;
+	if (a == b)
+		return 0;
+	if (!a)
+		return -1;
+	if (!b)
+		return 1;
+	rc = strncmp(a->subtype_name, b->subtype_name, sizeof(a->subtype_name));
+	if (rc)
+		return rc;
+
+	if (a->rate < b->rate)
+		return -1;
+	if (a->rate > b->rate)
+		return 1;
+
+	rc = strncmp(a->fmtp, b->fmtp, sizeof(a->fmtp));
+	if (rc)
+		return rc;
+
+	return 0;
+}
+
+/* Given a predefined fixed payload_type number, add an SDP audio codec entry, if not present yet.
+ * The payload_type must exist in sdp_msg_payload_type_names.
+ * Return the audio codec created or already existing for this payload type number.
+ */
+struct sdp_audio_codec *sdp_audio_codec_add(struct sdp_audio_codecs *ac, unsigned int payload_type,
+					    const char *subtype_name, unsigned int rate, const char *fmtp)
+{
+	struct sdp_audio_codec *codec;
+
+	/* Does an entry already exist? */
+	codec = sdp_audio_codec_by_payload_type(ac, payload_type, false);
+	if (codec) {
+		/* Already exists, sanity check */
+		if (!codec->subtype_name[0])
+			OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
+		else if (strcmp(codec->subtype_name, subtype_name)) {
+			/* There already is an entry with this payload_type number but a mismatching subtype_name. That is
+			 * weird, rather abort. */
+			return NULL;
+		}
+		if (codec->rate != rate
+		    || (fmtp && strcmp(fmtp, codec->fmtp))) {
+			/* Mismatching details. Rather abort */
+			return NULL;
+		}
+		return codec;
+	}
+
+	/* None exists, create codec entry for this payload type number */
+	codec = sdp_audio_codec_by_payload_type(ac, payload_type, true);
+	/* NULL means unable to add an entry */
+	if (!codec)
+		return NULL;
+
+	OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
+	if (fmtp)
+		OSMO_STRLCPY_ARRAY(codec->fmtp, fmtp);
+	return codec;
+}
+
+struct sdp_audio_codec *sdp_audio_codec_add_copy(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
+{
+	return sdp_audio_codec_add(ac, codec->payload_type, codec->subtype_name, codec->rate,
+				   codec->fmtp[0] ? codec->fmtp : NULL);
+}
+
+struct sdp_audio_codec *sdp_audio_codec_by_payload_type(struct sdp_audio_codecs *ac, unsigned int payload_type,
+							bool create)
+{
+	struct sdp_audio_codec *codec;
+	foreach_sdp_audio_codec(codec, ac) {
+		if (codec->payload_type == payload_type)
+			return codec;
+	}
+
+	if (!create)
+		return NULL;
+
+	/* Not found; codec points after the last entry now. */
+	if ((codec - ac->codec) >= ARRAY_SIZE(ac->codec))
+		return NULL;
+
+	*codec = (struct sdp_audio_codec){
+		.payload_type = payload_type,
+		.rate = 8000,
+	};
+
+	ac->count = (codec - ac->codec) + 1;
+	return codec;
+}
+
+/* Return a given sdp_msg's codec entry that matches the subtype_name, rate and fmtp of the given codec, or NULL if no
+ * match is found. Comparison is made by sdp_audio_codec_cmp(). */
+struct sdp_audio_codec *sdp_audio_codec_by_descr(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
+{
+	struct sdp_audio_codec *i;
+	foreach_sdp_audio_codec(i, ac) {
+		if (!sdp_audio_codec_cmp(i, codec))
+			return i;
+	}
+	return NULL;
+}
+
+/* Remove the codec entry pointed at by 'codec'. 'codec' must point at an entry of 'sdp' (to use an external codec
+ * instance, use sdp_audio_codec_by_descr()).
+ * Return 0 on success, -ENOENT if codec does not point at the sdp->codec array. */
+int sdp_audio_codec_remove(struct sdp_audio_codecs *ac, const struct sdp_audio_codec *codec)
+{
+	struct sdp_audio_codec *i;
+	if ((codec < ac->codec)
+	    || ((codec - ac->codec) >= OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec))))
+		return -ENOENT;
+
+	/* Move all following entries one up */
+	ac->count--;
+	foreach_sdp_audio_codec(i, ac) {
+		if (i < codec)
+			continue;
+		*i = *(i+1);
+	}
+	return 0;
+}
+
+/* Convert struct sdp_msg to the actual SDP protocol representation */
+int sdp_msg_to_str(char *dst, size_t dst_size, const struct sdp_msg *sdp)
+{
+	const struct sdp_audio_codec *codec;
+	struct osmo_strbuf sb = { .buf = dst, .len = dst_size };
+	const char *ip = sdp->rtp.ip[0] ? sdp->rtp.ip : "0.0.0.0";
+
+	OSMO_STRBUF_PRINTF(sb,
+			   "v=0\r\n"
+			   "o=OsmoMSC 0 0 IN IP4 %s\r\n"
+			   "s=GSM Call\r\n"
+			   "c=IN IP4 %s\r\n"
+			   "t=0 0\r\n"
+			   "m=audio %d RTP/AVP",
+			   ip, ip,
+			   sdp->rtp.port);
+
+	/* Append all payload type numbers to 'm=audio <port> RTP/AVP 3 4 112' line */
+	foreach_sdp_audio_codec(codec, &sdp->audio_codecs)
+		OSMO_STRBUF_PRINTF(sb, " %d", codec->payload_type);
+	OSMO_STRBUF_PRINTF(sb, "\r\n");
+
+	/* Add details for all codecs */
+	foreach_sdp_audio_codec(codec, &sdp->audio_codecs) {
+		if (codec->subtype_name[0]) {
+			OSMO_STRBUF_PRINTF(sb, "a=rtpmap:%d %s/%d\r\n", codec->payload_type, codec->subtype_name,
+					   codec->rate > 0? codec->rate : 8000);
+		}
+
+		if (codec->fmtp[0])
+			OSMO_STRBUF_PRINTF(sb, "a=fmtp:%d %s\r\n", codec->payload_type, codec->fmtp);
+	}
+
+	OSMO_STRBUF_PRINTF(sb, "a=ptime:%d\r\n", sdp->ptime > 0? sdp->ptime : 20);
+
+	return sb.chars_needed;
+}
+
+/* Return the first line ending (or the end of the string) at or after the given string position. */
+const char *sdp_msg_line_end(const char *src)
+{
+	const char *line_end = strchr(src, '\r');
+	if (!line_end)
+		line_end = strchr(src, '\n');
+	if (!line_end)
+		line_end = src + strlen(src);
+	return line_end;
+}
+
+/* parse a line like 'a=rtpmap:0 PCMU/8000', 'a=fmtp:112 octet-align=1; mode-set=4', 'a=ptime:20'.
+ * The src should point at the character after 'a=', e.g. at the start of 'rtpmap', 'fmtp', 'ptime'
+ */
+int sdp_parse_attrib(struct sdp_msg *sdp, const char *src)
+{
+	unsigned int payload_type;
+	struct sdp_audio_codec *codec;
+#define A_RTPMAP "rtpmap:"
+#define A_FMTP "fmtp:"
+#define A_PTIME "ptime:"
+#define A_RTCP "rtcp:"
+#define A_SENDRECV "sendrecv"
+#define A_SENDONLY "sendonly"
+#define A_RECVONLY "recvonly"
+
+	if (osmo_str_startswith(src, A_RTPMAP)) {
+		char *audio_name;
+		unsigned int channels = 1;
+		if (sscanf(src, A_RTPMAP "%u", &payload_type) != 1)
+			return -EINVAL;
+
+		audio_name = strchr(src, ' ');
+		if (!audio_name || audio_name >= sdp_msg_line_end(src))
+			return -EINVAL;
+
+		codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
+		if (!codec)
+			return -ENOSPC;
+
+		if (sscanf(audio_name, " %31[^/]/%u/%u", codec->subtype_name, &codec->rate, &channels) < 1)
+			return -EINVAL;
+
+		if (channels != 1)
+			return -ENOTSUP;
+	}
+
+	else if (osmo_str_startswith(src, A_FMTP)) {
+		char *fmtp_str;
+		const char *line_end = sdp_msg_line_end(src);
+		if (sscanf(src, A_FMTP "%u", &payload_type) != 1)
+			return -EINVAL;
+
+		fmtp_str = strchr(src, ' ');
+		if (!fmtp_str)
+			return -EINVAL;
+		fmtp_str++;
+		if (fmtp_str >= line_end)
+			return -EINVAL;
+
+		codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
+		if (!codec)
+			return -ENOSPC;
+
+		/* (+1 because osmo_strlcpy() interprets it as size including the '\0') */
+		osmo_strlcpy(codec->fmtp, fmtp_str, line_end - fmtp_str + 1);
+	}
+
+	else if (osmo_str_startswith(src, A_PTIME)) {
+		if (sscanf(src, A_PTIME "%u", &sdp->ptime) != 1)
+			return -EINVAL;
+
+	}
+
+	else if (osmo_str_startswith(src, A_RTCP)) {
+		/* TODO? */
+	}
+
+	else if (osmo_str_startswith(src, A_SENDRECV)) {
+		/* TODO? */
+	}
+
+	else if (osmo_str_startswith(src, A_SENDONLY)) {
+		/* TODO? */
+	}
+
+	else if (osmo_str_startswith(src, A_RECVONLY)) {
+		/* TODO? */
+	}
+
+	return 0;
+}
+
+const struct value_string sdp_msg_payload_type_names[] = {
+	{ 0, "PCMU" },
+	{ 3, "GSM" },
+	{ 8, "PCMA" },
+	{ 18, "G729" },
+	{ 110, "GSM-EFR" },
+	{ 111, "GSM-HR-08" },
+	{ 112, "AMR" },
+	{ 113, "AMR-WB" },
+	{}
+};
+
+/* Return payload type number matching given string ("AMR", "GSM", ...) or negative if not found. */
+int sdp_subtype_name_to_payload_type(const char *subtype_name)
+{
+	return get_string_value(sdp_msg_payload_type_names, subtype_name);
+}
+
+/* Parse a line like 'm=audio 16398 RTP/AVP 0 3 8 96 112', starting after the '=' */
+static int sdp_parse_media_description(struct sdp_msg *sdp, const char *src)
+{
+	unsigned int port;
+	int i;
+	const char *payload_type_str;
+	const char *line_end = sdp_msg_line_end(src);
+	if (sscanf(src, "audio %u RTP/AVP", &port) < 1)
+		return -ENOTSUP;
+
+	if (port < 0 || port > 0xffff)
+		return -EINVAL;
+
+	sdp->rtp.port = port;
+
+	/* skip "audio 12345 RTP/AVP ", i.e. 3 spaces on */
+	payload_type_str = src;
+	for (i = 0; i < 3; i++) {
+		payload_type_str = strchr(payload_type_str, ' ');
+		if (!payload_type_str)
+			return -EINVAL;
+		while (*payload_type_str == ' ')
+			payload_type_str++;
+		if (payload_type_str >= line_end)
+			return -EINVAL;
+	}
+
+	/* Parse listing of payload type numbers after "RTP/AVP" */
+	while (payload_type_str < line_end) {
+		unsigned int payload_type;
+		struct sdp_audio_codec *codec;
+		const char *subtype_name;
+		if (sscanf(payload_type_str, "%u", &payload_type) < 1)
+			return -EINVAL;
+
+		codec = sdp_audio_codec_by_payload_type(&sdp->audio_codecs, payload_type, true);
+		if (!codec)
+			return -ENOSPC;
+
+		/* Fill in subtype name for fixed payload types */
+		subtype_name = get_value_string_or_null(sdp_msg_payload_type_names, codec->payload_type);
+		if (subtype_name)
+			OSMO_STRLCPY_ARRAY(codec->subtype_name, subtype_name);
+
+		payload_type_str = strchr(payload_type_str, ' ');
+		if (!payload_type_str)
+			payload_type_str = line_end;
+		while (*payload_type_str == ' ')
+			payload_type_str++;
+	}
+
+	return 0;
+}
+
+/* parse a line like 'c=IN IP4 192.168.11.151' starting after the '=' */
+static int sdp_parse_connection_info(struct sdp_msg *sdp, const char *src)
+{
+	char ipv[10];
+	char addr_str[INET6_ADDRSTRLEN];
+	if (sscanf(src, "IN %s %s", ipv, addr_str) < 2)
+		return -EINVAL;
+
+	/* supporting only IPv4 */
+	if (strcmp(ipv, "IP4"))
+		return -ENOTSUP;
+
+	osmo_sockaddr_str_from_str(&sdp->rtp, addr_str, sdp->rtp.port);
+	return 0;
+}
+
+/* Parse SDP string into struct sdp_msg. Return 0 on success, negative on error. */
+int sdp_msg_from_str(struct sdp_msg *sdp, const char *src)
+{
+	const char *pos;
+	*sdp = (struct sdp_msg){};
+
+	for (pos = src; pos && *pos; pos++) {
+		char attrib;
+		int rc = 0;
+
+		if (*pos == '\r' || *pos == '\n')
+			continue;
+
+		/* Expecting only lines starting with 'X='. Not being too strict about it is probably alright. */
+		if (pos[1] != '=')
+			goto next_line;
+
+		attrib = *pos;
+		pos += 2;
+		switch (attrib) {
+			/* a=... */
+			case 'a':
+				rc = sdp_parse_attrib(sdp, pos);
+				break;
+			case 'm':
+				rc = sdp_parse_media_description(sdp, pos);
+				break;
+			case 'c':
+				rc = sdp_parse_connection_info(sdp, pos);
+				break;
+			default:
+				/* ignore any other parameters */
+				break;
+		}
+
+		if (rc) {
+			size_t line_len;
+			const char *line_end = sdp_msg_line_end(pos);
+			pos -= 2;
+			line_len = line_end - pos;
+			switch (rc) {
+			case -EINVAL:
+				LOGP(DMNCC, LOGL_ERROR,
+				     "Failed to parse SDP: invalid line: %s\n", osmo_quote_str(pos, line_len));
+				break;
+			case -ENOSPC:
+				LOGP(DMNCC, LOGL_ERROR,
+				     "Failed to parse SDP: no more space for: %s\n", osmo_quote_str(pos, line_len));
+				break;
+			case -ENOTSUP:
+				LOGP(DMNCC, LOGL_ERROR,
+				     "Failed to parse SDP: not supported: %s\n", osmo_quote_str(pos, line_len));
+				break;
+			default:
+				LOGP(DMNCC, LOGL_ERROR,
+				     "Failed to parse SDP: %s\n", osmo_quote_str(pos, line_len));
+				break;
+			}
+			return rc;
+		}
+next_line:
+		pos = strstr(pos, "\r\n");
+		if (!pos)
+			break;
+	}
+
+	return 0;
+}
+
+/* Leave only those codecs in 'ac_dest' that are also present in 'ac_other'.
+ * The matching is made by sdp_audio_codec_cmp(), i.e. payload_type numbers are not compared and fmtp parameters are
+ * compared 1:1 as plain strings.
+ * If translate_payload_type_numbers has an effect if ac_dest and ac_other have mismatching payload_type numbers for the
+ * same SDP codec descriptions. If translate_payload_type_numbers is true, take the payload_type numbers from ac_other.
+ * If false, keep payload_type numbers in ac_dest unchanged. */
+void sdp_audio_codecs_intersection(struct sdp_audio_codecs *ac_dest, const struct sdp_audio_codecs *ac_other,
+				   bool translate_payload_type_numbers)
+{
+	int i;
+	for (i = 0; i < ac_dest->count; i++) {
+		struct sdp_audio_codec *codec = &ac_dest->codec[i];
+		struct sdp_audio_codec *other;
+		OSMO_ASSERT(i < ARRAY_SIZE(ac_dest->codec));
+
+		other = sdp_audio_codec_by_descr((struct sdp_audio_codecs*)ac_other, codec);
+
+		if (!other) {
+			OSMO_ASSERT(sdp_audio_codec_remove(ac_dest, codec) == 0);
+			i--;
+			continue;
+		}
+
+		/* Doing payload_type number translation of part of the intersection because it makes the algorithm
+		 * simpler: we already know ac_dest is a subset of ac_other, and there is no need to resolve payload
+		 * type number conflicts. */
+		if (translate_payload_type_numbers)
+			codec->payload_type = other->payload_type;
+	}
+}
+
+/* Make sure the given codec is listed as the first codec. 'codec' must be an actual codec entry of the given audio
+ * codecs list. */
+void sdp_audio_codecs_select(struct sdp_audio_codecs *ac, struct sdp_audio_codec *codec)
+{
+	struct sdp_audio_codec tmp;
+	struct sdp_audio_codec *pos;
+	OSMO_ASSERT((codec >= ac->codec)
+		    && ((codec - ac->codec) < OSMO_MIN(ac->count, ARRAY_SIZE(ac->codec))));
+
+	/* Already the first? */
+	if (codec == ac->codec)
+		return;
+
+	tmp = *codec;
+	for (pos = codec - 1; pos >= ac->codec; pos--)
+		pos[1] = pos[0];
+
+	ac->codec[0] = tmp;
+	return;
+}
+
+/* Short single-line representation of an SDP audio codec, convenient for logging */
+int sdp_audio_codec_name_buf(char *buf, size_t buflen, const struct sdp_audio_codec *codec)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	OSMO_STRBUF_PRINTF(sb, "%s", codec->subtype_name);
+	if (codec->fmtp[0])
+		OSMO_STRBUF_PRINTF(sb, ":%s", codec->fmtp);
+	return sb.chars_needed;
+}
+
+char *sdp_audio_codec_name_c(void *ctx, const struct sdp_audio_codec *codec)
+{
+	NAME_C_IMPL(sdp_audio_codec_name_buf, codec)
+}
+
+const char *sdp_audio_codec_name(const struct sdp_audio_codec *codec)
+{
+	NAME_IMPL(sdp_audio_codec_name_c, codec)
+}
+
+/* Short single-line representation of a list of SDP audio codecs, convenient for logging */
+int sdp_audio_codecs_name_buf(char *buf, size_t buflen, const struct sdp_audio_codecs *ac)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	const struct sdp_audio_codec *codec;
+	if (!ac->count)
+		OSMO_STRBUF_PRINTF(sb, "(no-codecs)");
+	foreach_sdp_audio_codec(codec, ac) {
+		bool first = (codec == ac->codec);
+		if (!first)
+			OSMO_STRBUF_PRINTF(sb, ",");
+		OSMO_STRBUF_APPEND(sb, sdp_audio_codec_name_buf, codec);
+	}
+	return sb.chars_needed;
+}
+
+char *sdp_audio_codecs_name_c(void *ctx, const struct sdp_audio_codecs *ac)
+{
+	NAME_C_IMPL(sdp_audio_codecs_name_buf, ac)
+}
+
+const char *sdp_audio_codecs_name(const struct sdp_audio_codecs *ac)
+{
+	NAME_IMPL(sdp_audio_codecs_name_c, ac)
+}
+
+
+/* Short single-line representation of an SDP message, convenient for logging */
+int sdp_msg_name_buf(char *buf, size_t buflen, const struct sdp_msg *sdp)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	if (!sdp) {
+		OSMO_STRBUF_PRINTF(sb, "NULL");
+		return sb.chars_needed;
+	}
+
+	OSMO_STRBUF_PRINTF(sb, OSMO_SOCKADDR_STR_FMT, OSMO_SOCKADDR_STR_FMT_ARGS(&sdp->rtp));
+	OSMO_STRBUF_PRINTF(sb, "{");
+	OSMO_STRBUF_APPEND(sb, sdp_audio_codecs_name_buf, &sdp->audio_codecs);
+	OSMO_STRBUF_PRINTF(sb, "}");
+	return sb.chars_needed;
+}
+
+char *sdp_msg_name_c(void *ctx, const struct sdp_msg *sdp)
+{
+	NAME_C_IMPL(sdp_msg_name_buf, sdp)
+}
+
+const char *sdp_msg_name(const struct sdp_msg *sdp)
+{
+	NAME_IMPL(sdp_msg_name_c, sdp)
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 27c1205..864ac7c 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -2,6 +2,7 @@
 	sms_queue \
 	msc_vlr \
 	db_sms \
+	sdp_msg \
 	$(NULL)
 
 if BUILD_SMPP
diff --git a/tests/sdp_msg/Makefile.am b/tests/sdp_msg/Makefile.am
new file mode 100644
index 0000000..7d8def9
--- /dev/null
+++ b/tests/sdp_msg/Makefile.am
@@ -0,0 +1,32 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/include \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(NULL)
+
+LDADD = \
+	$(top_builddir)/src/libmsc/libmsc.a \
+	$(LIBOSMOCORE_LIBS) \
+	$(NULL)
+
+EXTRA_DIST = \
+	sdp_msg_test.ok \
+	sdp_msg_test.err \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	sdp_msg_test \
+	$(NULL)
+
+sdp_msg_test_SOURCES = \
+	sdp_msg_test.c \
+	$(NULL)
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/sdp_msg_test >$(srcdir)/sdp_msg_test.ok 2>$(srcdir)/sdp_msg_test.err
diff --git a/tests/sdp_msg/sdp_msg_test.c b/tests/sdp_msg/sdp_msg_test.c
new file mode 100644
index 0000000..b97127a
--- /dev/null
+++ b/tests/sdp_msg/sdp_msg_test.c
@@ -0,0 +1,569 @@
+#include <stdio.h>
+#include <string.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/msc/sdp_msg.h>
+
+struct sdp_test_data {
+	const char *sdp_input;
+	const char *expect_sdp_str;
+};
+
+static void dump_sdp(const char *str, const char *prefix)
+{
+	while (str && *str) {
+		const char *line_end = sdp_msg_line_end(str);
+		while (*line_end == '\r' || *line_end == '\n')
+			line_end++;
+		printf("%s%s\n", prefix, osmo_escape_str(str, line_end - str));
+		str = line_end;
+	}
+}
+
+struct sdp_test_data sdp_tests[] = {
+	{
+		"v=0\r\n"
+		"o=- 5628250 5628250 IN IP4 192.168.11.121\r\n"
+		"s=-\r\n"
+		"c=IN IP4 192.168.11.121\r\n"
+		"t=0 0\r\n"
+		"m=audio 10020 RTP/AVP 18 0 2 4 8 96 97 98 100 101\r\n"
+		"a=rtpmap:18 G729/8000\r\n"
+		"a=rtpmap:0 PCMU/8000\r\n"
+		"a=rtpmap:2 G726-32/8000\r\n"
+		"a=rtpmap:4 G723/8000\r\n"
+		"a=rtpmap:8 PCMA/8000\r\n"
+		"a=rtpmap:96 G726-40/8000\r\n"
+		"a=rtpmap:97 G726-24/8000\r\n"
+		"a=rtpmap:98 G726-16/8000\r\n"
+		"a=rtpmap:100 NSE/8000\r\n"
+		"a=fmtp:100 192-193\r\n"
+		"a=rtpmap:101 telephone-event/8000\r\n"
+		"a=fmtp:101 0-15\r\n"
+		"a=ptime:20\r\n"
+		"a=sendrecv\r\n"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 192.168.11.121\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 192.168.11.121\r\n"
+		"t=0 0\r\n"
+		"m=audio 10020 RTP/AVP 18 0 2 4 8 96 97 98 100 101\r\n"
+		"a=rtpmap:18 G729/8000\r\n"
+		"a=rtpmap:0 PCMU/8000\r\n"
+		"a=rtpmap:2 G726-32/8000\r\n"
+		"a=rtpmap:4 G723/8000\r\n"
+		"a=rtpmap:8 PCMA/8000\r\n"
+		"a=rtpmap:96 G726-40/8000\r\n"
+		"a=rtpmap:97 G726-24/8000\r\n"
+		"a=rtpmap:98 G726-16/8000\r\n"
+		"a=rtpmap:100 NSE/8000\r\n"
+		"a=fmtp:100 192-193\r\n"
+		"a=rtpmap:101 telephone-event/8000\r\n"
+		"a=fmtp:101 0-15\r\n"
+		"a=ptime:20\r\n"
+		,
+	},
+	{
+		"v=0\r\n"
+		"o=FooBar 1565090289 1565090290 IN IP4 192.168.11.151\r\n"
+		"s=FooBar\r\n"
+		"c=IN IP4 192.168.11.151\r\n"
+		"t=0 0\r\n"
+		"m=audio 16398 RTP/AVP 98\r\n"
+		"a=rtpmap:98 AMR/8000\r\n"
+		"a=fmtp:98 octet-align=1; mode-set=4\r\n"
+		"a=ptime:20\r\n"
+		"a=rtcp:16399 IN IP4 192.168.11.151\r\n"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 192.168.11.151\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 192.168.11.151\r\n"
+		"t=0 0\r\n"
+		"m=audio 16398 RTP/AVP 98\r\n"
+		"a=rtpmap:98 AMR/8000\r\n"
+		"a=fmtp:98 octet-align=1; mode-set=4\r\n"
+		"a=ptime:20\r\n"
+		,
+	},
+	{
+		"v=0\r\n"
+		"o=FooBar 1565090289 1565090290 IN IP4 192.168.11.151\r\n"
+		"s=FooBar\r\n"
+		"c=IN IP4 192.168.11.140\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 18 0 4 8 101\r\n"
+		"a=rtpmap:18 G729/8000\r\n"
+		"a=rtpmap:0 PCMU/8000\r\n"
+		"a=rtpmap:4 G723/8000\r\n"
+		"a=rtpmap:8 PCMA/8000\r\n"
+		"a=rtpmap:101 telephone-event/8000\r\n"
+		"a=fmtp:101 0-15\r\n"
+		"a=sendrecv\r\n"
+		"a=rtcp:30437\r\n"
+		"a=ptime:20\r\n"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 192.168.11.140\r\n" /* <- NOTE: loses the 'o=' address, uses only 'c=' */
+		"s=GSM Call\r\n"
+		"c=IN IP4 192.168.11.140\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 18 0 4 8 101\r\n"
+		"a=rtpmap:18 G729/8000\r\n"
+		"a=rtpmap:0 PCMU/8000\r\n"
+		"a=rtpmap:4 G723/8000\r\n"
+		"a=rtpmap:8 PCMA/8000\r\n"
+		"a=rtpmap:101 telephone-event/8000\r\n"
+		"a=fmtp:101 0-15\r\n"
+		"a=ptime:20\r\n"
+		,
+	},
+};
+
+void test_parse_and_compose()
+{
+	int i;
+	bool ok = true;
+
+	printf("\n\n%s\n", __func__);
+
+	for (i = 0; i < ARRAY_SIZE(sdp_tests); i++) {
+		struct sdp_test_data *t = &sdp_tests[i];
+		struct sdp_msg sdp = {};
+		char str[1024];
+		printf("\n[%d]\n", i);
+		dump_sdp(t->sdp_input, "sdp input: ");
+
+		sdp_msg_from_str(&sdp, t->sdp_input);
+		sdp_msg_to_str(str, sizeof(str), &sdp);
+
+		dump_sdp(str, "sdp_msg_to_str: ");
+		if (strcmp(str, t->expect_sdp_str)) {
+			int j;
+			ok = false;
+			printf("ERROR:\n");
+			dump_sdp(t->expect_sdp_str, "expect_sdp_str: ");
+			for (j = 0; t->expect_sdp_str[j]; j++) {
+				if (t->expect_sdp_str[j] != str[j]) {
+					printf("ERROR at position %d, at:\n", j);
+					dump_sdp(str + j, "     mismatch: ");
+					break;
+				}
+			}
+		} else
+			printf("[%d] ok\n", i);
+	}
+
+	OSMO_ASSERT(ok);
+}
+
+struct sdp_intersect_test_data {
+	const char *descr;
+	const char *sdp_a;
+	const char *sdp_b;
+	const char *expect_intersection;
+};
+
+#define SDP_1 \
+		"v=0\r\n" \
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n" \
+		"s=GSM Call\r\n" \
+		"c=IN IP4 23.42.23.42\r\n" \
+		"t=0 0\r\n" \
+		"m=audio 30436 RTP/AVP 112 3 111 110\r\n" \
+		"a=rtpmap:112 AMR/8000\r\n" \
+		"a=fmtp:112 octet-align=1\r\n" \
+		"a=rtpmap:3 GSM/8000\r\n" \
+		"a=rtpmap:111 GSM-HR-08/8000\r\n" \
+		"a=rtpmap:110 GSM-EFR/8000\r\n" \
+		"a=ptime:20\r\n"
+
+#define SDP_2 \
+		"v=0\r\n" \
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n" \
+		"s=GSM Call\r\n" \
+		"c=IN IP4 23.42.23.42\r\n" \
+		"t=0 0\r\n" \
+		"m=audio 30436 RTP/AVP 112 110\r\n" \
+		"a=rtpmap:112 AMR/8000\r\n" \
+		"a=fmtp:112 octet-align=1\r\n" \
+		"a=rtpmap:110 GSM-EFR/8000\r\n" \
+		"a=ptime:20\r\n"
+
+#define SDP_3 \
+		"v=0\r\n" \
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n" \
+		"s=GSM Call\r\n" \
+		"c=IN IP4 23.42.23.42\r\n" \
+		"t=0 0\r\n" \
+		"m=audio 30436 RTP/AVP 3 111\r\n" \
+		"a=rtpmap:3 GSM/8000\r\n" \
+		"a=rtpmap:111 GSM-HR-08/8000\r\n" \
+		"a=ptime:20\r\n"
+
+
+struct sdp_intersect_test_data sdp_intersect_tests[] = {
+	{
+		"identical codecs lead to no change"
+		,
+		SDP_1
+		,
+		"c=IN IP4 5.6.7.8\r\n" \
+		"m=audio 12345 RTP/AVP 112 3 111 110\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		,
+		SDP_1
+	},
+	{
+		"identical codecs in different order also lead to no change"
+		,
+		SDP_1
+		,
+		"c=IN IP4 5.6.7.8\r\n" \
+		"m=audio 12345 RTP/AVP 3 110 111 112\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		,
+		SDP_1
+	},
+	{
+		"identical codecs with mismatching payload type numbers also lead to no change"
+		,
+		SDP_1
+		,
+		"c=IN IP4 5.6.7.8\r\n" \
+		"m=audio 12345 RTP/AVP 96 97 98 99\r\n"
+		"a=rtpmap:96 GSM/8000\r\n"
+		"a=rtpmap:97 GSM-EFR/8000\r\n"
+		"a=rtpmap:98 GSM-HR-08/8000\r\n"
+		"a=rtpmap:99 AMR/8000\r\n"
+		"a=fmtp:99 octet-align=1\r\n"
+		,
+		SDP_1
+	},
+	{
+		"identical codecs plus some extra codecs also lead to no change"
+		,
+		SDP_1
+		,
+		"c=IN IP4 5.6.7.8\r\n" \
+		"m=audio 12345 RTP/AVP 8 0 96 97 98 99\r\n"
+		"a=rtpmap:8 PCMA/8000\r\n"
+		"a=rtpmap:0 PCMU/8000\r\n"
+		"a=rtpmap:96 GSM/8000\r\n"
+		"a=rtpmap:97 GSM-EFR/8000\r\n"
+		"a=rtpmap:98 GSM-HR-08/8000\r\n"
+		"a=rtpmap:99 AMR/8000\r\n"
+		"a=fmtp:99 octet-align=1\r\n"
+		,
+		SDP_1
+	},
+	{
+		"some codecs removed",
+		SDP_1,
+		SDP_2,
+		SDP_2,
+	},
+	{
+		"other codecs removed",
+		SDP_1,
+		SDP_3,
+		SDP_3,
+	},
+	{
+		"all codecs removed",
+		SDP_1
+		,
+		"s=empty"
+		,
+		"v=0\r\n" \
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n" \
+		"s=GSM Call\r\n" \
+		"c=IN IP4 23.42.23.42\r\n" \
+		"t=0 0\r\n" \
+		"m=audio 30436 RTP/AVP\r\n" \
+		"a=ptime:20\r\n"
+	},
+	{
+		"some real world test case"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 0.0.0.0\r\n"
+		"t=0 0\r\n"
+		"m=audio 0 RTP/AVP 112 113 110 3 111\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n"
+		"a=rtpmap:113 AMR-WB/8000\r\n"
+		"a=fmtp:113 octet-align=1\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 0.0.0.0\r\n"
+		"t=0 0\r\n"
+		"m=audio 0 RTP/AVP 112 113 110 3 111\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n"
+		"a=rtpmap:113 AMR-WB/8000\r\n"
+		"a=fmtp:113 octet-align=1\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 0.0.0.0\r\n"
+		"t=0 0\r\n"
+		"m=audio 0 RTP/AVP 112 113 110 3 111\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n"
+		"a=rtpmap:113 AMR-WB/8000\r\n"
+		"a=fmtp:113 octet-align=1\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=ptime:20\r\n"
+	}
+};
+
+const char *sdp_msg_logstr(const struct sdp_msg *sdp)
+{
+	static char buf[1024];
+	sdp_msg_to_str(buf, sizeof(buf), sdp);
+	return buf;
+}
+
+static void test_intersect()
+{
+	int i;
+	bool ok = true;
+	int rc;
+
+	printf("\n\n%s\n", __func__);
+
+	for (i = 0; i < ARRAY_SIZE(sdp_intersect_tests); i++) {
+		struct sdp_intersect_test_data *t = &sdp_intersect_tests[i];
+		struct sdp_msg sdp_a = {};
+		struct sdp_msg sdp_b = {};
+		char str[1024];
+		printf("\n[%d] %s\n", i, t->descr);
+		dump_sdp(t->sdp_a, "SDP A: ");
+		dump_sdp(t->sdp_b, " SDP B: ");
+
+		rc = sdp_msg_from_str(&sdp_a, t->sdp_a);
+		if (rc) {
+			printf("ERROR parsing SDP A: %d\n", rc);
+			break;
+		}
+		dump_sdp(sdp_msg_logstr(&sdp_a), "parsed SDP A: ");
+		rc = sdp_msg_from_str(&sdp_b, t->sdp_b);
+		if (rc) {
+			printf("ERROR parsing SDP A: %d\n", rc);
+			break;
+		}
+		dump_sdp(sdp_msg_logstr(&sdp_b), "parsed SDP B: ");
+		sdp_audio_codecs_intersection(&sdp_a.audio_codecs, &sdp_b.audio_codecs, false);
+		sdp_msg_to_str(str, sizeof(str), &sdp_a);
+
+		dump_sdp(str, "sdp_msg_intersection(a,b): ");
+		if (strcmp(str, t->expect_intersection)) {
+			int j;
+			ok = false;
+			printf("ERROR:\n");
+			dump_sdp(t->expect_intersection, "expect_intersection: ");
+			for (j = 0; t->expect_intersection[j]; j++) {
+				if (t->expect_intersection[j] != str[j]) {
+					printf("ERROR at position %d, at:\n", j);
+					dump_sdp(str + j, "     mismatch: ");
+					break;
+				}
+			}
+		} else
+			printf("[%d] ok\n", i);
+	}
+
+	OSMO_ASSERT(ok);
+}
+
+struct sdp_select_test_data {
+	const char *sdp;
+	unsigned int select_payload_type;
+	const char *expect_sdp;
+};
+
+struct sdp_select_test_data sdp_select_tests[] = {
+	{
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 112 3 111 110\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		112,
+		NULL
+	},
+	{
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 112 3 111 110\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		3,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 3 112 111 110\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+	},
+	{
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 112 3 111 110\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		111,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 111 112 3 110\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+	},
+	{
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 112 3 111 110\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=ptime:20\r\n"
+		,
+		110,
+		"v=0\r\n"
+		"o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n"
+		"s=GSM Call\r\n"
+		"c=IN IP4 23.42.23.42\r\n"
+		"t=0 0\r\n"
+		"m=audio 30436 RTP/AVP 110 112 3 111\r\n"
+		"a=rtpmap:110 GSM-EFR/8000\r\n"
+		"a=rtpmap:112 AMR/8000\r\n"
+		"a=fmtp:112 octet-align=1\r\n"
+		"a=rtpmap:3 GSM/8000\r\n"
+		"a=rtpmap:111 GSM-HR-08/8000\r\n"
+		"a=ptime:20\r\n"
+	},
+
+};
+
+static void test_select()
+{
+	int i;
+	bool ok = true;
+	int rc;
+
+	printf("\n\n%s\n", __func__);
+
+	for (i = 0; i < ARRAY_SIZE(sdp_select_tests); i++) {
+		struct sdp_select_test_data *t = &sdp_select_tests[i];
+		struct sdp_msg sdp = {};
+		struct sdp_audio_codec *codec;
+		char buf[1024];
+		printf("\n[%d]\n", i);
+		rc = sdp_msg_from_str(&sdp, t->sdp);
+		if (rc) {
+			printf("ERROR parsing SDP: %d\n", rc);
+			break;
+		}
+		printf("SDP: %s\n", sdp_audio_codecs_name(&sdp.audio_codecs));
+		codec = sdp_audio_codec_by_payload_type(&sdp.audio_codecs, t->select_payload_type, false);
+		OSMO_ASSERT(codec);
+		printf("Select: %s\n", sdp_audio_codec_name(codec));
+
+		sdp_audio_codecs_select(&sdp.audio_codecs, codec);
+
+		printf("SDP: %s\n", sdp_audio_codecs_name(&sdp.audio_codecs));
+		sdp_msg_to_str(buf, sizeof(buf), &sdp);
+
+		if (strcmp(buf, t->expect_sdp ? : t->sdp)) {
+			int j;
+			ok = false;
+			printf("ERROR:\n");
+			dump_sdp(buf, "selection result: ");
+			dump_sdp(t->expect_sdp, "expect result: ");
+			for (j = 0; t->expect_sdp[j]; j++) {
+				if (t->expect_sdp[j] != buf[j]) {
+					printf("ERROR at position %d, at:\n", j);
+					dump_sdp(buf + j, "     mismatch: ");
+					break;
+				}
+			}
+		} else
+			printf("[%d] ok\n", i);
+	}
+
+	OSMO_ASSERT(ok);
+}
+
+int main(void)
+{
+	test_parse_and_compose();
+	test_intersect();
+	test_select();
+	return 0;
+}
diff --git a/tests/sdp_msg/sdp_msg_test.err b/tests/sdp_msg/sdp_msg_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/sdp_msg/sdp_msg_test.err
diff --git a/tests/sdp_msg/sdp_msg_test.ok b/tests/sdp_msg/sdp_msg_test.ok
new file mode 100644
index 0000000..a7a3d51
--- /dev/null
+++ b/tests/sdp_msg/sdp_msg_test.ok
@@ -0,0 +1,590 @@
+
+
+test_parse_and_compose
+
+[0]
+sdp input: v=0\r\n
+sdp input: o=- 5628250 5628250 IN IP4 192.168.11.121\r\n
+sdp input: s=-\r\n
+sdp input: c=IN IP4 192.168.11.121\r\n
+sdp input: t=0 0\r\n
+sdp input: m=audio 10020 RTP/AVP 18 0 2 4 8 96 97 98 100 101\r\n
+sdp input: a=rtpmap:18 G729/8000\r\n
+sdp input: a=rtpmap:0 PCMU/8000\r\n
+sdp input: a=rtpmap:2 G726-32/8000\r\n
+sdp input: a=rtpmap:4 G723/8000\r\n
+sdp input: a=rtpmap:8 PCMA/8000\r\n
+sdp input: a=rtpmap:96 G726-40/8000\r\n
+sdp input: a=rtpmap:97 G726-24/8000\r\n
+sdp input: a=rtpmap:98 G726-16/8000\r\n
+sdp input: a=rtpmap:100 NSE/8000\r\n
+sdp input: a=fmtp:100 192-193\r\n
+sdp input: a=rtpmap:101 telephone-event/8000\r\n
+sdp input: a=fmtp:101 0-15\r\n
+sdp input: a=ptime:20\r\n
+sdp input: a=sendrecv\r\n
+sdp_msg_to_str: v=0\r\n
+sdp_msg_to_str: o=OsmoMSC 0 0 IN IP4 192.168.11.121\r\n
+sdp_msg_to_str: s=GSM Call\r\n
+sdp_msg_to_str: c=IN IP4 192.168.11.121\r\n
+sdp_msg_to_str: t=0 0\r\n
+sdp_msg_to_str: m=audio 10020 RTP/AVP 18 0 2 4 8 96 97 98 100 101\r\n
+sdp_msg_to_str: a=rtpmap:18 G729/8000\r\n
+sdp_msg_to_str: a=rtpmap:0 PCMU/8000\r\n
+sdp_msg_to_str: a=rtpmap:2 G726-32/8000\r\n
+sdp_msg_to_str: a=rtpmap:4 G723/8000\r\n
+sdp_msg_to_str: a=rtpmap:8 PCMA/8000\r\n
+sdp_msg_to_str: a=rtpmap:96 G726-40/8000\r\n
+sdp_msg_to_str: a=rtpmap:97 G726-24/8000\r\n
+sdp_msg_to_str: a=rtpmap:98 G726-16/8000\r\n
+sdp_msg_to_str: a=rtpmap:100 NSE/8000\r\n
+sdp_msg_to_str: a=fmtp:100 192-193\r\n
+sdp_msg_to_str: a=rtpmap:101 telephone-event/8000\r\n
+sdp_msg_to_str: a=fmtp:101 0-15\r\n
+sdp_msg_to_str: a=ptime:20\r\n
+[0] ok
+
+[1]
+sdp input: v=0\r\n
+sdp input: o=FooBar 1565090289 1565090290 IN IP4 192.168.11.151\r\n
+sdp input: s=FooBar\r\n
+sdp input: c=IN IP4 192.168.11.151\r\n
+sdp input: t=0 0\r\n
+sdp input: m=audio 16398 RTP/AVP 98\r\n
+sdp input: a=rtpmap:98 AMR/8000\r\n
+sdp input: a=fmtp:98 octet-align=1; mode-set=4\r\n
+sdp input: a=ptime:20\r\n
+sdp input: a=rtcp:16399 IN IP4 192.168.11.151\r\n
+sdp_msg_to_str: v=0\r\n
+sdp_msg_to_str: o=OsmoMSC 0 0 IN IP4 192.168.11.151\r\n
+sdp_msg_to_str: s=GSM Call\r\n
+sdp_msg_to_str: c=IN IP4 192.168.11.151\r\n
+sdp_msg_to_str: t=0 0\r\n
+sdp_msg_to_str: m=audio 16398 RTP/AVP 98\r\n
+sdp_msg_to_str: a=rtpmap:98 AMR/8000\r\n
+sdp_msg_to_str: a=fmtp:98 octet-align=1; mode-set=4\r\n
+sdp_msg_to_str: a=ptime:20\r\n
+[1] ok
+
+[2]
+sdp input: v=0\r\n
+sdp input: o=FooBar 1565090289 1565090290 IN IP4 192.168.11.151\r\n
+sdp input: s=FooBar\r\n
+sdp input: c=IN IP4 192.168.11.140\r\n
+sdp input: t=0 0\r\n
+sdp input: m=audio 30436 RTP/AVP 18 0 4 8 101\r\n
+sdp input: a=rtpmap:18 G729/8000\r\n
+sdp input: a=rtpmap:0 PCMU/8000\r\n
+sdp input: a=rtpmap:4 G723/8000\r\n
+sdp input: a=rtpmap:8 PCMA/8000\r\n
+sdp input: a=rtpmap:101 telephone-event/8000\r\n
+sdp input: a=fmtp:101 0-15\r\n
+sdp input: a=sendrecv\r\n
+sdp input: a=rtcp:30437\r\n
+sdp input: a=ptime:20\r\n
+sdp_msg_to_str: v=0\r\n
+sdp_msg_to_str: o=OsmoMSC 0 0 IN IP4 192.168.11.140\r\n
+sdp_msg_to_str: s=GSM Call\r\n
+sdp_msg_to_str: c=IN IP4 192.168.11.140\r\n
+sdp_msg_to_str: t=0 0\r\n
+sdp_msg_to_str: m=audio 30436 RTP/AVP 18 0 4 8 101\r\n
+sdp_msg_to_str: a=rtpmap:18 G729/8000\r\n
+sdp_msg_to_str: a=rtpmap:0 PCMU/8000\r\n
+sdp_msg_to_str: a=rtpmap:4 G723/8000\r\n
+sdp_msg_to_str: a=rtpmap:8 PCMA/8000\r\n
+sdp_msg_to_str: a=rtpmap:101 telephone-event/8000\r\n
+sdp_msg_to_str: a=fmtp:101 0-15\r\n
+sdp_msg_to_str: a=ptime:20\r\n
+[2] ok
+
+
+test_intersect
+
+[0] identical codecs lead to no change
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: c=IN IP4 5.6.7.8\r\n
+ SDP B: m=audio 12345 RTP/AVP 112 3 111 110\r\n
+ SDP B: a=rtpmap:112 AMR/8000\r\n
+ SDP B: a=fmtp:112 octet-align=1\r\n
+ SDP B: a=rtpmap:3 GSM/8000\r\n
+ SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+ SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 5.6.7.8\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 5.6.7.8\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 12345 RTP/AVP 112 3 111 110\r\n
+parsed SDP B: a=rtpmap:112 AMR/8000\r\n
+parsed SDP B: a=fmtp:112 octet-align=1\r\n
+parsed SDP B: a=rtpmap:3 GSM/8000\r\n
+parsed SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 112 3 111 110\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[0] ok
+
+[1] identical codecs in different order also lead to no change
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: c=IN IP4 5.6.7.8\r\n
+ SDP B: m=audio 12345 RTP/AVP 3 110 111 112\r\n
+ SDP B: a=rtpmap:3 GSM/8000\r\n
+ SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+ SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+ SDP B: a=rtpmap:112 AMR/8000\r\n
+ SDP B: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 5.6.7.8\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 5.6.7.8\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 12345 RTP/AVP 3 110 111 112\r\n
+parsed SDP B: a=rtpmap:3 GSM/8000\r\n
+parsed SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP B: a=rtpmap:112 AMR/8000\r\n
+parsed SDP B: a=fmtp:112 octet-align=1\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 112 3 111 110\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[1] ok
+
+[2] identical codecs with mismatching payload type numbers also lead to no change
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: c=IN IP4 5.6.7.8\r\n
+ SDP B: m=audio 12345 RTP/AVP 96 97 98 99\r\n
+ SDP B: a=rtpmap:96 GSM/8000\r\n
+ SDP B: a=rtpmap:97 GSM-EFR/8000\r\n
+ SDP B: a=rtpmap:98 GSM-HR-08/8000\r\n
+ SDP B: a=rtpmap:99 AMR/8000\r\n
+ SDP B: a=fmtp:99 octet-align=1\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 5.6.7.8\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 5.6.7.8\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 12345 RTP/AVP 96 97 98 99\r\n
+parsed SDP B: a=rtpmap:96 GSM/8000\r\n
+parsed SDP B: a=rtpmap:97 GSM-EFR/8000\r\n
+parsed SDP B: a=rtpmap:98 GSM-HR-08/8000\r\n
+parsed SDP B: a=rtpmap:99 AMR/8000\r\n
+parsed SDP B: a=fmtp:99 octet-align=1\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 112 3 111 110\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[2] ok
+
+[3] identical codecs plus some extra codecs also lead to no change
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: c=IN IP4 5.6.7.8\r\n
+ SDP B: m=audio 12345 RTP/AVP 8 0 96 97 98 99\r\n
+ SDP B: a=rtpmap:8 PCMA/8000\r\n
+ SDP B: a=rtpmap:0 PCMU/8000\r\n
+ SDP B: a=rtpmap:96 GSM/8000\r\n
+ SDP B: a=rtpmap:97 GSM-EFR/8000\r\n
+ SDP B: a=rtpmap:98 GSM-HR-08/8000\r\n
+ SDP B: a=rtpmap:99 AMR/8000\r\n
+ SDP B: a=fmtp:99 octet-align=1\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 5.6.7.8\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 5.6.7.8\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 12345 RTP/AVP 8 0 96 97 98 99\r\n
+parsed SDP B: a=rtpmap:8 PCMA/8000\r\n
+parsed SDP B: a=rtpmap:0 PCMU/8000\r\n
+parsed SDP B: a=rtpmap:96 GSM/8000\r\n
+parsed SDP B: a=rtpmap:97 GSM-EFR/8000\r\n
+parsed SDP B: a=rtpmap:98 GSM-HR-08/8000\r\n
+parsed SDP B: a=rtpmap:99 AMR/8000\r\n
+parsed SDP B: a=fmtp:99 octet-align=1\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 112 3 111 110\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[3] ok
+
+[4] some codecs removed
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: v=0\r\n
+ SDP B: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+ SDP B: s=GSM Call\r\n
+ SDP B: c=IN IP4 23.42.23.42\r\n
+ SDP B: t=0 0\r\n
+ SDP B: m=audio 30436 RTP/AVP 112 110\r\n
+ SDP B: a=rtpmap:112 AMR/8000\r\n
+ SDP B: a=fmtp:112 octet-align=1\r\n
+ SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+ SDP B: a=ptime:20\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 23.42.23.42\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 30436 RTP/AVP 112 110\r\n
+parsed SDP B: a=rtpmap:112 AMR/8000\r\n
+parsed SDP B: a=fmtp:112 octet-align=1\r\n
+parsed SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 112 110\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[4] ok
+
+[5] other codecs removed
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: v=0\r\n
+ SDP B: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+ SDP B: s=GSM Call\r\n
+ SDP B: c=IN IP4 23.42.23.42\r\n
+ SDP B: t=0 0\r\n
+ SDP B: m=audio 30436 RTP/AVP 3 111\r\n
+ SDP B: a=rtpmap:3 GSM/8000\r\n
+ SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+ SDP B: a=ptime:20\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 23.42.23.42\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 30436 RTP/AVP 3 111\r\n
+parsed SDP B: a=rtpmap:3 GSM/8000\r\n
+parsed SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP 3 111\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[5] ok
+
+[6] all codecs removed
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 23.42.23.42\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: s=empty
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 23.42.23.42\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 30436 RTP/AVP 112 3 111 110\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 0.0.0.0\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 0 RTP/AVP\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 23.42.23.42\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 30436 RTP/AVP\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[6] ok
+
+[7] some real world test case
+SDP A: v=0\r\n
+SDP A: o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+SDP A: s=GSM Call\r\n
+SDP A: c=IN IP4 0.0.0.0\r\n
+SDP A: t=0 0\r\n
+SDP A: m=audio 0 RTP/AVP 112 113 110 3 111\r\n
+SDP A: a=rtpmap:112 AMR/8000\r\n
+SDP A: a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n
+SDP A: a=rtpmap:113 AMR-WB/8000\r\n
+SDP A: a=fmtp:113 octet-align=1\r\n
+SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+SDP A: a=rtpmap:3 GSM/8000\r\n
+SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+SDP A: a=ptime:20\r\n
+ SDP B: v=0\r\n
+ SDP B: o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+ SDP B: s=GSM Call\r\n
+ SDP B: c=IN IP4 0.0.0.0\r\n
+ SDP B: t=0 0\r\n
+ SDP B: m=audio 0 RTP/AVP 112 113 110 3 111\r\n
+ SDP B: a=rtpmap:112 AMR/8000\r\n
+ SDP B: a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n
+ SDP B: a=rtpmap:113 AMR-WB/8000\r\n
+ SDP B: a=fmtp:113 octet-align=1\r\n
+ SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+ SDP B: a=rtpmap:3 GSM/8000\r\n
+ SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+ SDP B: a=ptime:20\r\n
+parsed SDP A: v=0\r\n
+parsed SDP A: o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+parsed SDP A: s=GSM Call\r\n
+parsed SDP A: c=IN IP4 0.0.0.0\r\n
+parsed SDP A: t=0 0\r\n
+parsed SDP A: m=audio 0 RTP/AVP 112 113 110 3 111\r\n
+parsed SDP A: a=rtpmap:112 AMR/8000\r\n
+parsed SDP A: a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n
+parsed SDP A: a=rtpmap:113 AMR-WB/8000\r\n
+parsed SDP A: a=fmtp:113 octet-align=1\r\n
+parsed SDP A: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP A: a=rtpmap:3 GSM/8000\r\n
+parsed SDP A: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP A: a=ptime:20\r\n
+parsed SDP B: v=0\r\n
+parsed SDP B: o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+parsed SDP B: s=GSM Call\r\n
+parsed SDP B: c=IN IP4 0.0.0.0\r\n
+parsed SDP B: t=0 0\r\n
+parsed SDP B: m=audio 0 RTP/AVP 112 113 110 3 111\r\n
+parsed SDP B: a=rtpmap:112 AMR/8000\r\n
+parsed SDP B: a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n
+parsed SDP B: a=rtpmap:113 AMR-WB/8000\r\n
+parsed SDP B: a=fmtp:113 octet-align=1\r\n
+parsed SDP B: a=rtpmap:110 GSM-EFR/8000\r\n
+parsed SDP B: a=rtpmap:3 GSM/8000\r\n
+parsed SDP B: a=rtpmap:111 GSM-HR-08/8000\r\n
+parsed SDP B: a=ptime:20\r\n
+sdp_msg_intersection(a,b): v=0\r\n
+sdp_msg_intersection(a,b): o=OsmoMSC 0 0 IN IP4 0.0.0.0\r\n
+sdp_msg_intersection(a,b): s=GSM Call\r\n
+sdp_msg_intersection(a,b): c=IN IP4 0.0.0.0\r\n
+sdp_msg_intersection(a,b): t=0 0\r\n
+sdp_msg_intersection(a,b): m=audio 0 RTP/AVP 112 113 110 3 111\r\n
+sdp_msg_intersection(a,b): a=rtpmap:112 AMR/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:112 octet-align=1;mode-set=0,1,2,3\r\n
+sdp_msg_intersection(a,b): a=rtpmap:113 AMR-WB/8000\r\n
+sdp_msg_intersection(a,b): a=fmtp:113 octet-align=1\r\n
+sdp_msg_intersection(a,b): a=rtpmap:110 GSM-EFR/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:3 GSM/8000\r\n
+sdp_msg_intersection(a,b): a=rtpmap:111 GSM-HR-08/8000\r\n
+sdp_msg_intersection(a,b): a=ptime:20\r\n
+[7] ok
+
+
+test_select
+
+[0]
+SDP: AMR:octet-align=1,GSM,GSM-HR-08,GSM-EFR
+Select: AMR:octet-align=1
+SDP: AMR:octet-align=1,GSM,GSM-HR-08,GSM-EFR
+[0] ok
+
+[1]
+SDP: AMR:octet-align=1,GSM,GSM-HR-08,GSM-EFR
+Select: GSM
+SDP: GSM,AMR:octet-align=1,GSM-HR-08,GSM-EFR
+[1] ok
+
+[2]
+SDP: AMR:octet-align=1,GSM,GSM-HR-08,GSM-EFR
+Select: GSM-HR-08
+SDP: GSM-HR-08,AMR:octet-align=1,GSM,GSM-EFR
+[2] ok
+
+[3]
+SDP: AMR:octet-align=1,GSM,GSM-HR-08,GSM-EFR
+Select: GSM-EFR
+SDP: GSM-EFR,AMR:octet-align=1,GSM,GSM-HR-08
+[3] ok
diff --git a/tests/testsuite.at b/tests/testsuite.at
index c0788b9..946d0db 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -107,3 +107,10 @@
 cat $abs_srcdir/msc_vlr/msc_vlr_test_ss.err > experr
 AT_CHECK([$abs_top_builddir/tests/msc_vlr/msc_vlr_test_ss], [], [expout], [experr])
 AT_CLEANUP
+
+AT_SETUP([sdp_msg_test])
+AT_KEYWORDS([sdp_msg_test])
+cat $abs_srcdir/sdp_msg/sdp_msg_test.ok > expout
+cat $abs_srcdir/sdp_msg/sdp_msg_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/sdp_msg/sdp_msg_test], [], [expout], [experr])
+AT_CLEANUP

-- 
To view, visit https://gerrit.osmocom.org/c/osmo-msc/+/15947
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-msc
Gerrit-Branch: master
Gerrit-Change-Id: If3ce23cd5bab15e2ab4c52ef3e4c75979dffe931
Gerrit-Change-Number: 15947
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr at sysmocom.de>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20191103/231420d6/attachment.htm>


More information about the gerrit-log mailing list