diff --git a/configure.ac b/configure.ac
index 6f467cc..b4807e0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -275,6 +275,7 @@ AG_GST_CHECK_PLUGIN(festival)
 AG_GST_CHECK_PLUGIN(flv)
 AG_GST_CHECK_PLUGIN(freeze)
 AG_GST_CHECK_PLUGIN(h264parse)
+AG_GST_CHECK_PLUGIN(id3tag)
 AG_GST_CHECK_PLUGIN(librfb)
 AG_GST_CHECK_PLUGIN(liveadder)
 AG_GST_CHECK_PLUGIN(mpegdemux)
@@ -1547,6 +1548,7 @@ gst/festival/Makefile
 gst/flv/Makefile
 gst/freeze/Makefile
 gst/h264parse/Makefile
+gst/id3tag/Makefile
 gst/librfb/Makefile
 gst/mpegdemux/Makefile
 gst/mpegtsmux/Makefile
diff --git a/gst/id3tag/Makefile.am b/gst/id3tag/Makefile.am
new file mode 100644
index 0000000..9595be0
--- /dev/null
+++ b/gst/id3tag/Makefile.am
@@ -0,0 +1,19 @@
+plugin_LTLIBRARIES = libgstid3tag.la
+
+libgstid3tag_la_SOURCES = \
+	gsttagmux.c \
+    id3tag.c \
+    gstid3tag.c
+
+libgstid3tag_la_CFLAGS = \
+	$(GST_PLUGINS_BASE_CFLAGS) \
+	$(GST_CFLAGS)
+
+libgstid3tag_la_LIBADD = \
+	$(GST_PLUGINS_BASE_LIBS) -lgsttag-$(GST_MAJORMINOR) \
+	$(GST_LIBS)
+
+libgstid3tag_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS)
+libgstid3tag_la_LIBTOOLFLAGS = --tag=disable-static
+
+noinst_HEADERS = gstid3tag.h id3tag.h gsttagmux.h
diff --git a/gst/id3tag/gstid3tag.c b/gst/id3tag/gstid3tag.c
new file mode 100644
index 0000000..f67d781
--- /dev/null
+++ b/gst/id3tag/gstid3tag.c
@@ -0,0 +1,229 @@
+/* GStreamer ID3 v1 and v2 muxer
+ *
+ * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * 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.
+ */
+
+/**
+ * SECTION:element-id3tag
+ * @see_also: #GstID3Demux, #GstTagSetter
+ *
+ * This element adds ID3v2 tags to the beginning of a stream, and ID3v1 tags
+ * to the end.
+ * 
+ * It defaults to writing ID3 version 2.3.0 tags (since those are the most
+ * widely supported), but can optionally write version 2.4.0 tags.
+ *
+ * Applications can set the tags to write using the #GstTagSetter interface.
+ * Tags sent by upstream elements will be picked up automatically (and merged
+ * according to the merge mode set via the tag setter interface).
+ *
+ * <refsect2>
+ * <title>Example pipelines</title>
+ * |[
+ * gst-launch -v filesrc location=foo.ogg ! decodebin ! audioconvert ! lame ! id3tag ! filesink location=foo.mp3
+ * ]| A pipeline that transcodes a file from Ogg/Vorbis to mp3 format with
+ * ID3 tags that contain the same metadata as the the Ogg/Vorbis file.
+ * Make sure the Ogg/Vorbis file actually has comments to preserve.
+ * |[
+ * gst-launch -m filesrc location=foo.mp3 ! id3demux ! fakesink silent=TRUE 2&gt; /dev/null | grep taglist
+ * ]| Verify that tags have been written.
+ * </refsect2>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "gstid3tag.h"
+#include <gst/tag/tag.h>
+
+#include <string.h>
+
+GST_DEBUG_CATEGORY (gst_id3tag_debug);
+#define GST_CAT_DEFAULT gst_id3tag_debug
+
+enum
+{
+  ARG_0,
+  ARG_WRITE_V1,
+  ARG_WRITE_V2,
+  ARG_V2_MAJOR_VERSION
+};
+
+#define DEFAULT_WRITE_V1 TRUE
+#define DEFAULT_WRITE_V2 TRUE
+#define DEFAULT_V2_MAJOR_VERSION 3
+
+static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
+    GST_PAD_SRC,
+    GST_PAD_ALWAYS,
+    GST_STATIC_CAPS ("application/x-id3"));
+
+GST_BOILERPLATE (GstID3Tag, gst_id3tag, GstTagMux, GST_TYPE_TAG_MUX);
+
+static GstBuffer *gst_id3tag_render_v2_tag (GstTagMux * mux,
+    GstTagList * taglist);
+static GstBuffer *gst_id3tag_render_v1_tag (GstTagMux * mux,
+    GstTagList * taglist);
+
+static void gst_id3tag_set_property (GObject * object, guint prop_id,
+    const GValue * value, GParamSpec * pspec);
+static void gst_id3tag_get_property (GObject * object, guint prop_id,
+    GValue * value, GParamSpec * pspec);
+
+static void
+gst_id3tag_base_init (gpointer g_class)
+{
+  GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
+
+  gst_element_class_add_pad_template (element_class,
+      gst_static_pad_template_get (&src_template));
+
+  gst_element_class_set_details_simple (element_class,
+      "ID3 v1 and v2 Muxer", "Formatter/Metadata",
+      "Adds an ID3v2 header and ID3v1 footer to a file",
+      "Michael Smith <msmith@songbirdnest.com>, "
+      "Tim-Philipp Müller <tim centricular net>");
+
+  GST_DEBUG_CATEGORY_INIT (gst_id3tag_debug, "id3tag", 0,
+      "ID3 v1 and v2 tag muxer");
+}
+
+static void
+gst_id3tag_class_init (GstID3TagClass * klass)
+{
+  GObjectClass *gobject_class = (GObjectClass *) klass;
+
+  gobject_class->set_property = gst_id3tag_set_property;
+  gobject_class->get_property = gst_id3tag_get_property;
+
+  g_object_class_install_property (gobject_class, ARG_WRITE_V1,
+      g_param_spec_boolean ("write-v1", "Write id3v1 tag",
+          "Write an id3v1 tag at the end of the file", DEFAULT_WRITE_V1,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+  g_object_class_install_property (gobject_class, ARG_WRITE_V2,
+      g_param_spec_boolean ("write-v2", "Write id3v2 tag",
+          "Write an id3v2 tag at the start of the file", DEFAULT_WRITE_V2,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+  g_object_class_install_property (gobject_class, ARG_V2_MAJOR_VERSION,
+      g_param_spec_int ("v2-version", "Version (3 or 4) of id3v2 tag",
+          "Set version (3 for id3v2.3, 4 for id3v2.4) of id3v2 tags",
+          3, 4, DEFAULT_V2_MAJOR_VERSION,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
+
+  GST_TAG_MUX_CLASS (klass)->render_start_tag =
+      GST_DEBUG_FUNCPTR (gst_id3tag_render_v2_tag);
+
+  GST_TAG_MUX_CLASS (klass)->render_end_tag = gst_id3tag_render_v1_tag;
+}
+
+static void
+gst_id3tag_init (GstID3Tag * id3mux, GstID3TagClass * id3mux_class)
+{
+  id3mux->write_v1 = DEFAULT_WRITE_V1;
+  id3mux->write_v2 = DEFAULT_WRITE_V2;
+
+  id3mux->v2_major_version = DEFAULT_V2_MAJOR_VERSION;
+}
+
+static void
+gst_id3tag_set_property (GObject * object, guint prop_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  GstID3Tag *mux = GST_ID3TAG (object);
+
+  switch (prop_id) {
+    case ARG_WRITE_V1:
+      mux->write_v1 = g_value_get_boolean (value);
+      break;
+    case ARG_WRITE_V2:
+      mux->write_v2 = g_value_get_boolean (value);
+      break;
+    case ARG_V2_MAJOR_VERSION:
+      mux->v2_major_version = g_value_get_int (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+gst_id3tag_get_property (GObject * object, guint prop_id,
+    GValue * value, GParamSpec * pspec)
+{
+  GstID3Tag *mux = GST_ID3TAG (object);
+
+  switch (prop_id) {
+    case ARG_WRITE_V1:
+      g_value_set_boolean (value, mux->write_v1);
+      break;
+    case ARG_WRITE_V2:
+      g_value_set_boolean (value, mux->write_v2);
+      break;
+    case ARG_V2_MAJOR_VERSION:
+      g_value_set_int (value, mux->v2_major_version);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static GstBuffer *
+gst_id3tag_render_v2_tag (GstTagMux * mux, GstTagList * taglist)
+{
+  GstID3Tag *id3mux = GST_ID3TAG (mux);
+
+  if (id3mux->write_v2)
+    return gst_id3mux_render_v2_tag (mux, taglist, id3mux->v2_major_version);
+  else
+    return NULL;
+}
+
+static GstBuffer *
+gst_id3tag_render_v1_tag (GstTagMux * mux, GstTagList * taglist)
+{
+  GstID3Tag *id3mux = GST_ID3TAG (mux);
+
+  if (id3mux->write_v1)
+    return gst_id3mux_render_v1_tag (mux, taglist);
+  else
+    return NULL;
+}
+
+static gboolean
+plugin_init (GstPlugin * plugin)
+{
+  if (!gst_element_register (plugin, "id3tag", GST_RANK_NONE, GST_TYPE_ID3TAG))
+    return FALSE;
+
+  gst_tag_register_musicbrainz_tags ();
+
+  return TRUE;
+}
+
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+    GST_VERSION_MINOR,
+    "id3tag",
+    "ID3 v1 and v2 muxing plugin",
+    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN);
diff --git a/gst/id3tag/gstid3tag.h b/gst/id3tag/gstid3tag.h
new file mode 100644
index 0000000..6b33df2
--- /dev/null
+++ b/gst/id3tag/gstid3tag.h
@@ -0,0 +1,63 @@
+/* GStreamer ID3 v1 and v2 muxer
+ *
+ * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * 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.
+ */
+
+#ifndef GST_ID3TAG_H
+#define GST_ID3TAG_H
+
+#include "gsttagmux.h"
+#include "id3tag.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GstID3Tag GstID3Tag;
+typedef struct _GstID3TagClass GstID3TagClass;
+
+struct _GstID3Tag {
+  GstTagMux  tagmux;
+
+  gboolean write_v1;
+  gboolean write_v2;
+
+  gint     v2_major_version;
+};
+
+struct _GstID3TagClass {
+  GstTagMuxClass  tagmux_class;
+};
+
+#define GST_TYPE_ID3TAG \
+  (gst_id3tag_get_type())
+#define GST_ID3TAG(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_ID3TAG,GstID3Tag))
+#define GST_ID3TAG_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_ID3TAG,GstID3TagClass))
+#define GST_IS_ID3TAG(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_ID3TAG))
+#define GST_IS_ID3TAG_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_ID3TAG))
+
+GType gst_id3tag_get_type (void);
+
+G_END_DECLS
+
+#endif /* GST_ID3TAG_H */
+
diff --git a/gst/id3tag/gsttagmux.c b/gst/id3tag/gsttagmux.c
new file mode 100644
index 0000000..bfa4e1b
--- /dev/null
+++ b/gst/id3tag/gsttagmux.c
@@ -0,0 +1,490 @@
+/* GStreamer tag muxer base class
+ *
+ * Copyright (C) 2006 Christophe Fergeau  <teuf@gnome.org>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2006 Sebastian Dröge <slomo@circular-chaos.org>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * 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.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <gst/gsttagsetter.h>
+#include <gst/tag/tag.h>
+
+#include "gsttagmux.h"
+
+GST_DEBUG_CATEGORY_STATIC (gst_tag_mux_debug);
+#define GST_CAT_DEFAULT gst_tag_mux_debug
+
+/* Subclass provides a src template and pad. We accept anything as input here,
+   however. */
+
+static GstStaticPadTemplate gst_tag_mux_sink_template =
+GST_STATIC_PAD_TEMPLATE ("sink",
+    GST_PAD_SINK,
+    GST_PAD_ALWAYS,
+    GST_STATIC_CAPS ("ANY"));
+
+static void
+gst_tag_mux_iface_init (GType tag_type)
+{
+  static const GInterfaceInfo tag_setter_info = {
+    NULL,
+    NULL,
+    NULL
+  };
+
+  g_type_add_interface_static (tag_type, GST_TYPE_TAG_SETTER, &tag_setter_info);
+}
+
+GST_BOILERPLATE_FULL (GstTagMux, gst_tag_mux,
+    GstElement, GST_TYPE_ELEMENT, gst_tag_mux_iface_init);
+
+
+static GstStateChangeReturn
+gst_tag_mux_change_state (GstElement * element, GstStateChange transition);
+static GstFlowReturn gst_tag_mux_chain (GstPad * pad, GstBuffer * buffer);
+static gboolean gst_tag_mux_sink_event (GstPad * pad, GstEvent * event);
+
+static void
+gst_tag_mux_finalize (GObject * obj)
+{
+  GstTagMux *mux = GST_TAG_MUX (obj);
+
+  if (mux->newsegment_ev) {
+    gst_event_unref (mux->newsegment_ev);
+    mux->newsegment_ev = NULL;
+  }
+
+  if (mux->event_tags) {
+    gst_tag_list_free (mux->event_tags);
+    mux->event_tags = NULL;
+  }
+
+  if (mux->final_tags) {
+    gst_tag_list_free (mux->final_tags);
+    mux->final_tags = NULL;
+  }
+
+  G_OBJECT_CLASS (parent_class)->finalize (obj);
+}
+
+static void
+gst_tag_mux_base_init (gpointer g_class)
+{
+  GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
+
+  gst_element_class_add_pad_template (element_class,
+      gst_static_pad_template_get (&gst_tag_mux_sink_template));
+
+  GST_DEBUG_CATEGORY_INIT (gst_tag_mux_debug, "tagmux", 0,
+      "tag muxer base class");
+}
+
+static void
+gst_tag_mux_class_init (GstTagMuxClass * klass)
+{
+  GObjectClass *gobject_class;
+  GstElementClass *gstelement_class;
+
+  gobject_class = (GObjectClass *) klass;
+  gstelement_class = (GstElementClass *) klass;
+
+  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_tag_mux_finalize);
+  gstelement_class->change_state = GST_DEBUG_FUNCPTR (gst_tag_mux_change_state);
+}
+
+static void
+gst_tag_mux_init (GstTagMux * mux, GstTagMuxClass * mux_class)
+{
+  GstElementClass *element_klass = GST_ELEMENT_CLASS (mux_class);
+  GstPadTemplate *tmpl;
+
+  /* pad through which data comes in to the element */
+  mux->sinkpad =
+      gst_pad_new_from_static_template (&gst_tag_mux_sink_template, "sink");
+  gst_pad_set_chain_function (mux->sinkpad,
+      GST_DEBUG_FUNCPTR (gst_tag_mux_chain));
+  gst_pad_set_event_function (mux->sinkpad,
+      GST_DEBUG_FUNCPTR (gst_tag_mux_sink_event));
+  gst_element_add_pad (GST_ELEMENT (mux), mux->sinkpad);
+
+  /* pad through which data goes out of the element */
+  tmpl = gst_element_class_get_pad_template (element_klass, "src");
+  if (tmpl) {
+    mux->srcpad = gst_pad_new_from_template (tmpl, "src");
+    gst_pad_use_fixed_caps (mux->srcpad);
+    gst_pad_set_caps (mux->srcpad, gst_pad_template_get_caps (tmpl));
+    gst_element_add_pad (GST_ELEMENT (mux), mux->srcpad);
+  }
+
+  mux->render_start_tag = TRUE;
+  mux->render_end_tag = TRUE;
+}
+
+static GstTagList *
+gst_tag_mux_get_tags (GstTagMux * mux)
+{
+  GstTagSetter *tagsetter = GST_TAG_SETTER (mux);
+  const GstTagList *tagsetter_tags;
+  GstTagMergeMode merge_mode;
+
+  if (mux->final_tags)
+    return mux->final_tags;
+
+  tagsetter_tags = gst_tag_setter_get_tag_list (tagsetter);
+  merge_mode = gst_tag_setter_get_tag_merge_mode (tagsetter);
+
+  GST_LOG_OBJECT (mux, "merging tags, merge mode = %d", merge_mode);
+  GST_LOG_OBJECT (mux, "event tags: %" GST_PTR_FORMAT, mux->event_tags);
+  GST_LOG_OBJECT (mux, "set   tags: %" GST_PTR_FORMAT, tagsetter_tags);
+
+  mux->final_tags =
+      gst_tag_list_merge (tagsetter_tags, mux->event_tags, merge_mode);
+
+  GST_LOG_OBJECT (mux, "final tags: %" GST_PTR_FORMAT, mux->final_tags);
+
+  return mux->final_tags;
+}
+
+static GstFlowReturn
+gst_tag_mux_render_start_tag (GstTagMux * mux)
+{
+  GstTagMuxClass *klass;
+  GstBuffer *buffer;
+  GstTagList *taglist;
+  GstEvent *event;
+  GstFlowReturn ret;
+
+  taglist = gst_tag_mux_get_tags (mux);
+
+  klass = GST_TAG_MUX_CLASS (G_OBJECT_GET_CLASS (mux));
+
+  if (klass->render_start_tag == NULL)
+    goto no_vfunc;
+
+  buffer = klass->render_start_tag (mux, taglist);
+
+  /* Null buffer is ok, just means we're not outputting anything */
+  if (buffer == NULL) {
+    GST_INFO_OBJECT (mux, "No start tag generated");
+    mux->start_tag_size = 0;
+    return GST_FLOW_OK;
+  }
+
+  mux->start_tag_size = GST_BUFFER_SIZE (buffer);
+  GST_LOG_OBJECT (mux, "tag size = %" G_GSIZE_FORMAT " bytes",
+      mux->start_tag_size);
+
+  /* Send newsegment event from byte position 0, so the tag really gets
+   * written to the start of the file, independent of the upstream segment */
+  gst_pad_push_event (mux->srcpad,
+      gst_event_new_new_segment (FALSE, 1.0, GST_FORMAT_BYTES, 0, -1, 0));
+
+  /* Send an event about the new tags to downstream elements */
+  /* gst_event_new_tag takes ownership of the list, so use a copy */
+  event = gst_event_new_tag (gst_tag_list_copy (taglist));
+  gst_pad_push_event (mux->srcpad, event);
+
+  GST_BUFFER_OFFSET (buffer) = 0;
+  ret = gst_pad_push (mux->srcpad, buffer);
+
+  mux->current_offset = mux->start_tag_size;
+  mux->max_offset = MAX (mux->max_offset, mux->current_offset);
+
+  return ret;
+
+no_vfunc:
+  {
+    GST_ERROR_OBJECT (mux, "Subclass does not implement "
+        "render_start_tag vfunc!");
+    return GST_FLOW_ERROR;
+  }
+}
+
+static GstFlowReturn
+gst_tag_mux_render_end_tag (GstTagMux * mux)
+{
+  GstTagMuxClass *klass;
+  GstBuffer *buffer;
+  GstTagList *taglist;
+  GstFlowReturn ret;
+
+  taglist = gst_tag_mux_get_tags (mux);
+
+  klass = GST_TAG_MUX_CLASS (G_OBJECT_GET_CLASS (mux));
+
+  if (klass->render_end_tag == NULL)
+    goto no_vfunc;
+
+  buffer = klass->render_end_tag (mux, taglist);
+
+  if (buffer == NULL) {
+    GST_INFO_OBJECT (mux, "No end tag generated");
+    mux->end_tag_size = 0;
+    return GST_FLOW_OK;
+  }
+
+  mux->end_tag_size = GST_BUFFER_SIZE (buffer);
+  GST_LOG_OBJECT (mux, "tag size = %" G_GSIZE_FORMAT " bytes",
+      mux->end_tag_size);
+
+  /* Send newsegment event from the end of the file, so it gets written there,
+     independent of whatever new segment events upstream has sent us */
+  gst_pad_push_event (mux->srcpad,
+      gst_event_new_new_segment (FALSE, 1.0, GST_FORMAT_BYTES, mux->max_offset,
+          -1, 0));
+
+  GST_BUFFER_OFFSET (buffer) = mux->max_offset;
+  ret = gst_pad_push (mux->srcpad, buffer);
+
+  return ret;
+
+no_vfunc:
+  {
+    GST_ERROR_OBJECT (mux, "Subclass does not implement "
+        "render_end_tag vfunc!");
+    return GST_FLOW_ERROR;
+  }
+}
+
+static GstEvent *
+gst_tag_mux_adjust_event_offsets (GstTagMux * mux,
+    const GstEvent * newsegment_event)
+{
+  GstFormat format;
+  gint64 start, stop, cur;
+
+  gst_event_parse_new_segment ((GstEvent *) newsegment_event, NULL, NULL,
+      &format, &start, &stop, &cur);
+
+  g_assert (format == GST_FORMAT_BYTES);
+
+  if (start != -1)
+    start += mux->start_tag_size;
+  if (stop != -1)
+    stop += mux->start_tag_size;
+  if (cur != -1)
+    cur += mux->start_tag_size;
+
+  GST_DEBUG_OBJECT (mux, "adjusting newsegment event offsets to start=%"
+      G_GINT64_FORMAT ", stop=%" G_GINT64_FORMAT ", cur=%" G_GINT64_FORMAT
+      " (delta = +%" G_GSIZE_FORMAT ")", start, stop, cur, mux->start_tag_size);
+
+  return gst_event_new_new_segment (TRUE, 1.0, format, start, stop, cur);
+}
+
+static GstFlowReturn
+gst_tag_mux_chain (GstPad * pad, GstBuffer * buffer)
+{
+  GstTagMux *mux = GST_TAG_MUX (GST_OBJECT_PARENT (pad));
+  GstFlowReturn ret;
+  int length;
+
+  if (mux->render_start_tag) {
+
+    GST_INFO_OBJECT (mux, "Adding tags to stream");
+    ret = gst_tag_mux_render_start_tag (mux);
+    if (ret != GST_FLOW_OK) {
+      GST_DEBUG_OBJECT (mux, "flow: %s", gst_flow_get_name (ret));
+      gst_buffer_unref (buffer);
+      return ret;
+    }
+
+    /* Now send the cached newsegment event that we got from upstream */
+    if (mux->newsegment_ev) {
+      gint64 start;
+      GstEvent *newseg;
+
+      GST_DEBUG_OBJECT (mux, "sending cached newsegment event");
+      newseg = gst_tag_mux_adjust_event_offsets (mux, mux->newsegment_ev);
+      gst_event_unref (mux->newsegment_ev);
+      mux->newsegment_ev = NULL;
+
+      gst_event_parse_new_segment (newseg, NULL, NULL, NULL, &start, NULL,
+          NULL);
+
+      gst_pad_push_event (mux->srcpad, newseg);
+      mux->current_offset = start;
+      mux->max_offset = MAX (mux->max_offset, mux->current_offset);
+    } else {
+      /* upstream sent no newsegment event or only one in a non-BYTE format */
+    }
+
+    mux->render_start_tag = FALSE;
+  }
+
+  buffer = gst_buffer_make_metadata_writable (buffer);
+
+  if (GST_BUFFER_OFFSET (buffer) != GST_BUFFER_OFFSET_NONE) {
+    GST_LOG_OBJECT (mux, "Adjusting buffer offset from %" G_GINT64_FORMAT
+        " to %" G_GINT64_FORMAT, GST_BUFFER_OFFSET (buffer),
+        GST_BUFFER_OFFSET (buffer) + mux->start_tag_size);
+    GST_BUFFER_OFFSET (buffer) += mux->start_tag_size;
+  }
+
+  length = GST_BUFFER_SIZE (buffer);
+
+  gst_buffer_set_caps (buffer, GST_PAD_CAPS (mux->srcpad));
+  ret = gst_pad_push (mux->srcpad, buffer);
+
+  mux->current_offset += length;
+  mux->max_offset = MAX (mux->max_offset, mux->current_offset);
+
+  return ret;
+}
+
+static gboolean
+gst_tag_mux_sink_event (GstPad * pad, GstEvent * event)
+{
+  GstTagMux *mux;
+  gboolean result;
+
+  mux = GST_TAG_MUX (gst_pad_get_parent (pad));
+  result = FALSE;
+
+  switch (GST_EVENT_TYPE (event)) {
+    case GST_EVENT_TAG:{
+      GstTagList *tags;
+
+      gst_event_parse_tag (event, &tags);
+
+      GST_INFO_OBJECT (mux, "Got tag event: %" GST_PTR_FORMAT, tags);
+
+      if (mux->event_tags != NULL) {
+        gst_tag_list_insert (mux->event_tags, tags, GST_TAG_MERGE_REPLACE);
+      } else {
+        mux->event_tags = gst_tag_list_copy (tags);
+      }
+
+      GST_INFO_OBJECT (mux, "Event tags are now: %" GST_PTR_FORMAT,
+          mux->event_tags);
+
+      /* just drop the event, we'll push a new tag event in render_start_tag */
+      gst_event_unref (event);
+      result = TRUE;
+      break;
+    }
+    case GST_EVENT_NEWSEGMENT:{
+      GstFormat fmt;
+      gint64 start;
+
+      gst_event_parse_new_segment (event, NULL, NULL, &fmt, &start, NULL, NULL);
+
+      if (fmt != GST_FORMAT_BYTES) {
+        GST_WARNING_OBJECT (mux, "dropping newsegment event in %s format",
+            gst_format_get_name (fmt));
+        gst_event_unref (event);
+        break;
+      }
+
+      if (mux->render_start_tag) {
+        /* we have not rendered the tag yet, which means that we don't know
+         * how large it is going to be yet, so we can't adjust the offsets
+         * here at this point and need to cache the newsegment event for now
+         * (also, there could be tag events coming after this newsegment event
+         *  and before the first buffer). */
+        if (mux->newsegment_ev) {
+          GST_WARNING_OBJECT (mux, "discarding old cached newsegment event");
+          gst_event_unref (mux->newsegment_ev);
+        }
+
+        GST_LOG_OBJECT (mux, "caching newsegment event for later");
+        mux->newsegment_ev = event;
+      } else {
+        GST_DEBUG_OBJECT (mux, "got newsegment event, adjusting offsets");
+        gst_pad_push_event (mux->srcpad,
+            gst_tag_mux_adjust_event_offsets (mux, event));
+        gst_event_unref (event);
+
+        mux->current_offset = start;
+        mux->max_offset = MAX (mux->max_offset, mux->current_offset);
+      }
+      event = NULL;
+      result = TRUE;
+      break;
+    }
+    case GST_EVENT_EOS:{
+      if (mux->render_end_tag) {
+        GstFlowReturn ret;
+
+        GST_INFO_OBJECT (mux, "Adding tags to stream");
+        ret = gst_tag_mux_render_end_tag (mux);
+        if (ret != GST_FLOW_OK) {
+          GST_DEBUG_OBJECT (mux, "flow: %s", gst_flow_get_name (ret));
+          return ret;
+        }
+
+        mux->render_end_tag = FALSE;
+      }
+
+      /* Now forward EOS */
+      result = gst_pad_event_default (pad, event);
+      break;
+    }
+    default:
+      result = gst_pad_event_default (pad, event);
+      break;
+  }
+
+  gst_object_unref (mux);
+
+  return result;
+}
+
+
+static GstStateChangeReturn
+gst_tag_mux_change_state (GstElement * element, GstStateChange transition)
+{
+  GstTagMux *mux;
+  GstStateChangeReturn result;
+
+  mux = GST_TAG_MUX (element);
+
+  result = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+  if (result != GST_STATE_CHANGE_SUCCESS) {
+    return result;
+  }
+
+  switch (transition) {
+    case GST_STATE_CHANGE_PAUSED_TO_READY:{
+      if (mux->newsegment_ev) {
+        gst_event_unref (mux->newsegment_ev);
+        mux->newsegment_ev = NULL;
+      }
+      if (mux->event_tags) {
+        gst_tag_list_free (mux->event_tags);
+        mux->event_tags = NULL;
+      }
+      mux->start_tag_size = 0;
+      mux->end_tag_size = 0;
+      mux->render_start_tag = TRUE;
+      mux->render_end_tag = TRUE;
+      mux->current_offset = 0;
+      mux->max_offset = 0;
+      break;
+    }
+    default:
+      break;
+  }
+
+  return result;
+}
diff --git a/gst/id3tag/gsttagmux.h b/gst/id3tag/gsttagmux.h
new file mode 100644
index 0000000..c13a732
--- /dev/null
+++ b/gst/id3tag/gsttagmux.h
@@ -0,0 +1,79 @@
+/* GStreamer tag muxer base class
+ *
+ * Copyright (C) 2006 Christophe Fergeau  <teuf@gnome.org>
+ * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * 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.
+ */
+
+#ifndef GST_TAG_MUX_H
+#define GST_TAG_MUX_H
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GstTagMux GstTagMux;
+typedef struct _GstTagMuxClass GstTagMuxClass;
+
+/* Definition of structure storing data for this element. */
+struct _GstTagMux {
+  GstElement    element;
+
+  GstPad       *srcpad;
+  GstPad       *sinkpad;
+  GstTagList   *event_tags; /* tags received from upstream elements */
+  GstTagList   *final_tags; /* Final set of tags used for muxing */
+  gsize         start_tag_size;
+  gsize         end_tag_size;
+  gboolean      render_start_tag;
+  gboolean      render_end_tag;
+
+  gint64        current_offset;
+  gint64        max_offset;
+
+  GstEvent     *newsegment_ev; /* cached newsegment event from upstream */
+};
+
+/* Standard definition defining a class for this element. */
+struct _GstTagMuxClass {
+  GstElementClass parent_class;
+
+  /* vfuncs */
+  GstBuffer  * (*render_start_tag) (GstTagMux * mux, GstTagList * tag_list);
+  GstBuffer  * (*render_end_tag) (GstTagMux * mux, GstTagList * tag_list);
+};
+
+/* Standard macros for defining types for this element.  */
+#define GST_TYPE_TAG_MUX \
+  (gst_tag_mux_get_type())
+#define GST_TAG_MUX(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAG_MUX,GstTagMux))
+#define GST_TAG_MUX_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAG_MUX,GstTagMuxClass))
+#define GST_IS_TAG_MUX(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_TAG_MUX))
+#define GST_IS_TAG_MUX_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_TAG_MUX))
+
+/* Standard function returning type information. */
+GType gst_tag_mux_get_type (void);
+
+G_END_DECLS
+
+#endif
+
diff --git a/gst/id3tag/id3tag.c b/gst/id3tag/id3tag.c
new file mode 100644
index 0000000..0e040f7
--- /dev/null
+++ b/gst/id3tag/id3tag.c
@@ -0,0 +1,1194 @@
+/* GStreamer ID3v2 tag writer
+ *
+ * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org>
+ * Copyright (C) 2006-2009 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * 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.
+ */
+
+#include "id3tag.h"
+#include <string.h>
+
+#include <gst/tag/tag.h>
+
+GST_DEBUG_CATEGORY_EXTERN (gst_id3tag_debug);
+#define GST_CAT_DEFAULT gst_id3tag_debug
+
+#define ID3V2_APIC_PICTURE_OTHER 0
+#define ID3V2_APIC_PICTURE_FILE_ICON 1
+
+/* ======================================================================== */
+
+typedef GString GstByteWriter;
+
+static inline GstByteWriter *
+gst_byte_writer_new (guint size)
+{
+  return (GstByteWriter *) g_string_sized_new (size);
+}
+
+static inline guint
+gst_byte_writer_get_length (GstByteWriter * w)
+{
+  return ((GString *) w)->len;
+}
+
+static inline void
+gst_byte_writer_write_bytes (GstByteWriter * w, const guint8 * data, guint len)
+{
+  g_string_append_len ((GString *) w, (const gchar *) data, len);
+}
+
+static inline void
+gst_byte_writer_write_uint8 (GstByteWriter * w, guint8 val)
+{
+  guint8 data[1];
+
+  GST_WRITE_UINT8 (data, val);
+  gst_byte_writer_write_bytes (w, data, 1);
+}
+
+static inline void
+gst_byte_writer_write_uint16 (GstByteWriter * w, guint16 val)
+{
+  guint8 data[2];
+
+  GST_WRITE_UINT16_BE (data, val);
+  gst_byte_writer_write_bytes (w, data, 2);
+}
+
+static inline void
+gst_byte_writer_write_uint32 (GstByteWriter * w, guint32 val)
+{
+  guint8 data[4];
+
+  GST_WRITE_UINT32_BE (data, val);
+  gst_byte_writer_write_bytes (w, data, 4);
+}
+
+static inline void
+gst_byte_writer_write_uint32_syncsafe (GstByteWriter * w, guint32 val)
+{
+  guint8 data[4];
+
+  data[0] = (guint8) ((val >> 21) & 0x7f);
+  data[1] = (guint8) ((val >> 14) & 0x7f);
+  data[2] = (guint8) ((val >> 7) & 0x7f);
+  data[3] = (guint8) ((val >> 0) & 0x7f);
+  gst_byte_writer_write_bytes (w, data, 4);
+}
+
+static void
+gst_byte_writer_copy_bytes (GstByteWriter * w, guint8 * dest, guint offset,
+    gint size)
+{
+  guint length;
+
+  length = gst_byte_writer_get_length (w);
+
+  if (size == -1)
+    size = length - offset;
+
+#if GLIB_CHECK_VERSION(2,16,0)
+  g_warn_if_fail (length >= (offset + size));
+#endif
+
+  memcpy (dest, w->str + offset, MIN (size, length - offset));
+}
+
+static inline void
+gst_byte_writer_free (GstByteWriter * w)
+{
+  g_string_free (w, TRUE);
+}
+
+/* ======================================================================== */
+
+/*
+typedef enum {
+  GST_ID3V2_FRAME_FLAG_NONE = 0,
+  GST_ID3V2_FRAME_FLAG_
+} GstID3v2FrameMsgFlags;
+*/
+
+typedef struct
+{
+  gchar id[5];
+  guint32 len;                  /* Length encoded in the header; this is the
+                                   total length - header size */
+  guint16 flags;
+  GstByteWriter *writer;
+  gboolean dirty;               /* TRUE if frame header needs updating */
+} GstId3v2Frame;
+
+typedef struct
+{
+  GArray *frames;
+  guint major_version;          /* The 3 in v2.3.0 */
+} GstId3v2Tag;
+
+typedef void (*GstId3v2AddTagFunc) (GstId3v2Tag * tag, const GstTagList * list,
+    const gchar * gst_tag, guint num_tags, const gchar * data);
+
+#define ID3V2_ENCODING_UTF8    0x03
+
+static gboolean id3v2_tag_init (GstId3v2Tag * tag, guint major_version);
+static void id3v2_tag_unset (GstId3v2Tag * tag);
+
+static void id3v2_frame_init (GstId3v2Frame * frame,
+    const gchar * frame_id, guint16 flags);
+static void id3v2_frame_unset (GstId3v2Frame * frame);
+static void id3v2_frame_finish (GstId3v2Tag * tag, GstId3v2Frame * frame);
+static guint id3v2_frame_get_size (GstId3v2Tag * tag, GstId3v2Frame * frame);
+
+static void id3v2_tag_add_text_frame (GstId3v2Tag * tag,
+    const gchar * frame_id, gchar ** strings, int num_strings);
+
+static gboolean
+id3v2_tag_init (GstId3v2Tag * tag, guint major_version)
+{
+  if (major_version != 3 && major_version != 4)
+    return FALSE;
+
+  tag->major_version = major_version;
+  tag->frames = g_array_new (TRUE, TRUE, sizeof (GstId3v2Frame));
+
+  return TRUE;
+}
+
+static void
+id3v2_tag_unset (GstId3v2Tag * tag)
+{
+  guint i;
+
+  for (i = 0; i < tag->frames->len; ++i)
+    id3v2_frame_unset (&g_array_index (tag->frames, GstId3v2Frame, i));
+
+  g_array_free (tag->frames, TRUE);
+  memset (tag, 0, sizeof (GstId3v2Tag));
+}
+
+#ifndef GST_ROUND_UP_1024
+#define GST_ROUND_UP_1024(num) (((num)+1023)&~1023)
+#endif
+
+static GstBuffer *
+id3v2_tag_to_buffer (GstId3v2Tag * tag)
+{
+  GstByteWriter *w;
+  GstBuffer *buf;
+  guint8 *dest;
+  guint i, size, offset, size_frames = 0;
+
+  GST_DEBUG ("Creating buffer for ID3v2 tag containing %d frames",
+      tag->frames->len);
+
+  for (i = 0; i < tag->frames->len; ++i) {
+    GstId3v2Frame *frame = &g_array_index (tag->frames, GstId3v2Frame, i);
+
+    id3v2_frame_finish (tag, frame);
+    size_frames += id3v2_frame_get_size (tag, frame);
+  }
+
+  size = GST_ROUND_UP_1024 (10 + size_frames);
+
+  w = gst_byte_writer_new (10);
+  gst_byte_writer_write_uint8 (w, 'I');
+  gst_byte_writer_write_uint8 (w, 'D');
+  gst_byte_writer_write_uint8 (w, '3');
+  gst_byte_writer_write_uint8 (w, tag->major_version);
+  gst_byte_writer_write_uint8 (w, 0);   /* micro version */
+  gst_byte_writer_write_uint8 (w, 0);   /* flags */
+  gst_byte_writer_write_uint32_syncsafe (w, size - 10);
+
+  buf = gst_buffer_new_and_alloc (size);
+  dest = GST_BUFFER_DATA (buf);
+  gst_byte_writer_copy_bytes (w, dest, 0, 10);
+  offset = 10;
+
+  for (i = 0; i < tag->frames->len; ++i) {
+    GstId3v2Frame *frame = &g_array_index (tag->frames, GstId3v2Frame, i);
+
+    gst_byte_writer_copy_bytes (frame->writer, dest + offset, 0, -1);
+    offset += id3v2_frame_get_size (tag, frame);
+  }
+
+  /* Zero out any additional space in our buffer as padding. */
+  memset (dest + offset, 0, size - offset);
+
+  gst_byte_writer_free (w);
+
+  return buf;
+}
+
+static inline void
+id3v2_frame_write_bytes (GstId3v2Frame * frame, const guint8 * data, guint len)
+{
+  gst_byte_writer_write_bytes (frame->writer, data, len);
+  frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint8 (GstId3v2Frame * frame, guint8 val)
+{
+  gst_byte_writer_write_uint8 (frame->writer, val);
+  frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint16 (GstId3v2Frame * frame, guint16 val)
+{
+  gst_byte_writer_write_uint16 (frame->writer, val);
+  frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint32 (GstId3v2Frame * frame, guint32 val)
+{
+  gst_byte_writer_write_uint32 (frame->writer, val);
+  frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint32_syncsafe (GstId3v2Frame * frame, guint32 val)
+{
+  guint8 data[4];
+
+  data[0] = (guint8) ((val >> 21) & 0x7f);
+  data[1] = (guint8) ((val >> 14) & 0x7f);
+  data[2] = (guint8) ((val >> 7) & 0x7f);
+  data[3] = (guint8) ((val >> 0) & 0x7f);
+  gst_byte_writer_write_bytes (frame->writer, data, 4);
+  frame->dirty = TRUE;
+}
+
+static void
+id3v2_frame_init (GstId3v2Frame * frame, const gchar * frame_id, guint16 flags)
+{
+  g_assert (strlen (frame_id) == 4);    /* we only handle 2.3.0/2.4.0 */
+  memcpy (frame->id, frame_id, 4 + 1);
+  frame->flags = flags;
+  frame->len = 0;
+  frame->writer = gst_byte_writer_new (64);
+  id3v2_frame_write_bytes (frame, (const guint8 *) frame->id, 4);
+  id3v2_frame_write_uint32 (frame, 0);  /* size, set later */
+  id3v2_frame_write_uint16 (frame, frame->flags);
+}
+
+static void
+id3v2_frame_finish (GstId3v2Tag * tag, GstId3v2Frame * frame)
+{
+  if (frame->dirty) {
+    frame->len = frame->writer->len - 10;
+    GST_LOG ("[%s] %u bytes", frame->id, frame->len);
+    if (tag->major_version == 3) {
+      GST_WRITE_UINT32_BE (frame->writer->str + 4, frame->len);
+    } else {
+      /* Version 4 uses a syncsafe int here */
+      GST_WRITE_UINT8 (frame->writer->str + 4, (frame->len >> 21) & 0x7f);
+      GST_WRITE_UINT8 (frame->writer->str + 5, (frame->len >> 14) & 0x7f);
+      GST_WRITE_UINT8 (frame->writer->str + 6, (frame->len >> 7) & 0x7f);
+      GST_WRITE_UINT8 (frame->writer->str + 7, (frame->len >> 0) & 0x7f);
+    }
+    frame->dirty = FALSE;
+  }
+}
+
+static guint
+id3v2_frame_get_size (GstId3v2Tag * tag, GstId3v2Frame * frame)
+{
+  id3v2_frame_finish (tag, frame);
+  return gst_byte_writer_get_length (frame->writer);
+}
+
+static void
+id3v2_frame_unset (GstId3v2Frame * frame)
+{
+  gst_byte_writer_free (frame->writer);
+  memset (frame, 0, sizeof (GstId3v2Frame));
+}
+
+static void
+id3v2_tag_add_text_frame (GstId3v2Tag * tag, const gchar * frame_id,
+    gchar ** strings_utf8, int num_strings)
+{
+  GstId3v2Frame frame;
+  guint len, i;
+
+  if (num_strings < 1 || strings_utf8 == NULL || strings_utf8[0] == NULL) {
+    GST_LOG ("Not adding text frame, no strings");
+    return;
+  }
+
+  id3v2_frame_init (&frame, frame_id, 0);
+  id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+
+  GST_LOG ("Adding text frame %s with %d strings", frame_id, num_strings);
+
+  for (i = 0; i < num_strings; ++i) {
+    len = strlen (strings_utf8[i]);
+    g_return_if_fail (g_utf8_validate (strings_utf8[i], len, NULL));
+
+    /* write NUL terminator as well */
+    id3v2_frame_write_bytes (&frame, (const guint8 *) strings_utf8[i], len + 1);
+
+    /* only v2.4.0 supports multiple strings per frame (according to the
+     * earlier specs tag readers should just ignore everything after the first
+     * string, but we probably shouldn't write anything there, just in case
+     * tag readers that only support the old version are not expecting
+     * more data after the first string) */
+    if (tag->major_version < 4)
+      break;
+  }
+
+  if (i < num_strings - 1) {
+    GST_WARNING ("Only wrote one of multiple string values for text frame %s "
+        "- ID3v2 supports multiple string values only since v2.4.0, but writing"
+        "v2.%u.0 tag", frame_id, tag->major_version);
+  }
+
+  g_array_append_val (tag->frames, frame);
+}
+
+/* ====================================================================== */
+
+static void
+add_text_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+  gchar **strings;
+  guint n, i;
+
+  GST_LOG ("Adding '%s' frame", frame_id);
+
+  strings = g_new0 (gchar *, num_tags + 1);
+  for (n = 0, i = 0; n < num_tags; ++n) {
+    if (gst_tag_list_get_string_index (list, tag, n, &strings[i]) &&
+        strings[i] != NULL) {
+      GST_LOG ("%s: %s[%u] = '%s'", frame_id, tag, i, strings[i]);
+      ++i;
+    }
+  }
+
+  if (strings[0] != NULL) {
+    id3v2_tag_add_text_frame (id3v2tag, frame_id, strings, i);
+  } else {
+    GST_WARNING ("Empty list for tag %s, skipping", tag);
+  }
+
+  g_strfreev (strings);
+}
+
+static void
+add_id3v2frame_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  guint i;
+
+  for (i = 0; i < num_tags; ++i) {
+    const GValue *val;
+    GstBuffer *buf;
+
+    val = gst_tag_list_get_value_index (list, tag, i);
+    buf = (GstBuffer *) gst_value_get_mini_object (val);
+
+    if (buf && GST_BUFFER_CAPS (buf)) {
+      GstStructure *s;
+      gint version = 0;
+
+      s = gst_caps_get_structure (GST_BUFFER_CAPS (buf), 0);
+      /* We can only add it if this private buffer is for the same ID3 version,
+         because we don't understand the contents at all. */
+      if (s && gst_structure_get_int (s, "version", &version) &&
+          version == id3v2tag->major_version) {
+        GstId3v2Frame frame;
+        gchar frame_id[5];
+        guint16 flags;
+        guint8 *data = GST_BUFFER_DATA (buf);
+        gint size = GST_BUFFER_SIZE (buf);
+
+        if (size < 10)          /* header size */
+          return;
+
+        /* We only reach here if the frame version matches the muxer. Since the
+           muxer only does v2.3 or v2.4, the frame must be one of those - and
+           so the frame header is the same format */
+        memcpy (frame_id, data, 4);
+        frame_id[4] = 0;
+        flags = GST_READ_UINT16_BE (data + 8);
+
+        id3v2_frame_init (&frame, frame_id, flags);
+        id3v2_frame_write_bytes (&frame, data + 10, size - 10);
+
+        g_array_append_val (id3v2tag->frames, frame);
+        GST_DEBUG ("Added unparsed tag with %d bytes", size);
+      } else {
+        GST_WARNING ("Discarding unrecognised ID3 tag for different ID3 "
+            "version");
+      }
+    }
+  }
+}
+
+static void
+add_text_tag_v4 (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+  if (id3v2tag->major_version == 4)
+    add_text_tag (id3v2tag, list, tag, num_tags, frame_id);
+  else {
+    GST_WARNING ("Cannot serialise tag '%s' in ID3v2.%d", frame_id,
+        id3v2tag->major_version);
+  }
+}
+
+static void
+add_count_or_num_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+  static const struct
+  {
+    const gchar *gst_tag;
+    const gchar *corr_count;    /* corresponding COUNT tag (if number) */
+    const gchar *corr_num;      /* corresponding NUMBER tag (if count) */
+  } corr[] = {
+    {
+    GST_TAG_TRACK_NUMBER, GST_TAG_TRACK_COUNT, NULL}, {
+    GST_TAG_TRACK_COUNT, NULL, GST_TAG_TRACK_NUMBER}, {
+    GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT, NULL}, {
+    GST_TAG_ALBUM_VOLUME_COUNT, NULL, GST_TAG_ALBUM_VOLUME_NUMBER}
+  };
+  guint idx;
+
+  for (idx = 0; idx < G_N_ELEMENTS (corr); ++idx) {
+    if (strcmp (corr[idx].gst_tag, tag) == 0)
+      break;
+  }
+
+  g_assert (idx < G_N_ELEMENTS (corr));
+  g_assert (frame_id && strlen (frame_id) == 4);
+
+  if (corr[idx].corr_num == NULL) {
+    guint number;
+
+    /* number tag */
+    if (gst_tag_list_get_uint_index (list, tag, 0, &number)) {
+      gchar *tag_str;
+      guint count;
+
+      if (gst_tag_list_get_uint_index (list, corr[idx].corr_count, 0, &count))
+        tag_str = g_strdup_printf ("%u/%u", number, count);
+      else
+        tag_str = g_strdup_printf ("%u", number);
+
+      GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
+
+      id3v2_tag_add_text_frame (id3v2tag, frame_id, &tag_str, 1);
+      g_free (tag_str);
+    }
+  } else if (corr[idx].corr_count == NULL) {
+    guint count;
+
+    /* count tag */
+    if (gst_tag_list_get_uint_index (list, corr[idx].corr_num, 0, &count)) {
+      GST_DEBUG ("%s handled with %s, skipping", tag, corr[idx].corr_num);
+    } else if (gst_tag_list_get_uint_index (list, tag, 0, &count)) {
+      gchar *tag_str = g_strdup_printf ("0/%u", count);
+      GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
+
+      id3v2_tag_add_text_frame (id3v2tag, frame_id, &tag_str, 1);
+      g_free (tag_str);
+    }
+  }
+
+  if (num_tags > 1) {
+    GST_WARNING ("more than one %s, can only handle one", tag);
+  }
+}
+
+static void
+add_comment_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  guint n;
+
+  GST_LOG ("Adding comment frames");
+  for (n = 0; n < num_tags; ++n) {
+    gchar *s = NULL;
+
+    if (gst_tag_list_get_string_index (list, tag, n, &s) && s != NULL) {
+      gchar *desc = NULL, *val = NULL, *lang = NULL;
+      int desclen, vallen;
+      GstId3v2Frame frame;
+
+      id3v2_frame_init (&frame, "COMM", 0);
+      id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+
+      if (strcmp (tag, GST_TAG_COMMENT) == 0 ||
+          !gst_tag_parse_extended_comment (s, &desc, &lang, &val, TRUE)) {
+        /* create dummy description fields */
+        desc = g_strdup ("Comment");
+        val = g_strdup (s);
+      }
+
+      /* If we don't have a valid language, match what taglib does for 
+         unknown languages */
+      if (!lang || strlen (lang) < 3)
+        lang = g_strdup ("XXX");
+
+      desclen = strlen (desc);
+      g_return_if_fail (g_utf8_validate (desc, desclen, NULL));
+      vallen = strlen (val);
+      g_return_if_fail (g_utf8_validate (val, vallen, NULL));
+
+      GST_LOG ("%s[%u] = '%s' (%s|%s|%s)", tag, n, s, GST_STR_NULL (desc),
+          GST_STR_NULL (lang), GST_STR_NULL (val));
+
+      id3v2_frame_write_bytes (&frame, (const guint8 *) lang, 3);
+      /* write description and value, each including NULL terminator */
+      id3v2_frame_write_bytes (&frame, (const guint8 *) desc, desclen + 1);
+      id3v2_frame_write_bytes (&frame, (const guint8 *) val, vallen + 1);
+
+      g_free (lang);
+      g_free (desc);
+      g_free (val);
+
+      g_array_append_val (id3v2tag->frames, frame);
+    }
+    g_free (s);
+  }
+}
+
+static void
+add_image_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  guint n;
+
+  for (n = 0; n < num_tags; ++n) {
+    const GValue *val;
+    GstBuffer *image;
+
+    GST_DEBUG ("image %u/%u", n + 1, num_tags);
+
+    val = gst_tag_list_get_value_index (list, tag, n);
+    image = (GstBuffer *) gst_value_get_mini_object (val);
+
+    if (GST_IS_BUFFER (image) && GST_BUFFER_SIZE (image) > 0 &&
+        GST_BUFFER_CAPS (image) != NULL &&
+        !gst_caps_is_empty (GST_BUFFER_CAPS (image))) {
+      const gchar *mime_type;
+      GstStructure *s;
+
+      s = gst_caps_get_structure (GST_BUFFER_CAPS (image), 0);
+      mime_type = gst_structure_get_name (s);
+      if (mime_type != NULL) {
+        const gchar *desc;
+        GstId3v2Frame frame;
+
+        /* APIC frame specifies "-->" if we're providing a URL to the image
+           rather than directly embedding it */
+        if (strcmp (mime_type, "text/uri-list") == 0)
+          mime_type = "-->";
+
+        GST_DEBUG ("Attaching picture of %u bytes and mime type %s",
+            GST_BUFFER_SIZE (image), mime_type);
+
+        id3v2_frame_init (&frame, "APIC", 0);
+        id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+        id3v2_frame_write_bytes (&frame, (const guint8 *) mime_type,
+            strlen (mime_type) + 1);
+
+        /* FIXME set image type properly from caps */
+        if (strcmp (tag, GST_TAG_PREVIEW_IMAGE) == 0)
+          id3v2_frame_write_uint8 (&frame, ID3V2_APIC_PICTURE_FILE_ICON);
+        else
+          id3v2_frame_write_uint8 (&frame, ID3V2_APIC_PICTURE_OTHER);
+
+        desc = gst_structure_get_string (s, "image-description");
+        if (!desc)
+          desc = "";
+        id3v2_frame_write_bytes (&frame, (const guint8 *) desc,
+            strlen (desc) + 1);
+
+        g_array_append_val (id3v2tag->frames, frame);
+      }
+    } else {
+      GST_WARNING ("NULL image or no caps on image buffer (%p, caps=%"
+          GST_PTR_FORMAT ")", image, (image) ? GST_BUFFER_CAPS (image) : NULL);
+    }
+  }
+}
+
+static void
+add_musicbrainz_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * data)
+{
+  static const struct
+  {
+    const gchar gst_tag[28];
+    const gchar spec_id[28];
+    const gchar realworld_id[28];
+  } mb_ids[] = {
+    {
+    GST_TAG_MUSICBRAINZ_ARTISTID, "MusicBrainz Artist Id",
+          "musicbrainz_artistid"}, {
+    GST_TAG_MUSICBRAINZ_ALBUMID, "MusicBrainz Album Id", "musicbrainz_albumid"}, {
+    GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "MusicBrainz Album Artist Id",
+          "musicbrainz_albumartistid"}, {
+    GST_TAG_MUSICBRAINZ_TRMID, "MusicBrainz TRM Id", "musicbrainz_trmid"}, {
+    GST_TAG_CDDA_MUSICBRAINZ_DISCID, "MusicBrainz DiscID",
+          "musicbrainz_discid"}, {
+      /* the following one is more or less made up, there seems to be little
+       * evidence that any popular application is actually putting this info
+       * into TXXX frames; the first one comes from a musicbrainz wiki 'proposed
+       * tags' page, the second one is analogue to the vorbis/ape/flac tag. */
+    GST_TAG_CDDA_CDDB_DISCID, "CDDB DiscID", "discid"}
+  };
+  guint i, idx;
+
+  idx = (guint8) data[0];
+  g_assert (idx < G_N_ELEMENTS (mb_ids));
+
+  for (i = 0; i < num_tags; ++i) {
+    gchar *id_str;
+
+    if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
+      /* add two frames, one with the ID the musicbrainz.org spec mentions
+       * and one with the ID that applications use in the real world */
+      GstId3v2Frame frame1, frame2;
+
+      GST_DEBUG ("Setting '%s' to '%s'", mb_ids[idx].spec_id, id_str);
+
+      id3v2_frame_init (&frame1, "TXXX", 0);
+      id3v2_frame_write_uint8 (&frame1, ID3V2_ENCODING_UTF8);
+      id3v2_frame_write_bytes (&frame1, (const guint8 *) mb_ids[idx].spec_id,
+          strlen (mb_ids[idx].spec_id) + 1);
+      id3v2_frame_write_bytes (&frame1, (const guint8 *) id_str,
+          strlen (id_str) + 1);
+      g_array_append_val (id3v2tag->frames, frame1);
+
+      id3v2_frame_init (&frame2, "TXXX", 0);
+      id3v2_frame_write_uint8 (&frame2, ID3V2_ENCODING_UTF8);
+      id3v2_frame_write_bytes (&frame2,
+          (const guint8 *) mb_ids[idx].realworld_id,
+          strlen (mb_ids[idx].realworld_id) + 1);
+      id3v2_frame_write_bytes (&frame2, (const guint8 *) id_str,
+          strlen (id_str) + 1);
+      g_array_append_val (id3v2tag->frames, frame2);
+
+      g_free (id_str);
+    }
+  }
+}
+
+static void
+add_unique_file_id_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  const gchar *origin = "http://musicbrainz.org";
+  gchar *id_str = NULL;
+
+  if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
+    GstId3v2Frame frame;
+
+    GST_LOG ("Adding %s (%s): %s", tag, origin, id_str);
+
+    id3v2_frame_init (&frame, "UFID", 0);
+    id3v2_frame_write_bytes (&frame, (const guint8 *) origin,
+        strlen (origin) + 1);
+    id3v2_frame_write_bytes (&frame, (const guint8 *) id_str,
+        strlen (id_str) + 1);
+    g_array_append_val (id3v2tag->frames, frame);
+
+    g_free (id_str);
+  }
+}
+
+static void
+add_date_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  guint n;
+  guint i = 0;
+  const gchar *frame_id;
+  gchar **strings;
+
+  if (id3v2tag->major_version == 3)
+    frame_id = "TYER";
+  else
+    frame_id = "TDRC";
+
+  GST_LOG ("Adding date frame");
+
+  strings = g_new0 (gchar *, num_tags + 1);
+  for (n = 0; n < num_tags; ++n) {
+    GDate *date = NULL;
+
+    if (gst_tag_list_get_date_index (list, tag, n, &date) && date != NULL) {
+      GDateYear year;
+      gchar *s;
+
+      year = g_date_get_year (date);
+      if (year > 500 && year < 2100) {
+        s = g_strdup_printf ("%u", year);
+        GST_LOG ("%s[%u] = '%s'", tag, n, s);
+        strings[i] = s;
+        i++;
+      } else {
+        GST_WARNING ("invalid year %u, skipping", year);
+      }
+
+      g_date_free (date);
+    }
+  }
+
+  if (strings[0] != NULL) {
+    id3v2_tag_add_text_frame (id3v2tag, frame_id, strings, i);
+  } else {
+    GST_WARNING ("Empty list for tag %s, skipping", tag);
+  }
+
+  g_strfreev (strings);
+}
+
+static void
+add_encoder_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  guint n;
+  gchar **strings;
+  int i = 0;
+
+  /* ENCODER_VERSION is either handled with the ENCODER tag or not at all */
+  if (strcmp (tag, GST_TAG_ENCODER_VERSION) == 0)
+    return;
+
+  strings = g_new0 (gchar *, num_tags + 1);
+  for (n = 0; n < num_tags; ++n) {
+    gchar *encoder = NULL;
+
+    if (gst_tag_list_get_string_index (list, tag, n, &encoder) && encoder) {
+      guint encoder_version;
+      gchar *s;
+
+      if (gst_tag_list_get_uint_index (list, GST_TAG_ENCODER_VERSION, n,
+              &encoder_version) && encoder_version > 0) {
+        s = g_strdup_printf ("%s %u", encoder, encoder_version);
+      } else {
+        s = g_strdup (encoder);
+      }
+
+      GST_LOG ("encoder[%u] = '%s'", n, s);
+      strings[i] = s;
+      i++;
+      g_free (encoder);
+    }
+  }
+
+  if (strings[0] != NULL) {
+    id3v2_tag_add_text_frame (id3v2tag, "TSSE", strings, i);
+  } else {
+    GST_WARNING ("Empty list for tag %s, skipping", tag);
+  }
+
+  g_strfreev (strings);
+}
+
+static void
+add_uri_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+  gchar *url = NULL;
+
+  g_assert (frame_id != NULL);
+
+  /* URI tags are limited to one of each per taglist */
+  if (gst_tag_list_get_string_index (list, tag, 0, &url) && url != NULL) {
+    guint url_len;
+
+    url_len = strlen (url);
+    if (url_len > 0 && gst_uri_is_valid (url)) {
+      GstId3v2Frame frame;
+
+      id3v2_frame_init (&frame, frame_id, 0);
+      id3v2_frame_write_bytes (&frame, (const guint8 *) url, strlen (url) + 1);
+      g_array_append_val (id3v2tag->frames, frame);
+    } else {
+      GST_WARNING ("Tag %s does not contain a valid URI (%s)", tag, url);
+    }
+
+    g_free (url);
+  }
+}
+
+static void
+add_relative_volume_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+    const gchar * tag, guint num_tags, const gchar * unused)
+{
+  const char *gain_tag_name;
+  const char *peak_tag_name;
+  gdouble peak_val;
+  gdouble gain_val;
+  const char *identification;
+  guint16 peak_int;
+  gint16 gain_int;
+  guint8 peak_bits;
+  GstId3v2Frame frame;
+  gchar *frame_id;
+
+  /* figure out tag names and the identification string to use */
+  if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
+      strcmp (tag, GST_TAG_TRACK_GAIN) == 0) {
+    gain_tag_name = GST_TAG_TRACK_GAIN;
+    peak_tag_name = GST_TAG_TRACK_PEAK;
+    identification = "track";
+    GST_DEBUG ("adding track relative-volume frame");
+  } else {
+    gain_tag_name = GST_TAG_ALBUM_GAIN;
+    peak_tag_name = GST_TAG_ALBUM_PEAK;
+    identification = "album";
+
+    if (id3v2tag->major_version == 3) {
+      GST_WARNING ("Cannot store replaygain album gain data in ID3v2.3");
+      return;
+    }
+    GST_DEBUG ("adding album relative-volume frame");
+  }
+
+  /* find the value for the paired tag (gain, if this is peak, and
+   * vice versa).  if both tags exist, only write the frame when
+   * we're processing the peak tag.
+   */
+  if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
+      strcmp (tag, GST_TAG_ALBUM_PEAK) == 0) {
+
+    gst_tag_list_get_double (list, tag, &peak_val);
+
+    if (gst_tag_list_get_tag_size (list, gain_tag_name) > 0) {
+      gst_tag_list_get_double (list, gain_tag_name, &gain_val);
+      GST_DEBUG ("setting volume adjustment %g", gain_val);
+      gain_int = (gint16) (gain_val * 512.0);
+    } else
+      gain_int = 0;
+
+    /* copying mutagen: always write as 16 bits for sanity. */
+    peak_int = (short) (peak_val * G_MAXSHORT);
+    peak_bits = 16;
+  } else {
+    gst_tag_list_get_double (list, tag, &gain_val);
+    GST_DEBUG ("setting volume adjustment %g", gain_val);
+
+    gain_int = (gint16) (gain_val * 512.0);
+    peak_bits = 0;
+    peak_int = 0;
+
+    if (gst_tag_list_get_tag_size (list, peak_tag_name) != 0) {
+      GST_DEBUG
+          ("both gain and peak tags exist, not adding frame this time around");
+      return;
+    }
+  }
+
+  if (id3v2tag->major_version == 4) {
+    /* 2.4: Use RVA2 tag */
+    frame_id = "RVA2";
+  } else {
+    /* 2.3: Use XRVA tag - this is experimental, but useful in the real world.
+       This version only officially supports the 'RVAD' tag, but that appears
+       to not be widely implemented in reality. */
+    frame_id = "XRVA";
+  }
+
+  id3v2_frame_init (&frame, frame_id, 0);
+  id3v2_frame_write_bytes (&frame, (const guint8 *) identification,
+      strlen (identification) + 1);
+  id3v2_frame_write_uint8 (&frame, 0x01);       /* Master volume */
+  id3v2_frame_write_uint16 (&frame, gain_int);
+  id3v2_frame_write_uint8 (&frame, peak_bits);
+  if (peak_bits)
+    id3v2_frame_write_uint16 (&frame, peak_int);
+
+  g_array_append_val (id3v2tag->frames, frame);
+}
+
+/* id3demux produces these for frames it cannot parse */
+#define GST_ID3_DEMUX_TAG_ID3V2_FRAME "private-id3v2-frame"
+
+static const struct
+{
+  const gchar *gst_tag;
+  const GstId3v2AddTagFunc func;
+  const gchar *data;
+} add_funcs[] = {
+  {
+    /* Simple text tags */
+  GST_TAG_ARTIST, add_text_tag, "TPE1"}, {
+  GST_TAG_TITLE, add_text_tag, "TIT2"}, {
+  GST_TAG_ALBUM, add_text_tag, "TALB"}, {
+  GST_TAG_COPYRIGHT, add_text_tag, "TCOP"}, {
+  GST_TAG_COMPOSER, add_text_tag, "TCOM"}, {
+  GST_TAG_GENRE, add_text_tag, "TCON"}, {
+
+    /* Private frames */
+  GST_ID3_DEMUX_TAG_ID3V2_FRAME, add_id3v2frame_tag, NULL}, {
+
+    /* Track and album numbers */
+  GST_TAG_TRACK_NUMBER, add_count_or_num_tag, "TRCK"}, {
+  GST_TAG_TRACK_COUNT, add_count_or_num_tag, "TRCK"}, {
+  GST_TAG_ALBUM_VOLUME_NUMBER, add_count_or_num_tag, "TPOS"}, {
+  GST_TAG_ALBUM_VOLUME_COUNT, add_count_or_num_tag, "TPOS"}, {
+
+    /* Comment tags */
+  GST_TAG_COMMENT, add_comment_tag, NULL}, {
+  GST_TAG_EXTENDED_COMMENT, add_comment_tag, NULL}, {
+
+    /* Images */
+  GST_TAG_IMAGE, add_image_tag, NULL}, {
+  GST_TAG_PREVIEW_IMAGE, add_image_tag, NULL}, {
+
+    /* Misc user-defined text tags for IDs (and UFID frame) */
+  GST_TAG_MUSICBRAINZ_ARTISTID, add_musicbrainz_tag, "\000"}, {
+  GST_TAG_MUSICBRAINZ_ALBUMID, add_musicbrainz_tag, "\001"}, {
+  GST_TAG_MUSICBRAINZ_ALBUMARTISTID, add_musicbrainz_tag, "\002"}, {
+  GST_TAG_MUSICBRAINZ_TRMID, add_musicbrainz_tag, "\003"}, {
+  GST_TAG_CDDA_MUSICBRAINZ_DISCID, add_musicbrainz_tag, "\004"}, {
+  GST_TAG_CDDA_CDDB_DISCID, add_musicbrainz_tag, "\005"}, {
+  GST_TAG_MUSICBRAINZ_TRACKID, add_unique_file_id_tag, NULL}, {
+
+    /* Info about encoder */
+  GST_TAG_ENCODER, add_encoder_tag, NULL}, {
+  GST_TAG_ENCODER_VERSION, add_encoder_tag, NULL}, {
+
+    /* URIs */
+  GST_TAG_COPYRIGHT_URI, add_uri_tag, "WCOP"}, {
+  GST_TAG_LICENSE_URI, add_uri_tag, "WCOP"}, {
+
+    /* Up to here, all the frame ids and contents have been the same between
+       versions 2.3 and 2.4. The rest of them differ... */
+    /* Date (in ID3v2.3, this is a TYER tag. In v2.4, it's a TDRC tag */
+  GST_TAG_DATE, add_date_tag, NULL}, {
+
+    /* Replaygain data (not really supported in 2.3, we use an experimental
+       tag there) */
+  GST_TAG_TRACK_PEAK, add_relative_volume_tag, NULL}, {
+  GST_TAG_TRACK_GAIN, add_relative_volume_tag, NULL}, {
+  GST_TAG_ALBUM_PEAK, add_relative_volume_tag, NULL}, {
+  GST_TAG_ALBUM_GAIN, add_relative_volume_tag, NULL}, {
+
+    /* Sortable version of various tags. These are all v2.4 ONLY */
+  GST_TAG_ARTIST_SORTNAME, add_text_tag_v4, "TSOP"}, {
+  GST_TAG_ALBUM_SORTNAME, add_text_tag_v4, "TSOA"}, {
+  GST_TAG_TITLE_SORTNAME, add_text_tag_v4, "TSOT"}
+};
+
+static void
+foreach_add_tag (const GstTagList * list, const gchar * tag, gpointer userdata)
+{
+  GstId3v2Tag *id3v2tag = (GstId3v2Tag *) userdata;
+  guint num_tags, i;
+
+  num_tags = gst_tag_list_get_tag_size (list, tag);
+
+  GST_LOG ("Processing tag %s (num=%u)", tag, num_tags);
+
+  if (num_tags > 1 && gst_tag_is_fixed (tag)) {
+    GST_WARNING ("Multiple occurences of fixed tag '%s', ignoring some", tag);
+    num_tags = 1;
+  }
+
+  for (i = 0; i < G_N_ELEMENTS (add_funcs); ++i) {
+    if (strcmp (add_funcs[i].gst_tag, tag) == 0) {
+      add_funcs[i].func (id3v2tag, list, tag, num_tags, add_funcs[i].data);
+      break;
+    }
+  }
+
+  if (i == G_N_ELEMENTS (add_funcs)) {
+    GST_WARNING ("Unsupported tag '%s' - not written", tag);
+  }
+}
+
+GstBuffer *
+gst_id3mux_render_v2_tag (GstTagMux * mux, GstTagList * taglist, int version)
+{
+  GstId3v2Tag tag;
+  GstBuffer *buf;
+
+  if (!id3v2_tag_init (&tag, version)) {
+    GST_WARNING_OBJECT (mux, "Unsupported version %d", version);
+    return NULL;
+  }
+
+  /* Render the tag */
+  gst_tag_list_foreach (taglist, foreach_add_tag, &tag);
+
+#if 0
+  /* Do we want to add our own signature to the tag somewhere? */
+  {
+    gchar *tag_producer_str;
+
+    tag_producer_str = g_strdup_printf ("(GStreamer id3v2mux %s, using "
+        "taglib %u.%u)", VERSION, TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION);
+    add_one_txxx_tag (id3v2tag, "tag_encoder", tag_producer_str);
+    g_free (tag_producer_str);
+  }
+#endif
+
+  /* Create buffer with tag */
+  buf = id3v2_tag_to_buffer (&tag);
+  gst_buffer_set_caps (buf, GST_PAD_CAPS (mux->srcpad));
+  GST_LOG_OBJECT (mux, "tag size = %d bytes", GST_BUFFER_SIZE (buf));
+
+  id3v2_tag_unset (&tag);
+
+  return buf;
+}
+
+#define ID3_V1_TAG_SIZE 128
+
+typedef void (*GstId3v1WriteFunc) (const GstTagList * list,
+    const gchar * gst_tag, guint8 * dst, int len);
+
+static void
+latin1_convert (const GstTagList * list, const gchar * tag,
+    guint8 * dst, int maxlen)
+{
+  gchar *str;
+  gsize len;
+  gchar *latin1;
+
+  if (!gst_tag_list_get_string (list, tag, &str))
+    return;
+
+  /* Convert to Latin-1 (ISO-8859-1), replacing unrepresentable characters
+     with '?' */
+  latin1 = g_convert_with_fallback (str, -1, "ISO-8859-1", "UTF-8", "?",
+      NULL, &len, NULL);
+
+  if (latin1) {
+    len = MIN (len, maxlen);
+    memcpy (dst, latin1, len);
+    g_free (latin1);
+  }
+
+  g_free (str);
+}
+
+static void
+date_v1_convert (const GstTagList * list, const gchar * tag,
+    guint8 * dst, int maxlen)
+{
+  GDate *date;
+
+  /* Only one date supported */
+  if (gst_tag_list_get_date_index (list, tag, 0, &date) && date != NULL) {
+    GDateYear year = g_date_get_year (date);
+    /* Check for plausible year */
+    if (year > 500 && year < 2100) {
+      gchar str[5];
+      g_snprintf (str, 5, "%.4u", year);
+      memcpy (dst, str, 4);
+    } else {
+      GST_WARNING ("invalid year %u, skipping", year);
+    }
+
+    g_date_free (date);
+  }
+}
+
+static void
+genre_v1_convert (const GstTagList * list, const gchar * tag,
+    guint8 * dst, int maxlen)
+{
+  gchar *str;
+  int genreidx = -1;
+  guint i, max;
+
+  /* We only support one genre */
+  if (!gst_tag_list_get_string_index (list, tag, 0, &str))
+    return;
+
+  max = gst_tag_id3_genre_count ();
+
+  for (i = 0; i < max; i++) {
+    const gchar *genre = gst_tag_id3_genre_get (i);
+    if (g_str_equal (str, genre)) {
+      genreidx = i;
+      break;
+    }
+  }
+
+  if (genreidx >= 0 && genreidx <= 127)
+    *dst = (guint8) genreidx;
+
+  g_free (str);
+}
+
+static void
+track_number_convert (const GstTagList * list, const gchar * tag,
+    guint8 * dst, int maxlen)
+{
+  guint tracknum;
+
+  /* We only support one track number */
+  if (!gst_tag_list_get_uint_index (list, tag, 0, &tracknum))
+    return;
+
+  if (tracknum <= 127)
+    *dst = (guint8) tracknum;
+}
+
+static const struct
+{
+  const gchar *gst_tag;
+  const gint offset;
+  const gint length;
+  const GstId3v1WriteFunc func;
+} v1_funcs[] = {
+  {
+  GST_TAG_TITLE, 3, 30, latin1_convert}, {
+  GST_TAG_ARTIST, 33, 30, latin1_convert}, {
+  GST_TAG_ALBUM, 63, 30, latin1_convert}, {
+  GST_TAG_DATE, 93, 4, date_v1_convert}, {
+  GST_TAG_COMMENT, 97, 28, latin1_convert}, {
+    /* Note: one-byte gap here */
+  GST_TAG_TRACK_NUMBER, 126, 1, track_number_convert}, {
+  GST_TAG_GENRE, 127, 1, genre_v1_convert}
+};
+
+GstBuffer *
+gst_id3mux_render_v1_tag (GstTagMux * mux, GstTagList * taglist)
+{
+  GstBuffer *buf = gst_buffer_new_and_alloc (ID3_V1_TAG_SIZE);
+  guint8 *data = GST_BUFFER_DATA (buf);
+  int i;
+
+  memset (data, 0, ID3_V1_TAG_SIZE);
+
+  data[0] = 'T';
+  data[1] = 'A';
+  data[2] = 'G';
+
+  for (i = 0; i < G_N_ELEMENTS (v1_funcs); i++) {
+    v1_funcs[i].func (taglist, v1_funcs[i].gst_tag, data + v1_funcs[i].offset,
+        v1_funcs[i].length);
+  }
+
+  gst_buffer_set_caps (buf, GST_PAD_CAPS (mux->srcpad));
+  return buf;
+}
diff --git a/gst/id3tag/id3tag.h b/gst/id3tag/id3tag.h
new file mode 100644
index 0000000..1fb5937
--- /dev/null
+++ b/gst/id3tag/id3tag.h
@@ -0,0 +1,32 @@
+/* GStreamer ID3v2 tag writer
+ * Copyright (C) 2009 Tim-Philipp Müller <tim centricular net>
+ *
+ * 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.
+ */
+
+#include "gsttagmux.h"
+
+G_BEGIN_DECLS
+
+#define ID3_VERSION_2_3 3
+#define ID3_VERSION_2_4 4
+
+GstBuffer * gst_id3mux_render_v2_tag (GstTagMux * mux, GstTagList * taglist,
+        int version);
+GstBuffer * gst_id3mux_render_v1_tag (GstTagMux * mux, GstTagList * taglist);
+
+G_END_DECLS
+
