Change in osmo-mgw[master]: client: add features to generate and parse codec information

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/.

dexter gerrit-no-reply at lists.osmocom.org
Fri Jun 15 16:34:12 UTC 2018


dexter has uploaded this change for review. ( https://gerrit.osmocom.org/9649


Change subject: client: add features to generate and parse codec information
......................................................................

client: add features to generate and parse codec information

The current implementation does not support any way to influence the
codec that is negotiated via SDP or LCO. The client statically
negotitates AMR on an invalid payload type number. Also we ignore
any codec information in the responses.

- Add struct members to allow setting of user defined codec information.
- Add struct members to retrieve parsed codec info from responses.
- Add code to generate codec information in SDP
- Add code to parse SDP codec info in MGCP responses

Change-Id: I78e72d41b73acfcb40599a0ff4823f17c3642059
Related: OS#2728
Related: OS#3334
---
M include/osmocom/mgcp_client/mgcp_client.h
M include/osmocom/mgcp_client/mgcp_client_fsm.h
M src/libosmo-mgcp-client/mgcp_client.c
M src/libosmo-mgcp-client/mgcp_client_fsm.c
M tests/mgcp_client/mgcp_client_test.c
M tests/mgcp_client/mgcp_client_test.err
M tests/mgcp_client/mgcp_client_test.ok
7 files changed, 721 insertions(+), 85 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-mgw refs/changes/49/9649/1

diff --git a/include/osmocom/mgcp_client/mgcp_client.h b/include/osmocom/mgcp_client/mgcp_client.h
index 73f3bba..3cecb8f 100644
--- a/include/osmocom/mgcp_client/mgcp_client.h
+++ b/include/osmocom/mgcp_client/mgcp_client.h
@@ -26,6 +26,33 @@
 
 typedef unsigned int mgcp_trans_id_t;
 
+/*! Enumeration of the codec types that mgcp_client is able to handle. */
+enum mgcp_codecs {
+	CODEC_PCMU_8000_1 = 0,
+	CODEC_GSM_8000_1 = 3,
+	CODEC_PCMA_8000_1 = 8,
+	CODEC_G729_8000_1 = 18,
+	CODEC_GSMEFR_8000_1 = 110,
+	CODEC_GSMHR_8000_1 = 111,	
+	CODEC_AMR_8000_1 = 112,
+	CODEC_AMRWB_16000_1 = 113,
+};
+/* Note: when new codec types are added, the corresponding value strings
+ * in mgcp_client.c (codec_table) must be updated as well. Enumerations
+ * in enum mgcp_codecs must correspond to a valid payload type. However,
+ * this is an internal assumption that is made to avoid lookup tables.
+ * The API-User should not rely on this coincidence! */
+
+/*! Structure to build a payload type map to allow the defiition custom payload
+ *  types. */
+struct ptmap {
+	/*!< codec for which a payload type number should be defined */
+	enum mgcp_codecs codec;
+
+	/*!< payload type number (96-127) */
+	unsigned int pt;
+};
+
 struct mgcp_response_head {
 	int response_code;
 	mgcp_trans_id_t trans_id;
@@ -39,6 +66,11 @@
 	struct mgcp_response_head head;
 	uint16_t audio_port;
 	char audio_ip[INET_ADDRSTRLEN];
+	unsigned int ptime;
+	enum mgcp_codecs codecs[MGCP_MAX_CODECS];
+	unsigned int codecs_len;
+	struct ptmap ptmap[MGCP_MAX_CODECS];
+	unsigned int ptmap_len;	
 };
 
 enum mgcp_verb {
@@ -66,6 +98,11 @@
 	uint16_t audio_port;
 	char *audio_ip;
 	enum mgcp_connection_mode conn_mode;
+	unsigned int ptime;
+	enum mgcp_codecs codecs[MGCP_MAX_CODECS];
+	unsigned int codecs_len;
+	struct ptmap ptmap[MGCP_MAX_CODECS];
+	unsigned int ptmap_len;
 };
 
 void mgcp_client_conf_init(struct mgcp_client_conf *conf);
@@ -117,3 +154,9 @@
 {
 	return get_value_string(mgcp_client_connection_mode_strs, mode);
 }
+
+enum mgcp_codecs map_str_to_codec(const char *str);
+unsigned int map_codec_to_pt(struct ptmap *ptmap, unsigned int ptmap_len,
+			     enum mgcp_codecs codec);
+enum mgcp_codecs map_pt_to_codec(struct ptmap *ptmap, unsigned int ptmap_len,
+				 unsigned int pt);
diff --git a/include/osmocom/mgcp_client/mgcp_client_fsm.h b/include/osmocom/mgcp_client/mgcp_client_fsm.h
index 93fe582..47d9fab 100644
--- a/include/osmocom/mgcp_client/mgcp_client_fsm.h
+++ b/include/osmocom/mgcp_client/mgcp_client_fsm.h
@@ -25,6 +25,15 @@
 
 	/*!< CALL ID (unique per connection) */
 	unsigned int call_id;
+
+	/*!< RTP packetization interval (optional) */
+	unsigned int ptime;
+
+	/*!< RTP codec list (optional) */
+	enum mgcp_codecs codecs[MGCP_MAX_CODECS];
+
+	/*!< Number of codecs in RTP codec list (optional) */
+	unsigned int codecs_len;
 };
 
 struct osmo_fsm_inst *mgcp_conn_create(struct mgcp_client *mgcp, struct osmo_fsm_inst *parent_fi, uint32_t parent_term_evt,
diff --git a/src/libosmo-mgcp-client/mgcp_client.c b/src/libosmo-mgcp-client/mgcp_client.c
index e054593..00b52f8 100644
--- a/src/libosmo-mgcp-client/mgcp_client.c
+++ b/src/libosmo-mgcp-client/mgcp_client.c
@@ -36,6 +36,150 @@
 #include <unistd.h>
 #include <string.h>
 
+/* Codec descripton for dynamic payload types (SDP) */
+static const struct value_string codec_table[] = {
+	{ CODEC_PCMU_8000_1, "PCMU/8000/1" },
+	{ CODEC_GSM_8000_1, "GSM/8000/1" },
+	{ CODEC_PCMA_8000_1, "PCMA/8000/1" },
+	{ CODEC_G729_8000_1, "G729/8000/1" },
+	{ CODEC_GSMEFR_8000_1, "GSM-EFR/8000/1" },
+	{ CODEC_GSMHR_8000_1, "GSM-HR-08/8000/1" },
+	{ CODEC_AMR_8000_1, "AMR/8000/1" },
+	{ CODEC_AMRWB_16000_1, "AMR-WB/16000/1" },
+	{ 0, NULL },
+};
+
+/* Get encoding name from a full codec string e,g.
+ * ("CODEC/8000/2" => returns "CODEC") */
+static char *extract_codec_name(const char *str)
+{
+	static char buf[64];
+	unsigned int i;
+
+	if (!str)
+		return NULL;
+
+	/* FIXME osmo_strlcpy */
+	osmo_strlcpy(buf, str, sizeof(buf));
+
+	for (i = 0; i < strlen(buf); i++) {
+		if (buf[i] == '/')
+			buf[i] = '\0';
+	}
+
+	return buf;
+}
+
+/*! Map a string to a codec.
+ *  \ptmap[in] str input string (e.g "GSM/8000/1", "GSM/8000" or "GSM")
+ *  \returns codec that corresponds to the given string representation. */
+enum mgcp_codecs map_str_to_codec(const char *str)
+{
+	unsigned int i;
+	char *codec_name;
+	char str_buf[64];
+
+	osmo_strlcpy(str_buf, extract_codec_name(str), sizeof(str_buf));
+
+	for (i = 0; i < ARRAY_SIZE(codec_table); i++) {
+		codec_name = extract_codec_name(codec_table[i].str);
+		if (!codec_name)
+			continue;
+		if (strcmp(codec_name, str_buf) == 0)
+			return codec_table[i].value;
+	}
+
+	return -1;
+}
+
+/* Check the ptmap for illegal mappings */
+static int check_ptmap(struct ptmap *ptmap)
+{
+	/* Check if there are mappings that leave the IANA assigned dynamic
+	 * payload type range. Under normal conditions such mappings should
+	 * not occur */
+
+	/* Its ok to have a 1:1 mapping in the statically defined
+	 * range, this won't hurt */
+	if (ptmap->codec == ptmap->pt)
+		return 0;
+
+	if (ptmap->codec < 96 || ptmap->codec > 127)
+		goto error;
+	if (ptmap->pt < 96 || ptmap->pt > 127)
+		goto error;
+
+	return 0;
+error:
+	LOGP(DLMGCP, LOGL_ERROR,
+	     "ptmap contains illegal mapping: codec=%u maps to pt=%u\n",
+	     ptmap->codec, ptmap->pt);
+	return -1;
+}
+
+/*! Map a codec to a payload type.
+ *  \ptmap[in] payload pointer to payload type map with specified payload types.
+ *  \ptmap[in] ptmap_len length of the payload type map.
+ *  \ptmap[in] codec the codec for which the payload type should be looked up.
+ *  \returns assigned payload type */
+unsigned int map_codec_to_pt(struct ptmap *ptmap, unsigned int ptmap_len,
+			     enum mgcp_codecs codec)
+{
+	unsigned int i;
+
+	/*! Note: If the payload type map is empty or the codec is not found
+	 *  in the map, then a 1:1 mapping is performed. If the codec falls
+	 *  into the statically defined range or if the mapping table isself
+	 *  tries to map to the statically defined range, then the mapping
+	 *  is also ignored and a 1:1 mapping is performed instead. */
+
+	/* we may return the codec directly since enum mgcp_codecs directly
+	 * corresponds to the statićally assigned payload types */
+	if (codec < 96 || codec > 127)
+		return codec;
+
+	for (i = 0; i < ptmap_len; i++) {
+		/* Skip illegal map entries */
+		if (check_ptmap(ptmap) == 0 && ptmap->codec == codec)
+			return ptmap->pt;
+		ptmap++;
+	}
+
+	/* If nothing is found, do not perform any mapping */
+	return codec;
+}
+
+/*! Map a payload type to a codec.
+ *  \ptmap[in] payload pointer to payload type map with specified payload types.
+ *  \ptmap[in] ptmap_len length of the payload type map.
+ *  \ptmap[in] payload type for which the codec should be looked up.
+ *  \returns codec that corresponds to the specified payload type */
+enum mgcp_codecs map_pt_to_codec(struct ptmap *ptmap, unsigned int ptmap_len,
+				 unsigned int pt)
+{
+	unsigned int i;
+
+	/*! Note: If the payload type map is empty or the payload type is not
+	 *  found in the map, then a 1:1 mapping is performed. If the payload
+	 *  type falls into the statically defined range or if the mapping
+	 *  table isself tries to map to the statically defined range, then
+	 *  the mapping is also ignored and a 1:1 mapping is performed
+	 *  instead. */
+
+	/* See also note in map_codec_to_pt() */
+	if (pt < 96 || pt > 127)
+		return pt;
+
+	for (i = 0; i < ptmap_len; i++) {
+		if (check_ptmap(ptmap) == 0 && ptmap->pt == pt)
+			return ptmap->codec;
+		ptmap++;
+	}
+
+	/* If nothing is found, do not perform any mapping */
+	return pt;
+}
+
 /*! Initalize MGCP client configuration struct with default values.
  *  \param[out] conf Client configuration.*/
 void mgcp_client_conf_init(struct mgcp_client_conf *conf)
@@ -178,22 +322,114 @@
 	return true;
 }
 
