diff --git a/.gitignore b/.gitignore
index 27c2603..9745fa4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ depcomp
 autoregen.sh
 ABOUT-NLS
 _stdint.h
+tests/examples/tagreading/gst-tagread
 
 gst-plugins-base-*.tar*
 
diff --git a/configure.ac b/configure.ac
index 600d2fb..41558c0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -328,6 +328,7 @@ AG_GST_CHECK_PLUGIN(gdp)
 AG_GST_CHECK_PLUGIN(playback)
 AG_GST_CHECK_PLUGIN(audioresample)
 AG_GST_CHECK_PLUGIN(subparse)
+AG_GST_CHECK_PLUGIN(tagreading)
 AG_GST_CHECK_PLUGIN(tcp)
 AG_GST_CHECK_PLUGIN(typefind)
 AG_GST_CHECK_PLUGIN(videotestsrc)
@@ -717,6 +718,7 @@ gst/gdp/Makefile
 gst/playback/Makefile
 gst/audioresample/Makefile
 gst/subparse/Makefile
+gst/tagreading/Makefile
 gst/tcp/Makefile
 gst/typefind/Makefile
 gst/videotestsrc/Makefile
@@ -793,6 +795,7 @@ tests/examples/dynamic/Makefile
 tests/examples/seek/Makefile
 tests/examples/volume/Makefile
 tests/examples/snapshot/Makefile
+tests/examples/tagreading/Makefile
 tests/icles/Makefile
 docs/Makefile
 docs/design/Makefile
diff --git a/docs/libs/gst-plugins-base-libs-docs.sgml b/docs/libs/gst-plugins-base-libs-docs.sgml
index 16db961..d608789 100644
--- a/docs/libs/gst-plugins-base-libs-docs.sgml
+++ b/docs/libs/gst-plugins-base-libs-docs.sgml
@@ -69,6 +69,7 @@
 <!ENTITY GstTagVorbis SYSTEM "xml/gsttagvorbis.xml">
 <!ENTITY GstTagID3 SYSTEM "xml/gsttagid3.xml">
 <!ENTITY GstTagDemux SYSTEM "xml/gsttagdemux.xml">
+<!ENTITY GstTagReader SYSTEM "xml/gsttagreader.xml">
 <!-- utils -->
 <!ENTITY GstPBUtils SYSTEM "xml/gstpbutils.xml">
 <!ENTITY GstPBUtilsDescriptions SYSTEM "xml/gstpbutilsdescriptions.xml">
@@ -265,6 +266,7 @@
       &GstTagVorbis;
       &GstTagID3;
       &GstTagDemux;
+      &GstTagReader;
     </chapter>
 
     <chapter id="gstreamer-base-utils">
diff --git a/docs/libs/gst-plugins-base-libs-sections.txt b/docs/libs/gst-plugins-base-libs-sections.txt
index 17ac6bf..51af691 100644
--- a/docs/libs/gst-plugins-base-libs-sections.txt
+++ b/docs/libs/gst-plugins-base-libs-sections.txt
@@ -1512,6 +1512,23 @@ GstTagDemuxClass
 GstTagDemuxResult
 </SECTION>
 
+<SECTION>
+<FILE>gsttagreader</FILE>
+<INCLUDE>gst/tag/gsttagreader.h</INCLUDE>
+GstTagReader
+GstTagReadingMode
+gst_tag_reader_add_interface_to_type
+gst_tag_reader_set_tag_reading_mode
+gst_tag_reader_get_tag_reading_mode
+<SUBSECTION Standard>
+gst_tag_reading_mode_get_type
+GST_TAG_READER
+GST_TYPE_TAG_READER
+GST_IS_TAG_READER
+GST_TAG_READER_GET_IFACE
+GstTagReaderInterface
+</SECTION>
+
 # base utils
 
 <SECTION>
diff --git a/docs/plugins/Makefile.am b/docs/plugins/Makefile.am
index 47bff89..f08eb69 100644
--- a/docs/plugins/Makefile.am
+++ b/docs/plugins/Makefile.am
@@ -107,6 +107,7 @@ EXTRA_HFILES = \
 	$(top_srcdir)/gst/gdp/gstgdppay.h \
 	$(top_srcdir)/gst/playback/gstplay-enum.h \
 	$(top_srcdir)/gst/audioresample/gstaudioresample.h \
+	$(top_srcdir)/gst/tagreading/gsttagreadbin.h \
 	$(top_srcdir)/gst/tcp/gstmultifdsink.h \
 	$(top_srcdir)/gst/tcp/gsttcpclientsrc.h \
 	$(top_srcdir)/gst/tcp/gsttcpclientsink.h \
diff --git a/docs/plugins/gst-plugins-base-plugins-docs.sgml b/docs/plugins/gst-plugins-base-plugins-docs.sgml
index 07f6a8d..8a30165 100644
--- a/docs/plugins/gst-plugins-base-plugins-docs.sgml
+++ b/docs/plugins/gst-plugins-base-plugins-docs.sgml
@@ -44,6 +44,7 @@
     <xi:include href="xml/element-oggmux.xml" />
     <xi:include href="xml/element-playbin.xml" />
     <xi:include href="xml/element-playbin2.xml" />
+    <xi:include href="xml/element-tagreadbin.xml" />
     <xi:include href="xml/element-tcpclientsrc.xml" />
     <xi:include href="xml/element-tcpclientsink.xml" />
     <xi:include href="xml/element-tcpserversrc.xml" />
@@ -90,6 +91,7 @@
     <xi:include href="xml/plugin-subparse.xml" />
     <xi:include href="xml/plugin-tcp.xml" />
     <xi:include href="xml/plugin-theora.xml" />
+    <xi:include href="xml/plugin-tagreading.xml" />
     <xi:include href="xml/plugin-typefindfunctions.xml" />
     <xi:include href="xml/plugin-uridecodebin.xml" />
     <xi:include href="xml/plugin-video4linux.xml" />
diff --git a/docs/plugins/gst-plugins-base-plugins-sections.txt b/docs/plugins/gst-plugins-base-plugins-sections.txt
index 7d440a2..f4a1aa5 100644
--- a/docs/plugins/gst-plugins-base-plugins-sections.txt
+++ b/docs/plugins/gst-plugins-base-plugins-sections.txt
@@ -455,6 +455,23 @@ GST_TYPE_PLAY_FLAGS
 </SECTION>
 
 <SECTION>
+<FILE>element-tagreadbin</FILE>
+<TITLE>tagreadbin</TITLE>
+GstTagReadBin
+<SUBSECTION Standard>
+GST_TAG_READ_BIN
+GST_TAG_READ_BIN_CLASS
+GST_TYPE_TAG_READ_BIN
+GST_IS_TAG_READ_BIN
+GST_IS_TAG_READ_BIN_CLASS
+GST_TAG_READ_BIN_DYN_LOCK
+GST_TAG_READ_BIN_DYN_UNLOCK
+GST_TAG_READ_BIN_SHUTDOWN_LOCK
+GST_TAG_READ_BIN_SHUTDOWN_UNLOCK
+GstTagReadBinClass
+</SECTION>
+
+<SECTION>
 <FILE>element-tcpclientsrc</FILE>
 <TITLE>tcpclientsrc</TITLE>
 GstTCPClientSrc
diff --git a/docs/plugins/inspect/plugin-tagreading.xml b/docs/plugins/inspect/plugin-tagreading.xml
new file mode 100644
index 0000000..bb8be1d
--- /dev/null
+++ b/docs/plugins/inspect/plugin-tagreading.xml
@@ -0,0 +1,23 @@
+<plugin>
+  <name>tagreading</name>
+  <description>Tag reading support</description>
+  <filename>../../gst/tagreading/.libs/libgsttagreading.so</filename>
+  <basename>libgsttagreading.so</basename>
+  <version>0.10.22.3</version>
+  <license>LGPL</license>
+  <source>gst-plugins-base</source>
+  <package>GStreamer Base Plug-ins CVS/prerelease</package>
+  <origin>Unknown package origin</origin>
+  <elements>
+    <element>
+      <name>tagreadbin</name>
+      <longname>Tag Read Bin</longname>
+      <class>Generic/Bin/Metadata</class>
+      <description>Extract metadata from an uri</description>
+      <author>Tim-Philipp Müller &lt;tim centricular net&gt;</author>
+      <pads>
+    
+      </pads>
+    </element>
+  </elements>
+</plugin>
\ No newline at end of file
diff --git a/gst-libs/gst/tag/Makefile.am b/gst-libs/gst/tag/Makefile.am
index 3b264a9..06ddf98 100644
--- a/gst-libs/gst/tag/Makefile.am
+++ b/gst-libs/gst/tag/Makefile.am
@@ -2,11 +2,11 @@ libgsttagincludedir = \
 	$(includedir)/gstreamer-@GST_MAJORMINOR@/gst/tag
 
 libgsttaginclude_HEADERS = \
-	tag.h gsttagdemux.h
+	tag.h gsttagdemux.h gsttagreader.h
 
 lib_LTLIBRARIES = libgsttag-@GST_MAJORMINOR@.la
 
-libgsttag_@GST_MAJORMINOR@_la_SOURCES = gstvorbistag.c gstid3tag.c tags.c gsttagdemux.c
+libgsttag_@GST_MAJORMINOR@_la_SOURCES = gstvorbistag.c gstid3tag.c tags.c gsttagdemux.c gsttagreader.c
 libgsttag_@GST_MAJORMINOR@_la_CFLAGS = $(GST_PLUGINS_BASE_CFLAGS) $(GST_BASE_CFLAGS) $(GST_CFLAGS)
 libgsttag_@GST_MAJORMINOR@_la_LIBADD = $(GST_BASE_LIBS) $(GST_LIBS) 
 libgsttag_@GST_MAJORMINOR@_la_LDFLAGS = $(GST_LIB_LDFLAGS) $(GST_ALL_LDFLAGS) $(GST_LT_LDFLAGS)
