gstreamer/gst/icydemux/gsticydemux.c
Søren Juul 1184429e21 icydemux: reset tags on empty value
Some radio streams uses StreamTitle='' to reset the title after a
track stopped playing, e.g. while the host talks between tracks or
during news segments.
This change forces an empty tag object to be distributed if
StreamTitle or StreamUrl is received with empty value, thus allowing
downstream elements to get notified about this.

https://bugzilla.gnome.org/show_bug.cgi?id=778437
2017-02-14 12:24:13 +02:00

677 lines
19 KiB
C

/* -*- Mode: C; tab-width: 2; indent-tabs-mode: t; c-basic-offset: 2 -*- */
/* Copyright 2005 Jan Schmidt <thaytan@mad.scientist.com>
* 2006 Michael Smith <msmith@fluendo.com>
* Copyright (C) 2003-2004 Benjamin Otte <otte@gnome.org>
*
* 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., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:element-icydemux
*
* icydemux accepts data streams with ICY metadata at known intervals, as
* transmitted from an upstream element (usually read as response headers from
* an HTTP stream). 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.
*
* <refsect2>
* <title>Example launch line</title>
* |[
* gst-launch-1.0 souphttpsrc location=http://some.server/ iradio-mode=true ! icydemux ! fakesink -t
* ]| This pipeline should read any available ICY tag information and output it.
* The contents of the stream should be detected, and the appropriate mime
* type set on buffers produced from icydemux. (Using gnomevfssrc, neonhttpsrc
* or giosrc instead of souphttpsrc should also work.)
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gst/gst.h>
#include <gst/gst-i18n-plugin.h>
#include <gst/tag/tag.h>
#include "gsticydemux.h"
#include <string.h>
#define ICY_TYPE_FIND_MAX_SIZE (40*1024)
GST_DEBUG_CATEGORY_STATIC (icydemux_debug);
#define GST_CAT_DEFAULT (icydemux_debug)
static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("application/x-icy, metadata-interval = (int)[0, MAX]")
);
static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_SOMETIMES,
GST_STATIC_CAPS ("ANY")
);
static void gst_icydemux_dispose (GObject * object);
static GstFlowReturn gst_icydemux_chain (GstPad * pad, GstObject * parent,
GstBuffer * buf);
static gboolean gst_icydemux_handle_event (GstPad * pad, GstObject * parent,
GstEvent * event);
static gboolean gst_icydemux_add_srcpad (GstICYDemux * icydemux,
GstCaps * new_caps);
static gboolean gst_icydemux_remove_srcpad (GstICYDemux * icydemux);
static GstStateChangeReturn gst_icydemux_change_state (GstElement * element,
GstStateChange transition);
static gboolean gst_icydemux_sink_setcaps (GstPad * pad, GstCaps * caps);
static gboolean gst_icydemux_send_tag_event (GstICYDemux * icydemux,
GstTagList * taglist);
#define gst_icydemux_parent_class parent_class
G_DEFINE_TYPE (GstICYDemux, gst_icydemux, GST_TYPE_ELEMENT);
static void
gst_icydemux_class_init (GstICYDemuxClass * klass)
{
GObjectClass *gobject_class;
GstElementClass *gstelement_class;
gobject_class = (GObjectClass *) klass;
gstelement_class = (GstElementClass *) klass;
parent_class = g_type_class_ref (GST_TYPE_ELEMENT);
gobject_class->dispose = gst_icydemux_dispose;
gstelement_class->change_state = gst_icydemux_change_state;
gst_element_class_add_static_pad_template (gstelement_class, &src_factory);
gst_element_class_add_static_pad_template (gstelement_class, &sink_factory);
gst_element_class_set_static_metadata (gstelement_class, "ICY tag demuxer",
"Codec/Demuxer/Metadata",
"Read and output ICY tags while demuxing the contents",
"Jan Schmidt <thaytan@mad.scientist.com>, "
"Michael Smith <msmith@fluendo.com>");
}
static void
gst_icydemux_reset (GstICYDemux * icydemux)
{
/* Unknown at the moment (this is a fatal error if don't have a value by the
* time we get to our chain function)
*/
icydemux->meta_interval = -1;
icydemux->remaining = 0;
icydemux->typefinding = TRUE;
gst_caps_replace (&(icydemux->src_caps), NULL);
gst_icydemux_remove_srcpad (icydemux);
if (icydemux->cached_tags) {
gst_tag_list_unref (icydemux->cached_tags);
icydemux->cached_tags = NULL;
}
if (icydemux->cached_events) {
g_list_foreach (icydemux->cached_events,
(GFunc) gst_mini_object_unref, NULL);
g_list_free (icydemux->cached_events);
icydemux->cached_events = NULL;
}
if (icydemux->meta_adapter) {
gst_adapter_clear (icydemux->meta_adapter);
g_object_unref (icydemux->meta_adapter);
icydemux->meta_adapter = NULL;
}
if (icydemux->typefind_buf) {
gst_buffer_unref (icydemux->typefind_buf);
icydemux->typefind_buf = NULL;
}
if (icydemux->content_type) {
g_free (icydemux->content_type);
icydemux->content_type = NULL;
}
}
static void
gst_icydemux_init (GstICYDemux * icydemux)
{
GstElementClass *klass = GST_ELEMENT_GET_CLASS (icydemux);
icydemux->sinkpad =
gst_pad_new_from_template (gst_element_class_get_pad_template (klass,
"sink"), "sink");
gst_pad_set_chain_function (icydemux->sinkpad,
GST_DEBUG_FUNCPTR (gst_icydemux_chain));
gst_pad_set_event_function (icydemux->sinkpad,
GST_DEBUG_FUNCPTR (gst_icydemux_handle_event));
gst_element_add_pad (GST_ELEMENT (icydemux), icydemux->sinkpad);
gst_icydemux_reset (icydemux);
}
static gboolean
gst_icydemux_sink_setcaps (GstPad * pad, GstCaps * caps)
{
GstICYDemux *icydemux = GST_ICYDEMUX (GST_PAD_PARENT (pad));
GstStructure *structure = gst_caps_get_structure (caps, 0);
const gchar *tmp;
if (!gst_structure_get_int (structure, "metadata-interval",
&icydemux->meta_interval))
return FALSE;
/* If incoming caps have the HTTP Content-Type, copy that over */
if ((tmp = gst_structure_get_string (structure, "content-type")))
icydemux->content_type = g_strdup (tmp);
/* We have a meta interval, so initialise the rest */
icydemux->remaining = icydemux->meta_interval;
icydemux->meta_remaining = 0;
return TRUE;
}
static void
gst_icydemux_dispose (GObject * object)
{
GstICYDemux *icydemux = GST_ICYDEMUX (object);
gst_icydemux_reset (icydemux);
G_OBJECT_CLASS (parent_class)->dispose (object);
}
typedef struct
{
GstCaps *caps;
GstPad *pad;
} CopyStickyEventsData;
static gboolean
copy_sticky_events (GstPad * pad, GstEvent ** event, gpointer user_data)
{
CopyStickyEventsData *data = user_data;
if (GST_EVENT_TYPE (*event) >= GST_EVENT_CAPS && data->caps) {
gst_pad_set_caps (data->pad, data->caps);
data->caps = NULL;
}
if (GST_EVENT_TYPE (*event) != GST_EVENT_CAPS)
gst_pad_push_event (data->pad, gst_event_ref (*event));
return TRUE;
}
static gboolean
gst_icydemux_add_srcpad (GstICYDemux * icydemux, GstCaps * new_caps)
{
if (icydemux->src_caps == NULL ||
!gst_caps_is_equal (new_caps, icydemux->src_caps)) {
gst_caps_replace (&(icydemux->src_caps), new_caps);
if (icydemux->srcpad != NULL) {
GST_DEBUG_OBJECT (icydemux, "Changing src pad caps to %" GST_PTR_FORMAT,
icydemux->src_caps);
gst_pad_set_caps (icydemux->srcpad, icydemux->src_caps);
}
} else {
/* Caps never changed */
gst_caps_unref (new_caps);
}
if (icydemux->srcpad == NULL) {
CopyStickyEventsData data;
icydemux->srcpad =
gst_pad_new_from_template (gst_element_class_get_pad_template
(GST_ELEMENT_GET_CLASS (icydemux), "src"), "src");
g_return_val_if_fail (icydemux->srcpad != NULL, FALSE);
gst_pad_use_fixed_caps (icydemux->srcpad);
gst_pad_set_active (icydemux->srcpad, TRUE);
data.pad = icydemux->srcpad;
data.caps = icydemux->src_caps;
gst_pad_sticky_events_foreach (icydemux->sinkpad, copy_sticky_events,
&data);
if (data.caps)
gst_pad_set_caps (data.pad, data.caps);
GST_DEBUG_OBJECT (icydemux, "Adding src pad with caps %" GST_PTR_FORMAT,
icydemux->src_caps);
if (!(gst_element_add_pad (GST_ELEMENT (icydemux), icydemux->srcpad)))
return FALSE;
gst_element_no_more_pads (GST_ELEMENT (icydemux));
}
return TRUE;
}
static gboolean
gst_icydemux_remove_srcpad (GstICYDemux * icydemux)
{
gboolean res = TRUE;
if (icydemux->srcpad != NULL) {
res = gst_element_remove_pad (GST_ELEMENT (icydemux), icydemux->srcpad);
g_return_val_if_fail (res != FALSE, FALSE);
icydemux->srcpad = NULL;
}
return res;
};
static gchar *
gst_icydemux_unicodify (const gchar * str)
{
const gchar *env_vars[] = { "GST_ICY_TAG_ENCODING",
"GST_TAG_ENCODING", NULL
};
return gst_tag_freeform_string_to_utf8 (str, -1, env_vars);
}
/* takes ownership of tag list */
static gboolean
gst_icydemux_tag_found (GstICYDemux * icydemux, GstTagList * tags)
{
/* send the tag event if we have finished typefinding and have a src pad */
if (icydemux->srcpad)
return gst_icydemux_send_tag_event (icydemux, tags);
/* if we haven't a source pad yet, cache the tags */
if (!icydemux->cached_tags) {
icydemux->cached_tags = tags;
} else {
gst_tag_list_insert (icydemux->cached_tags, tags,
GST_TAG_MERGE_REPLACE_ALL);
gst_tag_list_unref (tags);
}
return TRUE;
}
static void
gst_icydemux_parse_and_send_tags (GstICYDemux * icydemux)
{
GstTagList *tags;
const guint8 *data;
int length, i;
gboolean tags_found = FALSE;
gchar *buffer;
gchar **strings;
length = gst_adapter_available (icydemux->meta_adapter);
data = gst_adapter_map (icydemux->meta_adapter, length);
/* Now, copy this to a buffer where we can NULL-terminate it to make things
* a bit easier, then do that parsing. */
buffer = g_strndup ((const gchar *) data, length);
tags = gst_tag_list_new_empty ();
strings = g_strsplit (buffer, "';", 0);
for (i = 0; strings[i]; i++) {
if (!g_ascii_strncasecmp (strings[i], "StreamTitle=", 12)) {
char *title = gst_icydemux_unicodify (strings[i] + 13);
tags_found = TRUE;
if (title && *title) {
gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_TITLE,
title, NULL);
g_free (title);
}
} else if (!g_ascii_strncasecmp (strings[i], "StreamUrl=", 10)) {
char *url = gst_icydemux_unicodify (strings[i] + 11);
tags_found = TRUE;
if (url && *url) {
gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_HOMEPAGE,
url, NULL);
g_free (url);
}
}
}
g_strfreev (strings);
g_free (buffer);
gst_adapter_unmap (icydemux->meta_adapter);
gst_adapter_flush (icydemux->meta_adapter, length);
if (tags_found)
gst_icydemux_tag_found (icydemux, tags);
else
gst_tag_list_unref (tags);
}
static gboolean
gst_icydemux_handle_event (GstPad * pad, GstObject * parent, GstEvent * event)
{
GstICYDemux *icydemux = GST_ICYDEMUX (parent);
gboolean result;
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_TAG:
{
GstTagList *tags;
gst_event_parse_tag (event, &tags);
result = gst_icydemux_tag_found (icydemux, gst_tag_list_copy (tags));
gst_event_unref (event);
return result;
}
case GST_EVENT_CAPS:
{
GstCaps *caps;
gst_event_parse_caps (event, &caps);
result = gst_icydemux_sink_setcaps (pad, caps);
gst_event_unref (event);
return result;
}
default:
break;
}
if (icydemux->typefinding) {
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_FLUSH_STOP:
g_list_foreach (icydemux->cached_events,
(GFunc) gst_mini_object_unref, NULL);
g_list_free (icydemux->cached_events);
icydemux->cached_events = NULL;
return gst_pad_event_default (pad, parent, event);
default:
if (!GST_EVENT_IS_STICKY (event)) {
icydemux->cached_events =
g_list_append (icydemux->cached_events, event);
} else {
gst_event_unref (event);
}
return TRUE;
}
} else {
return gst_pad_event_default (pad, parent, event);
}
}
static void
gst_icydemux_send_cached_events (GstICYDemux * icydemux)
{
GList *l;
for (l = icydemux->cached_events; l != NULL; l = l->next) {
GstEvent *event = GST_EVENT (l->data);
gst_pad_push_event (icydemux->srcpad, event);
}
g_list_free (icydemux->cached_events);
icydemux->cached_events = NULL;
}
static GstFlowReturn
gst_icydemux_typefind_or_forward (GstICYDemux * icydemux, GstBuffer * buf)
{
if (icydemux->typefinding) {
GstBuffer *tf_buf;
GstCaps *caps = NULL;
GstTypeFindProbability prob;
/* If we have a content-type from upstream, let's see if we can shortcut
* typefinding */
if (G_UNLIKELY (icydemux->content_type)) {
if (!g_ascii_strcasecmp (icydemux->content_type, "video/nsv")) {
GST_DEBUG ("We have a NSV stream");
caps = gst_caps_new_empty_simple ("video/x-nsv");
} else {
GST_DEBUG ("Upstream Content-Type isn't supported");
g_free (icydemux->content_type);
icydemux->content_type = NULL;
}
}
if (icydemux->typefind_buf) {
icydemux->typefind_buf = gst_buffer_append (icydemux->typefind_buf, buf);
} else {
icydemux->typefind_buf = buf;
}
/* Only typefind if we haven't already got some caps */
if (caps == NULL) {
caps = gst_type_find_helper_for_buffer (GST_OBJECT (icydemux),
icydemux->typefind_buf, &prob);
if (caps == NULL) {
if (gst_buffer_get_size (icydemux->typefind_buf) <
ICY_TYPE_FIND_MAX_SIZE) {
/* Just break for more data */
return GST_FLOW_OK;
}
/* We failed typefind */
GST_ELEMENT_ERROR (icydemux, STREAM, TYPE_NOT_FOUND, (NULL),
("No caps found for contents within an ICY stream"));
gst_buffer_unref (icydemux->typefind_buf);
icydemux->typefind_buf = NULL;
return GST_FLOW_ERROR;
}
}
if (!gst_icydemux_add_srcpad (icydemux, caps)) {
GST_DEBUG_OBJECT (icydemux, "Failed to add srcpad");
gst_caps_unref (caps);
gst_buffer_unref (icydemux->typefind_buf);
icydemux->typefind_buf = NULL;
return GST_FLOW_ERROR;
}
gst_caps_unref (caps);
if (icydemux->cached_events) {
gst_icydemux_send_cached_events (icydemux);
}
if (icydemux->cached_tags) {
gst_icydemux_send_tag_event (icydemux, icydemux->cached_tags);
icydemux->cached_tags = NULL;
}
/* Move onto streaming: call ourselves recursively with the typefind buffer
* to get that forwarded. */
icydemux->typefinding = FALSE;
tf_buf = icydemux->typefind_buf;
icydemux->typefind_buf = NULL;
return gst_icydemux_typefind_or_forward (icydemux, tf_buf);
} else {
if (G_UNLIKELY (icydemux->srcpad == NULL)) {
gst_buffer_unref (buf);
return GST_FLOW_ERROR;
}
buf = gst_buffer_make_writable (buf);
/* Most things don't care, and it's a pain to track (we should preserve a
* 0 offset on the first buffer though if it's there, for id3demux etc.) */
if (GST_BUFFER_OFFSET (buf) != 0) {
GST_BUFFER_OFFSET (buf) = GST_BUFFER_OFFSET_NONE;
}
return gst_pad_push (icydemux->srcpad, buf);
}
}
static void
gst_icydemux_add_meta (GstICYDemux * icydemux, GstBuffer * buf)
{
if (!icydemux->meta_adapter)
icydemux->meta_adapter = gst_adapter_new ();
gst_adapter_push (icydemux->meta_adapter, buf);
}
static GstFlowReturn
gst_icydemux_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)
{
GstICYDemux *icydemux;
guint size, chunk, offset;
GstBuffer *sub;
GstFlowReturn ret = GST_FLOW_OK;
icydemux = GST_ICYDEMUX (parent);
if (G_UNLIKELY (icydemux->meta_interval < 0))
goto not_negotiated;
if (icydemux->meta_interval == 0) {
ret = gst_icydemux_typefind_or_forward (icydemux, buf);
buf = NULL;
goto done;
}
/* Go through the buffer, chopping it into appropriate chunks. Forward as
* tags or buffers, as appropriate
*/
size = gst_buffer_get_size (buf);
offset = 0;
while (size) {
if (icydemux->remaining) {
chunk = (size <= icydemux->remaining) ? size : icydemux->remaining;
if (offset == 0 && chunk == size) {
sub = buf;
buf = NULL;
} else {
sub = gst_buffer_copy_region (buf, GST_BUFFER_COPY_ALL, offset, chunk);
}
offset += chunk;
icydemux->remaining -= chunk;
size -= chunk;
/* This buffer goes onto typefinding, and/or directly pushed out */
ret = gst_icydemux_typefind_or_forward (icydemux, sub);
if (ret != GST_FLOW_OK)
goto done;
} else if (icydemux->meta_remaining) {
chunk = (size <= icydemux->meta_remaining) ?
size : icydemux->meta_remaining;
sub = gst_buffer_copy_region (buf, GST_BUFFER_COPY_ALL, offset, chunk);
gst_icydemux_add_meta (icydemux, sub);
offset += chunk;
icydemux->meta_remaining -= chunk;
size -= chunk;
if (icydemux->meta_remaining == 0) {
/* Parse tags from meta_adapter, send off as tag messages */
GST_DEBUG_OBJECT (icydemux, "No remaining metadata, parsing for tags");
gst_icydemux_parse_and_send_tags (icydemux);
icydemux->remaining = icydemux->meta_interval;
}
} else {
guint8 byte;
/* We need to read a single byte (always safe at this point in the loop)
* to figure out how many bytes of metadata exist.
* The 'spec' tells us to read 16 * (byte_value) bytes of metadata after
* this (zero is common, and means the metadata hasn't changed).
*/
gst_buffer_extract (buf, offset, &byte, 1);
icydemux->meta_remaining = 16 * byte;
if (icydemux->meta_remaining == 0)
icydemux->remaining = icydemux->meta_interval;
offset += 1;
size -= 1;
}
}
done:
if (buf)
gst_buffer_unref (buf);
return ret;
/* ERRORS */
not_negotiated:
{
GST_WARNING_OBJECT (icydemux, "meta_interval not set, buffer probably had "
"no caps set. Try enabling iradio-mode on the http source element");
gst_buffer_unref (buf);
return GST_FLOW_NOT_NEGOTIATED;
}
}
static GstStateChangeReturn
gst_icydemux_change_state (GstElement * element, GstStateChange transition)
{
GstStateChangeReturn ret;
GstICYDemux *icydemux = GST_ICYDEMUX (element);
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
gst_icydemux_reset (icydemux);
break;
default:
break;
}
return ret;
}
/* takes ownership of tag list */
static gboolean
gst_icydemux_send_tag_event (GstICYDemux * icydemux, GstTagList * tags)
{
GstEvent *event;
event = gst_event_new_tag (tags);
GST_EVENT_TIMESTAMP (event) = 0;
GST_DEBUG_OBJECT (icydemux, "Sending tag event on src pad");
return gst_pad_push_event (icydemux->srcpad, event);
}
static gboolean
plugin_init (GstPlugin * plugin)
{
GST_DEBUG_CATEGORY_INIT (icydemux_debug, "icydemux", 0,
"GStreamer ICY tag demuxer");
return gst_element_register (plugin, "icydemux",
GST_RANK_PRIMARY, GST_TYPE_ICYDEMUX);
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
GST_VERSION_MINOR,
icydemux,
"Demux ICY tags from a stream",
plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)