-/* Parse a line like "m=audio 16002 RTP/AVP 98" */
-static int mgcp_parse_audio_port(struct mgcp_response *r, const char *line)
+/* Parse a line like "m=audio 16002 RTP/AVP 98", extract port and payload types */
+static int mgcp_parse_audio_port_pt(struct mgcp_response *r, char *line)
 {
-	if (sscanf(line, "m=audio %hu",
-		   &r->audio_port) != 1)
-		goto response_parse_failure;
+	char *pt_str;
+	unsigned int pt;
+	unsigned int count = 0;
+	unsigned int i;
 
+	/* Extract port information */
+	if (sscanf(line, "m=audio %hu", &r->audio_port) != 1)
+		goto response_parse_failure_port;
 	if (r->audio_port == 0)
-		goto response_parse_failure;
+		goto response_parse_failure_port;
 
+	/* Extract payload types */
+	line = strstr(line, "RTP/AVP ");
+	if (!line)
+		goto exit;
+
+	pt_str = strtok(line, " ");
+	while (1) {
+		/* Do not allow excessive payload types */
+		if (count > ARRAY_SIZE(r->codecs))
+			goto response_parse_failure_pt;
+
+		pt_str = strtok(NULL, " ");
+		if (!pt_str)
+			break;
+		pt = atoi(pt_str);
+
+		/* Do not allow duplicate payload types */
+		for (i = 0; i < count; i++)
+			if (r->codecs[i] == pt)
+				goto response_parse_failure_pt;
+
+		/* Note: The payload type we store may not necessarly match
+		 * the codec types we have defined in enum mgcp_codecs. To
+		 * ensure that the end result only contains codec types which
+		 * match enum mgcp_codecs, we will go through afterwards and
+		 * remap the affected entries with the inrofmation we learn
+		 * from rtpmap */
+		r->codecs[count] = pt;
+		count++;
+	}
+
+	r->codecs_len = count;
+
+exit:
 	return 0;
 
-response_parse_failure:
+response_parse_failure_port:
 	LOGP(DLMGCP, LOGL_ERROR,
-	     "Failed to parse MGCP response header (audio port)\n");
+	     "Failed to parse SDP parameter port (%s)\n", line);
 	return -EINVAL;
