/* GStreamer * Copyright (C) <2005,2006> Wim Taymans * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /* * Unless otherwise indicated, Source Code is licensed under MIT license. * See further explanation attached in License Statement (distributed in the file * LICENSE). * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * SECTION:gstsdpmessage * @short_description: Helper methods for dealing with SDP messages * * * * The GstSDPMessage helper functions makes it easy to parse and create SDP * messages. * * * * Last reviewed on 2007-07-24 (0.10.14) */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #ifdef HAVE_SYS_TYPES_H #include #endif #include /* for G_OS_WIN32 */ #ifdef G_OS_WIN32 #ifdef _MSC_VER #include #endif #include #else #include #include #include #endif #include "gstsdpmessage.h" /* FIXME, is currently allocated on the stack */ #define MAX_LINE_LEN 1024 * 16 #define FREE_STRING(field) g_free (field); (field) = NULL #define REPLACE_STRING(field, val) FREE_STRING(field); (field) = g_strdup (val) #define INIT_ARRAY(field, type, init_func) \ G_STMT_START { \ if (field) { \ guint i; \ for(i = 0; i < (field)->len; i++) \ init_func (&g_array_index ((field), type, i)); \ g_array_set_size ((field), 0); \ } \ else \ (field) = g_array_new (FALSE, TRUE, sizeof (type)); \ } G_STMT_END #define FREE_ARRAY(field) \ G_STMT_START { \ if (field) \ g_array_free ((field), TRUE); \ (field) = NULL; \ } G_STMT_END #define INIT_PTR_ARRAY(field, type, init_func) \ G_STMT_START { \ if (field) { \ guint i; \ for(i = 0; i < (field)->len; i++) \ init_func (g_array_index ((field), type, i)); \ g_array_set_size ((field), 0); \ } \ else \ (field) = g_array_new (FALSE, TRUE, sizeof (type)); \ } G_STMT_END #define FREE_PTR_ARRAY(field) FREE_ARRAY(field) #define DEFINE_STRING_SETTER(field) \ GstSDPResult gst_sdp_message_set_##field (GstSDPMessage *msg, const gchar *val) { \ g_free (msg->field); \ msg->field = g_strdup (val); \ return GST_SDP_OK; \ } #define DEFINE_STRING_GETTER(field) \ const gchar* gst_sdp_message_get_##field (const GstSDPMessage *msg) { \ return msg->field; \ } #define DEFINE_ARRAY_LEN(field) \ guint gst_sdp_message_##field##_len (const GstSDPMessage *msg) { \ return msg->field->len; \ } #define DEFINE_ARRAY_GETTER(method, field, type) \ type * gst_sdp_message_get_##method (const GstSDPMessage *msg, guint idx) { \ return &g_array_index (msg->field, type, idx); \ } #define DEFINE_PTR_ARRAY_LEN(field) DEFINE_ARRAY_LEN(field) #define DEFINE_PTR_ARRAY_GETTER(method, field, type) \ type gst_sdp_message_get_##method (const GstSDPMessage *msg, guint idx) { \ return g_array_index (msg->field, type, idx); \ } #define DEFINE_PTR_ARRAY_ADDER(method, field, type, dup_method) \ GstSDPResult gst_sdp_message_add_##method (GstSDPMessage *msg, type val) { \ type v = dup_method (val); \ g_array_append_val (msg->field, v); \ return GST_SDP_OK; \ } static void gst_sdp_origin_init (GstSDPOrigin * origin) { FREE_STRING (origin->username); FREE_STRING (origin->sess_id); FREE_STRING (origin->sess_version); FREE_STRING (origin->nettype); FREE_STRING (origin->addrtype); FREE_STRING (origin->addr); } static void gst_sdp_connection_init (GstSDPConnection * connection) { FREE_STRING (connection->nettype); FREE_STRING (connection->addrtype); FREE_STRING (connection->address); connection->ttl = 0; connection->addr_number = 0; } static void gst_sdp_bandwidth_init (GstSDPBandwidth * bandwidth) { FREE_STRING (bandwidth->bwtype); bandwidth->bandwidth = 0; } static void gst_sdp_time_init (GstSDPTime * t) { FREE_STRING (t->start); FREE_STRING (t->stop); INIT_PTR_ARRAY (t->repeat, gchar *, g_free); FREE_PTR_ARRAY (t->repeat); } static void gst_sdp_zone_init (GstSDPZone * zone) { FREE_STRING (zone->time); FREE_STRING (zone->typed_time); } static void gst_sdp_key_init (GstSDPKey * key) { FREE_STRING (key->type); FREE_STRING (key->data); } static void gst_sdp_attribute_init (GstSDPAttribute * attr) { FREE_STRING (attr->key); FREE_STRING (attr->value); } /** * gst_sdp_message_new: * @msg: pointer to new #GstSDPMessage * * Allocate a new GstSDPMessage and store the result in @msg. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_new (GstSDPMessage ** msg) { GstSDPMessage *newmsg; g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); newmsg = g_new0 (GstSDPMessage, 1); *msg = newmsg; return gst_sdp_message_init (newmsg); } /** * gst_sdp_message_init: * @msg: a #GstSDPMessage * * Initialize @msg so that its contents are as if it was freshly allocated * with gst_sdp_message_new(). This function is mostly used to initialize a message * allocated on the stack. gst_sdp_message_uninit() undoes this operation. * * When this function is invoked on newly allocated data (with malloc or on the * stack), its contents should be set to 0 before calling this function. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_init (GstSDPMessage * msg) { g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); FREE_STRING (msg->version); gst_sdp_origin_init (&msg->origin); FREE_STRING (msg->session_name); FREE_STRING (msg->information); FREE_STRING (msg->uri); INIT_PTR_ARRAY (msg->emails, gchar *, g_free); INIT_PTR_ARRAY (msg->phones, gchar *, g_free); gst_sdp_connection_init (&msg->connection); INIT_ARRAY (msg->bandwidths, GstSDPBandwidth, gst_sdp_bandwidth_init); INIT_ARRAY (msg->times, GstSDPTime, gst_sdp_time_init); INIT_ARRAY (msg->zones, GstSDPZone, gst_sdp_zone_init); gst_sdp_key_init (&msg->key); INIT_ARRAY (msg->attributes, GstSDPAttribute, gst_sdp_attribute_init); INIT_ARRAY (msg->medias, GstSDPMedia, gst_sdp_media_uninit); return GST_SDP_OK; } /** * gst_sdp_message_uninit: * @msg: a #GstSDPMessage * * Free all resources allocated in @msg. @msg should not be used anymore after * this function. This function should be used when @msg was allocated on the * stack and initialized with gst_sdp_message_init(). * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_uninit (GstSDPMessage * msg) { g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); gst_sdp_message_init (msg); FREE_PTR_ARRAY (msg->emails); FREE_PTR_ARRAY (msg->phones); FREE_ARRAY (msg->bandwidths); FREE_ARRAY (msg->times); FREE_ARRAY (msg->zones); FREE_ARRAY (msg->attributes); FREE_ARRAY (msg->medias); return GST_SDP_OK; } /** * gst_sdp_message_free: * @msg: a #GstSDPMessage * * Free all resources allocated by @msg. @msg should not be used anymore after * this function. This function should be used when @msg was dynamically * allocated with gst_sdp_message_new(). * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_free (GstSDPMessage * msg) { g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); gst_sdp_message_uninit (msg); g_free (msg); return GST_SDP_OK; } static gboolean is_multicast_address (const gchar * host_name, guint * family) { struct addrinfo hints; struct addrinfo *ai; struct addrinfo *res; gboolean ret = FALSE; int err; hints.ai_socktype = SOCK_DGRAM; g_return_val_if_fail (host_name, FALSE); if ((err = getaddrinfo (host_name, NULL, &hints, &res)) < 0) return FALSE; for (ai = res; !ret && ai; ai = ai->ai_next) { if (ai->ai_family == AF_INET) ret = IN_MULTICAST (ntohl (((struct sockaddr_in *) ai->ai_addr)->sin_addr. s_addr)); else ret = IN6_IS_ADDR_MULTICAST (&((struct sockaddr_in6 *) ai->ai_addr)-> sin6_addr); if (ret && family) *family = ai->ai_family; } freeaddrinfo (res); return ret; } /** * gst_sdp_message_as_text: * @msg: a #GstSDPMessage * * Convert the contents of @msg to a text string. * * Returns: A dynamically allocated string representing the SDP description. */ gchar * gst_sdp_message_as_text (const GstSDPMessage * msg) { /* change all vars so they match rfc? */ GString *lines; guint i; g_return_val_if_fail (msg != NULL, NULL); lines = g_string_new (""); if (msg->version) g_string_append_printf (lines, "v=%s\r\n", msg->version); if (msg->origin.sess_id && msg->origin.sess_version && msg->origin.nettype && msg->origin.addrtype && msg->origin.addr) g_string_append_printf (lines, "o=%s %s %s %s %s %s\r\n", msg->origin.username ? msg->origin.username : "-", msg->origin.sess_id, msg->origin.sess_version, msg->origin.nettype, msg->origin.addrtype, msg->origin.addr); if (msg->session_name) g_string_append_printf (lines, "s=%s\r\n", msg->session_name); if (msg->information) g_string_append_printf (lines, "i=%s\r\n", msg->information); if (msg->uri) g_string_append_printf (lines, "u=%s\r\n", msg->uri); for (i = 0; i < gst_sdp_message_emails_len (msg); i++) g_string_append_printf (lines, "e=%s\r\n", gst_sdp_message_get_email (msg, i)); for (i = 0; i < gst_sdp_message_phones_len (msg); i++) g_string_append_printf (lines, "p=%s\r\n", gst_sdp_message_get_phone (msg, i)); if (gst_sdp_message_emails_len (msg) == 0 && gst_sdp_message_phones_len (msg) == 0) g_string_append_printf (lines, "e=NONE\r\n"); if (msg->connection.nettype && msg->connection.addrtype && msg->connection.address) { guint family; g_string_append_printf (lines, "c=%s %s %s", msg->connection.nettype, msg->connection.addrtype, msg->connection.address); if (is_multicast_address (msg->connection.address, &family)) { if (family == AF_INET) g_string_append_printf (lines, "/%u", msg->connection.ttl); if (msg->connection.addr_number > 1) g_string_append_printf (lines, "/%u", msg->connection.addr_number); } g_string_append_printf (lines, "\r\n"); } for (i = 0; i < gst_sdp_message_bandwidths_len (msg); i++) { const GstSDPBandwidth *bandwidth = gst_sdp_message_get_bandwidth (msg, i); g_string_append_printf (lines, "b=%s:%u\r\n", bandwidth->bwtype, bandwidth->bandwidth); } for (i = 0; i < gst_sdp_message_times_len (msg); i++) { const GstSDPTime *times = gst_sdp_message_get_time (msg, i); g_string_append_printf (lines, "t=%s %s\r\n", times->start, times->stop); if (times->repeat != NULL) { guint j; g_string_append_printf (lines, "r=%s", g_array_index (times->repeat, gchar *, 0)); for (j = 1; j < times->repeat->len; j++) g_string_append_printf (lines, " %s", g_array_index (times->repeat, gchar *, j)); g_string_append_printf (lines, "\r\n"); } } if (gst_sdp_message_zones_len (msg) > 0) { const GstSDPZone *zone = gst_sdp_message_get_zone (msg, 0); g_string_append_printf (lines, "z=%s %s", zone->time, zone->typed_time); for (i = 1; i < gst_sdp_message_zones_len (msg); i++) { zone = gst_sdp_message_get_zone (msg, i); g_string_append_printf (lines, " %s %s", zone->time, zone->typed_time); } g_string_append_printf (lines, "\r\n"); } if (msg->key.type) { g_string_append_printf (lines, "k=%s", msg->key.type); if (msg->key.data) g_string_append_printf (lines, ":%s", msg->key.data); g_string_append_printf (lines, "\r\n"); } for (i = 0; i < gst_sdp_message_attributes_len (msg); i++) { const GstSDPAttribute *attr = gst_sdp_message_get_attribute (msg, i); if (attr->key) { g_string_append_printf (lines, "a=%s", attr->key); if (attr->value) g_string_append_printf (lines, ":%s", attr->value); g_string_append_printf (lines, "\r\n"); } } for (i = 0; i < gst_sdp_message_medias_len (msg); i++) { const GstSDPMedia *media = gst_sdp_message_get_media (msg, i); gchar *sdp_media_str; sdp_media_str = gst_sdp_media_as_text (media); g_string_append_printf (lines, "%s", sdp_media_str); g_free (sdp_media_str); } return g_string_free (lines, FALSE); } /** * gst_sdp_message_set_version: * @msg: a #GstSDPMessage * @version: the version * * Set the version in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_SETTER (version); /** * gst_sdp_message_get_version: * @msg: a #GstSDPMessage * * Get the version in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_GETTER (version); /** * gst_sdp_message_set_origin: * @msg: a #GstSDPMessage * @username: the user name * @sess_id: a session id * @sess_version: a session version * @nettype: a network type * @addrtype: an address type * @addr: an address * * Configure the SDP origin in @msg with the given parameters. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_message_set_origin (GstSDPMessage * msg, const gchar * username, const gchar * sess_id, const gchar * sess_version, const gchar * nettype, const gchar * addrtype, const gchar * addr) { REPLACE_STRING (msg->origin.username, username); REPLACE_STRING (msg->origin.sess_id, sess_id); REPLACE_STRING (msg->origin.sess_version, sess_version); REPLACE_STRING (msg->origin.nettype, nettype); REPLACE_STRING (msg->origin.addrtype, addrtype); REPLACE_STRING (msg->origin.addr, addr); return GST_SDP_OK; } /** * gst_sdp_message_get_origin: * @msg: a #GstSDPMessage * * Get the origin of @msg. * * Returns: a #GstSDPOrigin. The result remains valid as long as @msg is valid. */ const GstSDPOrigin * gst_sdp_message_get_origin (const GstSDPMessage * msg) { return &msg->origin; } /** * gst_sdp_message_set_session_name: * @msg: a #GstSDPMessage * @session_name: the session name * * Set the session name in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_SETTER (session_name); /** * gst_sdp_message_get_session_name: * @msg: a #GstSDPMessage * * Get the session name in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_GETTER (session_name); /** * gst_sdp_message_set_information: * @msg: a #GstSDPMessage * @information: the information * * Set the information in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_SETTER (information); /** * gst_sdp_message_get_information: * @msg: a #GstSDPMessage * * Get the information in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_GETTER (information); /** * gst_sdp_message_set_uri: * @msg: a #GstSDPMessage * @uri: the URI * * Set the URI in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_SETTER (uri); /** * gst_sdp_message_get_uri: * @msg: a #GstSDPMessage * * Get the URI in @msg. * * Returns: a #GstSDPResult. */ DEFINE_STRING_GETTER (uri); /** * gst_sdp_message_emails_len: * @msg: a #GstSDPMessage * * Get the number of emails in @msg. * * Returns: the number of emails in @msg. */ DEFINE_ARRAY_LEN (emails); /** * gst_sdp_message_get_email: * @msg: a #GstSDPMessage * @idx: an email index * * Get the email with number @idx from @msg. * * Returns: the email at position @idx. */ DEFINE_PTR_ARRAY_GETTER (email, emails, const gchar *); /** * gst_sdp_message_add_email: * @msg: a #GstSDPMessage * @email: an email * * Add @email to the list of emails in @msg. * * Returns: a #GstSDPResult. */ DEFINE_PTR_ARRAY_ADDER (email, emails, const gchar *, g_strdup); /** * gst_sdp_message_phones_len: * @msg: a #GstSDPMessage * * Get the number of phones in @msg. * * Returns: the number of phones in @msg. */ DEFINE_ARRAY_LEN (phones); /** * gst_sdp_message_get_phone: * @msg: a #GstSDPMessage * @idx: a phone index * * Get the phone with number @idx from @msg. * * Returns: the phone at position @idx. */ DEFINE_PTR_ARRAY_GETTER (phone, phones, const gchar *); /** * gst_sdp_message_add_phone: * @msg: a #GstSDPMessage * @phone: a phone * * Add @phone to the list of phones in @msg. * * Returns: a #GstSDPResult. */ DEFINE_PTR_ARRAY_ADDER (phone, phones, const gchar *, g_strdup); /** * gst_sdp_message_set_connection: * @msg: a #GstSDPMessage * @nettype: the type of network. "IN" is defined to have the meaning * "Internet". * @addrtype: the type of address. * @address: the address * @ttl: the time to live of the address * @addr_number: the number of layers * * Configure the SDP connection in @msg with the given parameters. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_set_connection (GstSDPMessage * msg, const gchar * nettype, const gchar * addrtype, const gchar * address, guint ttl, guint addr_number) { REPLACE_STRING (msg->connection.nettype, nettype); REPLACE_STRING (msg->connection.addrtype, addrtype); REPLACE_STRING (msg->connection.address, address); msg->connection.ttl = ttl; msg->connection.addr_number = addr_number; return GST_SDP_OK; } /** * gst_sdp_message_get_connection: * @msg: a #GstSDPMessage * * Get the connection of @msg. * * Returns: a #GstSDPConnection. The result remains valid as long as @msg is valid. */ const GstSDPConnection * gst_sdp_message_get_connection (const GstSDPMessage * msg) { return &msg->connection; } /** * gst_sdp_message_bandwidths_len: * @msg: a #GstSDPMessage * * Get the number of bandwidth information in @msg. * * Returns: the number of bandwidth information in @msg. */ DEFINE_ARRAY_LEN (bandwidths); /** * gst_sdp_message_get_bandwidth: * @msg: a #GstSDPMessage * @idx: the bandwidth index * * Get the bandwidth at index @idx from @msg. * * Returns: a #GstSDPBandwidth. */ DEFINE_ARRAY_GETTER (bandwidth, bandwidths, const GstSDPBandwidth); /** * gst_sdp_message_add_bandwidth: * @msg: a #GstSDPMessage * @bwtype: the bandwidth modifier type * @bandwidth: the bandwidth in kilobits per second * * Add the specified bandwidth information to @msg. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_add_bandwidth (GstSDPMessage * msg, const gchar * bwtype, guint bandwidth) { GstSDPBandwidth bw; bw.bwtype = g_strdup (bwtype); bw.bandwidth = bandwidth; g_array_append_val (msg->bandwidths, bw); return GST_SDP_OK; } /** * gst_sdp_message_times_len: * @msg: a #GstSDPMessage * * Get the number of time information entries in @msg. * * Returns: the number of time information entries in @msg. */ DEFINE_ARRAY_LEN (times); /** * gst_sdp_message_get_time: * @msg: a #GstSDPMessage * @idx: the time index * * Get time information with index @idx from @msg. * * Returns: a #GstSDPTime. */ DEFINE_ARRAY_GETTER (time, times, const GstSDPTime); /** * gst_sdp_message_add_time: * @msg: a #GstSDPMessage * @start: the start time * @stop: the stop time * @repeat: the repeat times * * Add time information @start and @stop to @msg. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_add_time (GstSDPMessage * msg, const gchar * start, const gchar * stop, const gchar ** repeat) { GstSDPTime times; times.start = g_strdup (start); times.stop = g_strdup (stop); if (repeat) { times.repeat = g_array_new (FALSE, TRUE, sizeof (gchar *)); for (; *repeat; repeat++) { gchar *r = g_strdup (*repeat); g_array_append_val (times.repeat, r); } } else times.repeat = NULL; g_array_append_val (msg->times, times); return GST_SDP_OK; } /** * gst_sdp_message_zones_len: * @msg: a #GstSDPMessage * * Get the number of time zone information entries in @msg. * * Returns: the number of time zone information entries in @msg. */ DEFINE_ARRAY_LEN (zones); /** * gst_sdp_message_get_zone: * @msg: a #GstSDPMessage * @idx: the zone index * * Get time zone information with index @idx from @msg. * * Returns: a #GstSDPZone. */ DEFINE_ARRAY_GETTER (zone, zones, const GstSDPZone); /** * gst_sdp_message_add_zone: * @msg: a #GstSDPMessage * @adj_time: the NTP time that a time zone adjustment happens * @typed_time: the offset from the time when the session was first scheduled * * Add time zone information to @msg. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_add_zone (GstSDPMessage * msg, const gchar * adj_time, const gchar * typed_time) { GstSDPZone zone; zone.time = g_strdup (adj_time); zone.typed_time = g_strdup (typed_time); g_array_append_val (msg->zones, zone); return GST_SDP_OK; } /** * gst_sdp_message_set_key: * @msg: a #GstSDPMessage * @type: the encryption type * @data: the encryption data * * Adds the encryption information to @msg. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_set_key (GstSDPMessage * msg, const gchar * type, const gchar * data) { REPLACE_STRING (msg->key.type, type); REPLACE_STRING (msg->key.data, data); return GST_SDP_OK; } /** * gst_sdp_message_get_key: * @msg: a #GstSDPMessage * * Get the encryption information from @msg. * * Returns: a #GstSDPKey. */ const GstSDPKey * gst_sdp_message_get_key (const GstSDPMessage * msg) { return &msg->key; } /** * gst_sdp_message_attributes_len: * @msg: a #GstSDPMessage * * Get the number of attributes in @msg. * * Returns: the number of attributes in @msg. */ DEFINE_ARRAY_LEN (attributes); /** * gst_sdp_message_get_attribute: * @msg: a #GstSDPMessage * @idx: the index * * Get the attribute at position @idx in @msg. * * Returns: the #GstSDPAttribute at position @idx. */ DEFINE_ARRAY_GETTER (attribute, attributes, const GstSDPAttribute); /** * gst_sdp_message_get_attribute_val_n: * @msg: a #GstSDPMessage * @key: the key * @nth: the index * * Get the @nth attribute with key @key in @msg. * * Returns: the attribute value of the @nth attribute with @key. */ const gchar * gst_sdp_message_get_attribute_val_n (const GstSDPMessage * msg, const gchar * key, guint nth) { guint i; for (i = 0; i < msg->attributes->len; i++) { GstSDPAttribute *attr; attr = &g_array_index (msg->attributes, GstSDPAttribute, i); if (!strcmp (attr->key, key)) { if (nth == 0) return attr->value; else nth--; } } return NULL; } /** * gst_sdp_message_get_attribute_val: * @msg: a #GstSDPMessage * @key: the key * * Get the first attribute with key @key in @msg. * * Returns: the attribute value of the first attribute with @key. */ const gchar * gst_sdp_message_get_attribute_val (const GstSDPMessage * msg, const gchar * key) { return gst_sdp_message_get_attribute_val_n (msg, key, 0); } /** * gst_sdp_message_add_attribute: * @msg: a #GstSDPMessage * @key: the key * @value: the value * * Add the attribute with @key and @value to @msg. * * Returns: @GST_SDP_OK. */ GstSDPResult gst_sdp_message_add_attribute (GstSDPMessage * msg, const gchar * key, const gchar * value) { GstSDPAttribute attr; attr.key = g_strdup (key); attr.value = g_strdup (value); g_array_append_val (msg->attributes, attr); return GST_SDP_OK; } /** * gst_sdp_message_medias_len: * @msg: a #GstSDPMessage * * Get the number of media descriptions in @msg. * * Returns: the number of media descriptions in @msg. */ DEFINE_ARRAY_LEN (medias); /** * gst_sdp_message_get_media: * @msg: a #GstSDPMessage * @idx: the index * * Get the media description at index @idx in @msg. * * Returns: a #GstSDPMedia. */ DEFINE_ARRAY_GETTER (media, medias, const GstSDPMedia); /** * gst_sdp_message_add_media: * @msg: a #GstSDPMessage * @media: a #GstSDPMedia to add * * Adds @media to the array of medias in @msg. This function takes ownership of * the contents of @media so that @media will have to be reinitialized with * gst_media_init() before it can be used again. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_add_media (GstSDPMessage * msg, GstSDPMedia * media) { guint len; GstSDPMedia *nmedia; len = msg->medias->len; g_array_set_size (msg->medias, len + 1); nmedia = &g_array_index (msg->medias, GstSDPMedia, len); memcpy (nmedia, media, sizeof (GstSDPMedia)); memset (media, 0, sizeof (GstSDPMedia)); return GST_SDP_OK; } /* media access */ /** * gst_sdp_media_new: * @media: pointer to new #GstSDPMedia * * Allocate a new GstSDPMedia and store the result in @media. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_new (GstSDPMedia ** media) { GstSDPMedia *newmedia; g_return_val_if_fail (media != NULL, GST_SDP_EINVAL); newmedia = g_new0 (GstSDPMedia, 1); *media = newmedia; return gst_sdp_media_init (newmedia); } /** * gst_sdp_media_init: * @media: a #GstSDPMedia * * Initialize @media so that its contents are as if it was freshly allocated * with gst_sdp_media_new(). This function is mostly used to initialize a media * allocated on the stack. gst_sdp_media_uninit() undoes this operation. * * When this function is invoked on newly allocated data (with malloc or on the * stack), its contents should be set to 0 before calling this function. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_init (GstSDPMedia * media) { g_return_val_if_fail (media != NULL, GST_SDP_EINVAL); FREE_STRING (media->media); media->port = 0; media->num_ports = 0; FREE_STRING (media->proto); INIT_PTR_ARRAY (media->fmts, gchar *, g_free); FREE_STRING (media->information); INIT_ARRAY (media->connections, GstSDPConnection, gst_sdp_connection_init); INIT_ARRAY (media->bandwidths, GstSDPBandwidth, gst_sdp_bandwidth_init); gst_sdp_key_init (&media->key); INIT_ARRAY (media->attributes, GstSDPAttribute, gst_sdp_attribute_init); return GST_SDP_OK; } /** * gst_sdp_media_uninit: * @media: a #GstSDPMedia * * Free all resources allocated in @media. @media should not be used anymore after * this function. This function should be used when @media was allocated on the * stack and initialized with gst_sdp_media_init(). * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_uninit (GstSDPMedia * media) { g_return_val_if_fail (media != NULL, GST_SDP_EINVAL); gst_sdp_media_init (media); FREE_PTR_ARRAY (media->fmts); FREE_ARRAY (media->connections); FREE_ARRAY (media->bandwidths); FREE_ARRAY (media->attributes); return GST_SDP_OK; } /** * gst_sdp_media_free: * @media: a #GstSDPMedia * * Free all resources allocated by @media. @media should not be used anymore after * this function. This function should be used when @media was dynamically * allocated with gst_sdp_media_new(). * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_free (GstSDPMedia * media) { g_return_val_if_fail (media != NULL, GST_SDP_EINVAL); gst_sdp_media_uninit (media); g_free (media); return GST_SDP_OK; } /** * gst_sdp_media_as_text: * @media: a #GstSDPMedia * * Convert the contents of @media to a text string. * * Returns: A dynamically allocated string representing the media. */ gchar * gst_sdp_media_as_text (const GstSDPMedia * media) { GString *lines; guint i; g_return_val_if_fail (media != NULL, NULL); lines = g_string_new (""); if (media->media) g_string_append_printf (lines, "m=%s", media->media); g_string_append_printf (lines, " %u", media->port); if (media->num_ports > 1) g_string_append_printf (lines, "/%u", media->num_ports); g_string_append_printf (lines, " %s", media->proto); for (i = 0; i < gst_sdp_media_formats_len (media); i++) g_string_append_printf (lines, " %s", gst_sdp_media_get_format (media, i)); g_string_append_printf (lines, "\r\n"); if (media->information) g_string_append_printf (lines, "i=%s", media->information); for (i = 0; i < gst_sdp_media_connections_len (media); i++) { const GstSDPConnection *conn = gst_sdp_media_get_connection (media, i); if (conn->nettype && conn->addrtype && conn->address) { guint family; g_string_append_printf (lines, "c=%s %s %s", conn->nettype, conn->addrtype, conn->address); if (is_multicast_address (conn->address, &family)) { if (family == AF_INET) g_string_append_printf (lines, "/%u", conn->ttl); if (conn->addr_number > 1) g_string_append_printf (lines, "/%u", conn->addr_number); } g_string_append_printf (lines, "\r\n"); } } for (i = 0; i < gst_sdp_media_bandwidths_len (media); i++) { const GstSDPBandwidth *bandwidth = gst_sdp_media_get_bandwidth (media, i); g_string_append_printf (lines, "b=%s:%u\r\n", bandwidth->bwtype, bandwidth->bandwidth); } if (media->key.type) { g_string_append_printf (lines, "k=%s", media->key.type); if (media->key.data) g_string_append_printf (lines, ":%s", media->key.data); g_string_append_printf (lines, "\r\n"); } for (i = 0; i < gst_sdp_media_attributes_len (media); i++) { const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, i); if (attr->key) { g_string_append_printf (lines, "a=%s", attr->key); if (attr->value) g_string_append_printf (lines, ":%s", attr->value); g_string_append_printf (lines, "\r\n"); } } return g_string_free (lines, FALSE); } /** * gst_sdp_media_get_media: * @media: a #GstSDPMedia * * Get the media description of @media. * * Returns: the media description. */ const gchar * gst_sdp_media_get_media (const GstSDPMedia * media) { return media->media; } /** * gst_sdp_media_set_media: * @media: a #GstSDPMedia * @med: the media description * * Set the media description of @media to @med. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_set_media (GstSDPMedia * media, const gchar * med) { g_free (media->media); media->media = g_strdup (med); return GST_SDP_OK; } /** * gst_sdp_media_get_port: * @media: a #GstSDPMedia * * Get the port number for @media. * * Returns: the port number of @media. */ guint gst_sdp_media_get_port (const GstSDPMedia * media) { return media->port; } /** * gst_sdp_media_get_num_ports: * @media: a #GstSDPMedia * * Get the number of ports for @media. * * Returns: the number of ports for @media. */ guint gst_sdp_media_get_num_ports (const GstSDPMedia * media) { return media->num_ports; } /** * gst_sdp_media_set_port_info: * @media: a #GstSDPMedia * @port: the port number * @num_ports: the number of ports * * Set the port information in @media. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_set_port_info (GstSDPMedia * media, guint port, guint num_ports) { media->port = port; media->num_ports = num_ports; return GST_SDP_OK; } /** * gst_sdp_media_get_proto: * @media: a #GstSDPMedia * * Get the transport protocol of @media * * Returns: the transport protocol of @media. */ const gchar * gst_sdp_media_get_proto (const GstSDPMedia * media) { return media->proto; } /** * gst_sdp_media_set_proto: * @media: a #GstSDPMedia * @proto: the media transport protocol * * Set the media transport protocol of @media to @proto. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_set_proto (GstSDPMedia * media, const gchar * proto) { g_free (media->proto); media->proto = g_strdup (proto); return GST_SDP_OK; } /** * gst_sdp_media_formats_len: * @media: a #GstSDPMedia * * Get the number of formats in @media. * * Returns: the number of formats in @media. */ guint gst_sdp_media_formats_len (const GstSDPMedia * media) { return media->fmts->len; } /** * gst_sdp_media_get_format: * @media: a #GstSDPMedia * @idx: an index * * Get the format information at position @idx in @media. * * Returns: the format at position @idx. */ const gchar * gst_sdp_media_get_format (const GstSDPMedia * media, guint idx) { if (idx >= media->fmts->len) return NULL; return g_array_index (media->fmts, gchar *, idx); } /** * gst_sdp_media_add_format: * @media: a #GstSDPMedia * @format: the format * * Add the format information to @media. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_add_format (GstSDPMedia * media, const gchar * format) { gchar *fmt; fmt = g_strdup (format); g_array_append_val (media->fmts, fmt); return GST_SDP_OK; } /** * gst_sdp_media_get_information: * @media: a #GstSDPMedia * * Get the information of @media * * Returns: the information of @media. */ const gchar * gst_sdp_media_get_information (const GstSDPMedia * media) { return media->information; } /** * gst_sdp_media_set_information: * @media: a #GstSDPMedia * @information: the media information * * Set the media information of @media to @information. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_set_information (GstSDPMedia * media, const gchar * information) { g_free (media->information); media->information = g_strdup (information); return GST_SDP_OK; } /** * gst_sdp_media_connections_len: * @media: a #GstSDPMedia * * Get the number of connection fields in @media. * * Returns: the number of connections in @media. */ guint gst_sdp_media_connections_len (const GstSDPMedia * media) { return media->connections->len; } /** * gst_sdp_media_get_connection: * @media: a #GstSDPMedia * @idx: an index * * Get the connection at position @idx in @media. * * Returns: the #GstSDPConnection at position @idx. */ const GstSDPConnection * gst_sdp_media_get_connection (const GstSDPMedia * media, guint idx) { return &g_array_index (media->connections, GstSDPConnection, idx); } /** * gst_sdp_media_add_connection: * @media: a #GstSDPMedia * @nettype: the type of network. "IN" is defined to have the meaning * "Internet". * @addrtype: the type of address. * @address: the address * @ttl: the time to live of the address * @addr_number: the number of layers * * Add the given connection parameters to @media. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_add_connection (GstSDPMedia * media, const gchar * nettype, const gchar * addrtype, const gchar * address, guint ttl, guint addr_number) { GstSDPConnection conn; conn.nettype = g_strdup (nettype); conn.addrtype = g_strdup (addrtype); conn.address = g_strdup (address); conn.ttl = ttl; conn.addr_number = addr_number; g_array_append_val (media->connections, conn); return GST_SDP_OK; } /** * gst_sdp_media_bandwidths_len: * @media: a #GstSDPMedia * * Get the number of bandwidth fields in @media. * * Returns: the number of bandwidths in @media. */ guint gst_sdp_media_bandwidths_len (const GstSDPMedia * media) { return media->bandwidths->len; } /** * gst_sdp_media_get_bandwidth: * @media: a #GstSDPMedia * @idx: an index * * Get the bandwidth at position @idx in @media. * * Returns: the #GstSDPBandwidth at position @idx. */ const GstSDPBandwidth * gst_sdp_media_get_bandwidth (const GstSDPMedia * media, guint idx) { return &g_array_index (media->bandwidths, GstSDPBandwidth, idx); } /** * gst_sdp_media_add_bandwidth: * @media: a #GstSDPMedia * @bwtype: the bandwidth modifier type * @bandwidth: the bandwidth in kilobits per second * * Add the bandwidth information with @bwtype and @bandwidth to @media. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_add_bandwidth (GstSDPMedia * media, const gchar * bwtype, guint bandwidth) { GstSDPBandwidth bw; bw.bwtype = g_strdup (bwtype); bw.bandwidth = bandwidth; g_array_append_val (media->bandwidths, bw); return GST_SDP_OK; } /** * gst_sdp_media_set_key: * @media: a #GstSDPMedia * @type: the encryption type * @data: the encryption data * * Adds the encryption information to @media. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_media_set_key (GstSDPMedia * media, const gchar * type, const gchar * data) { g_free (media->key.type); media->key.type = g_strdup (type); g_free (media->key.data); media->key.data = g_strdup (data); return GST_SDP_OK; } /** * gst_sdp_media_get_key: * @media: a #GstSDPMedia * * Get the encryption information from @media. * * Returns: a #GstSDPKey. */ const GstSDPKey * gst_sdp_media_get_key (const GstSDPMedia * media) { return &media->key; } /** * gst_sdp_media_attributes_len: * @media: a #GstSDPMedia * * Get the number of attribute fields in @media. * * Returns: the number of attributes in @media. */ guint gst_sdp_media_attributes_len (const GstSDPMedia * media) { return media->attributes->len; } /** * gst_sdp_media_add_attribute: * @media: a #GstSDPMedia * @key: a key * @value: a value * * Add the attribute with @key and @value to @media. * * Returns: #GST_SDP_OK. */ GstSDPResult gst_sdp_media_add_attribute (GstSDPMedia * media, const gchar * key, const gchar * value) { GstSDPAttribute attr; attr.key = g_strdup (key); attr.value = g_strdup (value); g_array_append_val (media->attributes, attr); return GST_SDP_OK; } /** * gst_sdp_media_get_attribute: * @media: a #GstSDPMedia * @idx: an index * * Get the attribute at position @idx in @media. * * Returns: the #GstSDPAttribute at position @idx. */ const GstSDPAttribute * gst_sdp_media_get_attribute (const GstSDPMedia * media, guint idx) { return &g_array_index (media->attributes, GstSDPAttribute, idx); } /** * gst_sdp_media_get_attribute_val_n: * @media: a #GstSDPMedia * @key: a key * @nth: an index * * Get the @nth attribute value for @key in @media. * * Returns: the @nth attribute value. */ const gchar * gst_sdp_media_get_attribute_val_n (const GstSDPMedia * media, const gchar * key, guint nth) { guint i; for (i = 0; i < media->attributes->len; i++) { GstSDPAttribute *attr; attr = &g_array_index (media->attributes, GstSDPAttribute, i); if (!strcmp (attr->key, key)) { if (nth == 0) return attr->value; else nth--; } } return NULL; } /** * gst_sdp_media_get_attribute_val: * @media: a #GstSDPMedia * @key: a key * * Get the first attribute value for @key in @media. * * Returns: the first attribute value for @key. */ const gchar * gst_sdp_media_get_attribute_val (const GstSDPMedia * media, const gchar * key) { return gst_sdp_media_get_attribute_val_n (media, key, 0); } static void read_string (gchar * dest, guint size, gchar ** src) { guint idx; idx = 0; /* skip spaces */ while (g_ascii_isspace (**src)) (*src)++; while (!g_ascii_isspace (**src) && **src != '\0') { if (idx < size - 1) dest[idx++] = **src; (*src)++; } if (size > 0) dest[idx] = '\0'; } static void read_string_del (gchar * dest, guint size, gchar del, gchar ** src) { guint idx; idx = 0; /* skip spaces */ while (g_ascii_isspace (**src)) (*src)++; while (**src != del && **src != '\0') { if (idx < size - 1) dest[idx++] = **src; (*src)++; } if (size > 0) dest[idx] = '\0'; } enum { SDP_SESSION, SDP_MEDIA, }; typedef struct { guint state; GstSDPMessage *msg; GstSDPMedia *media; } SDPContext; static gboolean gst_sdp_parse_line (SDPContext * c, gchar type, gchar * buffer) { gchar str[8192]; gchar *p = buffer; #define READ_STRING(field) read_string (str, sizeof (str), &p); REPLACE_STRING (field, str) #define READ_UINT(field) read_string (str, sizeof (str), &p); field = strtoul (str, NULL, 10) switch (type) { case 'v': if (buffer[0] != '0') g_warning ("wrong SDP version"); gst_sdp_message_set_version (c->msg, buffer); break; case 'o': READ_STRING (c->msg->origin.username); READ_STRING (c->msg->origin.sess_id); READ_STRING (c->msg->origin.sess_version); READ_STRING (c->msg->origin.nettype); READ_STRING (c->msg->origin.addrtype); READ_STRING (c->msg->origin.addr); break; case 's': REPLACE_STRING (c->msg->session_name, buffer); break; case 'i': if (c->state == SDP_SESSION) { REPLACE_STRING (c->msg->information, buffer); } else { REPLACE_STRING (c->media->information, buffer); } break; case 'u': REPLACE_STRING (c->msg->uri, buffer); break; case 'e': gst_sdp_message_add_email (c->msg, buffer); break; case 'p': gst_sdp_message_add_phone (c->msg, buffer); break; case 'c': READ_STRING (c->msg->connection.nettype); READ_STRING (c->msg->connection.addrtype); READ_STRING (c->msg->connection.address); READ_UINT (c->msg->connection.ttl); READ_UINT (c->msg->connection.addr_number); break; case 'b': { gchar str2[MAX_LINE_LEN]; read_string_del (str, sizeof (str), ':', &p); read_string (str2, sizeof (str2), &p); if (c->state == SDP_SESSION) gst_sdp_message_add_bandwidth (c->msg, str, atoi (str2)); else gst_sdp_media_add_bandwidth (c->media, str, atoi (str2)); break; } case 't': break; case 'k': break; case 'a': read_string_del (str, sizeof (str), ':', &p); if (*p != '\0') p++; if (c->state == SDP_SESSION) gst_sdp_message_add_attribute (c->msg, str, p); else gst_sdp_media_add_attribute (c->media, str, p); break; case 'm': { gchar *slash; GstSDPMedia nmedia; c->state = SDP_MEDIA; memset (&nmedia, 0, sizeof (nmedia)); gst_sdp_media_init (&nmedia); /* m= / ... */ READ_STRING (nmedia.media); read_string (str, sizeof (str), &p); slash = g_strrstr (str, "/"); if (slash) { *slash = '\0'; nmedia.port = atoi (str); nmedia.num_ports = atoi (slash + 1); } else { nmedia.port = atoi (str); nmedia.num_ports = -1; } READ_STRING (nmedia.proto); do { read_string (str, sizeof (str), &p); gst_sdp_media_add_format (&nmedia, str); } while (*p != '\0'); gst_sdp_message_add_media (c->msg, &nmedia); c->media = &g_array_index (c->msg->medias, GstSDPMedia, c->msg->medias->len - 1); break; } default: break; } return TRUE; } /** * gst_sdp_message_parse_buffer: * @data: the start of the buffer * @size: the size of the buffer * @msg: the result #GstSDPMessage * * Parse the contents of @size bytes pointed to by @data and store the result in * @msg. * * Returns: #GST_SDP_OK on success. */ GstSDPResult gst_sdp_message_parse_buffer (const guint8 * data, guint size, GstSDPMessage * msg) { gchar *p; SDPContext c; gchar type; gchar buffer[MAX_LINE_LEN]; guint idx = 0; g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); g_return_val_if_fail (data != NULL, GST_SDP_EINVAL); g_return_val_if_fail (size != 0, GST_SDP_EINVAL); c.state = SDP_SESSION; c.msg = msg; c.media = NULL; p = (gchar *) data; while (TRUE) { while (g_ascii_isspace (*p)) p++; type = *p++; if (type == '\0') break; if (*p != '=') goto line_done; p++; idx = 0; while (*p != '\n' && *p != '\r' && *p != '\0') { if (idx < sizeof (buffer) - 1) buffer[idx++] = *p; p++; } buffer[idx] = '\0'; gst_sdp_parse_line (&c, type, buffer); line_done: while (*p != '\n' && *p != '\0') p++; if (*p == '\n') p++; } return GST_SDP_OK; } static void print_media (GstSDPMedia * media) { g_print (" media: '%s'\n", media->media); g_print (" port: '%u'\n", media->port); g_print (" num_ports: '%u'\n", media->num_ports); g_print (" proto: '%s'\n", media->proto); if (media->fmts->len > 0) { guint i; g_print (" formats:\n"); for (i = 0; i < media->fmts->len; i++) { g_print (" format '%s'\n", g_array_index (media->fmts, gchar *, i)); } } g_print (" information: '%s'\n", media->information); g_print (" key:\n"); g_print (" type: '%s'\n", media->key.type); g_print (" data: '%s'\n", media->key.data); if (media->attributes->len > 0) { guint i; g_print (" attributes:\n"); for (i = 0; i < media->attributes->len; i++) { GstSDPAttribute *attr = &g_array_index (media->attributes, GstSDPAttribute, i); g_print (" attribute '%s' : '%s'\n", attr->key, attr->value); } } } /** * gst_sdp_message_dump: * @msg: a #GstSDPMessage * * Dump the parsed contents of @msg to stdout. * * Returns: a #GstSDPResult. */ GstSDPResult gst_sdp_message_dump (const GstSDPMessage * msg) { g_return_val_if_fail (msg != NULL, GST_SDP_EINVAL); g_print ("sdp packet %p:\n", msg); g_print (" version: '%s'\n", msg->version); g_print (" origin:\n"); g_print (" username: '%s'\n", msg->origin.username); g_print (" sess_id: '%s'\n", msg->origin.sess_id); g_print (" sess_version: '%s'\n", msg->origin.sess_version); g_print (" nettype: '%s'\n", msg->origin.nettype); g_print (" addrtype: '%s'\n", msg->origin.addrtype); g_print (" addr: '%s'\n", msg->origin.addr); g_print (" session_name: '%s'\n", msg->session_name); g_print (" information: '%s'\n", msg->information); g_print (" uri: '%s'\n", msg->uri); if (msg->emails->len > 0) { guint i; g_print (" emails:\n"); for (i = 0; i < msg->emails->len; i++) { g_print (" email '%s'\n", g_array_index (msg->emails, gchar *, i)); } } if (msg->phones->len > 0) { guint i; g_print (" phones:\n"); for (i = 0; i < msg->phones->len; i++) { g_print (" phone '%s'\n", g_array_index (msg->phones, gchar *, i)); } } g_print (" connection:\n"); g_print (" nettype: '%s'\n", msg->connection.nettype); g_print (" addrtype: '%s'\n", msg->connection.addrtype); g_print (" address: '%s'\n", msg->connection.address); g_print (" ttl: '%u'\n", msg->connection.ttl); g_print (" addr_number: '%u'\n", msg->connection.addr_number); g_print (" key:\n"); g_print (" type: '%s'\n", msg->key.type); g_print (" data: '%s'\n", msg->key.data); if (msg->attributes->len > 0) { guint i; g_print (" attributes:\n"); for (i = 0; i < msg->attributes->len; i++) { GstSDPAttribute *attr = &g_array_index (msg->attributes, GstSDPAttribute, i); g_print (" attribute '%s' : '%s'\n", attr->key, attr->value); } } if (msg->medias->len > 0) { guint i; g_print (" medias:\n"); for (i = 0; i < msg->medias->len; i++) { g_print (" media %u:\n", i); print_media (&g_array_index (msg->medias, GstSDPMedia, i)); } } return GST_SDP_OK; }