diff --git a/gst-libs/gst/tag/gsttagreader.c b/gst-libs/gst/tag/gsttagreader.c
new file mode 100644
index 0000000..cfbd480
--- /dev/null
+++ b/gst-libs/gst/tag/gsttagreader.c
@@ -0,0 +1,286 @@
+/* GStreamer Tag Reader interface
+ * Copyright (C) 2009 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2008 Nokia Corporation.
+ *
+ * Contact: Stefan Kost <stefan.kost@nokia.com>
+ *
+ * tagreader.c: Tag Reader interface design
+ *
+ * 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:gsttagreader
+ * @short_description: Interface for setting a tag reading mode on elements
+ * supporting it
+ *
+ * <refsect2>
+ * <para>
+ * The #GstTagReader interface may be implemented by elements that extract
+ * metadata to allow the fine-tuning of the tag-reading process. The interface
+ * is not required by all elements that extract tags - it is perfectly
+ * acceptable to post tag messages on the bus and not implement this
+ * interface. The interface merely serves to indicate that an element supports
+ * one or more special tag reading modes which when set might result in
+ * more efficient tag processing in certain cases.
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>Use cases</title>
+ * <para>
+ * There are currently two main use cases for this interface:
+ * <itemizedlist>
+ * <listitem>
+ * by-passing possibly time- or memory-consuming tag extraction: useful
+ * for example if the goal is to just play a file, but things like embedded
+ * coverart or tags are not of interest.
+ * </listitem>
+ * <listitem>
+ * optimised tag and stream info extraction: useful if all that's of interest
+ * is tags and other metadata and full-scale playback functionality is not
+ * required; this allows demuxers to bypass possibly expensive index processing
+ * or index building, for example.
+ * </listitem>
+ * </itemizedlist>
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>How to use #GstTagReader in applications</title>
+ * <para>
+ * Application writers who want to avoid the (usually very small) overhead of
+ * tag extraction when playing back media can put uridecodebin or decodebin2
+ * (or the elements in their pipeline directly, if they implement the
+ * interface) into tag reading mode #GST_TAG_READING_MODE_SKIP_TAGS using
+ * gst_tag_reader_set_tag_reading_mode() before starting the pipeline.
+ * </para>
+ * <para>
+ * Application writers who want to extract tags and other metadata as fast and
+ * efficiently as possible should use the 'tagreadbin' element, which will
+ * handle everything for them, and which will then use the #GstTagReader
+ * interface internally.
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>How to implement the #GstTagReader in elements</title>
+ * <para>
+ * #GstTagReader is a 'dummy interface' similar to #GstTagSetter, meaning that
+ * there are no interface methods to implement. All that needs to be done is
+ * to add the interface to an element's GType in the element's _get_type()
+ * function or GST_BOILERPLATE_FULL macro. This can be done using the
+ * convenience function gst_tag_reader_add_interface_to_type().
+ * </para>
+ * <para>
+ * Elements implementing the interface should then query the configured tag
+ * reading mode when starting up (ie. in the start function or in the
+ * state change function in the NULL to READY transition), using
+ * gst_tag_reader_get_tag_reading_mode().
+ * </para>
+ * <para>
+ * In #GST_TAG_READING_MODE_SKIP_TAGS mode elements should skip metadata
+ * processing wherever this is possible without affecting functionality
+ * otherwise. Element should not post TAG messages on the bus in this mode.
+ * </para>
+ * <para>
+ * In #GST_TAG_READING_MODE_DEFAULT mode elements should behave like they
+ * normally would. For most elements this means tag extraction and normal
+ * stream handling.
+ * </para>
+ * <para>
+ * In #GST_TAG_READING_MODE_TAGS_ONLY mode elements are requested to skip
+ * any non-essential data processing. Demuxers, for example, may choose not
+ * to parse a seek index or build a seek table, but should still parse
+ * file headers and add pads and push newsegment events and data as they
+ * usually would (however, they are not required to reproduce output in the
+ * way they would for normal playback, e.g. they may push only key frames,
+ * which is all that is required for thumbnailing purposes).
+ * </para>
+ * <para>
+ * Elements in #GST_TAG_READING_MODE_TAGS_ONLY are required to properly
+ * aggregate or pass upstream flow returns. If they are driving the pipeline
+ * they will also need to EOS on #GST_FLOW_UNEXPECTED flow returns from
+ * downstream. This is how the <literal>tagreadbin</literal> element will
+ * stop streaming once it has collected all information.
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>Tag-handling guidelines for all demuxers and parsers</title>
+ * <para>
+ * Below some general guidelines on how demuxers, parsers and decoders should
+ * handle tags.
+ * </para>
+ * <para>
+ * Demuxers should push tag events for 'global' tag before pushing tags
+ * with per-stream information. This global tag list should contain a
+ * #GST_TAG_CONTAINER_FORMAT tag. They also should push global tags and
+ * per-stream tags separately.
+ * </para>
+ * <para>
+ * Parsers and decoders should ensure that they cache tag events received from
+ * upstream and push them on their source pad before any tags of their own
+ * are pushed, maintaining the order of the tag events as they came in.
+ * </para>
+ * <para>
+ * Since 0.10.24
+ * </para>
+ * </refsect2>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gsttagreader.h"
+
+/* key for g_object_{set|get}_qdata() */
+static GQuark tag_reading_mode_quark = 0;
+
+#define GST_TAG_READER_GET_MODE(obj) \
+    GPOINTER_TO_INT (g_object_get_qdata (obj, tag_reading_mode_quark))
+#define GST_TAG_READER_SET_MODE(obj,mode) \
+    g_object_set_qdata (obj, tag_reading_mode_quark, GINT_TO_POINTER (mode))
+
+GType
+gst_tag_reader_get_type (void)
+{
+  static GType gst_tag_reader_type = 0;
+
+  if (!gst_tag_reader_type) {
+    static const GTypeInfo gst_tag_reader_info = {
+      sizeof (GstTagReaderInterface),
+      (GBaseInitFunc) NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      0,
+      0,
+      NULL,
+    };
+
+    gst_tag_reader_type = g_type_register_static (G_TYPE_INTERFACE,
+        "GstTagReader", &gst_tag_reader_info, 0);
+
+    tag_reading_mode_quark = g_quark_from_static_string ("GstTagReadingMode");
+  }
+
+  return gst_tag_reader_type;
+}
+
+/**
+ * gst_tag_reader_add_interface_to_type:
+ * @type: a newly-registered GType
+ *
+ * Convenience function to add the #GstTagReader interface to an element.
+ * Use from within the element's _get_type() function or in connection
+ * with GST_BOILERPLATE_FULL.
+ *
+ * Since: 0.10.24
+ */
+void
+gst_tag_reader_add_interface_to_type (GType type)
+{
+  static const GInterfaceInfo tr_iface_info = { NULL, NULL, NULL, };
+
+  g_type_add_interface_static (type, GST_TYPE_TAG_READER, &tr_iface_info);
+}
+
+/**
+ * gst_tag_reader_set_tag_reading_mode:
+ * @reader: a #GstTagReader to set the tag reading mode on.
+ * @mode: tag reading mode to operate in.
+ *
+ * This will set the tag reader's mode of operation. You should
+ * use this method to tell a tag reader capable element in which
+ * #GstTagReadingMode the reader should operate in.
+ *
+ * This function will take the object lock and must not be called with the
+ * object lock already held.
+ *
+ * Since: 0.10.24
+ */
+void
+gst_tag_reader_set_tag_reading_mode (GstTagReader * reader,
+    GstTagReadingMode mode)
+{
+  GObject *obj;
+
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (GST_IS_TAG_READER (reader));
+  g_return_if_fail (GST_IS_ELEMENT (reader));
+
+  obj = G_OBJECT (reader);
+
+  GST_OBJECT_LOCK (obj);
+  if (GST_STATE (obj) == GST_STATE_NULL) {
+    GST_TAG_READER_SET_MODE (obj, mode);
+    GST_LOG_OBJECT (obj, "configured mode: %d", mode);
+  } else if (GST_TAG_READER_GET_MODE (obj) != mode) {
+    g_warning ("Can't set tag reading mode on element '%s': element must be "
+        "in NULL state", GST_ELEMENT_NAME (obj));
+  }
+  GST_OBJECT_UNLOCK (obj);
+}
+
+/**
+ * gst_tag_reader_get_tag_reading_mode:
+ * @reader: a #GstTagReader to get the tag reading mode from.
+ *
+ * Query the currently configured #GstTagReadingMode.
+ *
+ * This function will take the object lock and must not be called with the
+ * object lock already held.
+ *
+ * Returns: configured tag reading mode.
+ *
+ * Since: 0.10.24
+ */
+GstTagReadingMode
+gst_tag_reader_get_tag_reading_mode (GstTagReader * reader)
+{
+  GstTagReadingMode mode;
+  GObject *obj;
+
+  g_return_val_if_fail (reader != NULL, GST_TAG_READING_MODE_DEFAULT);
+  g_return_val_if_fail (GST_IS_TAG_READER (reader),
+      GST_TAG_READING_MODE_DEFAULT);
+  g_return_val_if_fail (GST_IS_ELEMENT (reader), GST_TAG_READING_MODE_DEFAULT);
+
+  obj = G_OBJECT (reader);
+
+  GST_OBJECT_LOCK (obj);
+  mode = GST_TAG_READER_GET_MODE (obj);
+  GST_OBJECT_UNLOCK (obj);
+
+  return mode;
+}
+
+GType
+gst_tag_reading_mode_get_type (void)
+{
+  static GType etype = 0;
+  if (etype == 0) {
+    static const GEnumValue values[] = {
+      {GST_TAG_READING_MODE_DEFAULT, "GST_TAG_READING_MODE_DEFAULT", "default"},
+      {GST_TAG_READING_MODE_TAGS_ONLY, "GST_TAG_READING_MODE_TAGS_ONLY",
+          "tags-only"},
+      {GST_TAG_READING_MODE_SKIP_TAGS, "GST_TAG_READING_MODE_SKIP_TAGS",
+          "skip-tags"},
+      {0, NULL, NULL}
+    };
+    etype = g_enum_register_static ("GstTagReadingMode", values);
+  }
+  return etype;
+}
diff --git a/gst-libs/gst/tag/gsttagreader.h b/gst-libs/gst/tag/gsttagreader.h
new file mode 100644
index 0000000..5c39f85
--- /dev/null
+++ b/gst-libs/gst/tag/gsttagreader.h
@@ -0,0 +1,110 @@
+/* GStreamer Tag Reader interface
+ * Copyright (C) 2009 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2008 Nokia Corporation.
+ *
+ * Contact: Stefan Kost <stefan.kost@nokia.com>
+ *
+ * tagreader.h: Tag Reader interface design
+ *
+ * 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_READER_H__
+#define __GST_TAG_READER_H__
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GstTagReadingMode:
+ * @GST_TAG_READING_MODE_DEFAULT: parse both metadata and audio/video stream
+ *     data
+ * @GST_TAG_READING_MODE_TAGS_ONLY: parse metadata and process only as much
+ *     audio/video data as required to extract basic information about the
+ *     stream such as tags, available streams and duration
+ * @GST_TAG_READING_MODE_SKIP_TAGS: don't extract any metadata, just process
+ *     the audio/video stream data (behaviour should generally be the same as
+ *     with #GST_TAG_READING_MODE_DEFAULT, only that tag extraction is skipped)
+ *
+ * Available tag reading modes.
+ *
+ * Since: 0.10.24
+ */
+/* keep gst_tag_reading_mode_get_type() in sync when adding enums here */
+typedef enum {
+  GST_TAG_READING_MODE_DEFAULT = 0,
+  GST_TAG_READING_MODE_TAGS_ONLY,
+  GST_TAG_READING_MODE_SKIP_TAGS
+} GstTagReadingMode;
+
+#define GST_TYPE_TAG_READING_MODE  (gst_tag_reading_mode_get_type())
+GType  gst_tag_reading_mode_get_type (void);
+
+
+#define GST_TYPE_TAG_READER \
+  (gst_tag_reader_get_type ())
+#define GST_TAG_READER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_TAG_READER, GstTagReader))
+#define GST_IS_TAG_READER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_TAG_READER))
+#define GST_TAG_READER_GET_IFACE(inst) \
+  (G_TYPE_INSTANCE_GET_INTERFACE ((inst), GST_TYPE_TAG_READER, \
+      GstTagReaderInterface))
+
+/**
+ * GstTagReader:
+ *
+ * Opaque #GstTagReader structure (dummy typedef to denote object instances
+ * which implement the #GstTagReader interface).
+ *
+ * Since: 0.10.24
+ */
+typedef struct _GstTagReader GstTagReader;
+typedef struct _GstTagReaderInterface GstTagReaderInterface;
+
+/**
+ * GstTagReaderInterface:
+ *
+ * Dummy interface structure. Elements do not need to implement any interface
+ * methods, all they need to do is query the desired tag reading mode via
+ * gst_tag_reader_get_tag_reading_mode() where and when appropriate/required.
+ *
+ * Since: 0.10.24
+ */
+struct _GstTagReaderInterface {
+  GTypeInterface iface;
+
+  /* methods: no methods to implement at this time */
+
+  /* interfaces don't need padding, so no padding here */
+};
+
+GType              gst_tag_reader_get_type (void);
+
+void               gst_tag_reader_add_interface_to_type (GType type);
+
+/* public methods */
+
+void               gst_tag_reader_set_tag_reading_mode (GstTagReader * reader,
+                                                        GstTagReadingMode mode);
+
+GstTagReadingMode  gst_tag_reader_get_tag_reading_mode (GstTagReader * reader);
+
+G_END_DECLS
+
+#endif /* __GST_TAG_READER_H__ */
+
diff --git a/gst/playback/Makefile.am b/gst/playback/Makefile.am
index f4e4b6a..d65f43b 100644
--- a/gst/playback/Makefile.am
+++ b/gst/playback/Makefile.am
@@ -43,6 +43,7 @@ nodist_libgstdecodebin2_la_SOURCES = $(built_sources)
 libgstdecodebin2_la_CFLAGS = $(GST_PLUGINS_BASE_CFLAGS) $(GST_CFLAGS)
 libgstdecodebin2_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS)
 libgstdecodebin2_la_LIBADD = \
+	$(top_builddir)/gst-libs/gst/tag/libgsttag-@GST_MAJORMINOR@.la \
 	$(top_builddir)/gst-libs/gst/pbutils/libgstpbutils-@GST_MAJORMINOR@.la \
 	$(GST_LIBS)
 libgstdecodebin2_la_LIBTOOLFLAGS = --tag=disable-static
diff --git a/gst/playback/gstdecodebin2.c b/gst/playback/gstdecodebin2.c
index a9f2475..1799bee 100644
--- a/gst/playback/gstdecodebin2.c
+++ b/gst/playback/gstdecodebin2.c
@@ -40,6 +40,7 @@
 #include <string.h>
 #include <gst/gst.h>
 #include <gst/pbutils/pbutils.h>
+#include <gst/tag/gsttagreader.h>
 
 #include "gstplay-marshal.h"
 #include "gstplay-enum.h"
@@ -109,6 +110,8 @@ struct _GstDecodeBin
   GMutex *dyn_lock;             /* lock protecting pad blocking */
   gboolean shutdown;            /* if we are shutting down */
   GList *blocked_pads;          /* pads that have set to block */
+
+  GstTagReadingMode tag_mode;
 };
 
 struct _GstDecodeBinClass
@@ -346,6 +349,8 @@ gst_decode_bin_get_type (void)
     gst_decode_bin_type =
         g_type_register_static (GST_TYPE_BIN, "GstDecodeBin2",
         &gst_decode_bin_info, 0);
+
+    gst_tag_reader_add_interface_to_type (gst_decode_bin_type);
   }
 
   return gst_decode_bin_type;
@@ -1200,6 +1205,15 @@ connect_pad (GstDecodeBin * dbin, GstElement * src, GstDecodePad * dpad,
       continue;
     }
 
+    /* if it's a tag reader, configure the tag reading mode */
+    if (dbin->tag_mode != GST_TAG_READING_MODE_DEFAULT &&
+        GST_IS_TAG_READER (element)) {
+      GST_DEBUG_OBJECT (dbin, "setting tag reading mode %d on element %s",
+          dbin->tag_mode, GST_ELEMENT_NAME (element));
+      gst_tag_reader_set_tag_reading_mode (GST_TAG_READER (element),
+          dbin->tag_mode);
+    }
+
     /* ... activate it ... We do this before adding it to the bin so that we
      * don't accidentally make it post error messages that will stop
      * everything. */
@@ -2633,6 +2647,9 @@ gst_decode_bin_change_state (GstElement * element, GstStateChange transition)
 
   switch (transition) {
     case GST_STATE_CHANGE_NULL_TO_READY:
+      dbin->tag_mode =
+          gst_tag_reader_get_tag_reading_mode (GST_TAG_READER (dbin));
+      GST_LOG_OBJECT (dbin, "tag reading mode: %d", dbin->tag_mode);
       if (dbin->typefind == NULL)
         goto missing_typefind;
       break;
diff --git a/gst/playback/gsturidecodebin.c b/gst/playback/gsturidecodebin.c
index 550de18..e6f5d5c 100644
--- a/gst/playback/gsturidecodebin.c
+++ b/gst/playback/gsturidecodebin.c
@@ -31,6 +31,7 @@
 
 #include <gst/gst.h>
 #include <gst/gst-i18n-plugin.h>
+#include <gst/tag/gsttagreader.h>
 
 #include "gstfactorylists.h"
 #include "gstplay-marshal.h"
@@ -90,6 +91,8 @@ struct _GstURIDecodeBin
   gint pending;
 
   gboolean async_pending;       /* async-start has been emited */
+
+  GstTagReadingMode tag_mode;
 };
 
 struct _GstURIDecodeBinClass
@@ -162,7 +165,8 @@ enum
 
 static guint gst_uri_decode_bin_signals[LAST_SIGNAL] = { 0 };
 
-GST_BOILERPLATE (GstURIDecodeBin, gst_uri_decode_bin, GstBin, GST_TYPE_BIN);
+GST_BOILERPLATE_FULL (GstURIDecodeBin, gst_uri_decode_bin, GstBin,
+    GST_TYPE_BIN, gst_tag_reader_add_interface_to_type);
 
 static void gst_uri_decode_bin_set_property (GObject * object, guint prop_id,
     const GValue * value, GParamSpec * pspec);
@@ -1123,6 +1127,10 @@ make_decoder (GstURIDecodeBin * decoder)
   if (!decodebin)
     goto no_decodebin;
 
+  /* configure tag reading mode */
+  gst_tag_reader_set_tag_reading_mode (GST_TAG_READER (decodebin),
+      decoder->tag_mode);
+
   /* connect signals to proxy */
   g_signal_connect (G_OBJECT (decodebin), "unknown-type",
       G_CALLBACK (proxy_unknown_type_signal), decoder);