+
+response_parse_failure_pt:
+	LOGP(DLMGCP, LOGL_ERROR,
+	     "Failed to parse SDP parameter payload types (%s)\n", line);
+	return -EINVAL;
+}
+
+/* Parse a line like "m=audio 16002 RTP/AVP 98", extract port and payload types */
+static int mgcp_parse_audio_ptime_rtpmap(struct mgcp_response *r, const char *line)
+{
+	unsigned int pt;
+	char codec_resp[64];
+	unsigned int codec;
+	
+	
+	if (strstr(line, "ptime")) {
+		if (sscanf(line, "a=ptime:%u", &r->ptime) != 1)
+			goto response_parse_failure_ptime;
+	} else if (strstr(line, "rtpmap")) {
+		if (sscanf(line, "a=rtpmap:%d %63s", &pt, codec_resp) == 2) {
+			/* The MGW may assign an own payload type in the
+			 * response if the choosen codec falls into the IANA
+			 * assigned dynamic payload type range (96-127).
+			 * Normally the MGW should obey the 3gpp payload type
+			 * assignments, which are fixed, so we likely wont see
+			 * anything unexpected here. In order to be sure that
+			 * we will now check the codec string and if the result
+			 * does not match to what is IANA / 3gpp assigned, we
+			 * will create an entry in the ptmap table so we can
+			 * lookup later what has been assigned. */
+			codec = map_str_to_codec(codec_resp);
+			if (codec != pt) {
+				if (r->ptmap_len < ARRAY_SIZE(r->ptmap)) {
+					r->ptmap[r->ptmap_len].pt = pt;
+					r->ptmap[r->ptmap_len].codec = codec;
+					r->ptmap_len++;
+				} else
+					goto response_parse_failure_rtpmap;
+			}
+
+		} else
+			goto response_parse_failure_rtpmap;
+	}
+	
+	return 0;
+
+response_parse_failure_ptime:
+	LOGP(DLMGCP, LOGL_ERROR,
+	     "Failed to parse SDP parameter, invalid ptime (%s)\n", line);
+	return -EINVAL;		
+response_parse_failure_rtpmap:
+	LOGP(DLMGCP, LOGL_ERROR,
+	     "Failed to parse SDP parameter, invalid rtpmap (%s)\n", line);
+	return -EINVAL;		
 }
 
 /* Parse a line like "c=IN IP4 10.11.12.13" */
@@ -253,6 +489,7 @@
 	int rc;
 	char *data;
 	char *data_ptr;
+	int i;
 
 	/* Since this functions performs a destructive parsing, we create a
 	 * local copy of the body data */
@@ -277,8 +514,13 @@
 			return -EINVAL;
 
 		switch (line[0]) {
+		case 'a':
+			rc = mgcp_parse_audio_ptime_rtpmap(r, line);
+			if (rc)
+				goto exit;
+			break;
 		case 'm':
-			rc = mgcp_parse_audio_port(r, line);
+			rc = mgcp_parse_audio_port_pt(r, line);
 			if (rc)
 				goto exit;
 			break;
@@ -293,6 +535,10 @@
 		}
 	}
 
+	/* See also note in mgcp_parse_audio_port_pt() */
+	for (i = 0; i < r->codecs_len; i++)
+	        r->codecs[i] =  map_pt_to_codec(r->ptmap, r->ptmap_len, r->codecs[i]);
+
 	rc = 0;
 exit:
 	talloc_free(data);
@@ -813,6 +1059,119 @@
 #define MGCP_AUEP_MANDATORY (MGCP_MSG_PRESENCE_ENDPOINT)
 #define MGCP_RSIP_MANDATORY 0	/* none */
 
