From 5bc69ce9bd487cce89fbd13277492c7106ef410e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim-Philipp=20M=C3=BCller?= Date: Mon, 6 Feb 2006 10:56:07 +0000 Subject: [PATCH] Add APE tag demuxer (#325649). Original commit message from CVS: * configure.ac: * docs/plugins/Makefile.am: * docs/plugins/gst-plugins-good-plugins-docs.sgml: * docs/plugins/gst-plugins-good-plugins-sections.txt: * docs/plugins/gst-plugins-good-plugins.hierarchy: * docs/plugins/inspect/plugin-apetag.xml: * gst/apetag/Makefile.am: * gst/apetag/gstapedemux.c: * gst/apetag/gstapedemux.h: * gst/apetag/gsttagdemux.c: * gst/apetag/gsttagdemux.h: Add APE tag demuxer (#325649). --- ChangeLog | 15 + configure.ac | 2 + docs/plugins/Makefile.am | 1 + docs/plugins/gst-plugins-good-plugins-docs.sgml | 2 + docs/plugins/gst-plugins-good-plugins-sections.txt | 8 + docs/plugins/gst-plugins-good-plugins.hierarchy | 2 + docs/plugins/inspect/plugin-apetag.xml | 20 + gst/apetag/Makefile.am | 11 + gst/apetag/gstapedemux.c | 356 ++++++ gst/apetag/gstapedemux.h | 51 + gst/apetag/gsttagdemux.c | 1344 ++++++++++++++++++++ gst/apetag/gsttagdemux.h | 105 ++ 12 files changed, 1917 insertions(+) create mode 100644 docs/plugins/inspect/plugin-apetag.xml create mode 100644 gst/apetag/Makefile.am create mode 100644 gst/apetag/gstapedemux.c create mode 100644 gst/apetag/gstapedemux.h create mode 100644 gst/apetag/gsttagdemux.c create mode 100644 gst/apetag/gsttagdemux.h diff --git a/ChangeLog b/ChangeLog index 13e2a93..781ac98 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,18 @@ +2006-02-06 Tim-Philipp Müller + + * configure.ac: + * docs/plugins/Makefile.am: + * docs/plugins/gst-plugins-good-plugins-docs.sgml: + * docs/plugins/gst-plugins-good-plugins-sections.txt: + * docs/plugins/gst-plugins-good-plugins.hierarchy: + * docs/plugins/inspect/plugin-apetag.xml: + * gst/apetag/Makefile.am: + * gst/apetag/gstapedemux.c: + * gst/apetag/gstapedemux.h: + * gst/apetag/gsttagdemux.c: + * gst/apetag/gsttagdemux.h: + Add APE tag demuxer (#325649). + 2006-02-05 Jan Schmidt * ext/gconf/gconf.c: (gst_gconf_get_default_audio_sink), diff --git a/configure.ac b/configure.ac index f2cce8c..8d494a7 100644 --- a/configure.ac +++ b/configure.ac @@ -71,6 +71,7 @@ dnl videofilter is at the top because others depend on it GST_PLUGINS_ALL="\ videofilter \ alpha \ + apetag \ auparse \ autodetect \ avi \ @@ -583,6 +584,7 @@ AC_CONFIG_FILES( Makefile gst/Makefile gst/alpha/Makefile +gst/apetag/Makefile gst/auparse/Makefile gst/autodetect/Makefile gst/avi/Makefile diff --git a/docs/plugins/Makefile.am b/docs/plugins/Makefile.am index 4dba0aa..958fb04 100644 --- a/docs/plugins/Makefile.am +++ b/docs/plugins/Makefile.am @@ -74,6 +74,7 @@ IGNORE_CFILES = # the registry won't have the element EXTRA_HFILES = \ + $(top_srcdir)/gst/apetag/gstapedemux.h \ $(top_srcdir)/gst/autodetect/gstautoaudiosink.h \ $(top_srcdir)/gst/autodetect/gstautovideosink.h \ $(top_srcdir)/gst/level/gstlevel.h \ diff --git a/docs/plugins/gst-plugins-good-plugins-docs.sgml b/docs/plugins/gst-plugins-good-plugins-docs.sgml index a469c7a..f5fa764 100644 --- a/docs/plugins/gst-plugins-good-plugins-docs.sgml +++ b/docs/plugins/gst-plugins-good-plugins-docs.sgml @@ -12,6 +12,7 @@ gst-plugins-good Elements + @@ -34,6 +35,7 @@ + diff --git a/docs/plugins/gst-plugins-good-plugins-sections.txt b/docs/plugins/gst-plugins-good-plugins-sections.txt index 6cdef5e..7a0524d 100644 --- a/docs/plugins/gst-plugins-good-plugins-sections.txt +++ b/docs/plugins/gst-plugins-good-plugins-sections.txt @@ -1,4 +1,12 @@
+element-apedemux +GstApeDemux +apedemux + +GstApeDemuxClass +
+ +
element-autoaudiosink GstAutoAudioSink autoaudiosink diff --git a/docs/plugins/gst-plugins-good-plugins.hierarchy b/docs/plugins/gst-plugins-good-plugins.hierarchy index 9c40f14..4baea48 100644 --- a/docs/plugins/gst-plugins-good-plugins.hierarchy +++ b/docs/plugins/gst-plugins-good-plugins.hierarchy @@ -104,6 +104,8 @@ GObject GstAviDemux GstAuParse GstAlpha + GstTagDemux + GstApeDemux GstPlugin GstRegistry GstPadTemplate diff --git a/docs/plugins/inspect/plugin-apetag.xml b/docs/plugins/inspect/plugin-apetag.xml new file mode 100644 index 0000000..18a3775 --- /dev/null +++ b/docs/plugins/inspect/plugin-apetag.xml @@ -0,0 +1,20 @@ + + apetag + APEv1/2 tag reader + ../../gst/apedemux/.libs/libgstapetag.so + libgstapetag.so + 0.10.1.1 + LGPL + gst-plugins-good + GStreamer Good Plug-ins CVS/prerelease + Unknown package origin + + + apedemux + APE tag demuxer + Codec/Demuxer/Metadata + Read and output APE tags while demuxing the contents + Ronald Bultje <rbultje@ronald.bitfreak.net>, Tim-Philipp Müller <tim centricular net> + + + \ No newline at end of file diff --git a/gst/apetag/Makefile.am b/gst/apetag/Makefile.am new file mode 100644 index 0000000..d249a84 --- /dev/null +++ b/gst/apetag/Makefile.am @@ -0,0 +1,11 @@ +plugin_LTLIBRARIES = libgstapetag.la + +libgstapetag_la_SOURCES = gstapedemux.c gsttagdemux.c +libgstapetag_la_CFLAGS = \ + -I$(top_srcdir)/gst-libs \ + $(GST_CFLAGS) +libgstapetag_la_LIBADD = \ + $(GST_LIBS) +libgstapetag_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) + +noinst_HEADERS = gstapedemux.h gsttagdemux.h diff --git a/gst/apetag/gstapedemux.c b/gst/apetag/gstapedemux.c new file mode 100644 index 0000000..37e9224 --- /dev/null +++ b/gst/apetag/gstapedemux.c @@ -0,0 +1,356 @@ +/* GStreamer APEv1/2 tag reader + * Copyright (C) 2004 Ronald Bultje + * Copyright (C) 2006 Tim-Philipp Müller + * + * 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-apedemux + * @short_description: reads tag information from APE tag data blocks and + * outputs them as GStreamer tag messages and events. + * + * + * + * apedemux accepts data streams with APE tags at the start or at the end + * (or both). The mime type of the data between the tag blocks is detected + * using typefind functions, and the appropriate output mime type set on + * outgoing buffers. + * + * + * The element is only able to read APE tags at the end of a stream from + * a seekable stream, ie. when get_range mode is supported by the upstream + * elements. If get_range operation is available, apedemux makes it available + * downstream. This means that elements which require get_range mode, such as + * wavparse or musepackdec, can operate on files containing APE tag + * information. + * + * Example launch line + * + * + * gst-launch -t filesrc location=file.mpc ! apedemux ! fakesink + * + * This pipeline should read any available APE tag information and output it. + * The contents of the file inside the APE tag regions should be detected, and + * the appropriate mime type set on buffers produced from apedemux. + * + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "gstapedemux.h" + +#include + +#define APE_VERSION_MAJOR(ver) ((ver)/1000) + +GST_DEBUG_CATEGORY (apedemux_debug); +#define GST_CAT_DEFAULT (apedemux_debug) + +static GstElementDetails gst_ape_demux_details = +GST_ELEMENT_DETAILS ("APE tag demuxer", + "Codec/Demuxer/Metadata", + "Read and output APE tags while demuxing the contents", + "Ronald Bultje , " + "Tim-Philipp Müller "); + +static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("application/x-apetag") + ); + +static gboolean gst_ape_demux_identify_tag (GstTagDemux * demux, + GstBuffer * buffer, gboolean start_tag, guint * tag_size); +static GstTagDemuxResult gst_ape_demux_parse_tag (GstTagDemux * demux, + GstBuffer * buffer, gboolean start_tag, guint * tag_size, + GstTagList ** tags); + +GST_BOILERPLATE (GstApeDemux, gst_ape_demux, GstTagDemux, GST_TYPE_TAG_DEMUX) + + static void gst_ape_demux_base_init (gpointer klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&sink_factory)); + + gst_element_class_set_details (element_class, &gst_ape_demux_details); + + GST_DEBUG_CATEGORY_INIT (apedemux_debug, "apedemux", 0, + "GStreamer APE tag demuxer"); +} + +static void +gst_ape_demux_class_init (GstApeDemuxClass * klass) +{ + GstTagDemuxClass *tagdemux_class; + + tagdemux_class = GST_TAG_DEMUX_CLASS (klass); + + tagdemux_class->identify_tag = GST_DEBUG_FUNCPTR (gst_ape_demux_identify_tag); + tagdemux_class->parse_tag = GST_DEBUG_FUNCPTR (gst_ape_demux_parse_tag); +} + +static void +gst_ape_demux_init (GstApeDemux * apedemux, GstApeDemuxClass * gclass) +{ + GstTagDemux *tagdemux = GST_TAG_DEMUX (apedemux); + + tagdemux->min_start_size = 32; + tagdemux->min_end_size = 32; + + tagdemux->prefer_start_tag = TRUE; +} + +static const struct _GstApeDemuxTagTableEntry +{ + const gchar *ape_tag; + const gchar *gst_tag; +} tag_table[] = { + { + "replaygain_track_gain", GST_TAG_TRACK_GAIN}, { + "replaygain_track_peak", GST_TAG_TRACK_PEAK}, { + "replaygain_album_gain", GST_TAG_ALBUM_GAIN}, { + "replaygain_album_peak", GST_TAG_ALBUM_PEAK}, { + "title", GST_TAG_TITLE}, { + "artist", GST_TAG_ARTIST}, { + "album", GST_TAG_ALBUM}, { + "comment", GST_TAG_COMMENT}, { + "comments", GST_TAG_COMMENT}, { + "copyright", GST_TAG_COPYRIGHT}, { + "genre", GST_TAG_GENRE}, { + "isrc", GST_TAG_ISRC}, { + "track", GST_TAG_TRACK_NUMBER}, { + "year", GST_TAG_DATE} +}; + +static gboolean +ape_demux_get_gst_tag_from_tag (const gchar * ape_tag, + const gchar ** gst_tag, GType * gst_tag_type) +{ + gint i; + + for (i = 0; i < G_N_ELEMENTS (tag_table); ++i) { + if (g_ascii_strcasecmp (tag_table[i].ape_tag, ape_tag) == 0) { + *gst_tag = tag_table[i].gst_tag; + *gst_tag_type = gst_tag_get_type (tag_table[i].gst_tag); + return TRUE; + } + } + + GST_WARNING ("Could not map APE tag '%s' to a GStreamer tag", ape_tag); + return FALSE; +} + +static GstTagList * +ape_demux_parse_tags (const guint8 * data, gint size) +{ + GstTagList *taglist = gst_tag_list_new (); + gboolean have_tag = FALSE; + + GST_LOG ("Reading tags from chunk of size %u bytes", size); + + /* get rid of header/footer */ + if (size >= 32 && memcmp (data, "APETAGEX", 8) == 0) { + data += 32; + size -= 32; + } + if (size > 32 && memcmp (data + size - 32, "APETAGEX", 8) == 0) { + size -= 32; + } + + /* read actual tags - at least 10 bytes for tag header */ + while (size >= 10) { + guint len, n = 8; + gchar *tag, *val; + const gchar *gst_tag; + GType gst_tag_type; + + /* find tag type and size */ + len = GST_READ_UINT32_LE (data); + while (n < size && data[n] != 0x0) + n++; + if (n == size) + break; + g_assert (data[n] == 0x0); + n++; + if (size - n < len) + break; + + /* read */ + tag = g_strndup ((gchar *) data + 8, n - 9); + val = g_strndup ((gchar *) data + n, len); + + if (ape_demux_get_gst_tag_from_tag (tag, &gst_tag, &gst_tag_type)) { + GValue v = { 0, }; + + switch (gst_tag_type) { + case G_TYPE_INT: + g_value_init (&v, G_TYPE_INT); + g_value_set_int (&v, atoi (val)); + break; + case G_TYPE_UINT: + g_value_init (&v, G_TYPE_UINT); + g_value_set_uint (&v, (guint) atof (val)); /* hmmm */ + break; + case G_TYPE_STRING: + g_value_init (&v, G_TYPE_STRING); + g_value_set_string (&v, val); + break; + case G_TYPE_DOUBLE: + g_value_init (&v, G_TYPE_DOUBLE); + g_value_set_double (&v, atof (val)); + break; + default:{ + if (gst_tag_type == GST_TYPE_DATE) { + GDate *date = g_date_new_dmy (1, 1, atoi (val)); + + g_value_init (&v, GST_TYPE_DATE); + gst_value_set_date (&v, date); + g_date_free (date); + } else { + GST_WARNING ("Unhandled tag type '%s' for tag '%s'", + g_type_name (gst_tag_type), gst_tag); + } + break; + } + } + if (G_VALUE_TYPE (&v) != 0) { + gst_tag_list_add_values (taglist, GST_TAG_MERGE_APPEND, + gst_tag, &v, NULL); + g_value_unset (&v); + have_tag = TRUE; + } + } + GST_DEBUG ("Read tag %s: %s", tag, val); + g_free (tag); + g_free (val); + + /* move data pointer */ + size -= len + n; + data += len + n; + } + + GST_DEBUG ("Taglist: %" GST_PTR_FORMAT, taglist); + return taglist; +} + +static gboolean +gst_ape_demux_identify_tag (GstTagDemux * demux, GstBuffer * buffer, + gboolean start_tag, guint * tag_size) +{ + if (memcmp (GST_BUFFER_DATA (buffer), "APETAGEX", 8) != 0) { + GST_DEBUG_OBJECT (demux, "No APETAGEX marker at %s - not an APE file", + (start_tag) ? "start" : "end"); + return FALSE; + } + + *tag_size = GST_READ_UINT32_LE (GST_BUFFER_DATA (buffer) + 12); + + /* size is without header, so add 32 to account for that */ + *tag_size += 32; + + return TRUE; +} + +static GstTagDemuxResult +gst_ape_demux_parse_tag (GstTagDemux * demux, GstBuffer * buffer, + gboolean start_tag, guint * tag_size, GstTagList ** tags) +{ + const guint8 *data; + const guint8 *footer; + gboolean have_header; + gboolean end_tag = !start_tag; + guint version, footer_size; + + GST_LOG_OBJECT (demux, "Parsing buffer of size %u", GST_BUFFER_SIZE (buffer)); + + data = GST_BUFFER_DATA (buffer); + footer = GST_BUFFER_DATA (buffer) + GST_BUFFER_SIZE (buffer) - 32; + + GST_LOG_OBJECT (demux, "Checking for footer at offset 0x%04x", + (guint) (footer - data)); + if (footer > data && memcmp (footer, "APETAGEX", 8) == 0) { + GST_DEBUG_OBJECT (demux, "Found footer"); + footer_size = 32; + } else { + GST_DEBUG_OBJECT (demux, "No footer"); + footer_size = 0; + } + + /* APE tags at the end must have a footer */ + if (end_tag && footer_size == 0) { + GST_WARNING_OBJECT (demux, "Tag at end of file without footer!"); + return GST_TAG_DEMUX_RESULT_BROKEN_TAG; + } + + /* don't trust the header/footer flags, better detect them ourselves */ + have_header = (memcmp (data, "APETAGEX", 8) == 0); + + if (start_tag && !have_header) { + GST_DEBUG_OBJECT (demux, "Tag at beginning of file without header!"); + return GST_TAG_DEMUX_RESULT_BROKEN_TAG; + } + + if (end_tag && !have_header) { + GST_DEBUG_OBJECT (demux, "Tag at end of file has no header (APEv1)"); + *tag_size -= 32; /* adjust tag size */ + } + + if (have_header) { + version = GST_READ_UINT32_LE (data + 8); + } else { + version = GST_READ_UINT32_LE (footer + 8); + } + + /* skip header */ + if (have_header) { + data += 32; + } + + GST_DEBUG_OBJECT (demux, "APE tag with version %u, size %u at offset 0x%08" + G_GINT64_MODIFIER "x", version, *tag_size, + GST_BUFFER_OFFSET (buffer) + ((have_header) ? 0 : 32)); + + if (APE_VERSION_MAJOR (version) != 1 && APE_VERSION_MAJOR (version) != 2) { + GST_WARNING ("APE tag is version %u.%03u, but decoder only supports " + "v1 or v2. Ignoring.", APE_VERSION_MAJOR (version), version % 1000); + return GST_TAG_DEMUX_RESULT_OK; + } + + *tags = ape_demux_parse_tags (data, *tag_size - footer_size); + + return GST_TAG_DEMUX_RESULT_OK; +} + +static gboolean +plugin_init (GstPlugin * plugin) +{ + return gst_element_register (plugin, "apedemux", + GST_RANK_PRIMARY, GST_TYPE_APE_DEMUX); +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + "apetag", + "APEv1/2 tag reader", + plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/gst/apetag/gstapedemux.h b/gst/apetag/gstapedemux.h new file mode 100644 index 0000000..93f390a --- /dev/null +++ b/gst/apetag/gstapedemux.h @@ -0,0 +1,51 @@ +/* GStreamer APEv1/2 tag reader + * Copyright (C) 2004 Ronald Bultje + * Copyright (C) 2006 Tim-Philipp Müller + * + * 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_APE_DEMUX_H__ +#define __GST_APE_DEMUX_H__ + +#include + +G_BEGIN_DECLS + +#define GST_TYPE_APE_DEMUX (gst_ape_demux_get_type()) +#define GST_APE_DEMUX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_APE_DEMUX,GstApeDemux)) +#define GST_APE_DEMUX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_APE_DEMUX,GstApeDemuxClass)) +#define GST_IS_APE_DEMUX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_APE_DEMUX)) +#define GST_IS_APE_DEMUX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_APE_DEMUX)) + +typedef struct _GstApeDemux GstApeDemux; +typedef struct _GstApeDemuxClass GstApeDemuxClass; + +struct _GstApeDemux +{ + GstTagDemux tagdemux; +}; + +struct _GstApeDemuxClass +{ + GstTagDemuxClass parent_class; +}; + +GType gst_ape_demux_get_type (void); + +G_END_DECLS + +#endif /* __GST_APE_DEMUX_H__ */ diff --git a/gst/apetag/gsttagdemux.c b/gst/apetag/gsttagdemux.c new file mode 100644 index 0000000..9bb43c8 --- /dev/null +++ b/gst/apetag/gsttagdemux.c @@ -0,0 +1,1344 @@ +/* GStreamer Base Class for Tag Demuxing + * Copyright (C) 2005 Jan Schmidt + * Copyright (C) 2006 Tim-Philipp Müller + * + * 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:gsttagdemux + * @see_also: GstApeDemux + * @short_description: Base class for demuxing tags that are in chunks + * directly at the beginning or at the end of a file + * + * + * + * Provides a base class for demuxing tags at the beginning or end of a + * stream and handles things like typefinding, querying, seeking, and + * different modes of operation (chain-based, pull_range-based, and providing + * downstream elements with random access if upstream supports that). The tag + * is stripped from the output, and all offsets are adjusted for the tag + * sizes, so that to the downstream element the stream will appear as if + * there was no tag at all. Also, once the tag has been parsed, GstTagDemux + * will try to determine the media type of the resulting stream and add a + * source pad with the appropriate caps in order to facilitate auto-plugging. + * + * Deriving from GstTagDemux + * + * Subclasses have to do four things: + * + * + * In their base init function, they must add a pad template for the sink + * pad to the element class, describing the media type they can parse in + * the caps of the pad template. + * + * + * In their class init function, they must override + * GST_TAG_DEMUX_CLASS(demux_klass)->identify_tag with their own identify + * function. + * + * + * In their class init function, they must override + * GST_TAG_DEMUX_CLASS(demux_klass)->parse_tag with their own parse + * function. + * + * + * In their instance init function, they must set + * GST_TAG_DEMUX(demux)->min_start_size and/or + * GST_TAG_DEMUX(demux)->min_end_size to the minimum size required for the + * identify function to decide whether the stream has a supported tag or + * not. A class parsing ID3v1 tags, for example, would set min_end_size to + * 128 bytes. + * + * + * + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gsttagdemux.h" + +#include +#include + +typedef enum +{ + GST_TAG_DEMUX_READ_START_TAG, + GST_TAG_DEMUX_TYPEFINDING, + GST_TAG_DEMUX_STREAMING +} GstTagDemuxState; + +struct _GstTagDemuxPrivate +{ + GstPad *srcpad; + GstPad *sinkpad; + + /* Number of bytes to remove from the + * start of file (tag at beginning) */ + guint strip_start; + + /* Number of bytes to remove from the + * end of file (tag at end) */ + guint strip_end; + + gint64 upstream_size; + + GstTagDemuxState state; + GstBuffer *collect; + GstCaps *src_caps; + + GstTagList *event_tags; + GstTagList *parsed_tags; + gboolean send_tag_event; +}; + +/* Require at least 8kB of data before we attempt typefind. + * Seems a decent value based on test files + * 40kB is massive overkill for the maximum, I think, but it + * doesn't do any harm (tpm: increased to 64kB after watching + * typefinding fail on a wavpack file that needed 42kB to succeed) */ +#define TYPE_FIND_MIN_SIZE 8192 +#define TYPE_FIND_MAX_SIZE 65536 + +GST_DEBUG_CATEGORY_STATIC (tagdemux_debug); +#define GST_CAT_DEFAULT (tagdemux_debug) + +static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS ("ANY") + ); + +static void gst_tag_demux_dispose (GObject * object); + +static GstFlowReturn gst_tag_demux_chain (GstPad * pad, GstBuffer * buf); + +static gboolean gst_tag_demux_src_activate_pull (GstPad * pad, gboolean active); +static GstFlowReturn gst_tag_demux_read_range (GstTagDemux * tagdemux, + guint64 offset, guint length, GstBuffer ** buffer); + +static gboolean gst_tag_demux_src_checkgetrange (GstPad * srcpad); +static GstFlowReturn gst_tag_demux_src_getrange (GstPad * srcpad, + guint64 offset, guint length, GstBuffer ** buffer); + +static gboolean gst_tag_demux_add_srcpad (GstTagDemux * tagdemux, + GstCaps * new_caps); +static gboolean gst_tag_demux_remove_srcpad (GstTagDemux * tagdemux); + +static gboolean gst_tag_demux_srcpad_event (GstPad * pad, GstEvent * event); +static gboolean gst_tag_demux_sink_activate (GstPad * sinkpad); +static GstStateChangeReturn gst_tag_demux_change_state (GstElement * element, + GstStateChange transition); +static gboolean gst_tag_demux_pad_query (GstPad * pad, GstQuery * query); +static const GstQueryType *gst_tag_demux_get_query_types (GstPad * pad); +static gboolean gst_tag_demux_get_upstream_size (GstTagDemux * tagdemux); +static GstCaps *gst_tag_demux_do_typefind (GstTagDemux * tagdemux, + GstBuffer * buffer); +static void gst_tag_demux_send_tag_event (GstTagDemux * tagdemux); + +static void gst_tag_demux_base_init (gpointer g_class); +static void gst_tag_demux_class_init (gpointer g_class, gpointer d); +static void gst_tag_demux_init (GstTagDemux * obj, GstTagDemuxClass * klass); + +static gpointer parent_class; /* NULL */ + +/* Cannot use boilerplate macros here because we want the abstract flag */ +GType +gst_tag_demux_get_type (void) +{ + static GType object_type; /* 0 */ + + if (object_type == 0) { + static const GTypeInfo object_info = { + sizeof (GstTagDemuxClass), + gst_tag_demux_base_init, + NULL, /* base_finalize */ + gst_tag_demux_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (GstTagDemux), + 0, /* n_preallocs */ + (GInstanceInitFunc) gst_tag_demux_init + }; + + /* FIXME: register as 'GstApeDemuxBase' for now */ + object_type = g_type_register_static (GST_TYPE_ELEMENT, + "GstTagDemux", &object_info, G_TYPE_FLAG_ABSTRACT); + } + + return object_type; +} + +static void +gst_tag_demux_base_init (gpointer klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&src_factory)); + + GST_DEBUG_CATEGORY_INIT (tagdemux_debug, "tagdemux", 0, + "GStreamer tag demux base class"); +} + +static void +gst_tag_demux_class_init (gpointer klass, gpointer d) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + parent_class = g_type_class_peek_parent (klass); + + gobject_class->dispose = gst_tag_demux_dispose; + + element_class->change_state = GST_DEBUG_FUNCPTR (gst_tag_demux_change_state); + + g_type_class_add_private (klass, sizeof (GstTagDemuxPrivate)); +} + +static void +gst_tag_demux_reset (GstTagDemux * tagdemux) +{ + tagdemux->priv->strip_start = 0; + tagdemux->priv->strip_end = 0; + tagdemux->priv->upstream_size = -1; + tagdemux->priv->state = GST_TAG_DEMUX_READ_START_TAG; + tagdemux->priv->send_tag_event = FALSE; + + gst_buffer_replace (&(tagdemux->priv->collect), NULL); + gst_caps_replace (&(tagdemux->priv->src_caps), NULL); + + gst_tag_demux_remove_srcpad (tagdemux); + + if (tagdemux->priv->event_tags) { + gst_tag_list_free (tagdemux->priv->event_tags); + tagdemux->priv->event_tags = NULL; + } + if (tagdemux->priv->parsed_tags) { + gst_tag_list_free (tagdemux->priv->parsed_tags); + tagdemux->priv->parsed_tags = NULL; + } +} + +static void +gst_tag_demux_init (GstTagDemux * demux, GstTagDemuxClass * gclass) +{ + GstElementClass *element_klass = GST_ELEMENT_CLASS (gclass); + GstPadTemplate *tmpl; + + demux->priv = g_type_instance_get_private ((GTypeInstance *) demux, + GST_TYPE_TAG_DEMUX); + + /* subclasses must set these to sane values */ + demux->min_start_size = 0; + demux->min_end_size = 0; + + /* subclasses may set these, we just set defaults */ + demux->prefer_start_tag = TRUE; + + tmpl = gst_element_class_get_pad_template (element_klass, "sink"); + if (tmpl) { + demux->priv->sinkpad = gst_pad_new_from_template (tmpl, "sink"); + + gst_pad_set_activate_function (demux->priv->sinkpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_sink_activate)); + gst_pad_set_chain_function (demux->priv->sinkpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_chain)); + gst_element_add_pad (GST_ELEMENT (demux), demux->priv->sinkpad); + } + + gst_tag_demux_reset (demux); +} + +static void +gst_tag_demux_dispose (GObject * object) +{ + GstTagDemux *tagdemux = GST_TAG_DEMUX (object); + + gst_tag_demux_reset (tagdemux); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static gboolean +gst_tag_demux_add_srcpad (GstTagDemux * tagdemux, GstCaps * new_caps) +{ + GstPad *srcpad = NULL; + + if (tagdemux->priv->src_caps == NULL || + !gst_caps_is_equal (new_caps, tagdemux->priv->src_caps)) { + + gst_caps_replace (&(tagdemux->priv->src_caps), new_caps); + if (tagdemux->priv->srcpad != NULL) { + GST_DEBUG_OBJECT (tagdemux, "Changing src pad caps to %" GST_PTR_FORMAT, + tagdemux->priv->src_caps); + + gst_pad_set_caps (tagdemux->priv->srcpad, tagdemux->priv->src_caps); + } + } else { + /* Caps never changed */ + gst_caps_unref (new_caps); + } + + if (tagdemux->priv->srcpad == NULL) { + srcpad = tagdemux->priv->srcpad = + gst_pad_new_from_template (gst_element_class_get_pad_template + (GST_ELEMENT_GET_CLASS (tagdemux), "src"), "src"); + g_return_val_if_fail (tagdemux->priv->srcpad != NULL, FALSE); + + gst_pad_set_query_type_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_get_query_types)); + gst_pad_set_query_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_pad_query)); + gst_pad_set_event_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_srcpad_event)); + gst_pad_set_activatepull_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_src_activate_pull)); + gst_pad_set_checkgetrange_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_src_checkgetrange)); + gst_pad_set_getrange_function (tagdemux->priv->srcpad, + GST_DEBUG_FUNCPTR (gst_tag_demux_src_getrange)); + + gst_pad_use_fixed_caps (tagdemux->priv->srcpad); + + if (tagdemux->priv->src_caps) + gst_pad_set_caps (tagdemux->priv->srcpad, tagdemux->priv->src_caps); + + GST_DEBUG_OBJECT (tagdemux, "Adding src pad with caps %" GST_PTR_FORMAT, + tagdemux->priv->src_caps); + + gst_object_ref (tagdemux->priv->srcpad); + if (!(gst_element_add_pad (GST_ELEMENT (tagdemux), tagdemux->priv->srcpad))) + return FALSE; + gst_element_no_more_pads (GST_ELEMENT (tagdemux)); + } + + return TRUE; +} + +static gboolean +gst_tag_demux_remove_srcpad (GstTagDemux * demux) +{ + gboolean res = TRUE; + + if (demux->priv->srcpad != NULL) { + GST_DEBUG_OBJECT (demux, "Removing src pad"); + res = gst_element_remove_pad (GST_ELEMENT (demux), demux->priv->srcpad); + g_return_val_if_fail (res != FALSE, FALSE); + gst_object_unref (demux->priv->srcpad); + demux->priv->srcpad = NULL; + } + + return res; +}; + +static gboolean +gst_tag_demux_trim_buffer (GstTagDemux * tagdemux, GstBuffer ** buf_ref) +{ + GstBuffer *buf = *buf_ref; + + guint trim_start = 0; + guint out_size = GST_BUFFER_SIZE (buf); + guint64 out_offset = GST_BUFFER_OFFSET (buf); + gboolean need_sub = FALSE; + + /* Adjust offset and length */ + if (!GST_BUFFER_OFFSET_IS_VALID (buf)) { + /* Can't change anything without an offset */ + return TRUE; + } + + /* If the buffer crosses the tag at the end of file, trim it */ + if (tagdemux->priv->strip_end > 0) { + if (gst_tag_demux_get_upstream_size (tagdemux)) { + guint64 v1tag_offset = + tagdemux->priv->upstream_size - tagdemux->priv->strip_end; + + if (out_offset >= v1tag_offset) { + GST_DEBUG_OBJECT (tagdemux, "Buffer is past the end of the data"); + goto no_out_buffer; + } + + if (out_offset + out_size > v1tag_offset) { + out_size = v1tag_offset - out_offset; + need_sub = TRUE; + } + } + } + + if (tagdemux->priv->strip_start > 0) { + /* If the buffer crosses the tag at the start of file, trim it */ + if (out_offset <= tagdemux->priv->strip_start) { + if (out_offset + out_size <= tagdemux->priv->strip_start) { + GST_DEBUG_OBJECT (tagdemux, "Buffer is before the start of the data"); + goto no_out_buffer; + } + + trim_start = tagdemux->priv->strip_start - out_offset; + out_size -= trim_start; + out_offset = 0; + } else { + out_offset -= tagdemux->priv->strip_start; + } + need_sub = TRUE; + } + + g_assert (out_size > 0); + + if (need_sub == TRUE) { + if (out_size != GST_BUFFER_SIZE (buf) || !gst_buffer_is_writable (buf)) { + GstBuffer *sub; + + GST_DEBUG_OBJECT (tagdemux, "Sub-buffering to trim size %d offset %" + G_GINT64_FORMAT " to %d offset %" G_GINT64_FORMAT, + GST_BUFFER_SIZE (buf), GST_BUFFER_OFFSET (buf), out_size, out_offset); + + sub = gst_buffer_create_sub (buf, trim_start, out_size); + g_return_val_if_fail (sub != NULL, FALSE); + gst_buffer_unref (buf); + *buf_ref = buf = sub; + } else { + GST_DEBUG_OBJECT (tagdemux, "Adjusting buffer from size %d offset %" + G_GINT64_FORMAT " to %d offset %" G_GINT64_FORMAT, + GST_BUFFER_SIZE (buf), GST_BUFFER_OFFSET (buf), out_size, out_offset); + } + + GST_BUFFER_OFFSET (buf) = out_offset; + GST_BUFFER_OFFSET_END (buf) = out_offset + out_size; + gst_buffer_set_caps (buf, tagdemux->priv->src_caps); + } + + return TRUE; + +no_out_buffer: + gst_buffer_unref (buf); + *buf_ref = NULL; + return TRUE; +} + +static void +gst_tag_demux_chain_parse_tag (GstTagDemux * demux, GstBuffer * collect) +{ + GstTagDemuxResult parse_ret; + GstTagDemuxClass *klass; + guint tagsize = 0; + guint available; + + klass = GST_TAG_DEMUX_CLASS (G_OBJECT_GET_CLASS (demux)); + + /* If we receive a buffer that's from the middle of the file, + * we can't read tags so move to typefinding */ + if (GST_BUFFER_OFFSET (collect) != 0) { + GST_DEBUG_OBJECT (demux, "Received buffer from non-zero offset %" + G_GINT64_FORMAT ". Can't read tags", GST_BUFFER_OFFSET (collect)); + demux->priv->state = GST_TAG_DEMUX_TYPEFINDING; + return; + } + + g_assert (klass->identify_tag != NULL); + g_assert (klass->parse_tag != NULL); + + available = GST_BUFFER_SIZE (collect); + + if (available < demux->min_start_size) { + GST_DEBUG_OBJECT (demux, "Only %u bytes available, but %u needed " + "to identify tag", available, demux->min_start_size); + return; /* wait for more data */ + } + + if (!klass->identify_tag (demux, collect, TRUE, &tagsize)) { + GST_DEBUG_OBJECT (demux, "Could not identify start tag"); + demux->priv->state = GST_TAG_DEMUX_TYPEFINDING; + return; + } + + GST_DEBUG_OBJECT (demux, "Identified tag, size = %u bytes", tagsize); + + do { + GstTagList *tags = NULL; + guint newsize, saved_size; + + demux->priv->strip_start = tagsize; + + if (available < tagsize) { + GST_DEBUG_OBJECT (demux, "Only %u bytes available, but %u needed " + "to parse tag", available, tagsize); + return; /* wait for more data */ + } + + saved_size = GST_BUFFER_SIZE (collect); + GST_BUFFER_SIZE (collect) = tagsize; + newsize = tagsize; + + parse_ret = klass->parse_tag (demux, collect, TRUE, &newsize, &tags); + + GST_BUFFER_SIZE (collect) = saved_size; + + switch (parse_ret) { + case GST_TAG_DEMUX_RESULT_OK: + demux->priv->strip_start = newsize; + demux->priv->parsed_tags = tags; + GST_DEBUG_OBJECT (demux, "Read start tag of size %u", newsize); + break; + case GST_TAG_DEMUX_RESULT_BROKEN_TAG: + demux->priv->strip_start = newsize; + demux->priv->parsed_tags = tags; + GST_WARNING_OBJECT (demux, "Ignoring broken start tag of size %d", + demux->priv->strip_start); + break; + case GST_TAG_DEMUX_RESULT_AGAIN: + GST_DEBUG_OBJECT (demux, "Re-parse, this time with %u bytes", newsize); + g_assert (newsize != tagsize); + tagsize = newsize; + break; + } + } while (parse_ret == GST_TAG_DEMUX_RESULT_AGAIN); + + GST_LOG_OBJECT (demux, "Parsed tag. Proceeding to typefinding"); + demux->priv->state = GST_TAG_DEMUX_TYPEFINDING; + demux->priv->send_tag_event = TRUE; +} + +static GstFlowReturn +gst_tag_demux_chain (GstPad * pad, GstBuffer * buf) +{ + GstTagDemux *demux; + + demux = GST_TAG_DEMUX (GST_PAD_PARENT (pad)); + g_return_val_if_fail (GST_IS_TAG_DEMUX (demux), GST_FLOW_ERROR); + + if (demux->priv->collect == NULL) { + demux->priv->collect = buf; + } else { + demux->priv->collect = gst_buffer_join (demux->priv->collect, buf); + } + buf = NULL; + + switch (demux->priv->state) { + case GST_TAG_DEMUX_READ_START_TAG: + gst_tag_demux_chain_parse_tag (demux, demux->priv->collect); + if (demux->priv->state != GST_TAG_DEMUX_TYPEFINDING) + break; + /* Fall-through */ + case GST_TAG_DEMUX_TYPEFINDING:{ + GstCaps *caps; + GstBuffer *typefind_buf = NULL; + + if (GST_BUFFER_SIZE (demux->priv->collect) < TYPE_FIND_MIN_SIZE) + break; /* Go get more data first */ + + GST_DEBUG_OBJECT (demux, "Typefinding with size %d", + GST_BUFFER_SIZE (demux->priv->collect)); + + /* Trim the buffer and adjust offset for typefinding */ + typefind_buf = demux->priv->collect; + gst_buffer_ref (typefind_buf); + if (!gst_tag_demux_trim_buffer (demux, &typefind_buf)) + return GST_FLOW_ERROR; + + caps = gst_tag_demux_do_typefind (demux, typefind_buf); + + if (caps == NULL) { + if (GST_BUFFER_SIZE (typefind_buf) < TYPE_FIND_MAX_SIZE) { + /* Just break for more data */ + gst_buffer_unref (typefind_buf); + return GST_FLOW_OK; + } + + /* We failed typefind */ + GST_ELEMENT_ERROR (demux, STREAM, TYPE_NOT_FOUND, (NULL), (NULL)); + gst_buffer_unref (typefind_buf); + gst_buffer_unref (demux->priv->collect); + demux->priv->collect = NULL; + return GST_FLOW_ERROR; + } + gst_buffer_unref (typefind_buf); + + if (!gst_tag_demux_add_srcpad (demux, caps)) { + GST_DEBUG_OBJECT (demux, "Failed to add srcpad"); + gst_caps_unref (caps); + goto error; + } + gst_caps_unref (caps); + + /* Move onto streaming and fall-through to push out existing + * data */ + demux->priv->state = GST_TAG_DEMUX_STREAMING; + /* fall-through */ + } + case GST_TAG_DEMUX_STREAMING:{ + GstBuffer *outbuf = NULL; + + if (demux->priv->send_tag_event) { + gst_tag_demux_send_tag_event (demux); + demux->priv->send_tag_event = FALSE; + } + + /* Trim the buffer and adjust offset */ + if (demux->priv->collect) { + outbuf = demux->priv->collect; + demux->priv->collect = NULL; + if (!gst_tag_demux_trim_buffer (demux, &outbuf)) + return GST_FLOW_ERROR; + } + if (outbuf) { + if (G_UNLIKELY (demux->priv->srcpad == NULL)) { + gst_buffer_unref (outbuf); + return GST_FLOW_ERROR; + } + + GST_DEBUG_OBJECT (demux, "Pushing buffer %p", outbuf); + /* gst_util_dump_mem (GST_BUFFER_DATA (outbuf), + GST_BUFFER_SIZE (outbuf)); */ + return gst_pad_push (demux->priv->srcpad, outbuf); + } + } + } + return GST_FLOW_OK; + +error: + GST_DEBUG_OBJECT (demux, "error in chain function"); + + return GST_FLOW_ERROR; +} + +static gboolean +gst_tag_demux_get_upstream_size (GstTagDemux * tagdemux) +{ + GstQuery *query; + GstPad *peer = NULL; + GstFormat format; + gint64 result; + gboolean res = FALSE; + + /* Short-cut if we already queried upstream */ + if (tagdemux->priv->upstream_size > 0) + return TRUE; + + if ((peer = gst_pad_get_peer (tagdemux->priv->sinkpad)) == NULL) + return FALSE; + + query = gst_query_new_duration (GST_FORMAT_BYTES); + gst_query_set_duration (query, GST_FORMAT_BYTES, -1); + + if (!gst_pad_query (peer, query)) + goto out; + + gst_query_parse_duration (query, &format, &result); + + if (format != GST_FORMAT_BYTES || result == -1) + goto out; + + tagdemux->priv->upstream_size = result; + res = TRUE; + +out: + gst_object_unref (peer); + return res; +} + +static gboolean +gst_tag_demux_srcpad_event (GstPad * pad, GstEvent * event) +{ + gboolean res = FALSE; + GstTagDemux *tagdemux = GST_TAG_DEMUX (GST_PAD_PARENT (pad)); + + /* Handle SEEK events, with adjusted byte offsets and sizes. */ + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_SEEK: + { + gdouble rate; + GstFormat format; + GstSeekType cur_type, stop_type; + GstSeekFlags flags; + gint64 cur, stop; + + gst_event_parse_seek (event, &rate, &format, &flags, + &cur_type, &cur, &stop_type, &stop); + + if (format == GST_FORMAT_BYTES && + tagdemux->priv->state == GST_TAG_DEMUX_STREAMING && + gst_pad_is_linked (tagdemux->priv->sinkpad)) { + GstEvent *upstream; + + switch (cur_type) { + case GST_SEEK_TYPE_SET: + cur += tagdemux->priv->strip_start; + break; + case GST_SEEK_TYPE_CUR: + break; + case GST_SEEK_TYPE_END: + cur += tagdemux->priv->strip_end; + break; + default: + g_assert_not_reached (); + break; + } + switch (stop_type) { + case GST_SEEK_TYPE_SET: + stop += tagdemux->priv->strip_start; + break; + case GST_SEEK_TYPE_CUR: + break; + case GST_SEEK_TYPE_END: + stop += tagdemux->priv->strip_end; + break; + default: + break; + } + upstream = gst_event_new_seek (rate, format, flags, + cur_type, cur, stop_type, stop); + res = gst_pad_push_event (tagdemux->priv->sinkpad, upstream); + } + break; + } + default: + break; + } + + gst_event_unref (event); + return res; +} + +/* takes ownership of new_tags */ + +static void +gst_tag_demux_merge_tags (GstTagList ** tags, GstTagList * new_tags) +{ + g_return_if_fail (tags != NULL); + + if (new_tags == NULL) + return; + + GST_LOG ("New tags: %" GST_PTR_FORMAT, new_tags); + + if (*tags != NULL) { + GstTagList *merged; + + merged = gst_tag_list_merge (*tags, new_tags, GST_TAG_MERGE_REPLACE); + gst_tag_list_free (*tags); + gst_tag_list_free (new_tags); + *tags = merged; + } else { + *tags = new_tags; + } + + GST_LOG ("Tags now: %" GST_PTR_FORMAT, *tags); +} + +/* Read and interpret any end tag when activating in pull_range. + * Returns FALSE if pad activation should fail. */ +static gboolean +gst_tag_demux_pull_end_tag (GstTagDemux * demux, GstTagList ** tags) +{ + GstTagDemuxResult parse_ret; + GstTagDemuxClass *klass; + GstFlowReturn flow_ret; + GstTagList *new_tags = NULL; + GstBuffer *buffer = NULL; + gboolean have_tag; + gboolean res = FALSE; + guint64 offset; + guint tagsize; + + klass = GST_TAG_DEMUX_CLASS (G_OBJECT_GET_CLASS (demux)); + + g_assert (klass->identify_tag != NULL); + g_assert (klass->parse_tag != NULL); + + if (demux->min_end_size == 0) { + GST_DEBUG_OBJECT (demux, "Not looking for tag at the end"); + return TRUE; + } + + if (demux->priv->upstream_size < demux->min_end_size) { + GST_DEBUG_OBJECT (demux, "File too small"); + return TRUE; + } + + /* Pull enough to identify the tag and retrieve its total size */ + offset = demux->priv->upstream_size - demux->min_end_size; + + flow_ret = gst_pad_pull_range (demux->priv->sinkpad, offset, + demux->min_end_size, &buffer); + + if (flow_ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT (demux, "Could not read tag header from end of file, " + "ret = %s", gst_flow_get_name (flow_ret)); + goto done; + } + + if (GST_BUFFER_SIZE (buffer) < demux->min_end_size) { + GST_DEBUG_OBJECT (demux, "Only managed to read %u bytes from file " + "(required: %u bytes)", GST_BUFFER_SIZE (buffer), demux->min_end_size); + goto done; + } + + have_tag = klass->identify_tag (demux, buffer, FALSE, &tagsize); + + if (!have_tag) { + GST_DEBUG_OBJECT (demux, "Could not find tag at end"); + goto done; + } + + /* Now pull the entire tag */ + do { + guint newsize, saved_size; + + GST_DEBUG_OBJECT (demux, "Identified tag at end, size=%u bytes", tagsize); + + demux->priv->strip_end = tagsize; + + g_assert (tagsize >= demux->min_end_size); + + /* Get buffer that's exactly the requested size */ + if (GST_BUFFER_SIZE (buffer) != tagsize) { + gst_buffer_unref (buffer); + buffer = NULL; + + offset = demux->priv->upstream_size - tagsize; + + flow_ret = gst_pad_pull_range (demux->priv->sinkpad, offset, + tagsize, &buffer); + + if (flow_ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT (demux, "Could not read data from end of file at " + "offset %" G_GUINT64_FORMAT ". ret = %s", offset, + gst_flow_get_name (flow_ret)); + goto done; + } + + if (GST_BUFFER_SIZE (buffer) < tagsize) { + GST_DEBUG_OBJECT (demux, "Only managed to read %u bytes from file", + GST_BUFFER_SIZE (buffer)); + goto done; + } + } + + GST_BUFFER_OFFSET (buffer) = offset; + + saved_size = GST_BUFFER_SIZE (buffer); + GST_BUFFER_SIZE (buffer) = tagsize; + newsize = tagsize; + + parse_ret = klass->parse_tag (demux, buffer, FALSE, &newsize, &new_tags); + + GST_BUFFER_SIZE (buffer) = saved_size; + + switch (parse_ret) { + case GST_TAG_DEMUX_RESULT_OK: + res = TRUE; + demux->priv->strip_end = newsize; + GST_DEBUG_OBJECT (demux, "Read tag at end, size %d", + demux->priv->strip_end); + break; + case GST_TAG_DEMUX_RESULT_BROKEN_TAG: + res = TRUE; + demux->priv->strip_end = newsize; + GST_WARNING_OBJECT (demux, "Ignoring broken tag at end, size %d", + demux->priv->strip_end); + break; + case GST_TAG_DEMUX_RESULT_AGAIN: + GST_DEBUG_OBJECT (demux, "Re-parse, this time with %d bytes", newsize); + g_assert (newsize != tagsize); + tagsize = newsize; + break; + } + } while (parse_ret == GST_TAG_DEMUX_RESULT_AGAIN); + + gst_tag_demux_merge_tags (tags, new_tags); + new_tags = NULL; + +done: + if (new_tags) + gst_tag_list_free (new_tags); + if (buffer) + gst_buffer_unref (buffer); + return res; +} + +/* Read and interpret any tag at the start when activating in + * pull_range. Returns FALSE if pad activation should fail. */ +static gboolean +gst_tag_demux_pull_start_tag (GstTagDemux * demux, GstTagList ** tags) +{ + GstTagDemuxResult parse_ret; + GstTagDemuxClass *klass; + GstFlowReturn flow_ret; + GstTagList *new_tags = NULL; + GstBuffer *buffer = NULL; + gboolean have_tag; + gboolean res = FALSE; + guint req, tagsize; + + klass = GST_TAG_DEMUX_CLASS (G_OBJECT_GET_CLASS (demux)); + + g_assert (klass->identify_tag != NULL); + g_assert (klass->parse_tag != NULL); + + if (demux->min_start_size == 0) { + GST_DEBUG_OBJECT (demux, "Not looking for tag at the beginning"); + return TRUE; + } + + /* Handle tag at start. Try with 4kB to start with */ + req = MAX (demux->min_start_size, 4096); + + /* Pull enough to identify the tag and retrieve its total size */ + flow_ret = gst_pad_pull_range (demux->priv->sinkpad, 0, req, &buffer); + if (flow_ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT (demux, "Could not read data from start of file ret=%s", + gst_flow_get_name (flow_ret)); + goto done; + } + + if (GST_BUFFER_SIZE (buffer) < demux->min_start_size) { + GST_DEBUG_OBJECT (demux, "Only managed to read %u bytes from file - " + "no tag in this file", GST_BUFFER_SIZE (buffer)); + goto done; + } + + have_tag = klass->identify_tag (demux, buffer, TRUE, &tagsize); + + if (!have_tag) { + GST_DEBUG_OBJECT (demux, "Could not find start tag"); + res = TRUE; + goto done; + } + + GST_DEBUG_OBJECT (demux, "Identified start tag, size = %u bytes", tagsize); + + do { + guint newsize, saved_size; + + demux->priv->strip_start = tagsize; + + /* Now pull the entire tag */ + g_assert (tagsize >= demux->min_start_size); + + if (GST_BUFFER_SIZE (buffer) < tagsize) { + gst_buffer_unref (buffer); + buffer = NULL; + + flow_ret = gst_pad_pull_range (demux->priv->sinkpad, 0, tagsize, &buffer); + if (flow_ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT (demux, "Could not read data from start of file, " + "ret = %s", gst_flow_get_name (flow_ret)); + goto done; + } + + if (GST_BUFFER_SIZE (buffer) < tagsize) { + GST_DEBUG_OBJECT (demux, "Only managed to read %u bytes from file", + GST_BUFFER_SIZE (buffer)); + goto done; + } + } + + saved_size = GST_BUFFER_SIZE (buffer); + GST_BUFFER_SIZE (buffer) = tagsize; + newsize = tagsize; + parse_ret = klass->parse_tag (demux, buffer, TRUE, &newsize, &new_tags); + + GST_BUFFER_SIZE (buffer) = saved_size; + + switch (parse_ret) { + case GST_TAG_DEMUX_RESULT_OK: + res = TRUE; + demux->priv->strip_start = newsize; + GST_DEBUG_OBJECT (demux, "Read start tag of size %d", newsize); + break; + case GST_TAG_DEMUX_RESULT_BROKEN_TAG: + res = TRUE; + demux->priv->strip_start = newsize; + GST_WARNING_OBJECT (demux, "Ignoring broken start tag of size %d", + demux->priv->strip_start); + break; + case GST_TAG_DEMUX_RESULT_AGAIN: + GST_DEBUG_OBJECT (demux, "Re-parse, this time with %d bytes", newsize); + g_assert (newsize != tagsize); + tagsize = newsize; + break; + } + } while (parse_ret == GST_TAG_DEMUX_RESULT_AGAIN); + + gst_tag_demux_merge_tags (tags, new_tags); + new_tags = NULL; + +done: + if (new_tags) + gst_tag_list_free (new_tags); + if (buffer) + gst_buffer_unref (buffer); + return res; +} + +/* This function operates similarly to gst_type_find_element_activate + * in the typefind element + * 1. try to activate in pull mode. if not, switch to push and succeed. + * 2. try to read tags in pull mode + * 3. typefind the contents + * 4. deactivate pull mode. + * 5. if we didn't find any caps, fail. + * 6. Add the srcpad + * 7. if the sink pad is activated, we are in pull mode. succeed. + * otherwise activate both pads in push mode and succeed. + */ +static gboolean +gst_tag_demux_sink_activate (GstPad * sinkpad) +{ + GstTagDemux *demux = GST_TAG_DEMUX (GST_PAD_PARENT (sinkpad)); + gboolean ret = FALSE; + GstBuffer *buf = NULL; + GstCaps *caps = NULL; + GstFlowReturn flow_ret; + + /* 1: */ + /* If we can activate pull_range upstream, then read any end and start + * tags, otherwise activate in push mode and the chain function will + * collect buffers, read the start tag and output a buffer to end + * preroll. + */ + if (!gst_pad_check_pull_range (sinkpad) || + !gst_pad_activate_pull (sinkpad, TRUE)) { + GST_DEBUG_OBJECT (demux, "No pull mode. Changing to push, but won't be " + "able to read end tags"); + demux->priv->state = GST_TAG_DEMUX_READ_START_TAG; + return gst_pad_activate_push (sinkpad, TRUE); + } + + /* Look for tags at start and end of file */ + GST_DEBUG_OBJECT (demux, "Activated pull mode. Looking for tags"); + if (!gst_tag_demux_get_upstream_size (demux)) + return FALSE; + + demux->priv->strip_start = 0; + demux->priv->strip_end = 0; + + /* we merge in REPLACE mode, so read the less important tag first */ + if (demux->prefer_start_tag) { + if (!gst_tag_demux_pull_end_tag (demux, &demux->priv->parsed_tags) && + !gst_tag_demux_pull_start_tag (demux, &demux->priv->parsed_tags)) { + return FALSE; + } + } else { + if (!gst_tag_demux_pull_start_tag (demux, &demux->priv->parsed_tags) && + !gst_tag_demux_pull_end_tag (demux, &demux->priv->parsed_tags)) { + return FALSE; + } + } + + if (demux->priv->parsed_tags != NULL) { + demux->priv->send_tag_event = TRUE; + } + + flow_ret = gst_tag_demux_read_range (demux, 0, TYPE_FIND_MAX_SIZE, &buf); + if (flow_ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT (demux, "Could not read data from start of file ret=%s", + gst_flow_get_name (flow_ret)); + goto done_activate; + } + + /* gst_util_dump_mem (GST_BUFFER_DATA (buf), GST_BUFFER_SIZE (buf)); */ + caps = gst_tag_demux_do_typefind (demux, buf); + gst_buffer_unref (buf); + buf = NULL; + + /* 4 - Deactivate pull mode */ + if (!gst_pad_activate_pull (sinkpad, FALSE)) { + if (caps) + gst_caps_unref (caps); + GST_DEBUG_OBJECT (demux, "Could not deactivate sinkpad after reading tags"); + return FALSE; + } + + /* 5 - If we didn't find the caps, fail */ + if (caps == NULL) { + GST_DEBUG_OBJECT (demux, "Could not detect type of contents"); + GST_ELEMENT_ERROR (demux, STREAM, TYPE_NOT_FOUND, (NULL), (NULL)); + goto done_activate; + } + + /* tag reading and typefinding were already done, don't do them again in + * the chain function if we end up in push mode */ + demux->priv->state = GST_TAG_DEMUX_STREAMING; + + /* 6 Add the srcpad for output now we know caps. */ + /* add_srcpad takes ownership of the caps */ + if (!gst_tag_demux_add_srcpad (demux, caps)) { + GST_DEBUG_OBJECT (demux, "Could not add source pad"); + goto done_activate; + } + + /* 7 - if the sinkpad is active, it was done by downstream so we're + * done, otherwise switch to push */ + ret = TRUE; + if (!gst_pad_is_active (sinkpad)) { + ret = gst_pad_activate_push (demux->priv->srcpad, TRUE); + ret &= gst_pad_activate_push (sinkpad, TRUE); + } + +done_activate: + if (buf) + gst_buffer_unref (buf); + + return ret; +} + +static gboolean +gst_tag_demux_src_activate_pull (GstPad * pad, gboolean active) +{ + GstTagDemux *demux = GST_TAG_DEMUX (GST_PAD_PARENT (pad)); + + return gst_pad_activate_pull (demux->priv->sinkpad, active); +} + +static gboolean +gst_tag_demux_src_checkgetrange (GstPad * srcpad) +{ + GstTagDemux *demux = GST_TAG_DEMUX (GST_PAD_PARENT (srcpad)); + + return gst_pad_check_pull_range (demux->priv->sinkpad); +} + +static GstFlowReturn +gst_tag_demux_read_range (GstTagDemux * demux, + guint64 offset, guint length, GstBuffer ** buffer) +{ + GstFlowReturn ret; + guint64 in_offset; + guint in_length; + + g_return_val_if_fail (buffer != NULL, GST_FLOW_ERROR); + + /* Adjust offset and length of the request to trim off tag information. + * For the returned buffer, adjust the output offset to match what downstream + * should see */ + in_offset = offset + demux->priv->strip_start; + + if (!gst_tag_demux_get_upstream_size (demux)) + return GST_FLOW_ERROR; + + if (in_offset + length >= demux->priv->upstream_size - demux->priv->strip_end) + in_length = demux->priv->upstream_size - demux->priv->strip_end - in_offset; + else + in_length = length; + + ret = gst_pad_pull_range (demux->priv->sinkpad, in_offset, in_length, buffer); + + if (ret == GST_FLOW_OK && *buffer) { + if (!gst_tag_demux_trim_buffer (demux, buffer)) + goto error; + } + + return ret; + +error: + if (*buffer != NULL) { + gst_buffer_unref (buffer); + *buffer = NULL; + } + return GST_FLOW_ERROR; +} + +static GstFlowReturn +gst_tag_demux_src_getrange (GstPad * srcpad, + guint64 offset, guint length, GstBuffer ** buffer) +{ + GstTagDemux *demux = GST_TAG_DEMUX (GST_PAD_PARENT (srcpad)); + GstFlowReturn flow_ret; + + if (demux->priv->send_tag_event) { + gst_tag_demux_send_tag_event (demux); + demux->priv->send_tag_event = FALSE; + } + flow_ret = gst_tag_demux_read_range (demux, offset, length, buffer); +/* + if (flow_ret == GST_FLOW_OK) { + gst_util_dump_mem (GST_BUFFER_DATA (*buffer), GST_BUFFER_SIZE (*buffer)); + } +*/ + return flow_ret; +} + +static GstStateChangeReturn +gst_tag_demux_change_state (GstElement * element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstTagDemux *demux = GST_TAG_DEMUX (element); + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + gst_tag_demux_reset (demux); + break; + default: + break; + } + return ret; +} + +static gboolean +gst_tag_demux_pad_query (GstPad * pad, GstQuery * query) +{ + /* For a position or duration query, adjust the returned + * bytes to strip off the end and start areas */ + + GstTagDemux *demux = GST_TAG_DEMUX (GST_PAD_PARENT (pad)); + GstPad *peer = NULL; + GstFormat format; + gint64 result; + + if ((peer = gst_pad_get_peer (demux->priv->sinkpad)) == NULL) + return FALSE; + + if (!gst_pad_query (peer, query)) { + gst_object_unref (peer); + return FALSE; + } + + gst_object_unref (peer); + + switch (GST_QUERY_TYPE (query)) { + case GST_QUERY_POSITION: + { + gst_query_parse_position (query, &format, &result); + if (format == GST_FORMAT_BYTES) { + result -= demux->priv->strip_start; + gst_query_set_position (query, format, result); + } + break; + } + case GST_QUERY_DURATION: + { + gst_query_parse_duration (query, &format, &result); + if (format == GST_FORMAT_BYTES) { + result -= demux->priv->strip_start + demux->priv->strip_end; + gst_query_set_duration (query, format, result); + } + break; + } + default: + break; + } + + return TRUE; +} + +static const GstQueryType * +gst_tag_demux_get_query_types (GstPad * pad) +{ + static const GstQueryType types[] = { + GST_QUERY_POSITION, + GST_QUERY_DURATION, + 0 + }; + + return types; +} + +typedef struct +{ + guint best_probability; + GstCaps *caps; + GstBuffer *buffer; +} SimpleTypeFind; + +static guint8 * +simple_find_peek (gpointer data, gint64 offset, guint size) +{ + SimpleTypeFind *find = (SimpleTypeFind *) data; + + if (offset < 0) + return NULL; + + if (GST_BUFFER_SIZE (find->buffer) >= offset + size) { + return GST_BUFFER_DATA (find->buffer) + offset; + } + return NULL; +} +static void +simple_find_suggest (gpointer data, guint probability, const GstCaps * caps) +{ + SimpleTypeFind *find = (SimpleTypeFind *) data; + + if (probability > find->best_probability) { + GstCaps *copy = gst_caps_copy (caps); + + gst_caps_replace (&find->caps, copy); + gst_caps_unref (copy); + find->best_probability = probability; + } +} + +static GstCaps * +gst_tag_demux_do_typefind (GstTagDemux * tagdemux, GstBuffer * buffer) +{ + GList *walk, *type_list; + SimpleTypeFind find; + GstTypeFind gst_find; + + walk = type_list = gst_type_find_factory_get_list (); + + find.buffer = buffer; + find.best_probability = 0; + find.caps = NULL; + gst_find.data = &find; + gst_find.peek = simple_find_peek; + gst_find.get_length = NULL; + gst_find.suggest = simple_find_suggest; + while (walk) { + GstTypeFindFactory *factory = GST_TYPE_FIND_FACTORY (walk->data); + + gst_type_find_factory_call_function (factory, &gst_find); + if (find.best_probability >= GST_TYPE_FIND_MAXIMUM) + break; + walk = g_list_next (walk); + } + gst_plugin_feature_list_free (type_list); + if (find.best_probability > 0) { + GST_DEBUG ("Found caps %" GST_PTR_FORMAT " with buf size %u", find.caps, + GST_BUFFER_SIZE (buffer)); + return find.caps; + } + + return NULL; +} + +static void +gst_tag_demux_send_tag_event (GstTagDemux * demux) +{ + /* FIXME: what's the correct merge mode? Docs need to tell... */ + GstTagList *merged = gst_tag_list_merge (demux->priv->event_tags, + demux->priv->parsed_tags, GST_TAG_MERGE_KEEP); + + if (demux->priv->parsed_tags) + gst_element_post_message (GST_ELEMENT (demux), + gst_message_new_tag (GST_OBJECT (demux), + gst_tag_list_copy (demux->priv->parsed_tags))); + + if (merged) { + GstEvent *event = gst_event_new_tag (merged); + + GST_EVENT_TIMESTAMP (event) = 0; + GST_DEBUG_OBJECT (demux, "Sending tag event on src pad"); + gst_pad_push_event (demux->priv->srcpad, event); + } +} diff --git a/gst/apetag/gsttagdemux.h b/gst/apetag/gsttagdemux.h new file mode 100644 index 0000000..631bafe --- /dev/null +++ b/gst/apetag/gsttagdemux.h @@ -0,0 +1,105 @@ +/* GStreamer Base Class for Tag Demuxing + * Copyright (C) 2005 Jan Schmidt + * Copyright (C) 2006 Tim-Philipp Müller + * + * 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_DEMUX_H__ +#define __GST_TAG_DEMUX_H__ + +#include + +G_BEGIN_DECLS + +#define GST_TYPE_TAG_DEMUX (gst_tag_demux_get_type()) +#define GST_TAG_DEMUX(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_TAG_DEMUX,GstTagDemux)) +#define GST_TAG_DEMUX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_TAG_DEMUX,GstTagDemuxClass)) +#define GST_IS_TAG_DEMUX(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_TAG_DEMUX)) +#define GST_IS_TAG_DEMUX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_TAG_DEMUX)) + +typedef struct _GstTagDemux GstTagDemux; +typedef struct _GstTagDemuxClass GstTagDemuxClass; +typedef struct _GstTagDemuxPrivate GstTagDemuxPrivate; + +/** + * GstTagDemuxResult: + * @GST_TAG_DEMUX_RESULT_BROKEN_TAG: cannot parse tag, just skip it + * @GST_TAG_DEMUX_RESULT_AGAIN : call again with less or more data + * @GST_TAG_DEMUX_RESULT_OK : parsed tag successfully + * + * Result values from the parse_tag virtual function. + */ +typedef enum { + GST_TAG_DEMUX_RESULT_BROKEN_TAG, + GST_TAG_DEMUX_RESULT_AGAIN, + GST_TAG_DEMUX_RESULT_OK +} GstTagDemuxResult; + +struct _GstTagDemux +{ + GstElement element; + + /* Minimum size required to identify a tag at the start and + * determine its total size (0 = not interested in start) */ + guint min_start_size; + + /* Minimum size required to identify a tag at the end and + * determine its total size (0 = not interested in end) */ + guint min_end_size; + + /* Prefer start tags over end tags (default: yes) */ + gboolean prefer_start_tag; + + /*< private >*/ + gpointer reserved[GST_PADDING]; + GstTagDemuxPrivate *priv; +}; + +/* Note: subclass must also add a sink pad template in its base_init */ +struct _GstTagDemuxClass +{ + GstElementClass parent_class; + + /* vtable */ + + /* Identify tag and determine the size required to parse the tag. Buffer + * may be larger than the specified minimum size. */ + gboolean (*identify_tag) (GstTagDemux * demux, + GstBuffer * buffer, + gboolean start_tag, + guint * tag_size); + + /* Parse the tag. Buffer should be exactly the size determined by + * identify_tag() before. parse_tag() may change tag_size and return + * GST_TAG_DEMUX_RESULT_AGAIN to request a larger or smaller buffer. + * parse_tag() is also permitted to adjust tag_size to a smaller value + * and return GST_TAG_DEMUX_RESULT_OK. */ + GstTagDemuxResult (*parse_tag) (GstTagDemux * demux, + GstBuffer * buffer, + gboolean start_tag, + guint * tag_size, + GstTagList ** tags); + + /*< private >*/ + gpointer reserved[GST_PADDING]; +}; + +GType gst_tag_demux_get_type (void); + +G_END_DECLS + +#endif /* __GST_TAG_DEMUX_H__ */ -- 2.7.4