Blob Blame History Raw
/*
 * Farstream - Farstream Codec
 *
 * Copyright 2007 Collabora Ltd.
 *  @author: Philippe Kalaf <philippe.kalaf@collabora.co.uk>
 * Copyright 2007 Nokia Corp.
 *
 * Copyright 2005 Collabora Ltd.
 *   @author: Rob Taylor <rob.taylor@collabora.co.uk>
 *
 * fs-codec.c - A Farstream codec
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "fs-codec.h"

#include <string.h>

#include "fs-private.h"

#define GST_CAT_DEFAULT _fs_conference_debug

/**
 * SECTION:fs-codec
 * @short_description: Structure representing a media codec
 *
 * An #FsCodec is a way to exchange codec information between the client and
 * Farstream. The information specified in this structure is usually
 * representative of the codec information exchanged in the signaling.
 *
 */

GType
fs_codec_get_type (void)
{
  static GType codec_type = 0;
  if (codec_type == 0)
  {
    codec_type = g_boxed_type_register_static (
        "FsCodec",
        (GBoxedCopyFunc)fs_codec_copy,
        (GBoxedFreeFunc)fs_codec_destroy);
  }

  return codec_type;
}

GType
fs_codec_list_get_type (void)
{
  static GType codec_list_type = 0;
  if (codec_list_type == 0)
  {
    codec_list_type = g_boxed_type_register_static (
        "FsCodecGList",
        (GBoxedCopyFunc)fs_codec_list_copy,
        (GBoxedFreeFunc)fs_codec_list_destroy);
  }

  return codec_list_type;
}

G_DEFINE_BOXED_TYPE (FsCodecParameter,
    fs_codec_parameter,
    fs_codec_parameter_copy,
    fs_codec_parameter_free)


G_DEFINE_BOXED_TYPE (FsFeedbackParameter,
    fs_feedback_parameter,
    fs_feedback_parameter_copy,
    fs_feedback_parameter_free)

/**
 * fs_codec_new:
 * @id: codec identifier, if RTP this should be based on IETF RTP payload types
 * @encoding_name: Name of media type this encodes
 * @media_type: #FsMediaType for type of codec
 * @clock_rate: The clock rate this codec encodes at, if applicable
 *
 * Allocates and initializes a #FsCodec structure
 *
 * Returns: A newly allocated #FsCodec
 */
FsCodec *
fs_codec_new (int id, const char *encoding_name,
              FsMediaType media_type, guint clock_rate)
{
  FsCodec *codec = g_slice_new0 (FsCodec);

  codec->id = id;
  codec->encoding_name = g_strdup (encoding_name);
  codec->media_type = media_type;
  codec->clock_rate = clock_rate;
  codec->minimum_reporting_interval = G_MAXUINT;

  return codec;
}

/**
 * fs_codec_parameter_free:
 * @param: a #FsCodecParameter to free
 *
 * Frees a #FsCodecParameter
 *
 */
void
fs_codec_parameter_free (FsCodecParameter *param)

{
  g_free (param->name);
  g_free (param->value);
  g_slice_free (FsCodecParameter, param);
}

/**
 * fs_feedback_parameter_free:
 * @param: a #FsFeedbackParameter to free
 *
 * Frees a #FsFeedbackParameter
 *
 */
void
fs_feedback_parameter_free (FsFeedbackParameter *param)
{
  g_free (param->type);
  g_free (param->subtype);
  g_free (param->extra_params);
  g_slice_free (FsFeedbackParameter, param);
}

/**
 * fs_codec_destroy: (skip)
 * @codec: #FsCodec structure to free
 *
 * Deletes a #FsCodec structure and all its data. Is a no-op on %NULL codec
 */
void
fs_codec_destroy (FsCodec * codec)
{
  if (codec == NULL)
    return;

  g_free (codec->encoding_name);

  g_list_foreach (codec->optional_params, (GFunc) fs_codec_parameter_free,
        NULL);
  g_list_free (codec->optional_params);

  g_list_foreach (codec->feedback_params,
      (GFunc) fs_feedback_parameter_free, NULL);
  g_list_free (codec->feedback_params);

  g_slice_free (FsCodec, codec);
}

/**
 * fs_codec_copy:
 * @codec: codec to copy
 *
 * Copies a #FsCodec structure.
 *
 * Returns: a copy of the codec
 */