+/* Helper function for mgcp_msg_gen(): Add LCO information to MGCP message */
+static int add_lco(struct msgb *msg, struct mgcp_msg *mgcp_msg)
+{
+	unsigned int i;
+	int rc = 0;
+	const char *codec;
+	unsigned int pt;
+
+	rc += msgb_printf(msg, "L:");
+
+	if (mgcp_msg->ptime)
+		rc += msgb_printf(msg, " p:%u,", mgcp_msg->ptime);
+
+	if (mgcp_msg->codecs_len) {
+		rc += msgb_printf(msg, " a:");
+		for (i = 0; i < mgcp_msg->codecs_len; i++) {
+			pt = mgcp_msg->codecs[i];
+			codec = get_value_string_or_null(codec_table, pt);
+			
+			/* Note: Use codec descriptors from enum mgcp_codecs
+			 * in mgcp_client only! */
+			OSMO_ASSERT(codec);
+			rc += msgb_printf(msg, "%s", extract_codec_name(codec));
+			if (i < mgcp_msg->codecs_len - 1)
+				rc += msgb_printf(msg, ";");
+		}
+		rc += msgb_printf(msg, ",");
+	}
+
+	rc += msgb_printf(msg, " nt:IN\r\n");
+
+	return rc;
+}
+
+/* Helper function for mgcp_msg_gen(): Add SDP information to MGCP message */
+static int add_sdp(struct msgb *msg, struct mgcp_msg *mgcp_msg, struct mgcp_client *mgcp)
+{
+	unsigned int i;
+	int rc = 0;
+	char local_ip[INET_ADDRSTRLEN];
+	const char *codec;
+	unsigned int pt;
+
+	/* Add separator to mark the beginning of the SDP block */
+	rc += msgb_printf(msg, "\r\n");
+
+	/* Add SDP protocol version */
+	rc += msgb_printf(msg, "v=0\r\n");
+
+	/* Determine local IP-Address */
+	if (osmo_sock_local_ip(local_ip, mgcp->actual.remote_addr) < 0) {
+		LOGP(DLMGCP, LOGL_ERROR,
+		     "Could not determine local IP-Address!\n");
+		msgb_free(msg);
+		return -2;
+	}
+
+	/* Add owner/creator (SDP) */
+	rc += msgb_printf(msg, "o=- %x 23 IN IP4 %s\r\n",
+			  mgcp_msg->call_id, local_ip);
+
+	/* Add session name (none) */
+	rc += msgb_printf(msg, "s=-\r\n");
+
+	/* Add RTP address and port */
+	if (mgcp_msg->audio_port == 0) {
+		LOGP(DLMGCP, LOGL_ERROR,
+		     "Invalid port number, can not generate MGCP message\n");
+		msgb_free(msg);
+		return -2;
+	}
+	if (strlen(mgcp_msg->audio_ip) <= 0) {
+		LOGP(DLMGCP, LOGL_ERROR,
+		     "Empty ip address, can not generate MGCP message\n");
+		msgb_free(msg);
+		return -2;
+	}
+	rc += msgb_printf(msg, "c=IN IP4 %s\r\n", mgcp_msg->audio_ip);
+
+	/* Add time description, active time (SDP) */
+	rc += msgb_printf(msg, "t=0 0\r\n");
+
+	rc += msgb_printf(msg, "m=audio %u RTP/AVP", mgcp_msg->audio_port);
+	for (i = 0; i < mgcp_msg->codecs_len; i++) {
+		pt = map_codec_to_pt(mgcp_msg->ptmap, mgcp_msg->ptmap_len, mgcp_msg->codecs[i]);
+		rc += msgb_printf(msg, " %u", pt);
+
+	}
+	rc += msgb_printf(msg, "\r\n");
+
+	for (i = 0; i < mgcp_msg->codecs_len; i++) {
+		pt = map_codec_to_pt(mgcp_msg->ptmap, mgcp_msg->ptmap_len, mgcp_msg->codecs[i]);
+		
+		/* Note: Only dynamic payload type from the range 96-127
+		 * require to be explained further via rtpmap. All others
+		 * are implcitly definedby the number in m=audio */
+		if (pt >= 96 && pt <= 127) {
+			codec = get_value_string_or_null(codec_table, mgcp_msg->codecs[i]);
+
+			/* Note: Use codec descriptors from enum mgcp_codecs
+			 * in mgcp_client only! */
+			OSMO_ASSERT(codec);
+			
+			rc += msgb_printf(msg, "a=rtpmap:%u %s\r\n", pt, codec);
+		}
+	}
+	
+	if (mgcp_msg->ptime)
+		rc += msgb_printf(msg, "a=ptime:%u\r\n", mgcp_msg->ptime);
+
+	return rc;
+}
+
 /*! Generate an MGCP message
  *  \param[in] mgcp MGCP client descriptor.
  *  \param[in] mgcp_msg Message description
@@ -823,7 +1182,8 @@
 	uint32_t mandatory_mask;
 	struct msgb *msg = msgb_alloc_headroom(4096, 128, "MGCP tx");
 	int rc = 0;
-	char local_ip[INET_ADDRSTRLEN];
+	int rc_sdp;
+	bool use_sdp = false;
 
 	msg->l2h = msg->data;
 	msg->cb[MSGB_CB_MGCP_TRANS_ID] = trans_id;
@@ -902,9 +1262,17 @@
 		rc += msgb_printf(msg, "I: %s\r\n", mgcp_msg->conn_id);
 	}
 
-	/* Add local connection options */
-	if (mgcp_msg->verb == MGCP_VERB_CRCX)
-		rc += msgb_printf(msg, "L: p:20, a:AMR, nt:IN\r\n");
+	/* Using SDP makes sense when a valid IP/Port combination is specifiec,
+	 * if we do not know this information yet, we fall back to LCO */
+	if (mgcp_msg->presence & MGCP_MSG_PRESENCE_AUDIO_IP
+	    && mgcp_msg->presence & MGCP_MSG_PRESENCE_AUDIO_PORT)
+		use_sdp = true;
+
+	/* Add local connection options (LCO) */
+	if (!use_sdp
+	    && (mgcp_msg->verb == MGCP_VERB_CRCX
+		|| mgcp_msg->verb == MGCP_VERB_MDCX))
+		rc += add_lco(msg, mgcp_msg);
 
 	/* Add mode */
 	if (mgcp_msg->presence & MGCP_MSG_PRESENCE_CONN_MODE)
@@ -912,52 +1280,15 @@
 		    msgb_printf(msg, "M: %s\r\n",
 				mgcp_client_cmode_name(mgcp_msg->conn_mode));
 