@@ -1847,6 +1855,9 @@ gst_uri_decode_bin_change_state (GstElement * element,
     case GST_STATE_CHANGE_READY_TO_PAUSED:
       if (!setup_source (decoder))
         goto source_failed;
+      decoder->tag_mode =
+          gst_tag_reader_get_tag_reading_mode (GST_TAG_READER (decoder));
+      GST_LOG_OBJECT (decoder, "tag reading mode: %d", decoder->tag_mode);
       break;
     default:
       break;
diff --git a/gst/tagreading/Makefile.am b/gst/tagreading/Makefile.am
new file mode 100644
index 0000000..0a0d32d
--- /dev/null
+++ b/gst/tagreading/Makefile.am
@@ -0,0 +1,28 @@
+plugindir = $(libdir)/gstreamer-@GST_MAJORMINOR@
+
+plugin_LTLIBRARIES = libgsttagreading.la
+
+libgsttagreading_la_SOURCES = \
+	gsttagreadbin.c \
+	gsttagreadbin.h \
+	gsttagreadutils.c \
+	gsttagreadutils.h \
+	gsttagsink.c \
+	gsttagsink.h
+libgsttagreading_la_CFLAGS = \
+	$(GST_PLUGINS_BASE_CFLAGS) \
+	$(GST_BASE_CFLAGS) \
+	$(GST_CFLAGS)
+libgsttagreading_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS)
+libgsttagreading_la_LIBADD = \
+	$(top_builddir)/gst-libs/gst/tag/libgsttag-@GST_MAJORMINOR@.la \
+	$(top_builddir)/gst-libs/gst/pbutils/libgstpbutils-@GST_MAJORMINOR@.la \
+	$(GST_BASE_LIBS) \
+	$(GST_LIBS)
+libgsttagreading_la_LIBTOOLFLAGS = --tag=disable-static
+
+noinst_HEADERS = \
+	gsttagreadbin.h \
+	gsttagreadutils.h \
+	gsttagsink.h
+
diff --git a/gst/tagreading/gsttagreadbin.c b/gst/tagreading/gsttagreadbin.c
new file mode 100644
index 0000000..77b9c25
--- /dev/null
+++ b/gst/tagreading/gsttagreadbin.c
@@ -0,0 +1,892 @@
+/* GStreamer tag read bin
+ * 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.
+ */
+
+/**
+ * SECTION:element-tagreadbin
+ * @short_description: Extract metadata (tags and stream info)
+ *
+ * <refsect2>
+ * <para>
+ * Tagreadbin is a stand-alone element that extracts metadata (tags and
+ * other stream info such as video resolution, audio sample rate etc.) from
+ * files given an URI.
+ * </para>
+ * <para>
+ * <note>
+ * This element is still considered EXPERIMENTAL. Even though we don't expect
+ * any changes, the API provided in properties, signals and bus messages may
+ * yet change in the near future.
+ * </note>
+ * </para>
+ * <para>
+ * Tagreadbin will open the file requested and automatically plug the elements
+ * required to extract the metadata. Only demuxers, parsers or specialised tag
+ * reading elements (implementing the #GstTagReader interface) will be used. No
+ * decoders will be plugged, unless video thumbnailing has been requested
+ * (which is not implemented yet).
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>Basic usage</title>
+ * <para>
+ * Tagreadbin is a #GstPipeline, so it can be used like any other GStreamer
+ * pipeline. In particular, you can retrieve the #GstBus to watch for tag
+ * and error messages using gst_pipeline_get_bus(). Once the
+ * <literal>uri</literal> property has been set, the element should be
+ * set to PLAYING state using gst_element_set_state(). TAG and ERROR messages
+ * will then be posted on the pipeline's #GstBus soon after. If there was no
+ * error, one or more TAG messages will be posted on the bus, followed by an
+ * EOS message. Once an ERROR or EOS message has been received, the element
+ * should be shut down by setting it to NULL state with gst_element_set_state().
+ * It can then be re-used to extract metadata from another file.
+ * </para>
+ * </refsect2>
+ * <refsect2>
+ * <title>Tag messages</title>
+ * <para>
+ * Tagreadbin will collect tags from various elements and try to group them.
+ * The first tag message posted on the bus should contain the 'global tags',
+ * ie. tags that are valid for all sub-streams contained in the file. At the
+ * very least, this taglist should contain information about the container
+ * format in a #GST_TAG_CONTAINER_FORMAT tag (where applicable, otherwise it
+ * will contain a translated string saying 'Unknown'). If there is a
+ * #GST_TAG_DURATION tag it will contain the maximum value of all the stream
+ * durations (as queried).
+ * </para>
+ * <para>
+ * After the tag message with the global tags there will be one tag message
+ * for each audio/video/subtitle stream, containing the information for that
+ * particular stream. At the very least, there should be a #GST_TAG_AUDIO_CODEC,
+ * #GST_TAG_VIDEO_CODEC, #GST_TAG_SUBITLE_CODEC, or #GST_TAG_CODEC tag. There
+ * may also be a #GST_TAG_DURATION tag with the stream's duration.
+ * </para>
+ * <para>
+ * For audio streams, there will be "channel" and "rate" tags (of type int) in
+ * the taglist, provided that information could be extracted.
+ * </para>
+ * <para>
+ * For video streams, there will be "width", "height", "pixel-aspect-ratio"
+ * and "framerate" fields (of type int, int, fraction and fraction respectively)
+ * if that information was available.
+ * </para>
+ * <para>
+ * Just like playbin, this element does not expose any pads.
+ * </para>
+ * <para>
+ * Since 0.10.24
+ * </para>
+ * </refsect2>
+ */
+
+/* TODO:
+ *  - thumbnailing
+ *
+ *  - wait with preroll until duration stabilises (e.g. for mp3/aac/mpeg)
+ *
+ *  - missing plugin messages
+ *
+ *  - Fix parser/decoder elements so that global tags / stream tags
+ *    differentiation actually works right, ie. make sure they send tags
+ *    received from upstream before pushing their own tags (we'll only consider
+ *    taglists received before the first taglist with a *_CODEC tag suitable
+ *    for global tags)
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gsttagreadbin.h"
+#include "gsttagreadutils.h"
+#include "gsttagsink.h"
+
+#include <gst/pbutils/pbutils.h>
+#include <gst/tag/gsttagreader.h>
+
+#include <string.h>
+
+GST_DEBUG_CATEGORY (tagreadbin_debug);
+#define GST_CAT_DEFAULT tagreadbin_debug
+
+/* from gst/playback/gstplay-enum.h */
+typedef enum
+{
+  GST_AUTOPLUG_SELECT_TRY,
+  GST_AUTOPLUG_SELECT_EXPOSE,
+  GST_AUTOPLUG_SELECT_SKIP
+} GstAutoplugSelectResult;
+
+/* props */
+#define DEFAULT_URI               NULL
+
+enum
+{
+  PROP_0,
+  PROP_URI
+};
+
+static void gst_tag_read_bin_dispose (GObject * object);
+static void gst_tag_read_bin_finalize (GObject * object);
+static void gst_tag_read_bin_set_property (GObject * object, guint prop_id,
+    const GValue * value, GParamSpec * spec);
+static void gst_tag_read_bin_get_property (GObject * object, guint prop_id,
+    GValue * value, GParamSpec * spec);
+
+static GstStateChangeReturn gst_tag_read_bin_change_state (GstElement * element,
+    GstStateChange transition);
+static void gst_tag_read_bin_handle_message (GstBin * bin, GstMessage * msg);
+
+GST_BOILERPLATE (GstTagReadBin, gst_tag_read_bin, GstPipeline,
+    GST_TYPE_PIPELINE);
+
+static void
+gst_tag_read_bin_base_init (gpointer g_klass)
+{
+  /* nothing to do */
+}
+
+static void
+gst_tag_read_bin_class_init (GstTagReadBinClass * klass)
+{
+  GstElementClass *gstelement_klass = (GstElementClass *) klass;
+  GObjectClass *gobject_klass = (GObjectClass *) klass;
+  GstBinClass *gstbin_klass = (GstBinClass *) klass;
+
+  gst_element_class_set_details_simple (gstelement_klass,
+      "Tag Read Bin", "Generic/Bin/Metadata", "Extract metadata from an uri",
+      "Tim-Philipp Müller <tim centricular net>");
+
+  gobject_klass->set_property = gst_tag_read_bin_set_property;
+  gobject_klass->get_property = gst_tag_read_bin_get_property;
+
+  gobject_klass->dispose = GST_DEBUG_FUNCPTR (gst_tag_read_bin_dispose);
+  gobject_klass->finalize = GST_DEBUG_FUNCPTR (gst_tag_read_bin_finalize);
+
+  /**
+   * GstTagReadBin:uri
+   *
+   * URI to extract metadata from. Only file:// URIs are supported at the
+   * moment. To create an URI from a filename, you can use g_filename_to_uri().
+   */
+  g_object_class_install_property (gobject_klass, PROP_URI,
+      g_param_spec_string ("uri", "URI", "URI to extract tags from",
+          NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gstelement_klass->change_state =
+      GST_DEBUG_FUNCPTR (gst_tag_read_bin_change_state);
+
+  gstbin_klass->handle_message =
+      GST_DEBUG_FUNCPTR (gst_tag_read_bin_handle_message);
+}
+
+static gboolean
+gst_tag_read_bin_factory_is_demuxer (GstElementFactory * factory)
+{
+  const gchar *klass = gst_element_factory_get_klass (factory);
+
+  return (klass != NULL && strstr (klass, "Demux") != NULL);
+}
+
+static gboolean
+gst_tag_read_bin_factory_is_parser (GstElementFactory * factory)
+{
+  const gchar *klass = gst_element_factory_get_klass (factory);
+
+  return (klass != NULL && strstr (klass, "Parse") != NULL);
+}
+
+static gboolean
+gst_tag_read_bin_factory_is_metadata_extractor (GstElementFactory * factory)
+{
+  const gchar *klass = gst_element_factory_get_klass (factory);
+
+  return (klass && strstr (klass, "Metadata") && strstr (klass, "Extracter"));
+}
+
+static gboolean
+gst_tag_read_bin_factory_is_tag_reader (GstElementFactory * factory)
+{
+  guint rank;
+  const gchar *klass;
+
+  klass = gst_element_factory_get_klass (factory);
+
+  /* only demuxers, parsers or metadata extractor elements can play
+   * (note: in parcticular, we don't want to plug decodebin2 even though
+   * it does implement the tag reader interface as well) */
+  if (!gst_tag_read_bin_factory_is_demuxer (factory) &&
+      !gst_tag_read_bin_factory_is_parser (factory) &&
+      !gst_tag_read_bin_factory_is_metadata_extractor (factory)) {
+    return FALSE;
+  }
+
+  /* tag readers are fine whatever their rank */
+  if (gst_element_factory_has_interface (factory, "GstTagReader")) {
+    GST_LOG ("selecting %s, it implements the tag reader interface",
+        GST_PLUGIN_FEATURE_NAME (factory));
+    return TRUE;
+  }
+
+  /* only select elements with autoplugging rank */
+  rank = gst_plugin_feature_get_rank (GST_PLUGIN_FEATURE (factory));
+  if (rank < GST_RANK_MARGINAL)
+    return FALSE;
+
+  GST_LOG ("selecting %s, class/rank ok", GST_PLUGIN_FEATURE_NAME (factory));
+  return TRUE;
+}
+
+/* function used to sort element features. We first sort on the tag reader
+ * interface, then rank, then on the element name (to get a consistent,
+ * predictable list) */
+static gint
+gst_tag_read_bin_compare_ranks (GValue * v1, GValue * v2)
+{
+  const gchar *rname1, *rname2;
+  GstPluginFeature *f1, *f2;
+  gboolean is_tagreader1, is_tagreader2;
+  gint diff;
+
+  f1 = g_value_get_object (v1);
+  f2 = g_value_get_object (v2);
+
+  is_tagreader1 = gst_element_factory_has_interface (GST_ELEMENT_FACTORY (f1),
+      "GstTagReader");
+  is_tagreader2 = gst_element_factory_has_interface (GST_ELEMENT_FACTORY (f2),
+      "GstTagReader");
+
+  if (is_tagreader1 != is_tagreader2)
+    return (is_tagreader2) ? 1 : -1;
+
+  diff = gst_plugin_feature_get_rank (f2) - gst_plugin_feature_get_rank (f1);
+  if (diff != 0)
+    return diff;
+
+  rname1 = gst_plugin_feature_get_name (f1);
+  rname2 = gst_plugin_feature_get_name (f2);
+
+  diff = strcmp (rname2, rname1);
+
+  return diff;
+}
+
+static GValueArray *
+gst_tag_read_bin_get_tag_readers (void)
+{
+  GValueArray *result;
+  GList *l, *list;
+
+  result = g_value_array_new (0);
+
+  /* get the feature list using the filter */
+  list = gst_registry_get_feature_list (gst_registry_get_default (),
+      GST_TYPE_ELEMENT_FACTORY);
+
+  /* filter and convert to an array */
+  for (l = list; l != NULL; l = l->next) {
+    GstElementFactory *factory = GST_ELEMENT_FACTORY (l->data);
+
+    if (gst_tag_read_bin_factory_is_tag_reader (factory)) {
+      GValue val = { 0, };
+
+      g_value_init (&val, GST_TYPE_ELEMENT_FACTORY);
+      g_value_set_object (&val, factory);
+      g_value_array_append (result, &val);
+      g_value_unset (&val);
+    }
+  }
+
+  gst_plugin_feature_list_free (list);
+
+  /* sort on tag reader interface rank and name */
+  g_value_array_sort (result, (GCompareFunc) gst_tag_read_bin_compare_ranks);
+
+  return result;
+}
+
+static gboolean
+gst_tag_read_bin_factory_list_has_demuxer (GValueArray * factories)
+{
+  guint i;
+
+  for (i = 0; i < factories->n_values; ++i) {
+    GstElementFactory *factory;
+    GValue *val;
+
+    val = g_value_array_get_nth (factories, i);
+    factory = GST_ELEMENT_FACTORY (g_value_get_object (val));
+
+    if (gst_tag_read_bin_factory_is_demuxer (factory))
+      return TRUE;
+  }
+
+  return FALSE;
+}
+
+static void
+gst_tag_read_bin_init (GstTagReadBin * tbin, GstTagReadBinClass * klass)
+{
+  tbin->dyn_lock = g_mutex_new ();
+
+  /* cache interesting element factories */
+  tbin->elements = gst_tag_read_bin_get_tag_readers ();
+}
+
+static void
+gst_tag_read_bin_dispose (GObject * object)
+{
+  GstTagReadBin *tbin = GST_TAG_READ_BIN (object);
+
+  g_value_array_free (tbin->elements);
+
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gst_tag_read_bin_finalize (GObject * object)
+{
+  GstTagReadBin *tbin = GST_TAG_READ_BIN (object);
+
+  g_mutex_free (tbin->dyn_lock);
+  tbin->dyn_lock = NULL;
+  g_free (tbin->uri);
+  tbin->uri = NULL;
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gst_tag_read_bin_post_tags (GstTagReadBin * tbin)
+{
+  GstTagList *tags;
+  guint i;
+
+  GST_DEBUG_OBJECT (tbin, "posting tags");
+
+  tags = gst_tag_sink_get_global_tags (GST_TAG_SINK (tbin->tagsink));
+  GST_INFO_OBJECT (tbin, "global tags: %" GST_PTR_FORMAT, tags);
+  if (tags != NULL) {
+    /* if there's no container format in the tags, add one based on caps */
+    if (!gst_tag_read_utils_list_has_container_tag (tags) &&
+        tbin->container_caps != NULL) {
+      gst_tag_read_utils_add_container_tag_from_caps (tags,
+          tbin->container_caps);
+    }
+
+    GST_BIN_CLASS (parent_class)->handle_message (GST_BIN (tbin),
+        gst_message_new_tag (GST_OBJECT_CAST (tbin), tags));
+  }
+
+  for (i = 0;; ++i) {
+    tags = gst_tag_sink_get_stream_tags (GST_TAG_SINK (tbin->tagsink), i);
+
+    /* there should always at least be a *_CODEC tag based on the caps */
+    if (tags == NULL)
+      break;
+
+    GST_BIN_CLASS (parent_class)->handle_message (GST_BIN (tbin),
+        gst_message_new_tag (GST_OBJECT_CAST (tbin), tags));
+  }
+}
+
+static void
+gst_tag_read_bin_handle_message (GstBin * bin, GstMessage * msg)
+{
+  GstTagReadBin *tbin = GST_TAG_READ_BIN (bin);
+
+  /* Suppress all tag messages from other elements. We'll collect tags from
+   * the pads and then post messages of our own when we're done and we have
+   * all the information */
+  switch (GST_MESSAGE_TYPE (msg)) {
+    case GST_MESSAGE_TAG:
+    {
+      GST_LOG_OBJECT (bin, "suppressing tags from element %s: %" GST_PTR_FORMAT,
+          GST_OBJECT_NAME (msg->src), msg->structure);
+      GST_OBJECT_LOCK (tbin);
+      tbin->tag_messages = g_list_append (tbin->tag_messages, msg);
+      msg = NULL;
+      GST_OBJECT_UNLOCK (tbin);
+      break;
+    }
+    case GST_MESSAGE_ASYNC_DONE:
+    {
+      if (msg->src == GST_OBJECT_CAST (tbin->tagsink)) {
+        GST_DEBUG_OBJECT (bin, "done collecting tags");
+        gst_tag_read_bin_post_tags (tbin);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+
+  if (msg != NULL)
+    GST_BIN_CLASS (parent_class)->handle_message (bin, msg);
+}
+
+
+static void
+gst_tag_read_bin_set_uri (GstTagReadBin * tbin, const gchar * uri)
+{
+  if (uri == NULL || !gst_uri_is_valid (uri)) {
+    g_warning ("Can't set invalid URI '%s' on tagreadbin element %s",
+        GST_STR_NULL (uri), GST_ELEMENT_NAME (tbin));
+    return;
+  }
+
+  GST_FIXME_OBJECT (tbin, "Add support for non-file:// URIs");
+
+  if (!gst_uri_has_protocol (uri, "file")) {
+    g_warning ("Tagreadbin only supports file:// URIs for the time being");
+    return;
+  }
+
+  GST_OBJECT_LOCK (tbin);
+  if (tbin->uri != uri) {
+    g_free (tbin->uri);
+    tbin->uri = g_strdup (uri);
+  }
+  GST_OBJECT_UNLOCK (tbin);
+
+  GST_INFO_OBJECT (tbin, "Set URI to '%s'", uri);
+}
+
+static void
+gst_tag_read_bin_set_property (GObject * object, guint prop_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  GstTagReadBin *tbin = GST_TAG_READ_BIN (object);
+
+  switch (prop_id) {
+    case PROP_URI:
+      gst_tag_read_bin_set_uri (tbin, g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+gst_tag_read_bin_get_property (GObject * object, guint prop_id, GValue * value,
+    GParamSpec * pspec)
+{
+  GstTagReadBin *tbin = GST_TAG_READ_BIN (object);
+
+  switch (prop_id) {
+    case PROP_URI:
+      GST_OBJECT_LOCK (tbin);
+      g_value_set_string (value, tbin->uri);
+      GST_OBJECT_UNLOCK (tbin);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+/* This function is called when a new pad is added to uridecodebin. */
+static void
+tag_read_bin_decodebin_pad_added (GstElement * decodebin, GstPad * pad,
+    GstTagReadBin * tbin)
+{
+  GstPadLinkReturn res;
+  GstPad *request_pad;
+
+  GST_TAG_READ_BIN_SHUTDOWN_LOCK (tbin, shutdown);
+
+  request_pad = gst_element_get_request_pad (tbin->tagsink, "sink%d");
+  GST_LOG_OBJECT (request_pad, "got new request pad");
+  tbin->request_pads = g_list_prepend (tbin->request_pads, request_pad);
+
+  res = gst_pad_link (pad, request_pad);
+
+  if (GST_PAD_LINK_SUCCESSFUL (res)) {
+    GST_DEBUG_OBJECT (tbin, "linked new pad %s:%s to %s:%s",
+        GST_DEBUG_PAD_NAME (pad), GST_DEBUG_PAD_NAME (request_pad));
+  } else {
+    GST_WARNING_OBJECT (tbin, "failed to link new pad %s:%s to %s:%s: %d",
+        GST_DEBUG_PAD_NAME (pad), GST_DEBUG_PAD_NAME (request_pad), res);
+  }
+
+  GST_TAG_READ_BIN_SHUTDOWN_UNLOCK (tbin);
+  return;
+
+shutdown:
+  {
+    GST_DEBUG_OBJECT (tbin, "shutdown");
+    return;
+  }
+}
+
+/* we get called when all pads are available and we must connect the sinks to
+ * them.
+ * The main purpose of the code is to see if we have video/audio and subtitles
+ * and pick the right pipelines to display them.
+ *
+ * The selectors installed on the group tell us about the presence of
+ * audio/video and subtitle streams. This allows us to see if we need
+ * visualisation, video or/and audio.
+ */
+static void
+tag_read_bin_decodebin_no_more_pads (GstElement * decodebin,
+    GstTagReadBin * tbin)
+{
+  GST_DEBUG_OBJECT (decodebin, "no more pads");
+}
+
+static GValueArray *
+gst_factory_list_filter_by_sinkcaps (GValueArray * array, const GstCaps * caps)
+{
+  GValueArray *result;
+  gint i;
+
+  result = g_value_array_new (0);
+
+  GST_DEBUG ("finding factories for %" GST_PTR_FORMAT, caps);
+
+  /* loop over all the factories */
+  for (i = 0; i < array->n_values; i++) {
+    GValue *value;
+    GstElementFactory *factory;
+    const GList *templates;
+    GList *walk;
+
+    value = g_value_array_get_nth (array, i);
+    factory = g_value_get_object (value);
+
+    /* get the templates from the element factory */
+    templates = gst_element_factory_get_static_pad_templates (factory);
+    for (walk = (GList *) templates; walk; walk = g_list_next (walk)) {
+      GstStaticPadTemplate *templ = walk->data;
+
+      /* we only care about the sink templates */
+      if (templ->direction == GST_PAD_SINK) {
+        GstCaps *intersect;
+        GstCaps *tmpl_caps;
+
+        /* try to intersect the caps with the caps of the template */
+        tmpl_caps = gst_static_caps_get (&templ->static_caps);
+
+        /* FIXME, intersect is not the right method, we ideally want to check
+         * for a subset here */
+        intersect = gst_caps_intersect (caps, tmpl_caps);
+        gst_caps_unref (tmpl_caps);
+
+        /* check if the intersection is empty */
+        if (!gst_caps_is_empty (intersect)) {
+          /* non empty intersection, we can use this element */
+          GValue resval = { 0, };
+          g_value_init (&resval, G_TYPE_OBJECT);
+          g_value_set_object (&resval, factory);
+          g_value_array_append (result, &resval);
+          g_value_unset (&resval);
+          gst_caps_unref (intersect);
+          break;
+        }
+        gst_caps_unref (intersect);
+      }
+    }
+  }
+  GST_DEBUG ("found %u factories for %" GST_PTR_FORMAT, result->n_values, caps);
+  return result;
+}
+
+/* Called when we must provide a list of factories to plug pad with caps */
+static GValueArray *
+tag_read_bin_decodebin_autoplug_factories (GstElement * decodebin,
+    GstPad * pad, GstCaps * caps, GstTagReadBin * tbin)
+{
+  GValueArray *result = NULL;
+
+  GST_TAG_READ_BIN_SHUTDOWN_LOCK (tbin, shutdown);
+
+  GST_DEBUG_OBJECT (tbin, "getting factories for caps %" GST_PTR_FORMAT, caps);
+
+  /* filter out the elements based on the caps */
+  result = gst_factory_list_filter_by_sinkcaps (tbin->elements, caps);
+
+  if (tbin->container_caps == NULL &&
+      gst_tag_read_bin_factory_list_has_demuxer (result)) {
+    GST_DEBUG_OBJECT (tbin, "possible container caps: %" GST_PTR_FORMAT, caps);
+    gst_caps_replace (&tbin->container_caps, caps);
+  }
+
+  GST_DEBUG_OBJECT (tbin, "found %u factories", result->n_values);
+
+  /* nothing else found => expose pad as it is now and hope for the best */
+  if (result->n_values == 0) {
+    if (!gst_tag_read_utils_caps_complete (caps)) {
+      GST_FIXME ("caps without all the info we want and no more tagreaders/"
+          "parsers to plug for %" GST_PTR_FORMAT, caps);
+    }
+    g_value_array_free (result);
+    result = NULL;
+  }
+
+  GST_TAG_READ_BIN_SHUTDOWN_UNLOCK (tbin);
+
+  return result;
+
+shutdown:
+  {
+    GST_DEBUG_OBJECT (tbin, "shutdown");
+    return NULL;
+  }
+}
+
+static gboolean
+tag_read_bin_decodebin_autoplug_continue (GstElement * decodebin, GstPad * pad,
+    GstCaps * caps, GstTagReadBin * tbin)
+{
+  GstFormat fmt = GST_FORMAT_TIME;
+  gboolean do_continue = FALSE;
+  gboolean query_ok, capsinfo_ok;
+  gint64 dur = 0;
+
+  GST_TAG_READ_BIN_SHUTDOWN_LOCK (tbin, shutdown);
+
+  GST_DEBUG_OBJECT (tbin, "continue with caps %" GST_PTR_FORMAT "?", caps);
+
+  query_ok = gst_pad_query_duration (pad, &fmt, &dur) && (dur > 0);
+  GST_LOG_OBJECT (tbin, "duration query: res=%d, duration=%" G_GINT64_FORMAT,
+      query_ok, dur);
+
+  capsinfo_ok = gst_tag_read_utils_caps_complete (caps);
+  GST_LOG_OBJECT (tbin, "caps info complete: %d", capsinfo_ok);
+
+  /* stop if we have both a duration and the caps are complete enough */
+  do_continue = !query_ok || !capsinfo_ok;
+  GST_LOG_OBJECT (tbin, "continue: %d", do_continue);
+
+  GST_TAG_READ_BIN_SHUTDOWN_UNLOCK (tbin);
+  return do_continue;
+
+shutdown:
+  {
+    GST_DEBUG_OBJECT (tbin, "shutting down");
+    return FALSE;
+  }
+}
+
+/* We are asked to select an element. We can return TRY or EXPOSE */
+static GstAutoplugSelectResult
+tag_read_bin_decodebin_autoplug_select (GstElement * decodebin, GstPad * pad,
+    GstCaps * caps, GstElementFactory * factory, GstTagReadBin * tbin)
+{
+  GST_LOG_OBJECT (tbin, "selecting %s for caps %" GST_PTR_FORMAT,
+      GST_PLUGIN_FEATURE_NAME (factory), caps);
+
+  return GST_AUTOPLUG_SELECT_TRY;
+}
+
+#define REMOVE_SIGNAL(obj,id)            \
+if (id) {                                \
+  g_signal_handler_disconnect (obj, id); \
+  id = 0;                                \
+}
+
+static void
+gst_tag_read_bin_cleanup (GstTagReadBin * tbin)
+{
+  while (tbin->request_pads) {
+    GstPad *pad = tbin->request_pads->data;
+
+    gst_element_release_request_pad (tbin->tagsink, pad);
+    gst_object_unref (pad);
+    tbin->request_pads =
+        g_list_delete_link (tbin->request_pads, tbin->request_pads);
+  }
+  if (tbin->uridecodebin) {
+    GST_LOG_OBJECT (tbin, "removing existing decodebin");
+    REMOVE_SIGNAL (tbin->uridecodebin, tbin->pad_added_id);
+    REMOVE_SIGNAL (tbin->uridecodebin, tbin->no_more_pads_id);
+    REMOVE_SIGNAL (tbin->uridecodebin, tbin->autoplug_factories_id);
+    REMOVE_SIGNAL (tbin->uridecodebin, tbin->autoplug_select_id);
+    gst_element_set_state (tbin->uridecodebin, GST_STATE_NULL);
+    gst_bin_remove (GST_BIN (tbin), tbin->uridecodebin);
+    tbin->uridecodebin = NULL;
+    GST_DEBUG_OBJECT (tbin, "removed decodebin");
+  }
+  if (tbin->tagsink) {
+    GST_LOG_OBJECT (tbin, "removing tagsink");
+    gst_element_set_state (tbin->tagsink, GST_STATE_NULL);
+    gst_bin_remove (GST_BIN (tbin), tbin->tagsink);
+    tbin->tagsink = NULL;
+    GST_DEBUG_OBJECT (tbin, "removed tagsink");
+  }
+  while (tbin->tag_messages) {
+    gst_message_unref (tbin->tag_messages->data);
+    tbin->tag_messages = g_list_delete_link (tbin->tag_messages,
+        tbin->tag_messages);
+  }
+  gst_caps_replace (&tbin->container_caps, NULL);
+}
+
+static gboolean
+gst_tag_read_bin_setup (GstTagReadBin * tbin)
+{
+  GstStateChangeReturn state_ret;
+  GstState pending;
+
+  tbin->container_caps = NULL;
+
+  GST_DEBUG_OBJECT (tbin, "creating new decodebin");
+  tbin->uridecodebin = gst_element_factory_make ("uridecodebin", NULL);
+  if (tbin->uridecodebin == NULL)
+    goto no_decodebin;
+
+  gst_tag_reader_set_tag_reading_mode (GST_TAG_READER (tbin->uridecodebin),
+      GST_TAG_READING_MODE_TAGS_ONLY);
+
+  gst_bin_add (GST_BIN_CAST (tbin), tbin->uridecodebin);
+
+  /* pad-added: we connect the newly exposed pad to the tagsink  */
+  tbin->pad_added_id = g_signal_connect (tbin->uridecodebin, "pad-added",
+      G_CALLBACK (tag_read_bin_decodebin_pad_added), tbin);
+
+  /* no-more-pads: we just connect this for logging purposes */
+  tbin->no_more_pads_id = g_signal_connect (tbin->uridecodebin, "no-more-pads",
+      G_CALLBACK (tag_read_bin_decodebin_no_more_pads), tbin);
+
+  /* autoplug-factories: will be called when a new media type is found. We
+   * return a list of tagreaders or parser for decodebin to try */
+  tbin->autoplug_factories_id =
+      g_signal_connect (tbin->uridecodebin, "autoplug-factories",
+      G_CALLBACK (tag_read_bin_decodebin_autoplug_factories), tbin);
+
+  /* autoplug-continue: determine whether we've got the kind of caps we're
+   * looking for etc. or if we want to plug further tagreaders/parsers */
+  tbin->autoplug_continue_id = g_signal_connect (tbin->uridecodebin,
+      "autoplug-continue",
+      G_CALLBACK (tag_read_bin_decodebin_autoplug_continue), tbin);
+
+  /* autoplug-select: report what's being plugged next */
+  tbin->autoplug_select_id = g_signal_connect (tbin->uridecodebin,
+      "autoplug-select", G_CALLBACK (tag_read_bin_decodebin_autoplug_select),
+      tbin);
+
+  g_object_set (tbin->uridecodebin, "uri", tbin->uri, NULL);
+
+  GST_STATE_LOCK (tbin);
+  pending = GST_STATE_PENDING (tbin);
+  GST_STATE_UNLOCK (tbin);
+
+  tbin->tagsink = g_object_new (GST_TYPE_TAG_SINK, NULL);
+  gst_bin_add (GST_BIN (tbin), tbin->tagsink);
+  gst_element_set_state (tbin->tagsink, pending);
+
+  state_ret = gst_element_set_state (tbin->uridecodebin, pending);
+  if (state_ret == GST_STATE_CHANGE_FAILURE)
+    goto decodebin_failure;
+
+  return TRUE;
+
+  /* ERRORS */
+no_decodebin:
+  {
+    gst_element_post_message (GST_ELEMENT_CAST (tbin),
+        gst_missing_element_message_new (GST_ELEMENT (tbin), "uridecodebin"));
+    GST_ELEMENT_ERROR (tbin, CORE, MISSING_PLUGIN, (NULL), ("no uridecodebin"));
+    return FALSE;
+  }
+decodebin_failure:
+  {
+    GST_DEBUG_OBJECT (tbin, "decodebin state change failed");
+    gst_tag_read_bin_cleanup (tbin);
+    /* an error message should have been posted already, but better be safe */
+    GST_ELEMENT_ERROR (tbin, CORE, STATE_CHANGE, (NULL), (NULL));
+    return FALSE;
+  }
+}
+
+static GstStateChangeReturn
+gst_tag_read_bin_change_state (GstElement * element, GstStateChange transition)
+{
+  GstStateChangeReturn ret;
+  GstTagReadBin *tbin;
+
+  tbin = GST_TAG_READ_BIN (element);
+
+  GST_LOG_OBJECT (tbin, "transition %s -> %s",
+      gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
+      gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
+
+  switch (transition) {
+    case GST_STATE_CHANGE_READY_TO_PAUSED:
+      GST_LOG_OBJECT (tbin, "clearing shutdown flag");
+      g_atomic_int_set (&tbin->shutdown, 0);
+      if (!gst_tag_read_bin_setup (tbin))
+        return GST_STATE_CHANGE_FAILURE;
+      break;
+    case GST_STATE_CHANGE_PAUSED_TO_READY:
+      GST_LOG_OBJECT (tbin, "setting shutdown flag");
+      g_atomic_int_set (&tbin->shutdown, 1);
+
+      /* wait for all callbacks to end by taking the lock.
+       * No dynamic (critical) new callbacks will
+       * be able to happen as we set the shutdown flag. */
+      GST_TAG_READ_BIN_DYN_LOCK (tbin);
+      GST_LOG_OBJECT (tbin, "dynamic lock taken, we can continue shutdown");
+      GST_TAG_READ_BIN_DYN_UNLOCK (tbin);
+      break;
+    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
+      gst_element_post_message (element,
+          gst_message_new_eos (GST_OBJECT_CAST (element)));
+      break;
+    case GST_STATE_CHANGE_READY_TO_NULL:
+      break;
+    default:
+      break;
+  }
+
+  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+  if (ret == GST_STATE_CHANGE_FAILURE)
+    return ret;
+
+  switch (transition) {
+    case GST_STATE_CHANGE_PAUSED_TO_READY:
+    case GST_STATE_CHANGE_READY_TO_NULL:
+      gst_tag_read_bin_cleanup (tbin);
+      break;
+    default:
+      break;
+  }
+
+  GST_LOG_OBJECT (tbin, "transition %s -> %s, returning %s",
+      gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
+      gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)),
+      gst_element_state_change_return_get_name (ret));
+
+  return ret;
+}
+
+static gboolean
+plugin_init (GstPlugin * plugin)
+{
+  GST_DEBUG_CATEGORY_INIT (tagreadbin_debug, "tagreadbin", 0, "Tag read bin");
+
+  gst_tag_read_utils_register_caps_tags ();
+
+  return gst_element_register (plugin, "tagreadbin", GST_RANK_NONE,
+      GST_TYPE_TAG_READ_BIN);
+}
+
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR,
+    "tagreading", "Tag reading support", plugin_init, VERSION, GST_LICENSE,
+    GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
diff --git a/gst/tagreading/gsttagreadbin.h b/gst/tagreading/gsttagreadbin.h
new file mode 100644
index 0000000..c330b4e
--- /dev/null
+++ b/gst/tagreading/gsttagreadbin.h
@@ -0,0 +1,102 @@
+/* GStreamer tag read bin
+ * 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.
+ */
+
+#ifndef __GST_TAG_READ_BIN_H__
+#define __GST_TAG_READ_BIN_H__
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_TAG_READ_BIN               (gst_tag_read_bin_get_type())
+#define GST_TAG_READ_BIN(obj)               (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAG_READ_BIN,GstTagReadBin))
+#define GST_TAG_READ_BIN_CLASS(klass)       (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAG_READ_BIN,GstTagReadBinClass))
+#define GST_IS_TAG_READ_BIN(obj)            (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_TAG_READ_BIN))
+#define GST_IS_TAG_READ_BIN_CLASS(klass)    (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_TAG_READ_BIN))
+
+#define GST_TAG_READ_BIN_DYN_LOCK(tbin)    g_mutex_lock ((tbin)->dyn_lock)
+#define GST_TAG_READ_BIN_DYN_UNLOCK(tbin)  g_mutex_unlock ((tbin)->dyn_lock)
+
+/* lock for shutdown */
+#define GST_TAG_READ_BIN_SHUTDOWN_LOCK(tbin,label)       \
+G_STMT_START {                                           \
+  if (G_UNLIKELY (g_atomic_int_get (&tbin->shutdown)))   \
+    goto label;                                          \
+  GST_TAG_READ_BIN_DYN_LOCK (tbin);                      \
+  if (G_UNLIKELY (g_atomic_int_get (&tbin->shutdown))) { \
+    GST_TAG_READ_BIN_DYN_UNLOCK (tbin);                  \
+    goto label;                                          \
+  }                                                      \
+} G_STMT_END
+
+/* unlock for shutdown */
+#define GST_TAG_READ_BIN_SHUTDOWN_UNLOCK(bin)         \
+  GST_TAG_READ_BIN_DYN_UNLOCK (bin);                  \
+
+typedef struct _GstTagReadBin GstTagReadBin;
+typedef struct _GstTagReadBinClass GstTagReadBinClass;
+
+/**
+ * GstTagReadBin:
+ *
+ * tagreadbin element structure (opaque)
+ *
+ */
+struct _GstTagReadBin
+{
+  GstPipeline parent;
+
+  /*< private >*/
+  GstElement   * uridecodebin;
+  gchar        * uri;
+
+  GstElement   * tagsink;
+
+  /* factories we can use for selecting elements */
+  GValueArray  * elements;
+
+  /* lock protecting dynamic adding/removing */
+  GMutex       * dyn_lock;
+
+  /* if we are shutting down or not */
+  gint           shutdown;                             /* ATOMIC */
+
+  /* tag messages we dropped (to compare to tag events collected from pads) */
+  GList         *tag_messages;                         /* OBJECT_LOCK */
+
+  GstCaps       *container_caps;                       /* DYN_LOCK */
+
+  GList         *request_pads;                         /* DYN_LOCK */
+
+  /* decodebin signals */
+  gulong         pad_added_id;
+  gulong         no_more_pads_id;
+  gulong         autoplug_factories_id;
+  gulong         autoplug_continue_id;
+  gulong         autoplug_select_id;
+};
+
+struct _GstTagReadBinClass
+{
+  GstPipelineClass parent_class;
+};
+
+G_END_DECLS
+
+#endif /* __GST_TAG_READ_BIN_H__ */
diff --git a/gst/tagreading/gsttagreadutils.c b/gst/tagreading/gsttagreadutils.c
new file mode 100644
index 0000000..a38bd85
--- /dev/null
+++ b/gst/tagreading/gsttagreadutils.c
@@ -0,0 +1,308 @@
+/* GStreamer tag read bin utilities
+ * 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 "gsttagreadutils.h"
+#include <gst/pbutils/pbutils.h>
+
+GST_DEBUG_CATEGORY_EXTERN (tagreadbin_debug);
+#define GST_CAT_DEFAULT tagreadbin_debug
+
+GstTagReadStreamType
+gst_tag_read_utils_get_caps_type (const GstCaps * caps)
+{
+  GstStructure *s;
+  const gchar *name;
+
+  s = gst_caps_get_structure (caps, 0);
+  name = gst_structure_get_name (s);
+
+  if (g_str_equal (name, "video/x-dvd-subpicture") ||
+      g_str_has_prefix (name, "text/")) {
+    return GST_TAG_READ_STREAM_TYPE_SUBTITLE;
+  }
+
+  if (g_str_has_prefix (name, "application/x-subtitle"))
+    return GST_TAG_READ_STREAM_TYPE_SUBTITLE;
+
+  if (g_str_has_prefix (name, "audio/"))
+    return GST_TAG_READ_STREAM_TYPE_AUDIO;
+
+  if (g_str_has_prefix (name, "video/") || g_str_has_prefix (name, "image/")) {
+    return GST_TAG_READ_STREAM_TYPE_VIDEO;
+  }
+
+  return GST_TAG_READ_STREAM_TYPE_UNKNOWN;
+}
+
+gboolean
+gst_tag_read_utils_caps_complete (const GstCaps * caps)
+{
+  GstStructure *s;
+  const gchar *name;
+  gboolean complete;
+
+  s = gst_caps_get_structure (caps, 0);
+  name = gst_structure_get_name (s);
+
+  if (g_str_equal (name, "video/x-dvd-subpicture") ||
+      g_str_has_prefix (name, "text/")) {
+    complete = TRUE;
+  } else if (g_str_has_prefix (name, "application/x-subtitle")) {
+    /* subtitle parser might give us more info */
+    complete = FALSE;
+  } else if (g_str_has_prefix (name, "audio/")) {
+    complete = gst_structure_has_field (s, "channels") &&
+        gst_structure_has_field (s, "rate");
+  } else if (g_str_has_prefix (name, "video/") ||
+      g_str_has_prefix (name, "image/")) {
+    complete = gst_structure_has_field (s, "width") &&
+        gst_structure_has_field (s, "height");
+  } else {
+    complete = FALSE;
+  }
+
+  return complete;
+}
+
+void
+gst_tag_read_utils_add_codec_tag_from_caps (GstTagList * tags,
+    const GstCaps * caps)
+{
+  GstTagReadStreamType type;
+  const gchar *tag_name, *tag_name_unknown;
+
+  gst_pb_utils_init ();
+
+  type = gst_tag_read_utils_get_caps_type (caps);
+  switch (type) {
+    case GST_TAG_READ_STREAM_TYPE_AUDIO:
+      tag_name = GST_TAG_AUDIO_CODEC;
+      tag_name_unknown = GST_TAG_AUDIO_CODEC "-unknown";
+      break;
+    case GST_TAG_READ_STREAM_TYPE_VIDEO:
+      tag_name = GST_TAG_VIDEO_CODEC;
+      tag_name_unknown = GST_TAG_VIDEO_CODEC "-unknown";
+      break;
+    case GST_TAG_READ_STREAM_TYPE_SUBTITLE:
+      tag_name = GST_TAG_SUBTITLE_CODEC;
+      tag_name_unknown = GST_TAG_SUBTITLE_CODEC "-unknown";
+      break;
+    default:
+      tag_name = GST_TAG_CODEC;
+      tag_name_unknown = GST_TAG_CODEC "-unknown";
+      break;
+  }
+
+  GST_LOG ("Adding %s field to tags from %" GST_PTR_FORMAT, tag_name, caps);
+  gst_pb_utils_add_codec_description_to_tag_list (tags, tag_name, caps);
+
+  if (!gst_tag_read_utils_list_has_codec_tag (tags)) {
+    GST_FIXME ("Couldn't get codec name for caps %" GST_PTR_FORMAT, caps);
+    if (!gst_tag_exists (tag_name_unknown)) {
+      gst_tag_register (tag_name_unknown, GST_TAG_FLAG_ENCODED,
+          G_TYPE_BOOLEAN, "codec unknown",
+          "TRUE if the codec  is not known and the *-codec "
+          "tag contains the string 'unknown' or somesuch", NULL);
+
+      /* FIXME: translate */
+      gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, tag_name,
+          "Unknown", tag_name_unknown, TRUE, NULL);
+    }
+  }
+}
+
+/* FIXME: remove once we depend on 0.10.24 */
+#ifndef GST_TAG_CONTAINER_FORMAT
+#define GST_TAG_CONTAINER_FORMAT "container-format"
+#endif
+
+void
+gst_tag_read_utils_add_container_tag_from_caps (GstTagList * tags,
+    const GstCaps * caps)
+{
+  gst_pb_utils_init ();
+
+  /* FIXME: remove once we depend on 0.10.24 */
+  if (!gst_tag_exists (GST_TAG_CONTAINER_FORMAT)) {
+    gst_tag_register (GST_TAG_CONTAINER_FORMAT, GST_TAG_FLAG_ENCODED,
+        G_TYPE_STRING, "container format", "container format", NULL);
+  }
+
+  GST_LOG ("Adding container-format field to tags from %" GST_PTR_FORMAT, caps);
+  gst_pb_utils_add_codec_description_to_tag_list (tags,
+      GST_TAG_CONTAINER_FORMAT, caps);
+
+  /* Adding bogus container-format tag */
+  if (!gst_tag_read_utils_list_has_container_tag (tags)) {
+    GST_FIXME ("Couldn't get container format for caps %" GST_PTR_FORMAT, caps);
+
+    if (!gst_tag_exists ("container-format-unknown")) {
+      gst_tag_register ("container-format-unknown", GST_TAG_FLAG_ENCODED,
+          G_TYPE_BOOLEAN, "container format unknown",
+          "TRUE if the container format is not known and the contaier-format "
+          "tag contains the string 'unknown' or somesuch", NULL);
+    }
+
+    /* FIXME: translate */
+    gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_CONTAINER_FORMAT,
+        "Unknown", "container-format-unknown", TRUE, NULL);
+  }
+}
+
+gboolean
+gst_tag_read_utils_list_has_container_tag (GstTagList * tags)
+{
+  const GValue *v;
+
+  v = gst_tag_list_get_value_index (tags, GST_TAG_CONTAINER_FORMAT, 0);
+  return (v != NULL);
+}
+
+gboolean
+gst_tag_read_utils_list_has_codec_tag (GstTagList * tags)
+{
+  return (gst_tag_list_get_value_index (tags, GST_TAG_CODEC, 0) != NULL) ||
+      (gst_tag_list_get_value_index (tags, GST_TAG_VIDEO_CODEC, 0) != NULL) ||
+      (gst_tag_list_get_value_index (tags, GST_TAG_AUDIO_CODEC, 0) != NULL);
+}
+
+static void
+gst_tag_read_utils_list_merge_field (GstTagList * tags, GstStructure * s,
+    const gchar * field, GType expected_type)
+{
+  const GValue *v;
+
+  if ((v = gst_structure_get_value (s, field))) {
+    if (G_VALUE_TYPE (v) == expected_type) {
+      if (gst_tag_list_get_value_index (tags, field, 0) != NULL) {
+        GST_WARNING ("overwriting tag '%s' with value from caps", field);
+      }
+      gst_tag_list_add_values (tags, GST_TAG_MERGE_REPLACE, field, v, NULL);
+      GST_LOG ("added field '%s' to taglist %" GST_PTR_FORMAT, field, tags);
+    } else {
+      GST_WARNING ("field '%s' in caps %" GST_PTR_FORMAT " does not have the "
+          "expected type (%s)", field, s, g_type_name (expected_type));
+    }
+  } else {
+    GST_LOG ("no field '%s' in caps %" GST_PTR_FORMAT, field, s);
+  }
+}
+
+void
+gst_tag_read_utils_register_caps_tags (void)
+{
+  /* audio tags */
+  if (!gst_tag_exists ("channels")) {
+    gst_tag_register ("channels", GST_TAG_FLAG_ENCODED,
+        G_TYPE_INT, "channels", "number of audio channels", NULL);
+  }
+  if (!gst_tag_exists ("rate")) {
+    gst_tag_register ("rate", GST_TAG_FLAG_ENCODED,
+        G_TYPE_INT, "rate", "sample rate of audio", NULL);
+  }
+
+  /* video/image tags */
+  if (!gst_tag_exists ("width")) {
+    gst_tag_register ("width", GST_TAG_FLAG_ENCODED,
+        G_TYPE_INT, "width", "width of video or image", NULL);
+  }
+  if (!gst_tag_exists ("height")) {
+    gst_tag_register ("height", GST_TAG_FLAG_ENCODED,
+        G_TYPE_INT, "height", "height of video or image", NULL);
+  }
+  if (!gst_tag_exists ("framerate")) {
+    gst_tag_register ("framerate", GST_TAG_FLAG_ENCODED,
+        GST_TYPE_FRACTION, "framerate", "framerate of video", NULL);
+  }
+  if (!gst_tag_exists ("pixel-aspect-ratio")) {
+    gst_tag_register ("pixel-aspect-ratio", GST_TAG_FLAG_ENCODED,
+        GST_TYPE_FRACTION, "pixel-aspect-ratio", "PAR of video", NULL);
+  }
+}
+
+void
+gst_tag_read_utils_list_add_caps_info (GstTagList * tags, const GstCaps * caps)
+{
+  GstTagReadStreamType type;
+  GstStructure *s;
+
+  if (caps == NULL)
+    return;
+
+  gst_tag_read_utils_register_caps_tags ();
+
+  s = gst_caps_get_structure (caps, 0);
+
+  type = gst_tag_read_utils_get_caps_type (caps);
+  switch (type) {
+      /* keep fields in sync with gst_tag_read_utils_register_caps_tags() */
+    case GST_TAG_READ_STREAM_TYPE_AUDIO:
+      gst_tag_read_utils_list_merge_field (tags, s, "channels", G_TYPE_INT);
+      gst_tag_read_utils_list_merge_field (tags, s, "rate", G_TYPE_INT);
+      break;
+      /* keep fields in sync with gst_tag_read_utils_register_caps_tags() */
+    case GST_TAG_READ_STREAM_TYPE_VIDEO:
+      gst_tag_read_utils_list_merge_field (tags, s, "width", G_TYPE_INT);
+      gst_tag_read_utils_list_merge_field (tags, s, "height", G_TYPE_INT);
+      gst_tag_read_utils_list_merge_field (tags, s, "framerate",
+          GST_TYPE_FRACTION);
+      gst_tag_read_utils_list_merge_field (tags, s, "pixel-aspect-ratio",
+          GST_TYPE_FRACTION);
+      break;
+    case GST_TAG_READ_STREAM_TYPE_SUBTITLE:
+      break;
+    default:
+      break;
+  }
+}
+
+void
+gst_tag_read_utils_list_add_duration (GstTagList * tags, GstClockTime dur)
+{
+  if (GST_CLOCK_TIME_IS_VALID (dur) && dur > 0) {
+    gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_DURATION, dur, NULL);
+    GST_LOG ("added field 'duration' to taglist %" GST_PTR_FORMAT, tags);
+  } else {
+    GST_DEBUG ("not adding 'duration' field to taglist, duration not valid");
+  }
+}
+
+static void
+taglist_remove_private_foreach_func (const GstTagList * list,
+    const gchar * tag, gpointer user_data)
+{
+  GList **p_list = user_data;
+
+  /* tag is a quark string, we don't need to worry about it getting freed */
+  if (g_str_has_prefix (tag, "private-"))
+    *p_list = g_list_prepend (*p_list, (gpointer) tag);
+}
+
+void
+gst_tag_read_utils_list_remove_private_tags (GstTagList * tags)
+{
+  GList *list = NULL;
+
+  gst_tag_list_foreach (tags, taglist_remove_private_foreach_func, &list);
+  while (list != NULL) {
+    gst_tag_list_remove_tag (tags, list->data);
+    GST_LOG ("Removed tag %s, taglist now %" GST_PTR_FORMAT, list->data, tags);
+    list = g_list_delete_link (list, list);
+  }
+}
diff --git a/gst/tagreading/gsttagreadutils.h b/gst/tagreading/gsttagreadutils.h
new file mode 100644
index 0000000..06ce9ea
--- /dev/null
+++ b/gst/tagreading/gsttagreadutils.h
@@ -0,0 +1,62 @@
+/* GStreamer tag read bin utilities
+ * 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.
+ */
+
+#ifndef __GST_TAG_READ_UTILS_H__
+#define __GST_TAG_READ_UTILS_H__
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  GST_TAG_READ_STREAM_TYPE_UNKNOWN = 0,
+  GST_TAG_READ_STREAM_TYPE_AUDIO,
+  GST_TAG_READ_STREAM_TYPE_VIDEO,
+  GST_TAG_READ_STREAM_TYPE_SUBTITLE
+} GstTagReadStreamType;
+
+GstTagReadStreamType  gst_tag_read_utils_get_caps_type (const GstCaps * caps);
+
+gboolean              gst_tag_read_utils_caps_complete (const GstCaps * caps);
+
+void                  gst_tag_read_utils_add_codec_tag_from_caps (GstTagList    * tags,
+                                                                  const GstCaps * caps);
+
+void                  gst_tag_read_utils_add_container_tag_from_caps (GstTagList    * tags,
+                                                                      const GstCaps * caps);
+
+gboolean              gst_tag_read_utils_list_has_codec_tag (GstTagList * tags);
+
+gboolean              gst_tag_read_utils_list_has_container_tag (GstTagList * tags);
+
+
+void                  gst_tag_read_utils_register_caps_tags (void);
+
+void                  gst_tag_read_utils_list_add_caps_info (GstTagList    * tags,
+                                                             const GstCaps * caps);
+
+void                  gst_tag_read_utils_list_add_duration (GstTagList * tags,
+                                                            GstClockTime dur);
+
+void                  gst_tag_read_utils_list_remove_private_tags (GstTagList * tags);
+
+G_END_DECLS
+
+#endif /* __GST_TAG_UTILS_H__ */
diff --git a/gst/tagreading/gsttagsink.c b/gst/tagreading/gsttagsink.c
new file mode 100644
index 0000000..3741897
--- /dev/null
+++ b/gst/tagreading/gsttagsink.c
@@ -0,0 +1,785 @@
+/* GStreamer tag read bin sink
+ * 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.
+ */
+
+/* Sink with N request sink pads; collects caps and tags on each stream
+ * until we have seen a buffer or EOS event on all streams. Once we've
+ * collected all the data we want, the sink "prerolls".
+ *
+ * We derive from basesink to avoid having to implement async state change
+ * behaviour ourselves, which is quite tricky to get right.
+ *
+ * Of course we could have done all this very differently, eg. use one sink per
+ * stream or use buffer probes in the tagreadbin etc., but having a separate
+ * element that handles all the collection logic seems conceptually nicer.
+ * If http://bugzilla.gnome.org/show_bug.cgi?id=342155 gets implemented,
+ * we could just derive one tagsink per stream and then use that API.
+ * (we currently don't even need this, but we might in the future, e.g.
+ * should we want to preroll only once the duration returned is somewhat
+ * stable, or if we want to get a thumbnail of the N-th keyframe or so.)
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <gst/gst-i18n-plugin.h>
+
+#include "gsttagsink.h"
+#include "gsttagreadutils.h"
+#include <string.h>
+
+/* after this many buffers we mark a stream as done even if haven't gotten
+ * all the information we're looking for yet (caps, duration etc.) */
+#define GST_TAG_SINK_MAX_BUFFERS  90
+
+GST_DEBUG_CATEGORY_EXTERN (tagreadbin_debug);
+#define GST_CAT_DEFAULT tagreadbin_debug
+
+static GstStaticPadTemplate sinkn_template =
+GST_STATIC_PAD_TEMPLATE ("sink%d", GST_PAD_SINK,
+    GST_PAD_REQUEST, GST_STATIC_CAPS_ANY);
+static GstStaticPadTemplate sink_template =
+GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK,
+    GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY);
+
+static void gst_tag_sink_dispose (GObject * object);
+static void gst_tag_sink_finalize (GObject * object);
+static gboolean gst_tag_sink_start (GstBaseSink * basesink);
+static gboolean gst_tag_sink_stop (GstBaseSink * basesink);
+static gboolean gst_tag_sink_unlock (GstBaseSink * basesink);
+static GstPad *gst_tag_sink_request_pad (GstElement * element,
+    GstPadTemplate * templ, const gchar * unused);
+static void gst_tag_sink_release_pad (GstElement * element, GstPad * pad);
+static gboolean gst_tag_sink_sink_event (GstPad * pad, GstEvent * event);
+static gboolean gst_tag_sink_setcaps (GstPad * pad, GstCaps * caps);
+
+GST_BOILERPLATE (GstTagSink, gst_tag_sink, GstBaseSink, GST_TYPE_BASE_SINK);
+
+static void
+gst_tag_sink_base_init (gpointer g_klass)
+{
+  /* nothing to do */
+}
+
+static void
+gst_tag_sink_class_init (GstTagSinkClass * klass)
+{
+  GstBaseSinkClass *basesink_klass = (GstBaseSinkClass *) klass;
+  GstElementClass *element_klass = (GstElementClass *) klass;
+  GObjectClass *gobject_klass = (GObjectClass *) klass;
+
+  /* dummy template for the normal base sink "sink" pad, which is never
+   * actually linked, we only use it to force basesink to preroll for us
+   * when we're ready */
+  gst_element_class_add_pad_template (element_klass,
+      gst_static_pad_template_get (&sink_template));
+
+  /* template for the request pads, which are used */
+  gst_element_class_add_pad_template (element_klass,
+      gst_static_pad_template_get (&sinkn_template));
+
+  gobject_klass->dispose = GST_DEBUG_FUNCPTR (gst_tag_sink_dispose);
+  gobject_klass->finalize = GST_DEBUG_FUNCPTR (gst_tag_sink_finalize);
+
+  element_klass->release_pad = GST_DEBUG_FUNCPTR (gst_tag_sink_release_pad);
+  element_klass->request_new_pad = GST_DEBUG_FUNCPTR (gst_tag_sink_request_pad);
+
+  basesink_klass->start = GST_DEBUG_FUNCPTR (gst_tag_sink_start);
+  basesink_klass->stop = GST_DEBUG_FUNCPTR (gst_tag_sink_stop);
+  basesink_klass->unlock = GST_DEBUG_FUNCPTR (gst_tag_sink_unlock);
+}
+
+static void
+gst_tag_sink_init (GstTagSink * tagsink, GstTagSinkClass * klass)
+{
+  g_static_rec_mutex_init (&tagsink->streams_lock);
+  tagsink->streams = g_array_new (FALSE, FALSE, sizeof (GstTagSinkStream));
+
+  gst_base_sink_set_async_enabled (GST_BASE_SINK (tagsink), TRUE);
+  gst_base_sink_set_sync (GST_BASE_SINK (tagsink), FALSE);
+}
+
+static void
+gst_tag_sink_dispose (GObject * object)
+{
+  GstTagSink *tagsink = GST_TAG_SINK (object);
+
+  if (tagsink->streams != NULL) {
+    if (tagsink->streams->len > 0) {
+      g_warning ("GstTagSink: %u unreleased pads in ::dispose()",
+          tagsink->streams->len);
+    }
+    /* just leak the contents of any unreleased streams (shouldn't happen) */
+    g_array_free (tagsink->streams, TRUE);
+    tagsink->streams = NULL;
+  }
+
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gst_tag_sink_finalize (GObject * object)
+{
+  GstTagSink *tagsink = GST_TAG_SINK (object);
+
+  g_static_rec_mutex_free (&tagsink->streams_lock);
+
+  if (tagsink->global_tags) {
+    gst_tag_list_free (tagsink->global_tags);
+    tagsink->global_tags = NULL;
+  }
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+/* must be called with STREAMS_LOCK held */
+static gboolean
+gst_tag_sink_stream_query_duration (GstTagSinkStream * stream)
+{
+  GstFormat fmt = GST_FORMAT_TIME;
+  gboolean res;
+  gint64 dur = -1;
+
+  res = gst_pad_query_peer_duration (stream->pad, &fmt, &dur) && (dur > 0);
+
+  /* this likely indicates a bug in some upstream element */
+  if (!res && GST_CLOCK_TIME_IS_VALID (stream->duration))
+    GST_ERROR_OBJECT (stream->pad, "previous duration good, now we got none");
+  else
+    stream->duration = dur;
+
+  GST_LOG_OBJECT (stream->pad, "duration: %" GST_TIME_FORMAT,
+      GST_TIME_ARGS (dur));
+
+  return res;
+}
+
+static gboolean
+gst_tag_sink_stream_is_done (GstTagSinkStream * stream)
+{
+  /* we're definitely done if we've seen an EOS */
+  if (stream->got_eos) {
+    /* warn if we didn't get anything before the EOS */
+    if (stream->last_buffer == NULL)
+      GST_WARNING_OBJECT (stream->pad, "EOS without any buffer");
+    if (!gst_tag_sink_stream_query_duration (stream))
+      GST_WARNING_OBJECT (stream->pad, "EOS and duration query failed");
+    return TRUE;
+  }
+
+  if (stream->last_buffer == NULL) {
+    GST_LOG_OBJECT (stream->pad, "no buffer yet");
+    return FALSE;
+  }
+
+  /* bail out after this many buffers */
+  if (stream->num_buffers >= GST_TAG_SINK_MAX_BUFFERS) {
+    GST_WARNING_OBJECT (stream->pad, "Marking stream as done after %u buffers,"
+        " even if we don't have caps and/or duration yet", stream->num_buffers);
+    return TRUE;
+  }
+
+  if (stream->caps == NULL) {
+    GST_LOG_OBJECT (stream->pad, "got buffer, but no caps!");
+    return FALSE;
+  }
+
+  if (!gst_tag_sink_stream_query_duration (stream)) {
+    GST_LOG_OBJECT (stream->pad, "no duration yet");
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/* must be called with STREAMS_LOCK held */
+static gboolean
+gst_tag_sink_check_preroll (GstTagSink * tagsink, GstTagSinkStream * stream)
+{
+  /* have we prerolled already? */
+  if (!tagsink->async_pending)
+    return TRUE;
+
+  /* is this stream done? */
+  if (!gst_tag_sink_stream_is_done (stream)) {
+    GST_LOG_OBJECT (stream->pad, "stream not done yet");
+    return FALSE;
+  }
+
+  if (!stream->done) {
+    stream->done = TRUE;
+    --tagsink->num_streams_pending;
+    GST_INFO_OBJECT (stream->pad, "stream is done now, %u more pending",
+        tagsink->num_streams_pending);
+  }
+
+  if (tagsink->num_streams_pending > 0) {
+    GST_LOG_OBJECT (tagsink, "still %u streams pending",
+        tagsink->num_streams_pending);
+    return FALSE;
+  }
+
+  GST_INFO_OBJECT (tagsink, "info on all sink pads complete, preroll");
+
+  tagsink->async_pending = FALSE;
+
+  /* just send an EOS event on basesink's sink pad to make us preroll. We
+   * can't use gst_base_sink_do_preroll() because it won't set the internal
+   * EOS flag right, which will make it do a _wait_for_preroll() when going
+   * back from PLAYING to PAUSED.
+   * If you think this is ugly, try deriving from GstElement and implement
+   * async state change behaviour in a correct and race-free way. */
+  GST_LOG_OBJECT (tagsink, "pushing EOS event on sinkpad to make us preroll");
+  gst_pad_send_event (GST_BASE_SINK_PAD (tagsink),
+      gst_event_new_new_segment (FALSE, 1.0, GST_FORMAT_TIME, 0, -1, 0));
+  gst_pad_send_event (GST_BASE_SINK_PAD (tagsink), gst_event_new_eos ());
+  GST_LOG_OBJECT (tagsink, "pushed EOS");
+  return TRUE;
+}
+
+/* must be called with the STREAMS_LOCK held */
+static GstTagSinkStream *
+gst_tag_sink_get_stream_from_pad (GstTagSink * tagsink, GstPad * pad)
+{
+  gint i;
+
+  for (i = 0; i < tagsink->streams->len; ++i) {
+    GstTagSinkStream *s;
+
+    s = &g_array_index (tagsink->streams, GstTagSinkStream, i);
+    if (s->pad == pad)
+      return s;
+  }
+
+  return NULL;
+}
+
+static gboolean
+gst_tag_sink_setcaps (GstPad * pad, GstCaps * caps)
+{
+  GstTagSinkStream *s;
+  GstTagSink *tagsink;
+
+  tagsink = GST_TAG_SINK (GST_PAD_PARENT (pad));
+
+  GST_INFO_OBJECT (pad, "caps: %" GST_PTR_FORMAT, caps);
+
+  GST_TAG_SINK_STREAMS_LOCK (tagsink);
+  s = gst_tag_sink_get_stream_from_pad (tagsink, pad);
+  gst_caps_replace (&s->caps, caps);
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+
+  return TRUE;
+}
+
+static gboolean
+gst_tag_sink_sink_event (GstPad * pad, GstEvent * event)
+{
+  GstTagSink *tagsink;
+
+  tagsink = GST_TAG_SINK (GST_PAD_PARENT (pad));
+
+  GST_LOG_OBJECT (pad, "%s event: %" GST_PTR_FORMAT,
+      GST_EVENT_TYPE_NAME (event), event->structure);
+
+  GST_TAG_SINK_SHUTDOWN_LOCK (tagsink, shutdown);
+
+  switch (GST_EVENT_TYPE (event)) {
+    case GST_EVENT_EOS:
+    {
+      GstTagSinkStream *s;
+
+      s = gst_tag_sink_get_stream_from_pad (tagsink, pad);
+      if (!s->got_eos) {
+        s->got_eos = TRUE;
+        gst_tag_sink_check_preroll (tagsink, s);
+      }
+      break;
+    }
+    case GST_EVENT_TAG:
+    {
+      GstTagSinkStream *s;
+      GstTagList *taglist = NULL;
+
+      gst_event_parse_tag (event, &taglist);
+
+      s = gst_tag_sink_get_stream_from_pad (tagsink, pad);
+      taglist = gst_tag_list_copy (taglist);
+      gst_tag_read_utils_list_remove_private_tags (taglist);
+      s->tags = g_list_append (s->tags, taglist);
+      break;
+    }
+    default:
+      break;
+  }
+
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+  return gst_pad_event_default (pad, event);
+
+shutdown:
+  {
+    GST_DEBUG_OBJECT (tagsink, "shutting down");
+    return FALSE;
+  }
+}
+
+static GstFlowReturn
+gst_tag_sink_chain (GstPad * pad, GstBuffer * buf)
+{
+  GstTagSinkStream *s;
+  GstFlowReturn flow;
+  GstTagSink *tagsink;
+
+  tagsink = GST_TAG_SINK (GST_PAD_PARENT (pad));
+
+  GST_TAG_SINK_SHUTDOWN_LOCK (tagsink, shutdown);
+
+  s = gst_tag_sink_get_stream_from_pad (tagsink, pad);
+
+  /* if we're done already, just skip straight to the preroll check */
+  if (s->done)
+    goto check_preroll;
+
+  if (s->caps == NULL)
+    GST_ERROR_OBJECT (pad, "received buffer without caps");
+
+  ++s->num_buffers;
+  gst_buffer_replace (&s->last_buffer, buf);
+  GST_LOG_OBJECT (pad, "buffer %d", s->num_buffers);
+
+check_preroll:
+
+  if (gst_tag_sink_check_preroll (tagsink, s)) {
+    gst_pad_push_event (pad, gst_event_new_flush_start ());
+    flow = GST_FLOW_WRONG_STATE;
+  } else {
+    flow = GST_FLOW_OK;
+  }
+
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+
+done:
+
+  gst_buffer_unref (buf);
+  return flow;
+
+shutdown:
+  {
+    GST_DEBUG_OBJECT (tagsink, "shutting down");
+    flow = GST_FLOW_WRONG_STATE;
+    goto done;
+  }
+}
+
+static GstPad *
+gst_tag_sink_request_pad (GstElement * element, GstPadTemplate * templ,
+    const gchar * unused)
+{
+  GstTagSinkStream stream = { NULL, };
+  GstTagSink *tagsink;
+  GstPad *pad;
+  gchar *name;
+  gint padcount;
+
+  if (templ->direction != GST_PAD_SINK)
+    goto not_sink;
+
+  tagsink = GST_TAG_SINK (element);
+
+  /* increment pad counter */
+  padcount = g_atomic_int_exchange_and_add (&tagsink->padcount, 1);
+
+  name = g_strdup_printf ("sink%d", padcount);
+  pad = gst_pad_new_from_template (templ, name);
+  GST_DEBUG_OBJECT (tagsink, "request new pad %s, got pad %p", name, pad);
+  g_free (name);
+
+  gst_pad_set_setcaps_function (pad, GST_DEBUG_FUNCPTR (gst_tag_sink_setcaps));
+  gst_pad_set_event_function (pad, GST_DEBUG_FUNCPTR (gst_tag_sink_sink_event));
+  gst_pad_set_chain_function (pad, GST_DEBUG_FUNCPTR (gst_tag_sink_chain));
+
+  gst_pad_set_active (pad, TRUE);
+
+  stream.pad = gst_object_ref (pad);
+  stream.duration = GST_CLOCK_TIME_NONE;
+  stream.done = FALSE;
+
+  /* takes ownership of the pad */
+  if (!gst_element_add_pad (element, pad))
+    goto could_not_add;
+
+  GST_TAG_SINK_STREAMS_LOCK (tagsink);
+  ++tagsink->num_streams_pending;
+  g_array_append_vals (tagsink->streams, &stream, 1);
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+
+  GST_INFO_OBJECT (stream.pad, "added pad");
+  return stream.pad;
+
+  /* ERRORS */
+not_sink:
+  {
+    g_warning ("GstTagSink: requested new pad that is not a SINK pad!");
+    return NULL;
+  }
+could_not_add:
+  {
+    GST_DEBUG_OBJECT (element, "could not add pad %s", GST_PAD_NAME (pad));
+    gst_object_unref (pad);
+    return NULL;
+  }
+}
+
+static void
+gst_tag_sink_release_pad (GstElement * element, GstPad * pad)
+{
+  GstTagSinkStream stream = { NULL, };
+  GstTagSink *tagsink;
+  gint i;
+
+  tagsink = GST_TAG_SINK (element);
+  GST_DEBUG_OBJECT (tagsink, "release pad %s", GST_PAD_NAME (pad));
+
+  GST_TAG_SINK_STREAMS_LOCK (tagsink);
+  for (i = 0; i < tagsink->streams->len; ++i) {
+    GstTagSinkStream *s;
+
+    s = &g_array_index (tagsink->streams, GstTagSinkStream, i);
+    if (s->pad == pad) {
+      stream = *s;
+      g_array_remove_index (tagsink->streams, i);
+      break;
+    }
+  }
+
+  if (stream.pad != NULL && !stream.done)
+    --tagsink->num_streams_pending;
+
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+
+  if (stream.pad == NULL) {
+    GST_ERROR_OBJECT (tagsink, "pad %s:%s to release not found in pad list!",
+        GST_DEBUG_PAD_NAME (pad));
+  } else {
+    GST_INFO_OBJECT (stream.pad, "releasing pad");
+    gst_caps_replace (&stream.caps, NULL);
+    g_list_foreach (stream.tags, (GFunc) gst_tag_list_free, NULL);
+    g_list_free (stream.tags);
+    stream.tags = NULL;
+    if (stream.stream_tags) {
+      gst_tag_list_free (stream.stream_tags);
+      stream.stream_tags = NULL;
+    }
+    gst_pad_set_active (stream.pad, FALSE);
+    gst_element_remove_pad (element, stream.pad);
+    gst_object_unref (stream.pad);
+    stream.pad = NULL;
+    gst_buffer_replace (&stream.last_buffer, NULL);
+    GST_INFO_OBJECT (tagsink, "released pad");
+  }
+}
+
+static gboolean
+gst_tag_sink_start (GstBaseSink * basesink)
+{
+  GstTagSink *tagsink = GST_TAG_SINK (basesink);
+
+  GST_LOG ("here we go");
+  tagsink->async_pending = TRUE;
+  tagsink->num_streams_pending = 0;
+  return TRUE;
+}
+
+static gboolean
+gst_tag_sink_unlock (GstBaseSink * basesink)
+{
+  GstTagSink *tagsink = GST_TAG_SINK (basesink);
+
+  GST_LOG_OBJECT (tagsink, "setting shutdown flag");
+  g_atomic_int_set (&tagsink->shutdown, 1);
+  return TRUE;
+}
+
+static gboolean
+gst_tag_sink_stop (GstBaseSink * basesink)
+{
+  GstTagSink *tagsink = GST_TAG_SINK (basesink);
+
+  g_atomic_int_set (&tagsink->shutdown, 1);
+  /* wait for all pad callbacks to end by taking the lock.
+   * No dynamic (critical) new callbacks will
+   * be able to happen as we set the shutdown flag. */
+  GST_LOG_OBJECT (tagsink, "trying to acquire streams lock");
+  GST_TAG_SINK_STREAMS_LOCK (tagsink);
+  GST_LOG_OBJECT (tagsink, "streams lock taken, we can continue shutdown");
+  GST_TAG_SINK_STREAMS_UNLOCK (tagsink);
+  return TRUE;
+}
+
+/* methods used by tagreadbin */
+
+/* FIXME: make API in core to test structures/taglists for equality public */
+static gboolean
+tag_lists_are_equal (GstTagList * tags1, GstTagList * tags2)
+{
+  GstCaps *caps1, *caps2;
+  gboolean res;
+
+  /* not very efficient, but simple .. */
+  caps1 = gst_caps_new_full (gst_structure_copy (tags1), NULL);
+  caps2 = gst_caps_new_full (gst_structure_copy (tags2), NULL);
+  res = gst_caps_is_equal (caps1, caps2);
+  gst_caps_unref (caps1);
+  gst_caps_unref (caps2);
+  return res;
+}
+
+/* must be called with STREAMS_LOCK held */
+static GList *
+gst_tag_sink_stream_find_tags (GstTagSinkStream * stream, GstTagList * tags)
+{
+  GList *l;
+
+  for (l = stream->tags; l != NULL; l = l->next) {
+    if (tag_lists_are_equal (l->data, tags))
+      return l;
+  }
+
+  return NULL;
+}
+
+/* must be called with STREAMS_LOCK held */
+static void
+gst_tag_sink_remove_tags_from_all_streams (GstTagSink * tsink,
+    GstTagList * global_tags)
+{
+  GstTagList *copy;
+  guint i;
+
+  copy = gst_tag_list_copy (global_tags);
+  for (i = 0; i < tsink->streams->len; ++i) {
+    GstTagSinkStream *stream;
+    GList *node;
+
+    stream = &g_array_index (tsink->streams, GstTagSinkStream, i);
+    if ((node = gst_tag_sink_stream_find_tags (stream, copy))) {
+      gst_tag_list_free (node->data);
+      stream->tags = g_list_delete_link (stream->tags, node);
+    }
+  }
+  gst_tag_list_free (copy);
+}
+
+/* must be called with STREAMS_LOCK held */
+static gboolean
+gst_tag_sink_stream_has_tags (GstTagSinkStream * stream, GstTagList * tags)
+{
+  return (gst_tag_sink_stream_find_tags (stream, tags) != NULL);
+}
+
+/* must be called with STREAMS_LOCK held */
+static gboolean
+gst_tag_sink_tags_are_global (GstTagSink * tsink, GstTagList * tags)
+{
+  gboolean res = TRUE;
+  guint i;
+
+  /* Are the given tags common to all streams? */
+  for (i = 0; i < tsink->streams->len; ++i) {
+    GstTagSinkStream *stream;
+
+    stream = &g_array_index (tsink->streams, GstTagSinkStream, i);
+    if (!gst_tag_sink_stream_has_tags (stream, tags)) {
+      res = FALSE;
+      break;
+    }
+  }
+
+  GST_LOG_OBJECT (tsink, "tags %" GST_PTR_FORMAT " are %s", tags,
+      (res) ? "global" : "not global");
+
+  return res;
+}
+
+/* Try to determine 'global tags' common to all streams. Must be called with
+ * STREAMS_LOCK held. */
+static void
+gst_tag_sink_find_global_tags (GstTagSink * tsink)
+{
+  GList *l, *global_lists = NULL;
+  GstClockTime max_dur = 0;
+  guint i;
+
+  if (tsink->global_tags != NULL)
+    return;
+
+start:
+
+  /* Find all tags that are common to all streams */
+  for (i = 0; i < tsink->streams->len; ++i) {
+    GstTagSinkStream *stream;
+
+    stream = &g_array_index (tsink->streams, GstTagSinkStream, i);
+    for (l = stream->tags; l != NULL; l = l->next) {
+      GstTagList *tags = l->data;
+
+      /* only tags received before a taglist with a *_CODEC tag are global */
+      if (gst_tag_read_utils_list_has_codec_tag (tags))
+        break;
+
+      if (gst_tag_sink_tags_are_global (tsink, tags)) {
+        GST_DEBUG_OBJECT (tsink, "global tags: %" GST_PTR_FORMAT, tags);
+        global_lists = g_list_prepend (global_lists, gst_tag_list_copy (tags));
+        gst_tag_sink_remove_tags_from_all_streams (tsink, tags);
+        goto start;
+      }
+    }
+  }
+
+  global_lists = g_list_reverse (global_lists);
+
+  if (global_lists == NULL)
+    goto no_global_tags;
+
+  /* merge all global tags into one tag list */
+  tsink->global_tags = gst_tag_list_new ();
+  while ((l = global_lists)) {
+    gst_tag_list_insert (tsink->global_tags, l->data, GST_TAG_MERGE_APPEND);
+    gst_tag_list_free (l->data);
+    global_lists = g_list_delete_link (global_lists, l);
+  }
+
+done:
+
+  /* Find biggest duration of all streams */
+  max_dur = 0;
+  for (i = 0; i < tsink->streams->len; ++i) {
+    GstTagSinkStream *stream;
+
+    stream = &g_array_index (tsink->streams, GstTagSinkStream, i);
+    if (GST_CLOCK_TIME_IS_VALID (stream->duration))
+      max_dur = MAX (stream->duration, max_dur);
+  }
+
+  gst_tag_read_utils_list_add_duration (tsink->global_tags, max_dur);
+
+  GST_INFO_OBJECT (tsink, "global tags: %" GST_PTR_FORMAT, tsink->global_tags);
+  return;
+
+no_global_tags:
+  {
+    GST_LOG_OBJECT (tsink, "couldn't find any global tags");
+    tsink->global_tags = gst_tag_list_new ();
+    goto done;
+  }
+
+}
+
+GstTagList *
+gst_tag_sink_get_global_tags (GstTagSink * tsink)
+{
+  GstTagList *ret = NULL;
+
+  GST_TAG_SINK_STREAMS_LOCK (tsink);
+
+  gst_tag_sink_find_global_tags (tsink);
+
+  if (tsink->global_tags != NULL) {
+    ret = gst_tag_list_copy (tsink->global_tags);
+  } else {
+    ret = gst_tag_list_new ();
+  }
+
+  GST_TAG_SINK_STREAMS_UNLOCK (tsink);
+
+  return ret;
+}
+
+/* Postprocess stream tags. Must be called with the STREAMS_LOCK held */
+static void
+gst_tag_sink_stream_analyse_tags (GstTagSink * tsink, GstTagSinkStream * stream)
+{
+  GList *l;
+
+  /* have we done all of this before? */
+  if (stream->stream_tags != NULL)
+    return;
+
+  /* remove global tags first if we haven't done that yet */
+  gst_tag_sink_find_global_tags (tsink);
+
+  /* merge all remaining tags into one tag list */
+  stream->stream_tags = gst_tag_list_new ();
+  while ((l = stream->tags)) {
+    gst_tag_list_insert (stream->stream_tags, l->data, GST_TAG_MERGE_APPEND);
+    gst_tag_list_free (l->data);
+    stream->tags = g_list_delete_link (stream->tags, l);
+  }
+
+  /* if we don't have a codec tag for this stream, add one based on the caps */
+  if (!gst_tag_read_utils_list_has_codec_tag (stream->stream_tags)) {
+    gst_tag_read_utils_add_codec_tag_from_caps (stream->stream_tags,
+        stream->caps);
+  }
+
+  /* merge interesting stuff from tags into the tags */
+  gst_tag_read_utils_list_add_caps_info (stream->stream_tags, stream->caps);
+
+  /* merge duration into the tags */
+  gst_tag_read_utils_list_add_duration (stream->stream_tags, stream->duration);
+
+  GST_INFO_OBJECT (tsink, "stream tags: %" GST_PTR_FORMAT, stream->stream_tags);
+}
+
+/* will return an empty tag list for 'no tags' and NULL if no such stream
+ * (although there should always be at least a codec tag based on the caps) */
+GstTagList *
+gst_tag_sink_get_stream_tags (GstTagSink * tsink, guint num)
+{
+  GstTagSinkStream *stream;
+  GstTagList *ret;
+
+  GST_TAG_SINK_STREAMS_LOCK (tsink);
+
+  if (num >= tsink->streams->len) {
+    ret = NULL;
+    goto out;
+  }
+
+  stream = &g_array_index (tsink->streams, GstTagSinkStream, num);
+
+  if (stream->stream_tags != NULL) {
+    ret = gst_tag_list_copy (stream->stream_tags);
+    goto out;
+  }
+
+  gst_tag_sink_stream_analyse_tags (tsink, stream);
+
+  if (stream->stream_tags != NULL) {
+    ret = gst_tag_list_copy (stream->stream_tags);
+  } else {
+    ret = gst_tag_list_new ();
+    GST_LOG_OBJECT (tsink, "no stream tags for stream %u", num);
+  }
+
+out:
+
+  GST_TAG_SINK_STREAMS_UNLOCK (tsink);
+
+  return ret;
+}
diff --git a/gst/tagreading/gsttagsink.h b/gst/tagreading/gsttagsink.h
new file mode 100644
index 0000000..adf5ff3
--- /dev/null
+++ b/gst/tagreading/gsttagsink.h
@@ -0,0 +1,103 @@
+/* GStreamer tag read bin sink
+ * 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.
+ */
+
+#ifndef __GST_TAG_SINK_H__
+#define __GST_TAG_SINK_H__
+
+#include <gst/gst.h>
+#include <gst/base/gstbasesink.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_TAG_SINK               (gst_tag_sink_get_type())
+#define GST_TAG_SINK(obj)               (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAG_SINK,GstTagSink))
+#define GST_TAG_SINK_CLASS(klass)       (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAG_SINK,GstTagSinkClass))
+#define GST_IS_TAG_SINK(obj)            (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_TAG_SINK))
+#define GST_IS_TAG_SINK_CLASS(klass)    (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_TAG_SINK))
+
+#define GST_TAG_SINK_STREAMS_LOCK(tsink)    g_static_rec_mutex_lock (&tsink->streams_lock)
+#define GST_TAG_SINK_STREAMS_UNLOCK(tsink)  g_static_rec_mutex_unlock (&tsink->streams_lock)
+
+/* lock for shutdown */
+#define GST_TAG_SINK_SHUTDOWN_LOCK(tsink,label)           \
+G_STMT_START {                                            \
+  if (G_UNLIKELY (g_atomic_int_get (&tsink->shutdown)))   \
+    goto label;                                           \
+  GST_TAG_SINK_STREAMS_LOCK (tsink);                      \
+  if (G_UNLIKELY (g_atomic_int_get (&tsink->shutdown))) { \
+    GST_TAG_SINK_STREAMS_UNLOCK (tsink);                  \
+    goto label;                                           \
+  }                                                       \
+} G_STMT_END
+
+/* unlock for shutdown */
+#define GST_TAG_SINK_SHUTDOWN_UNLOCK(tsink)              \
+  GST_TAG_SINK_STREAMS_UNLOCK (tsink);                   \
+
+typedef struct _GstTagSink GstTagSink;
+typedef struct _GstTagSinkClass GstTagSinkClass;
+
+typedef struct {
+  GstPad        *pad;
+  GstCaps       *caps;
+  GList         *tags;          /* tags received through events, sorted     */
+  GstBuffer     *last_buffer;
+  guint          num_buffers;
+  gboolean       got_eos;
+  GstTagList    *stream_tags;   /* merged (non-global) tags                 */
+  GstClockTime   duration;      /* duration, or GST_CLOCK_TIME_NONE         */
+  gboolean       done;          /* got all the info we need                 */
+} GstTagSinkStream;
+
+struct _GstTagSink
+{
+  GstBaseSink       basesink;
+
+  /*< private >*/
+  gint              padcount;                                 /* ATOMIC */
+
+  /* if we are shutting down or not */
+  gint              shutdown;                                 /* ATOMIC */
+
+  gboolean          async_pending;                            /* STREAMS_LOCK */
+
+  /* number of streams for which we don't have all the info we want yet */
+  guint             num_streams_pending;                      /* STREAMS_LOCK */
+
+  GStaticRecMutex   streams_lock;
+  GArray           *streams;                                  /* STREAMS_LOCK */
+
+  GstTagList       *global_tags;                              /* STREAMS_LOCK */
+};
+
+struct _GstTagSinkClass
+{
+  GstBaseSinkClass basesink_class;
+};
+
+GType         gst_tag_sink_get_type (void);
+
+GstTagList  * gst_tag_sink_get_global_tags (GstTagSink * sink);
+
+GstTagList  * gst_tag_sink_get_stream_tags (GstTagSink * sink, guint num);
+
+
+G_END_DECLS
+
+#endif /* __GST_TAG_SINK_H__ */
diff --git a/tests/examples/Makefile.am b/tests/examples/Makefile.am
index e57d47a..9880e79 100644
--- a/tests/examples/Makefile.am
+++ b/tests/examples/Makefile.am
@@ -4,6 +4,6 @@ else
 FT2_SUBDIRS =
 endif
 
-SUBDIRS = app $(FT2_SUBDIRS) volume dynamic
+SUBDIRS = app $(FT2_SUBDIRS) volume dynamic tagreading
 
-DIST_SUBDIRS = app seek volume dynamic snapshot
+DIST_SUBDIRS = app seek volume dynamic snapshot tagreading
diff --git a/tests/examples/tagreading/Makefile.am b/tests/examples/tagreading/Makefile.am
new file mode 100644
index 0000000..2d34c54
--- /dev/null
+++ b/tests/examples/tagreading/Makefile.am
@@ -0,0 +1,6 @@
+noinst_PROGRAMS = gst-tagread
+
+gst_tagread_SOURCES = gst-tagread.c
+gst_tagread_CFLAGS = $(GST_PLUGINS_BASE_CFLAGS) $(GST_CFLAGS)
+gst_tagread_LDFLAGS = $(GST_LIBS)
+
diff --git a/tests/examples/tagreading/gst-tagread.c b/tests/examples/tagreading/gst-tagread.c
new file mode 100644
index 0000000..bb70ae5
--- /dev/null
+++ b/tests/examples/tagreading/gst-tagread.c
@@ -0,0 +1,282 @@
+/* GStreamer Command-Line Tag Reading Example
+ * 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.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <gst/gst.h>
+#include <glib/gi18n.h>
+
+static gboolean print_version = FALSE;
+
+static GstElement *tagreadbin = NULL;
+
+static void
+print_tag (const GstTagList * list, const gchar * tag, gpointer unused)
+{
+  gint i, count;
+
+  count = gst_tag_list_get_tag_size (list, tag);
+
+  for (i = 0; i < count; i++) {
+    gchar *str;
+
+    if (gst_tag_get_type (tag) == G_TYPE_STRING) {
+      if (!gst_tag_list_get_string_index (list, tag, i, &str))
+        g_assert_not_reached ();
+    } else if (gst_tag_get_type (tag) == GST_TYPE_BUFFER) {
+      GstBuffer *img;
+
+      img = gst_value_get_buffer (gst_tag_list_get_value_index (list, tag, i));
+      if (img) {
+        gchar *caps_str;
+
+        caps_str = GST_BUFFER_CAPS (img) ?
+            gst_caps_to_string (GST_BUFFER_CAPS (img)) : g_strdup ("unknown");
+        str = g_strdup_printf ("buffer of %u bytes, type: %s",
+            GST_BUFFER_SIZE (img), caps_str);
+        g_free (caps_str);
+      } else {
+        str = g_strdup ("NULL buffer");
+      }
+    } else {
+      str =
+          g_strdup_value_contents (gst_tag_list_get_value_index (list, tag, i));
+    }
+
+    if (i == 0) {
+      g_print ("%25s: %s\n", gst_tag_get_nick (tag), str);
+    } else {
+      g_print ("%25s: %s\n", "", str);
+    }
+
+    g_free (str);
+  }
+}
+
+static GList *
+extract_tags (const gchar * uri)
+{
+  GstStateChangeReturn ret;
+  gboolean got_error = FALSE;
+  gboolean got_eos = FALSE;
+  GList *list = NULL;
+
+  g_print ("\n\nURI: %s\n", uri);
+
+  if (tagreadbin == NULL) {
+    tagreadbin = gst_element_factory_make ("tagreadbin", NULL);
+    if (tagreadbin == NULL)
+      g_error ("Could not create 'tagreadbin' element, check your setup!");
+  }
+
+  g_object_set (tagreadbin, "uri", uri, NULL);
+
+  /* this might fail, but if it does there'll be an error message on the bus */
+  ret = gst_element_set_state (tagreadbin, GST_STATE_PLAYING);
+  do {
+    GstMessage *msg;
+
+    msg = gst_bus_timed_pop_filtered (GST_ELEMENT_BUS (tagreadbin),
+        5 * GST_SECOND, GST_MESSAGE_TAG | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
+
+    /* no message for 5 seconds? - bad */
+    if (msg == NULL) {
+      if (ret == GST_STATE_CHANGE_FAILURE) {
+        g_warning ("tagreadbin state change failed, but did not get an error "
+            "message! (uri: %s)", uri);
+      } else {
+        g_warning ("No tags or EOS for more than 5 seconds, bailing out! "
+            "(uri: %s)", uri);
+      }
+      break;
+    }
+
+    switch (GST_MESSAGE_TYPE (msg)) {
+      case GST_MESSAGE_TAG:{
+        GstTagList *tags = NULL;
+
+        gst_message_parse_tag (msg, &tags);
+        GST_LOG ("TAG: %" GST_PTR_FORMAT, tags);
+        list = g_list_append (list, tags);
+        break;
+      }
+      case GST_MESSAGE_EOS:
+        GST_LOG ("EOS");
+        got_eos = TRUE;
+        break;
+      case GST_MESSAGE_ERROR:{
+        GError *err = NULL;
+        gchar *dbg = NULL;
+
+        gst_message_parse_error (msg, &err, &dbg);
+        if (err) {
+          g_warning ("Failed to read tags from %s:\nError: %s\nDebug: %s\n",
+              uri, err->message, (dbg) ? dbg : "(No debug details)");
+          g_error_free (err);
+          g_free (dbg);
+          got_error = TRUE;
+        }
+        break;
+      }
+      default:
+        g_assert_not_reached ();
+        break;
+    }
+    gst_message_unref (msg);
+  }
+  while (!(got_error || got_eos));
+
+  /* In case we got here after a timeout, some applications may prefer to not
+   * set the state to NULL here and instead throw away (and leak) the
+   * tagreadbin instance and continue with a new one, in case the reason for
+   * the timeout was that the streaming thread deadlocked due to a buggy
+   * element, in which case _set_state() is likely to deadlock too). */
+  gst_element_set_state (tagreadbin, GST_STATE_NULL);
+
+  /* FIXME: should be able to re-use */
+  gst_object_unref (tagreadbin);
+  tagreadbin = NULL;
+
+  if (got_error) {
+    g_list_foreach (list, (GFunc) gst_tag_list_free, NULL);
+    g_list_free (list);
+    list = NULL;
+  }
+
+  return list;
+}
+
+static void
+process_file (const gchar * filename)
+{
+  GError *err = NULL;
+  GList *tags;
+  GDir *dir;
+  gchar *uri, *path;
+  guint i;
+
+  /* Recurse into directories */
+  if ((dir = g_dir_open (filename, 0, NULL))) {
+    const gchar *entry;
+
+    while ((entry = g_dir_read_name (dir))) {
+      gchar *path;
+
+      path = g_strconcat (filename, G_DIR_SEPARATOR_S, entry, NULL);
+      process_file (path);
+      g_free (path);
+    }
+
+    g_dir_close (dir);
+    return;
+  }
+
+  if (!g_path_is_absolute (filename)) {
+    gchar *cur_dir;
+
+    cur_dir = g_get_current_dir ();
+    path = g_build_filename (cur_dir, filename, NULL);
+    g_free (cur_dir);
+  } else {
+    path = g_strdup (filename);
+  }
+
+  uri = g_filename_to_uri (path, NULL, &err);
+  if (err) {
+    g_warning ("Couldn't convert filename to URI: %s\n", err->message);
+    g_error_free (err);
+    return;
+  }
+
+  /* now get the tags */
+  tags = extract_tags (uri);
+
+  /* .. and print them */
+  i = 0;
+  while (tags != NULL) {
+    GstTagList *taglist = tags->data;
+
+    if (i == 0)
+      g_print ("\nCONTAINER TAGS:\n");
+    else
+      g_print ("\nSTREAM #%u TAGS:\n", i);
+
+    gst_tag_list_foreach (taglist, print_tag, NULL);
+    gst_tag_list_free (taglist);
+    tags = g_list_delete_link (tags, tags);
+    ++i;
+  }
+
+  g_free (uri);
+}
+
+int
+main (int argc, char **argv)
+{
+  gchar **filenames = NULL;
+  guint num, i;
+  GError *err = NULL;
+  GOptionContext *ctx;
+  GOptionEntry options[] = {
+    {"version", 0, 0, G_OPTION_ARG_NONE, &print_version,
+        N_("Print version information and exit"), NULL},
+    {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL},
+    {NULL}
+  };
+
+#ifdef ENABLE_NLS
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+  textdomain (GETTEXT_PACKAGE);
+#endif
+
+  if (!g_thread_supported ())
+    g_thread_init (NULL);
+
+  ctx = g_option_context_new ("FILES");
+  g_option_context_add_main_entries (ctx, options, GETTEXT_PACKAGE);
+  g_option_context_add_group (ctx, gst_init_get_option_group ());
+  if (!g_option_context_parse (ctx, &argc, &argv, &err)) {
+    g_print ("Error initializing: %s\n", GST_STR_NULL (err->message));
+    exit (1);
+  }
+  g_option_context_free (ctx);
+
+  if (print_version) {
+    g_print ("gst-tagread version " VERSION "\n");
+    exit (0);
+  }
+
+  if (filenames == NULL || *filenames == NULL) {
+    g_print ("Please pass one or more filenames to gst-tagread\n\n");
+    return 1;
+  }
+
+  num = g_strv_length (filenames);
+
+  for (i = 0; i < num; ++i) {
+    process_file (filenames[i]);
+  }
+
+  g_strfreev (filenames);
+
+  return 0;
+}