FsCodec *
fs_codec_copy (const FsCodec * codec)
{
  FsCodec *copy = NULL;
  GList *lp;
  GQueue list_copy = G_QUEUE_INIT;

  if (codec == NULL)
    return NULL;

  copy = fs_codec_new (codec->id, codec->encoding_name, codec->media_type,
      codec->clock_rate);

  copy->channels = codec->channels;
  copy->minimum_reporting_interval = codec->minimum_reporting_interval;

  for (lp = codec->optional_params; lp; lp = g_list_next (lp))
  {
    FsCodecParameter *param_copy;
    FsCodecParameter *param = lp->data;;

    param_copy = g_slice_new (FsCodecParameter);
    param_copy->name = g_strdup (param->name);
    param_copy->value = g_strdup (param->value);

    g_queue_push_tail (&list_copy, param_copy);
  }
  copy->optional_params = list_copy.head;

  g_queue_init (&list_copy);
  for (lp = codec->feedback_params; lp; lp = g_list_next (lp))
  {
    FsFeedbackParameter *param_copy;
    FsFeedbackParameter *param = lp->data;;

    param_copy = g_slice_new (FsFeedbackParameter);
    param_copy->type = g_strdup (param->type);
    param_copy->subtype = g_strdup (param->subtype);
    param_copy->extra_params = g_strdup (param->extra_params);

    g_queue_push_tail (&list_copy, param_copy);
  }
  copy->feedback_params = list_copy.head;

  return copy;
}

/**
 * fs_codec_list_destroy: (skip)
 * @codec_list: a GList of #FsCodec to delete
 *
 * Deletes a list of #FsCodec structures and the list itself.
 * Does nothing on %NULL lists.
 */
void
fs_codec_list_destroy (GList *codec_list)
{
  GList *lp;
  FsCodec *codec;

  for (lp = codec_list; lp; lp = g_list_next (lp)) {
    codec = (FsCodec *) lp->data;
    fs_codec_destroy (codec);
    lp->data = NULL;
  }
  g_list_free (codec_list);
}

/**
 * fs_codec_list_copy:
 * @codec_list: (transfer none) (element-type FsCodec):
 *   a GList of #FsCodec to copy
 *
 * Copies a list of #FsCodec structures.
 *
 * Returns: (element-type FsCodec) (transfer full): The new list.
 */
GList *
fs_codec_list_copy (const GList *codec_list)
{
  GQueue copy = G_QUEUE_INIT;
  const GList *lp;

  for (lp = codec_list; lp; lp = g_list_next (lp)) {
    FsCodec *codec = (FsCodec *) lp->data;

    g_queue_push_tail (&copy, fs_codec_copy (codec));
  }

  return copy.head;
}

/**
 * fs_codec_list_from_keyfile:
 * @filename: Name of the #GKeyFile to read the codecs parameters from
 * @error: location of a #GError, or NULL if no error occured
 *
 * Reads the content of a #GKeyFile of the following format into
 * a #GList of #FsCodec structures.
 *
 *
 * Example:
 * |[
 * [audio/codec1]
 * clock-rate=8000
 *
 * [audio/codec1:1]
 * clock-rate=16000
 *
 * [audio/codec2]
 * one_param=QCIF
 * another_param=WOW
 *
 * [video/codec3]
 * wierd_param=42
 * feedback:nack/pli=1
 * feedback:tfrc=
 * ]|
 *
 * Return value: (element-type FsCodec) (transfer full):
 * The #GList of #FsCodec or %NULL if the keyfile was empty or an error occured.
 */