-	/* Add SDP body */
-	if (mgcp_msg->presence & MGCP_MSG_PRESENCE_AUDIO_IP
-	    && mgcp_msg->presence & MGCP_MSG_PRESENCE_AUDIO_PORT) {
-
-		/* Add separator to mark the beginning of the SDP block */
-		rc += msgb_printf(msg, "\r\n");
-
-		/* Add SDP protocol version */
-		rc += msgb_printf(msg, "v=0\r\n");
-
-		/* Determine local IP-Address */
-		if (osmo_sock_local_ip(local_ip, mgcp->actual.remote_addr) < 0) {
-			LOGP(DLMGCP, LOGL_ERROR,
-			     "Could not determine local IP-Address!\n");
-			msgb_free(msg);
+	/* Add session description protocol (SDP) */
+	if (use_sdp
+	    && (mgcp_msg->verb == MGCP_VERB_CRCX
+		|| mgcp_msg->verb == MGCP_VERB_MDCX)) {
+		rc_sdp = add_sdp(msg, mgcp_msg, mgcp);
+		if (rc_sdp == -2)
 			return NULL;
-		}
-
-		/* Add owner/creator (SDP) */
-		rc += msgb_printf(msg, "o=- %x 23 IN IP4 %s\r\n",
-				  mgcp_msg->call_id, local_ip);
-
-		/* Add session name (none) */
-		rc += msgb_printf(msg, "s=-\r\n");
-
-		/* Add RTP address and port */
-		if (mgcp_msg->audio_port == 0) {
-			LOGP(DLMGCP, LOGL_ERROR,
-			     "Invalid port number, can not generate MGCP message\n");
-			msgb_free(msg);
-			return NULL;
-		}
-		if (strlen(mgcp_msg->audio_ip) <= 0) {
-			LOGP(DLMGCP, LOGL_ERROR,
-			     "Empty ip address, can not generate MGCP message\n");
-			msgb_free(msg);
-			return NULL;
-		}
-		rc += msgb_printf(msg, "c=IN IP4 %s\r\n", mgcp_msg->audio_ip);
-
-		/* Add time description, active time (SDP) */
-		rc += msgb_printf(msg, "t=0 0\r\n");
-
-		rc +=
-		    msgb_printf(msg, "m=audio %u RTP/AVP 255\r\n",
-				mgcp_msg->audio_port);
+		else
+			rc += rc_sdp;
 	}
 
 	if (rc != 0) {
diff --git a/src/libosmo-mgcp-client/mgcp_client_fsm.c b/src/libosmo-mgcp-client/mgcp_client_fsm.c
index 10a5b6d..eb97949 100644
--- a/src/libosmo-mgcp-client/mgcp_client_fsm.c
+++ b/src/libosmo-mgcp-client/mgcp_client_fsm.c
@@ -113,8 +113,11 @@
 		.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_MODE),
 		.call_id = mgcp_ctx->conn_peer_local.call_id,
 		.conn_mode = MGCP_CONN_RECV_ONLY,
+		.ptime = mgcp_ctx->conn_peer_local.ptime,
+		.codecs_len = mgcp_ctx->conn_peer_local.codecs_len
 	};
 	osmo_strlcpy(mgcp_msg.endpoint, mgcp_ctx->conn_peer_local.endpoint, MGCP_ENDPOINT_MAXLEN);
+	memcpy(mgcp_msg.codecs, mgcp_ctx->conn_peer_local.codecs, sizeof(mgcp_msg.codecs));
 
 	return mgcp_msg_gen(mgcp_ctx->mgcp, &mgcp_msg);
 }
@@ -124,15 +127,19 @@
 	struct mgcp_msg mgcp_msg;
 
 	mgcp_msg = (struct mgcp_msg) {
-		.verb = MGCP_VERB_CRCX,.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
-						    MGCP_MSG_PRESENCE_CONN_MODE | MGCP_MSG_PRESENCE_AUDIO_IP |
-						    MGCP_MSG_PRESENCE_AUDIO_PORT),
+		.verb = MGCP_VERB_CRCX,
+		.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
+			     MGCP_MSG_PRESENCE_CONN_MODE | MGCP_MSG_PRESENCE_AUDIO_IP |
+			     MGCP_MSG_PRESENCE_AUDIO_PORT),
 		.call_id = mgcp_ctx->conn_peer_local.call_id,
 		.conn_mode = MGCP_CONN_RECV_SEND,
 		.audio_ip = mgcp_ctx->conn_peer_local.addr,
 		.audio_port = mgcp_ctx->conn_peer_local.port,
+		.ptime = mgcp_ctx->conn_peer_local.ptime,
+		.codecs_len = mgcp_ctx->conn_peer_local.codecs_len
 	};
 	osmo_strlcpy(mgcp_msg.endpoint, mgcp_ctx->conn_peer_local.endpoint, MGCP_ENDPOINT_MAXLEN);
+	memcpy(mgcp_msg.codecs, mgcp_ctx->conn_peer_local.codecs, sizeof(mgcp_msg.codecs));
 
 	return mgcp_msg_gen(mgcp_ctx->mgcp, &mgcp_msg);
 }
@@ -150,8 +157,11 @@
 		.conn_mode = MGCP_CONN_RECV_SEND,
 		.audio_ip = mgcp_ctx->conn_peer_local.addr,
 		.audio_port = mgcp_ctx->conn_peer_local.port,
+		.ptime = mgcp_ctx->conn_peer_local.ptime,
+		.codecs_len = mgcp_ctx->conn_peer_local.codecs_len
 	};
 	osmo_strlcpy(mgcp_msg.endpoint, mgcp_ctx->conn_peer_remote.endpoint, MGCP_ENDPOINT_MAXLEN);
+	memcpy(mgcp_msg.codecs, mgcp_ctx->conn_peer_local.codecs, sizeof(mgcp_msg.codecs));
 
 	/* Note: We take the endpoint and the call_id from the remote
 	 * connection info, because we can be confident that the
@@ -573,7 +583,7 @@
 	OSMO_ASSERT(mgcp);
 	OSMO_ASSERT(conn_peer);
 
-	/* Check if IP/Port informstaion in conn info makes sense */
+	/* Check if IP/Port information in conn info makes sense */
 	if (conn_peer->port && inet_aton(conn_peer->addr, &ip_test) == 0)
 		return NULL;
 
diff --git a/tests/mgcp_client/mgcp_client_test.c b/tests/mgcp_client/mgcp_client_test.c
index 007b90c..9978f79 100644
--- a/tests/mgcp_client/mgcp_client_test.c
+++ b/tests/mgcp_client/mgcp_client_test.c
@@ -95,21 +95,26 @@
 
 void test_response_cb(struct mgcp_response *response, void *priv)
 {
+	unsigned int i;
 	OSMO_ASSERT(priv == mgcp);
 	mgcp_response_parse_params(response);
 
-	printf("response cb received:\n"
-	       "  head.response_code = %d\n"
-	       "  head.trans_id = %u\n"
-	       "  head.comment = %s\n"
-	       "  audio_port = %u\n"
-	       "  audio_ip = %s\n",
-	       response->head.response_code,
-	       response->head.trans_id,
-	       response->head.comment,
-	       response->audio_port,
-	       response->audio_ip
-	      );
+	printf("response cb received:\n");
+	printf("  head.response_code = %d\n", response->head.response_code);
+	printf("  head.trans_id = %u\n", response->head.trans_id);
+	printf("  head.comment = %s\n", response->head.comment);
+	printf("  audio_port = %u\n", response->audio_port);
+	printf("  audio_ip = %s\n", response->audio_ip);
+	printf("  ptime = %u\n", response->ptime);
+	printf("  codecs_len = %u\n", response->codecs_len);
+	for(i=0;i<response->codecs_len;i++)
+		printf("  codecs[%u] = %u\n", i, response->codecs[i]);
+	printf("  ptmap_len = %u\n", response->ptmap_len);
+	for(i=0;i<response->ptmap_len;i++) {
+		printf("  ptmap[%u].codec = %u\n", i, response->ptmap[i].codec);
+		printf("  ptmap[%u].pt = %u\n", i, response->ptmap[i].pt);		
+	}
+	
 }
 
 mgcp_trans_id_t dummy_mgcp_send(struct msgb *msg)
@@ -149,8 +154,9 @@
 		"s=-\r\n"
 		"c=IN IP4 10.9.1.120\r\n"
 		"t=0 0\r\n"
-		"m=audio 16002 RTP/AVP 98\r\n"
-		"a=rtpmap:98 AMR/8000\r\n"
+		"m=audio 16002 RTP/AVP 110 96\r\n"
+		"a=rtpmap:110 AMR/8000\r\n"
+		"a=rtpmap:96 GSM-EFR/8000\r\n"
 		"a=ptime:20\r\n");
 }
 
@@ -166,7 +172,15 @@
 		.audio_port = 1234,
 		.call_id = 47,
 		.conn_id = "11",
-		.conn_mode = MGCP_CONN_RECV_SEND
+		.conn_mode = MGCP_CONN_RECV_SEND,
+		.ptime = 20,
+		.codecs[0] = CODEC_GSM_8000_1,
+		.codecs[1] = CODEC_AMR_8000_1,
+		.codecs[2] = CODEC_GSMEFR_8000_1,
+		.codecs_len = 1,
+		.ptmap[0].codec = CODEC_GSMEFR_8000_1,
+		.ptmap[0].pt = 96,
+		.ptmap_len = 1
 	};
 
 	if (mgcp)
@@ -183,6 +197,26 @@
 	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
 	printf("%s\n", (char *)msg->data);
 
+	printf("Generated CRCX message (two codecs):\n");
+	mgcp_msg.verb = MGCP_VERB_CRCX;
+	mgcp_msg.presence =
+	    (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
+	     MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE);
+	mgcp_msg.codecs_len = 2;
+	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
+	mgcp_msg.codecs_len = 1;	
+	printf("%s\n", (char *)msg->data);
+
+	printf("Generated CRCX message (three codecs, one with custom pt):\n");
+	mgcp_msg.verb = MGCP_VERB_CRCX;
+	mgcp_msg.presence =
+	    (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
+	     MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE);
+	mgcp_msg.codecs_len = 3;
+	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
+	mgcp_msg.codecs_len = 1;	
+	printf("%s\n", (char *)msg->data);		
+
 	printf("Generated MDCX message:\n");
 	mgcp_msg.verb = MGCP_VERB_MDCX;
 	mgcp_msg.presence =
@@ -192,6 +226,28 @@
 	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
 	printf("%s\n", (char *)msg->data);
 
+	printf("Generated MDCX message (two codecs):\n");
+	mgcp_msg.verb = MGCP_VERB_MDCX;
+	mgcp_msg.presence =
+	    (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
+	     MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE |
+	     MGCP_MSG_PRESENCE_AUDIO_IP | MGCP_MSG_PRESENCE_AUDIO_PORT);
+	mgcp_msg.codecs_len = 2;
+	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
+	mgcp_msg.codecs_len = 1;	
+	printf("%s\n", (char *)msg->data);
+
+	printf("Generated MDCX message (three codecs, one with custom pt):\n");
+	mgcp_msg.verb = MGCP_VERB_MDCX;
+	mgcp_msg.presence =
+	    (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID |
+	     MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE |
+	     MGCP_MSG_PRESENCE_AUDIO_IP | MGCP_MSG_PRESENCE_AUDIO_PORT);
+	mgcp_msg.codecs_len = 3;
+	msg = mgcp_msg_gen(mgcp, &mgcp_msg);
+	mgcp_msg.codecs_len = 1;	
+	printf("%s\n", (char *)msg->data);	
+
 	printf("Generated DLCX message:\n");
 	mgcp_msg.verb = MGCP_VERB_DLCX;
 	mgcp_msg.presence =
@@ -242,6 +298,9 @@
 		.conn_mode = MGCP_CONN_RECV_SEND,
 		.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID
 			     | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE),
+		.ptime = 20,
+		.codecs[0] = CODEC_AMR_8000_1,
+		.codecs_len = 1		
 	};
 
 	printf("\n%s():\n", __func__);
@@ -376,6 +435,99 @@
 	OSMO_ASSERT(!failures);
 }
 