GList *
fs_codec_list_from_keyfile (const gchar *filename, GError **error)
{
  GKeyFile *keyfile = NULL;
  GList *codecs = NULL;
  GError *gerror = NULL;
  gchar **groups = NULL;
  gsize groups_count = 0;
  int i;

  g_return_val_if_fail (filename, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  keyfile = g_key_file_new ();

  if (!g_key_file_load_from_file (keyfile, filename,
          G_KEY_FILE_NONE, error)) {
    goto out;
  }

  groups = g_key_file_get_groups (keyfile, &groups_count);

  if (!groups)
    goto out;

  for (i=0; i < groups_count && groups[i]; i++) {
    FsCodec *codec;
    gchar **keys = NULL;
    gsize keys_count;
    int j;
    gchar *encoding_name = NULL;
    gchar *next_tok = NULL;
    FsMediaType media_type;

    keys = g_key_file_get_keys (keyfile, groups[i], &keys_count, &gerror);

    if (!keys || gerror) {
      if (gerror)
        GST_WARNING ("Unable to read parameters for %s: %s\n",
            groups[i], gerror->message);
      else
        GST_WARNING ("Unknown errors while reading parameters for %s",
            groups[i]);

      g_clear_error (&gerror);

      goto next_codec;
    }

    next_tok = strchr (groups[i], '/');
    if (!next_tok)
    {
      GST_WARNING ("Invalid codec name: %s", groups[i]);
      goto next_codec;
    }

    if ((next_tok - groups[i]) == 5 /* strlen ("audio") */ &&
        !g_ascii_strncasecmp ("audio", groups[i], 5))
    {
      media_type = FS_MEDIA_TYPE_AUDIO;
    }
    else if ((next_tok - groups[i]) == 5 /* strlen ("video") */ &&
        !g_ascii_strncasecmp ("video", groups[i], 5))
    {
      media_type = FS_MEDIA_TYPE_VIDEO;
    }
    else if ((next_tok - groups[i]) == 11 /* strlen ("application") */ &&
        !g_ascii_strncasecmp ("application", groups[i], 11))
    {
      media_type = FS_MEDIA_TYPE_APPLICATION;
    }
    else
    {
      GST_WARNING ("Invalid media type in codec name name %s", groups[i]);
      goto next_codec;
    }

    encoding_name = next_tok + 1;

    next_tok = strchr (encoding_name, ':');

    if (encoding_name[0] == 0 || next_tok - encoding_name == 1)
      goto next_codec;

    if (next_tok)
      encoding_name = g_strndup (encoding_name,
          next_tok - encoding_name);
    else
      encoding_name = g_strdup (encoding_name);

    codec = fs_codec_new (FS_CODEC_ID_ANY, encoding_name, media_type, 0);

    g_free (encoding_name);

    for (j = 0; j < keys_count && keys[j]; j++) {
      if (!g_ascii_strcasecmp ("clock-rate", keys[j])) {
        codec->clock_rate = g_key_file_get_integer (keyfile, groups[i], keys[j],
            &gerror);
        if (gerror) {
          codec->clock_rate = 0;
          goto keyerror;
        }

      } else if (!g_ascii_strcasecmp ("id", keys[j])) {
         codec->id = g_key_file_get_integer (keyfile, groups[i], keys[j],
            &gerror);
        if (gerror) {
          codec->id = FS_CODEC_ID_ANY;
          goto keyerror;
        }

        if (codec->id < 0)
          codec->id = FS_CODEC_ID_DISABLE;

      } else if (!g_ascii_strcasecmp ("channels", keys[j])) {
         codec->channels = g_key_file_get_integer (keyfile, groups[i], keys[j],
            &gerror);
        if (gerror) {
          codec->channels = 0;
          goto keyerror;
        }
      } else if (!g_ascii_strcasecmp ("trr-int", keys[j])) {
        codec->minimum_reporting_interval =
            g_key_file_get_integer (keyfile, groups[i], keys[j], &gerror);
        if (gerror) {
          codec->minimum_reporting_interval = G_MAXUINT;
          goto keyerror;
        }
      } else if (g_str_has_prefix (keys[j], "feedback:")) {
        gchar *type = keys[j] + strlen ("feedback:");
        gchar *subtype = strchr (type, '/');
        gchar *extra_params;

        extra_params = g_key_file_get_string (keyfile, groups[i], keys[j],
            &gerror);
        if (gerror)
          goto keyerror;

        /* Replace / with \0 and point to name (the next char) */
        if (subtype)
        {
          *subtype=0;
          subtype++;
        }
        else
        {
          subtype = "";
        }

        fs_codec_add_feedback_parameter (codec, type, subtype,
            extra_params);
        g_free (extra_params);
      } else {
        FsCodecParameter *param = g_slice_new (FsCodecParameter);

        param->name = g_strdup (keys[j]);
        param->value = g_key_file_get_string (keyfile, groups[i], keys[j],
            &gerror);
        if (gerror) {
          fs_codec_parameter_free (param);
          goto keyerror;
        }

        if (!param->name || !param->value)
          fs_codec_parameter_free (param);
        else
          codec->optional_params = g_list_append (codec->optional_params,
              param);
      }
      continue;
    keyerror:
      GST_WARNING ("Error reading key %s codec %s: %s", keys[j], groups[i],
          gerror->message);
      g_clear_error (&gerror);

    }

    codecs = g_list_append (codecs, codec);

  next_codec:
    g_strfreev (keys);
  }


 out:

  g_strfreev (groups);
  g_key_file_free (keyfile);

  return codecs;
}

/**
 * fs_media_type_to_string:
 * @media_type: A media type
 *
 * Gives a user-printable string representing the media type
 *
 * Return value: a static string representing the media type
 */

const gchar *
fs_media_type_to_string (FsMediaType media_type)
{
  if (media_type == FS_MEDIA_TYPE_AUDIO) {
    return "audio";
  } else if (media_type == FS_MEDIA_TYPE_VIDEO) {
    return "video";
  } else if (media_type == FS_MEDIA_TYPE_APPLICATION) {
    return "application";
  } else {
    return NULL;
  }
}

/**
 * fs_codec_to_string:
 * @codec: A farstream codec
 *
 * Returns a newly-allocated string representing the codec
 *
 * Return value: the newly-allocated string
 */
gchar *
fs_codec_to_string (const FsCodec *codec)
{
  GString *string = NULL;
  GList *item;
  gchar *charstring;

  if (codec == NULL)
    return g_strdup ("(NULL)");

  string = g_string_new ("");

  g_string_printf (string, "%d: %s %s clock:%d channels:%d",
      codec->id, fs_media_type_to_string (codec->media_type),
      codec->encoding_name, codec->clock_rate, codec->channels);

  if (codec->minimum_reporting_interval != G_MAXUINT)
    g_string_append_printf (string, " trr-int=%u",
        codec->minimum_reporting_interval);

  for (item = codec->optional_params;
       item;
       item = g_list_next (item)) {
    FsCodecParameter *param = item->data;
    g_string_append_printf (string, " %s=%s", param->name, param->value);
  }

  for (item = codec->feedback_params;
       item;
       item = g_list_next (item)) {
    FsFeedbackParameter *param = item->data;
    g_string_append_printf (string, " %s/%s=%s", param->type, param->subtype,
        param->extra_params);
  }

  charstring = string->str;
  g_string_free (string, FALSE);

  return charstring;
}


static gboolean
compare_optional_params (const gpointer p1, const gpointer p2)
{
  const FsCodecParameter *param1 = p1;
  const FsCodecParameter *param2 = p2;

  if (!g_ascii_strcasecmp (param1->name, param2->name) &&
      !strcmp (param1->value, param2->value))
    return TRUE;
  else
    return FALSE;
}

static gboolean
compare_feedback_params (const gpointer p1, const gpointer p2)
{
  const FsFeedbackParameter *param1 = p1;
  const FsFeedbackParameter *param2 = p2;

  if (!g_ascii_strcasecmp (param1->subtype, param2->subtype) &&
      !g_ascii_strcasecmp (param1->type, param2->type) &&
      !g_strcmp0 (param1->extra_params, param2->extra_params))
    return TRUE;
  else
    return FALSE;
}

/*
 * Check if all of the elements of list1 are in list2
 * It compares GLists of X using the comparison function
 */
static gboolean
compare_lists (GList *list1, GList *list2,
    gboolean (*compare_params) (const gpointer p1, const gpointer p2))
{
  GList *item1;

  for (item1 = g_list_first (list1);
       item1;
       item1 = g_list_next (item1)) {
    FsCodecParameter *param1 = item1->data;
    GList *item2 = NULL;

    for (item2 = g_list_first (list2);
         item2;
         item2 = g_list_next (item2)) {
      FsCodecParameter *param2 = item2->data;

      if (compare_params (param1, param2))
        break;
    }
    if (!item2)
      return FALSE;
  }

  return TRUE;
}


/**
 * fs_codec_are_equal:
 * @codec1: First codec
 * @codec2: Second codec
 *
 * Compare two codecs, it will declare two codecs to be identical even
 * if their optional parameters are in a different order. %NULL encoding names
 * are ignored.
 *
 * Return value: %TRUE of the codecs are identical, %FALSE otherwise
 */

gboolean
fs_codec_are_equal (const FsCodec *codec1, const FsCodec *codec2)
{
  if (codec1 == codec2)
    return TRUE;

  if (!codec1 || !codec2)
    return FALSE;

  if (codec1->id != codec2->id ||
      codec1->media_type != codec2->media_type ||
      codec1->clock_rate != codec2->clock_rate ||
      codec1->channels != codec2->channels ||
      codec1->minimum_reporting_interval !=
      codec2->minimum_reporting_interval ||
      codec1->encoding_name == NULL ||
      codec2->encoding_name == NULL ||
      g_ascii_strcasecmp (codec1->encoding_name, codec2->encoding_name))
    return FALSE;


  /* Is there a smarter way to compare to un-ordered linked lists
   * to make sure they contain exactly the same elements??
   */
  if (!compare_lists (codec1->optional_params, codec2->optional_params,
          compare_optional_params) ||
      !compare_lists (codec2->optional_params, codec1->optional_params,
          compare_optional_params))
    return FALSE;

  if (!compare_lists (codec1->feedback_params,
          codec2->feedback_params, compare_feedback_params) ||
      !compare_lists (codec2->feedback_params,
          codec1->feedback_params, compare_feedback_params))
    return FALSE;

  return TRUE;
}

/**
 * fs_codec_list_are_equal:
 * @list1: (element-type FsCodec) (allow-none): a #GList of #FsCodec
 * @list2: (element-type FsCodec) (allow-none): a #GList of #FsCodec
 *
 * Verifies if two glist of fscodecs are identical
 *
 * Returns: %TRUE if they are identical, %FALSE otherwise
 */

gboolean
fs_codec_list_are_equal (GList *list1, GList *list2)
{

  for (;
       list1 && list2;
       list1 = g_list_next (list1), list2 = g_list_next (list2))
  {
    if (!fs_codec_are_equal (list1->data, list2->data))
      return FALSE;
  }

  if (list1 == NULL && list2 == NULL)
    return TRUE;
  else
    return FALSE;
}

/**
 * fs_codec_add_optional_parameter:
 * @codec: The #FsCodec to add the parameter to
 * @name: The name of the optional parameter
 * @value: The extra_params of the optional parameter
 *
 * This function adds an new optional parameter to a #FsCodec
 */

void
fs_codec_add_optional_parameter (FsCodec *codec,
    const gchar *name,
    const gchar *value)
{
  FsCodecParameter *param;

  g_return_if_fail (name != NULL && value != NULL);

  param = g_slice_new (FsCodecParameter);

  param->name = g_strdup (name);
  param->value = g_strdup (value);

  codec->optional_params = g_list_append (codec->optional_params, param);
}

/**
 * fs_codec_remove_optional_parameter:
 * @codec: a #FsCodec
 * @param: a pointer to the #FsCodecParameter to remove
 *
 * Removes an optional parameter from a codec.
 *
 * NULL param will do nothing.
 */

void
fs_codec_remove_optional_parameter (FsCodec *codec,
    FsCodecParameter *param)
{
  g_return_if_fail (codec);

  if (!param)
    return;

  fs_codec_parameter_free (param);
  codec->optional_params = g_list_remove (codec->optional_params, param);
}

/**
 * fs_codec_get_optional_parameter:
 * @codec: a #FsCodec
 * @name: The name of the parameter to search for
 * @value: (allow-none): The value of the parameter to search for or %NULL for
 * any value
 *
 * Finds the #FsCodecParameter in the #FsCodec that has the requested name
 * and, if not %NULL, the requested value
 *
 * Returns: (transfer none): the #FsCodecParameter from the #FsCodec or %NULL
 */

FsCodecParameter *
fs_codec_get_optional_parameter (FsCodec *codec, const gchar *name,
    const gchar *value)
{
  GList *item = NULL;

  g_return_val_if_fail (codec != NULL, NULL);
  g_return_val_if_fail (name != NULL, NULL);

  for (item = g_list_first (codec->optional_params);
       item;
       item = g_list_next (item))
  {
    FsCodecParameter *param = item->data;
    if (!g_ascii_strcasecmp (param->name, name) &&
        (value == NULL || !g_ascii_strcasecmp (param->value, value)))
      return param;
  }

  return NULL;
}

/**
 * fs_codec_add_feedback_parameter:
 * @codec: The #FsCodec to add the parameter to
 * @type: The type of the feedback parameter
 * @subtype: The subtype of the feedback parameter
 * @extra_params: The extra_params of the feeback parameter
 *
 * This function adds an new feedback parameter to a #FsCodec
 */

void
fs_codec_add_feedback_parameter (FsCodec *codec, const gchar *type,
    const gchar *subtype, const gchar *extra_params)
{
  FsFeedbackParameter *param;

  g_return_if_fail (type != NULL);
  g_return_if_fail (subtype != NULL);
  g_return_if_fail (extra_params != NULL);

  param = g_slice_new (FsFeedbackParameter);

  param->type = g_strdup (type);
  param->subtype = g_strdup (subtype);
  param->extra_params = g_strdup (extra_params);

  codec->feedback_params = g_list_append (codec->feedback_params, param);
}


/**
 * fs_codec_get_feedback_parameter:
 * @codec: a #FsCodec
 * @type: (allow-none): The subtype of the parameter to search for or %NULL for
 * any type
 * @subtype: (allow-none): The subtype of the parameter to search for or %NULL
 * for any subtype
 * @extra_params: (allow-none): The extra_params of the parameter to search for
 * or %NULL for any extra_params
 *
 * Finds the #FsFeedbackParameter in the #FsCodec that has the requested
 * subtype, type and extra_params. One of which must be non-NULL;
 *
 * Returns: the #FsFeedbackParameter from the #FsCodec or %NULL
 */

FsFeedbackParameter *
fs_codec_get_feedback_parameter (FsCodec *codec,
    const gchar *type, const gchar *subtype, const gchar *extra_params)
{
  GList *item = NULL;

  g_return_val_if_fail (codec != NULL, NULL);
  g_return_val_if_fail (type != NULL || subtype != NULL, NULL);

  for (item = g_list_first (codec->feedback_params);
       item;
       item = g_list_next (item))
  {
    FsFeedbackParameter *param = item->data;
    if (!g_ascii_strcasecmp (param->type, type) &&
        (subtype == NULL || !g_ascii_strcasecmp (param->subtype, subtype)) &&
        (extra_params == NULL || !g_ascii_strcasecmp (param->extra_params,
            extra_params)))
      return param;
  }

  return NULL;
}



/**
 * fs_codec_remove_feedback_parameter:
 * @codec: a #FsCodec
 * @item: (transfer none) (element-type FsFeedbackParameter):
 *    a pointer to the #GList element to remove that contains a
 * #FsFeedbackParameter
 *
 * Removes an optional parameter from a codec.
 *
 * NULL param will do nothing.
 */

void
fs_codec_remove_feedback_parameter (FsCodec *codec, GList *item)
{
  g_return_if_fail (codec);

  if (!item)
    return;

  fs_feedback_parameter_free (item->data);
  codec->feedback_params =
      g_list_delete_link (codec->feedback_params, item);
}

/**
 * fs_codec_parameter_copy:
 * @param: a #FsCodecParameter
 *
 * Makes a copy of a #FsCodecParameter
 *
 * Returns: a newly allocated #FsCodecParameter
 */

FsCodecParameter *
fs_codec_parameter_copy (const FsCodecParameter *param)
{
  FsCodecParameter *outparam = g_slice_new (FsCodecParameter);

  outparam->name = g_strdup (param->name);
  outparam->value = g_strdup (param->value);

  return outparam;
}


/**
 * fs_feedback_parameter_copy:
 * @param: a #FsFeedbackParameter
 *
 * Makes a copy of a #FsFeedbackParameter
 *
 * Returns: a newly allocated #FsFeedbackParameter
 */

FsFeedbackParameter *
fs_feedback_parameter_copy (const FsFeedbackParameter *param)
{
  FsFeedbackParameter *outparam = g_slice_new (FsFeedbackParameter);

  outparam->type = g_strdup (param->type);
  outparam->subtype = g_strdup (param->subtype);
  outparam->extra_params = g_strdup (param->extra_params);

  return outparam;
}