+static void test_map_pt_to_codec(void)
+{
+	/* Full form */
+	OSMO_ASSERT(map_str_to_codec("PCMU/8000/1") == CODEC_PCMU_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM/8000/1") == CODEC_GSM_8000_1);
+	OSMO_ASSERT(map_str_to_codec("PCMA/8000/1") == CODEC_PCMA_8000_1);
+	OSMO_ASSERT(map_str_to_codec("G729/8000/1") == CODEC_G729_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM-EFR/8000/1") == CODEC_GSMEFR_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM-HR-08/8000/1") == CODEC_GSMHR_8000_1);
+	OSMO_ASSERT(map_str_to_codec("AMR/8000/1") == CODEC_AMR_8000_1);
+	OSMO_ASSERT(map_str_to_codec("AMR-WB/16000/1") == CODEC_AMRWB_16000_1);
+
+	/* Short form */
+	OSMO_ASSERT(map_str_to_codec("GSM-EFR") == CODEC_GSMEFR_8000_1);
+	OSMO_ASSERT(map_str_to_codec("G729") == CODEC_G729_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM-HR-08") == CODEC_GSMHR_8000_1);
+
+	/* We do not care about what is after the first delimiter */
+	OSMO_ASSERT(map_str_to_codec("AMR-WB/123///456") == CODEC_AMRWB_16000_1);
+	OSMO_ASSERT(map_str_to_codec("PCMA/asdf") == CODEC_PCMA_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM/qwertz") == CODEC_GSM_8000_1);
+
+	/* A trailing delimiter should not hurt */
+	OSMO_ASSERT(map_str_to_codec("AMR/") == CODEC_AMR_8000_1);
+	OSMO_ASSERT(map_str_to_codec("G729/") == CODEC_G729_8000_1);
+	OSMO_ASSERT(map_str_to_codec("GSM/") == CODEC_GSM_8000_1);
+
+	/* This is expected to fail */
+	OSMO_ASSERT(map_str_to_codec("INVALID/1234/7") == -1);
+	OSMO_ASSERT(map_str_to_codec(NULL) == -1);
+	OSMO_ASSERT(map_str_to_codec("") == -1);
+	OSMO_ASSERT(map_str_to_codec("/////") == -1);
+
+	/* The buffers are 64 bytes long, check what happens with overlong
+	 * strings as input (This schould still work.) */
+	OSMO_ASSERT(map_str_to_codec("AMR-WB/16000/1############################################################################################################") == CODEC_AMRWB_16000_1);
+
+	/* This should not work, as there is no delimiter after the codec
+	 * name */
+	OSMO_ASSERT(map_str_to_codec("AMR-WB####################################################################################################################") == -1);
+}
+
+static void test_map_codec_to_pt_and_map_pt_to_codec(void)
+{
+	struct ptmap ptmap[10];
+	unsigned int ptmap_len;
+	unsigned int i;
+
+	ptmap[0].codec = CODEC_GSMEFR_8000_1;
+	ptmap[0].pt = 96;
+	ptmap[1].codec = CODEC_GSMHR_8000_1;
+	ptmap[1].pt = 97;
+	ptmap[2].codec = CODEC_AMR_8000_1;
+	ptmap[2].pt = 98;
+	ptmap[3].codec = CODEC_AMRWB_16000_1;
+	ptmap[3].pt = 99;
+	ptmap_len = 4;
+
+	/* Mappings that are covered by the table */
+	for (i = 0; i < ptmap_len; i++)
+		printf(" %u => %u\n", ptmap[i].codec, map_codec_to_pt(ptmap, ptmap_len, ptmap[i].codec));
+	for (i = 0; i < ptmap_len; i++)
+		printf(" %u <= %u\n", ptmap[i].pt, map_pt_to_codec(ptmap, ptmap_len, ptmap[i].pt));
+	printf("\n");
+
+	/* Map some codecs/payload types from the static range, result must
+	 * always be a 1:1 mapping */
+	printf(" %u => %u\n", CODEC_PCMU_8000_1, map_codec_to_pt(ptmap, ptmap_len, CODEC_PCMU_8000_1));
+	printf(" %u => %u\n", CODEC_GSM_8000_1, map_codec_to_pt(ptmap, ptmap_len, CODEC_GSM_8000_1));
+	printf(" %u => %u\n", CODEC_PCMA_8000_1, map_codec_to_pt(ptmap, ptmap_len, CODEC_PCMA_8000_1));
+	printf(" %u => %u\n", CODEC_G729_8000_1, map_codec_to_pt(ptmap, ptmap_len, CODEC_G729_8000_1));
+	printf(" %u <= %u\n", CODEC_PCMU_8000_1, map_pt_to_codec(ptmap, ptmap_len, CODEC_PCMU_8000_1));
+	printf(" %u <= %u\n", CODEC_GSM_8000_1, map_pt_to_codec(ptmap, ptmap_len, CODEC_GSM_8000_1));
+	printf(" %u <= %u\n", CODEC_PCMA_8000_1, map_pt_to_codec(ptmap, ptmap_len, CODEC_PCMA_8000_1));
+	printf(" %u <= %u\n", CODEC_G729_8000_1, map_pt_to_codec(ptmap, ptmap_len, CODEC_G729_8000_1));
+	printf("\n");
+
+	/* Try to do mappings from statically defined range to danymic range and vice versa. This
+	 * is illegal and should result into a 1:1 mapping */
+	ptmap[3].codec = CODEC_AMRWB_16000_1;
+	ptmap[3].pt = 2;
+	ptmap[4].codec = CODEC_PCMU_8000_1;
+	ptmap[4].pt = 100;
+	ptmap_len = 5;
+
+	/* Apply all mappings again, the illegal ones we defined should result into 1:1 mappings */
+	for (i = 0; i < ptmap_len; i++)
+		printf(" %u => %u\n", ptmap[i].codec, map_codec_to_pt(ptmap, ptmap_len, ptmap[i].codec));
+	for (i = 0; i < ptmap_len; i++)
+		printf(" %u <= %u\n", ptmap[i].pt, map_pt_to_codec(ptmap, ptmap_len, ptmap[i].pt));
+	printf("\n");
+}
+
 static const struct log_info_cat log_categories[] = {
 };
 
@@ -403,6 +555,8 @@
 	test_mgcp_msg();
 	test_mgcp_client_cancel();
 	test_sdp_section_start();
+	test_map_codec_to_pt_and_map_pt_to_codec();
+	test_map_pt_to_codec();
 
 	printf("Done\n");
 	fprintf(stderr, "Done\n");
diff --git a/tests/mgcp_client/mgcp_client_test.err b/tests/mgcp_client/mgcp_client_test.err
index 7309242..1d5a1a0 100644
--- a/tests/mgcp_client/mgcp_client_test.err
+++ b/tests/mgcp_client/mgcp_client_test.err
@@ -62,4 +62,8 @@
 body: "some mgcp header data\r\nand header params\n\r\rm=audio 23\r\n"
 DLMGCP MGCP response: cannot find start of SDP parameters
 got rc=-22
+DLMGCP ptmap contains illegal mapping: codec=113 maps to pt=2
+DLMGCP ptmap contains illegal mapping: codec=0 maps to pt=100
+DLMGCP ptmap contains illegal mapping: codec=113 maps to pt=2
+DLMGCP ptmap contains illegal mapping: codec=0 maps to pt=100
 Done
diff --git a/tests/mgcp_client/mgcp_client_test.ok b/tests/mgcp_client/mgcp_client_test.ok
index fc6db30..454ee3d 100644
--- a/tests/mgcp_client/mgcp_client_test.ok
+++ b/tests/mgcp_client/mgcp_client_test.ok
@@ -18,8 +18,9 @@
 s=-
 c=IN IP4 10.9.1.120
 t=0 0
-m=audio 16002 RTP/AVP 98
-a=rtpmap:98 AMR/8000
+m=audio 16002 RTP/AVP 110 96
+a=rtpmap:110 AMR/8000
+a=rtpmap:96 GSM-EFR/8000
 a=ptime:20
 
 -----
@@ -29,16 +30,39 @@
   head.comment = OK
   audio_port = 16002
   audio_ip = 10.9.1.120
+  ptime = 20
+  codecs_len = 2
+  codecs[0] = 112
+  codecs[1] = 110
+  ptmap_len = 2
+  ptmap[0].codec = 112
+  ptmap[0].pt = 110
+  ptmap[1].codec = 110
+  ptmap[1].pt = 96
 
 Generated CRCX message:
 CRCX 1 23 at mgw MGCP 1.0
 C: 2f
 I: 11
-L: p:20, a:AMR, nt:IN
+L: p:20, a:GSM, nt:IN
+M: sendrecv
+
+Generated CRCX message (two codecs):
+CRCX 2 23 at mgw MGCP 1.0
+C: 2f
+I: 11
+L: p:20, a:GSM;AMR, nt:IN
+M: sendrecv
+
+Generated CRCX message (three codecs, one with custom pt):
+CRCX 3 23 at mgw MGCP 1.0
+C: 2f
+I: 11
+L: p:20, a:GSM;AMR;GSM-EFR, nt:IN
 M: sendrecv
 
 Generated MDCX message:
-MDCX 2 23 at mgw MGCP 1.0
+MDCX 4 23 at mgw MGCP 1.0
 C: 2f
 I: 11
 M: sendrecv
@@ -48,18 +72,50 @@
 s=-
 c=IN IP4 192.168.100.23
 t=0 0
-m=audio 1234 RTP/AVP 255
+m=audio 1234 RTP/AVP 3
+a=ptime:20
+
+Generated MDCX message (two codecs):
+MDCX 5 23 at mgw MGCP 1.0
+C: 2f
+I: 11
+M: sendrecv
+
+v=0
+o=- 2f 23 IN IP4 127.0.0.1
+s=-
+c=IN IP4 192.168.100.23
+t=0 0
+m=audio 1234 RTP/AVP 3 112
+a=rtpmap:112 AMR/8000/1
+a=ptime:20
+
+Generated MDCX message (three codecs, one with custom pt):
+MDCX 6 23 at mgw MGCP 1.0
+C: 2f
+I: 11
+M: sendrecv
+
+v=0
+o=- 2f 23 IN IP4 127.0.0.1
+s=-
+c=IN IP4 192.168.100.23
+t=0 0
+m=audio 1234 RTP/AVP 3 112 96
+a=rtpmap:112 AMR/8000/1
+a=rtpmap:96 GSM-EFR/8000/1
+a=ptime:20
 
 Generated DLCX message:
-DLCX 3 23 at mgw MGCP 1.0
+DLCX 7 23 at mgw MGCP 1.0
 C: 2f
 I: 11
 
 Generated AUEP message:
-AUEP 4 23 at mgw MGCP 1.0
+AUEP 8 23 at mgw MGCP 1.0
 
 Generated RSIP message:
-RSIP 5 23 at mgw MGCP 1.0
+RSIP 9 23 at mgw MGCP 1.0
 
 Overfolow test:
 
@@ -102,4 +158,33 @@
 test_sdp_section_start() test [8]:
 
 test_sdp_section_start() test [9]:
+ 110 => 96
+ 111 => 97
+ 112 => 98
+ 113 => 99
+ 96 <= 110
+ 97 <= 111
+ 98 <= 112
+ 99 <= 113
+
+ 0 => 0
+ 3 => 3
+ 8 => 8
+ 18 => 18
+ 0 <= 0
+ 3 <= 3
+ 8 <= 8
+ 18 <= 18
+
+ 110 => 96
+ 111 => 97
+ 112 => 98
+ 113 => 113
+ 0 => 0
+ 96 <= 110
+ 97 <= 111
+ 98 <= 112
+ 2 <= 2
+ 100 <= 100
+
 Done

-- 
To view, visit https://gerrit.osmocom.org/9649
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-mgw
Gerrit-Branch: master
Gerrit-MessageType: newchange
Gerrit-Change-Id: I78e72d41b73acfcb40599a0ff4823f17c3642059
Gerrit-Change-Number: 9649
Gerrit-PatchSet: 1
Gerrit-Owner: dexter <pmaier at sysmocom.de>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20180615/1389f6b2/attachment.htm>


More information about the gerrit-log mailing list