From fb204786dfcc0f5498f8335cc553d902617ff8e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= <sebastian@centricular.com>
Date: Wed, 22 Oct 2014 14:07:09 +0200
Subject: [PATCH] capsfilter: Add an optional delayed caps change mode

In this mode we accept previously set filter caps until
upstream renegotiates to something that is compatible
to the current filter caps.

This allows dynamic caps changes in the pipeline even
if there is a queue between any conversion element
and the capsfilter. Without this we would get not-negotiated
errors if timing is bad.

https://bugzilla.gnome.org/show_bug.cgi?id=739002
---
 plugins/elements/gstcapsfilter.c  | 132 +++++++++++++++++++++++++++++++++-
 plugins/elements/gstcapsfilter.h  |   8 +++
 tests/check/elements/capsfilter.c | 146 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 284 insertions(+), 2 deletions(-)

diff --git a/plugins/elements/gstcapsfilter.c b/plugins/elements/gstcapsfilter.c
index 97793e8..6c7ae0d 100644
--- a/plugins/elements/gstcapsfilter.c
+++ b/plugins/elements/gstcapsfilter.c
@@ -43,9 +43,11 @@
 enum
 {
   PROP_0,
-  PROP_FILTER_CAPS
+  PROP_FILTER_CAPS,
+  PROP_CAPS_CHANGE_MODE
 };
 
+#define DEFAULT_CAPS_CHANGE_MODE (GST_CAPS_FILTER_CAPS_CHANGE_MODE_IMMEDIATE)
 
 static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
     GST_PAD_SINK,
@@ -61,6 +63,26 @@ static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
 GST_DEBUG_CATEGORY_STATIC (gst_capsfilter_debug);
 #define GST_CAT_DEFAULT gst_capsfilter_debug
 
+/* TODO: Add a drop-buffers mode */
+#define GST_TYPE_CAPS_FILTER_CAPS_CHANGE_MODE (gst_caps_filter_caps_change_mode_get_type())
+static GType
+gst_caps_filter_caps_change_mode_get_type (void)
+{
+  static GType type = 0;
+  static const GEnumValue data[] = {
+    {GST_CAPS_FILTER_CAPS_CHANGE_MODE_IMMEDIATE,
+        "Only accept the current filter caps", "immediate"},
+    {GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED,
+        "Temporarily accept previous filter caps", "delayed"},
+    {0, NULL, NULL},
+  };
+
+  if (!type) {
+    type = g_enum_register_static ("GstCapsFilterCapsChangeMode", data);
+  }
+  return type;
+}
+
 #define _do_init \
     GST_DEBUG_CATEGORY_INIT (gst_capsfilter_debug, "capsfilter", 0, \
     "capsfilter element");
@@ -107,6 +129,13 @@ gst_capsfilter_class_init (GstCapsFilterClass * klass)
           G_PARAM_READWRITE | GST_PARAM_MUTABLE_PLAYING |
           G_PARAM_STATIC_STRINGS));
 
+  g_object_class_install_property (gobject_class, PROP_CAPS_CHANGE_MODE,
+      g_param_spec_enum ("caps-change-mode", _("Caps Change Mode"),
+          _("Filter caps change behaviour"),
+          GST_TYPE_CAPS_FILTER_CAPS_CHANGE_MODE, DEFAULT_CAPS_CHANGE_MODE,
+          G_PARAM_READWRITE | GST_PARAM_MUTABLE_PLAYING |
+          G_PARAM_STATIC_STRINGS));
+
   gstelement_class = GST_ELEMENT_CLASS (klass);
   gst_element_class_set_static_metadata (gstelement_class,
       "CapsFilter",
@@ -136,6 +165,7 @@ gst_capsfilter_init (GstCapsFilter * filter)
   gst_base_transform_set_gap_aware (trans, TRUE);
   gst_base_transform_set_prefer_passthrough (trans, FALSE);
   filter->filter_caps = gst_caps_new_any ();
+  filter->caps_change_mode = DEFAULT_CAPS_CHANGE_MODE;
 }
 
 static void
@@ -160,6 +190,17 @@ gst_capsfilter_set_property (GObject * object, guint prop_id,
       GST_OBJECT_LOCK (capsfilter);
       old_caps = capsfilter->filter_caps;
       capsfilter->filter_caps = new_caps;
+      if (old_caps
+          && capsfilter->caps_change_mode ==
+          GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED) {
+        capsfilter->previous_caps =
+            g_list_prepend (capsfilter->previous_caps, gst_caps_ref (old_caps));
+      } else if (capsfilter->caps_change_mode !=
+          GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED) {
+        g_list_free_full (capsfilter->previous_caps,
+            (GDestroyNotify) gst_caps_unref);
+        capsfilter->previous_caps = NULL;
+      }
       GST_OBJECT_UNLOCK (capsfilter);
 
       gst_caps_unref (old_caps);
@@ -169,6 +210,9 @@ gst_capsfilter_set_property (GObject * object, guint prop_id,
       gst_base_transform_reconfigure_sink (GST_BASE_TRANSFORM (object));
       break;
     }
+    case PROP_CAPS_CHANGE_MODE:
+      capsfilter->caps_change_mode = g_value_get_enum (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -187,6 +231,9 @@ gst_capsfilter_get_property (GObject * object, guint prop_id, GValue * value,
       gst_value_set_caps (value, capsfilter->filter_caps);
       GST_OBJECT_UNLOCK (capsfilter);
       break;
+    case PROP_CAPS_CHANGE_MODE:
+      g_value_set_enum (value, capsfilter->caps_change_mode);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -211,11 +258,15 @@ gst_capsfilter_transform_caps (GstBaseTransform * base,
 {
   GstCapsFilter *capsfilter = GST_CAPSFILTER (base);
   GstCaps *ret, *filter_caps, *tmp;
+  gboolean retried = FALSE;
+  GstCapsFilterCapsChangeMode caps_change_mode;
 
   GST_OBJECT_LOCK (capsfilter);
   filter_caps = gst_caps_ref (capsfilter->filter_caps);
+  caps_change_mode = capsfilter->caps_change_mode;
   GST_OBJECT_UNLOCK (capsfilter);
 
+retry:
   if (filter) {
     tmp =
         gst_caps_intersect_full (filter, filter_caps, GST_CAPS_INTERSECT_FIRST);
@@ -231,6 +282,26 @@ gst_capsfilter_transform_caps (GstBaseTransform * base,
       filter_caps);
   GST_DEBUG_OBJECT (capsfilter, "intersect: %" GST_PTR_FORMAT, ret);
 
+  if (gst_caps_is_empty (ret)
+      && caps_change_mode ==
+      GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED && capsfilter->previous_caps
+      && !retried) {
+    GList *l;
+
+    GST_DEBUG_OBJECT (capsfilter,
+        "Current filter caps are not compatible, retry with previous");
+    GST_OBJECT_LOCK (capsfilter);
+    gst_caps_unref (filter_caps);
+    gst_caps_unref (ret);
+    filter_caps = gst_caps_new_empty ();
+    for (l = capsfilter->previous_caps; l; l = l->next) {
+      filter_caps = gst_caps_merge (filter_caps, gst_caps_ref (l->data));
+    }
+    GST_OBJECT_UNLOCK (capsfilter);
+    retried = TRUE;
+    goto retry;
+  }
+
   gst_caps_unref (filter_caps);
 
   return ret;
@@ -250,6 +321,25 @@ gst_capsfilter_accept_caps (GstBaseTransform * base,
 
   ret = gst_caps_can_intersect (caps, filter_caps);
   GST_DEBUG_OBJECT (capsfilter, "can intersect: %d", ret);
+  if (!ret
+      && capsfilter->caps_change_mode ==
+      GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED) {
+    GList *l;
+
+    GST_OBJECT_LOCK (capsfilter);
+    for (l = capsfilter->previous_caps; l; l = l->next) {
+      ret = gst_caps_can_intersect (caps, l->data);
+      if (ret)
+        break;
+    }
+    GST_OBJECT_UNLOCK (capsfilter);
+
+    /* Tell upstream to reconfigure, it's still
+     * looking at old caps */
+    if (ret)
+      gst_base_transform_reconfigure_sink (base);
+  }
+
   if (ret) {
     /* if we can intersect, see if the other end also accepts */
     if (direction == GST_PAD_SRC)
@@ -381,6 +471,7 @@ static gboolean
 gst_capsfilter_sink_event (GstBaseTransform * trans, GstEvent * event)
 {
   GstCapsFilter *filter = GST_CAPSFILTER (trans);
+  gboolean ret;
 
   if (GST_EVENT_TYPE (event) == GST_EVENT_FLUSH_STOP) {
     GList *l;
@@ -422,7 +513,39 @@ gst_capsfilter_sink_event (GstBaseTransform * trans, GstEvent * event)
 done:
 
   GST_LOG_OBJECT (trans, "Forwarding %s event", GST_EVENT_TYPE_NAME (event));
-  return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
+  ret =
+      GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans,
+      gst_event_ref (event));
+
+  if (GST_EVENT_TYPE (event) == GST_EVENT_CAPS
+      && filter->caps_change_mode == GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED) {
+    GList *l;
+    GstCaps *caps;
+
+    gst_event_parse_caps (event, &caps);
+
+    /* Remove all previous caps up to one that works.
+     * Note that this might keep some leftover caps if there
+     * are multiple compatible caps */
+    GST_OBJECT_LOCK (filter);
+    for (l = g_list_last (filter->previous_caps); l; l = l->prev) {
+      if (gst_caps_can_intersect (caps, l->data)) {
+        while (l->next) {
+          gst_caps_unref (l->next->data);
+          l = g_list_delete_link (l, l->next);
+        }
+        break;
+      }
+    }
+    if (!l && gst_caps_can_intersect (caps, filter->filter_caps)) {
+      g_list_free_full (filter->previous_caps, (GDestroyNotify) gst_caps_unref);
+      filter->previous_caps = NULL;
+    }
+    GST_OBJECT_UNLOCK (filter);
+  }
+  gst_event_unref (event);
+
+  return ret;
 }
 
 static gboolean
@@ -433,5 +556,10 @@ gst_capsfilter_stop (GstBaseTransform * trans)
   g_list_free_full (filter->pending_events, (GDestroyNotify) gst_event_unref);
   filter->pending_events = NULL;
 
+  GST_OBJECT_LOCK (filter);
+  g_list_free_full (filter->previous_caps, (GDestroyNotify) gst_caps_unref);
+  filter->previous_caps = NULL;
+  GST_OBJECT_UNLOCK (filter);
+
   return TRUE;
 }
diff --git a/plugins/elements/gstcapsfilter.h b/plugins/elements/gstcapsfilter.h
index ab9ac1f..19f851d 100644
--- a/plugins/elements/gstcapsfilter.h
+++ b/plugins/elements/gstcapsfilter.h
@@ -44,6 +44,11 @@ G_BEGIN_DECLS
 typedef struct _GstCapsFilter GstCapsFilter;
 typedef struct _GstCapsFilterClass GstCapsFilterClass;
 
+typedef enum {
+  GST_CAPS_FILTER_CAPS_CHANGE_MODE_IMMEDIATE,
+  GST_CAPS_FILTER_CAPS_CHANGE_MODE_DELAYED
+} GstCapsFilterCapsChangeMode;
+
 /**
  * GstCapsFilter:
  *
@@ -53,7 +58,10 @@ struct _GstCapsFilter {
   GstBaseTransform trans;
 
   GstCaps *filter_caps;
+  GstCapsFilterCapsChangeMode caps_change_mode;
+
   GList *pending_events;
+  GList *previous_caps;
 };
 
 struct _GstCapsFilterClass {
diff --git a/tests/check/elements/capsfilter.c b/tests/check/elements/capsfilter.c
index 39bc6bd..008fee9 100644
--- a/tests/check/elements/capsfilter.c
+++ b/tests/check/elements/capsfilter.c
@@ -36,6 +36,16 @@ static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
     GST_STATIC_CAPS (CAPS_TEMPLATE_STRING)
     );
 
+static GstStaticPadTemplate any_sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
+    GST_PAD_SINK,
+    GST_PAD_ALWAYS,
+    GST_STATIC_CAPS_ANY);
+
+static GstStaticPadTemplate any_srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
+    GST_PAD_SRC,
+    GST_PAD_ALWAYS,
+    GST_STATIC_CAPS_ANY);
+
 static GList *events = NULL;
 
 static gboolean
@@ -325,6 +335,141 @@ GST_START_TEST (test_push_pending_events)
 
 GST_END_TEST;
 
+GST_START_TEST (test_caps_change_mode_delayed)
+{
+  GstElement *filter;
+  GstPad *mysinkpad;
+  GstPad *mysrcpad;
+  GstSegment segment;
+  GstEvent *event;
+  GstCaps *caps;
+
+  filter = gst_check_setup_element ("capsfilter");
+  mysinkpad = gst_check_setup_sink_pad (filter, &any_sinktemplate);
+  gst_pad_set_event_function (mysinkpad, test_pad_eventfunc);
+  gst_pad_set_active (mysinkpad, TRUE);
+  mysrcpad = gst_check_setup_src_pad (filter, &any_srctemplate);
+  gst_pad_set_active (mysrcpad, TRUE);
+
+  g_object_set (filter, "caps-change-mode", 1, NULL);
+
+  fail_unless_equals_int (gst_element_set_state (filter, GST_STATE_PLAYING),
+      GST_STATE_CHANGE_SUCCESS);
+
+  /* push the stream start */
+  fail_unless (gst_pad_push_event (mysrcpad,
+          gst_event_new_stream_start ("test-stream")));
+  fail_unless_equals_int (g_list_length (events), 1);
+  event = events->data;
+  fail_unless (GST_EVENT_TYPE (event) == GST_EVENT_STREAM_START);
+  g_list_free_full (events, (GDestroyNotify) gst_event_unref);
+  events = NULL;
+
+  /* push a caps */
+  caps = gst_caps_from_string ("audio/x-raw, "
+      "channels=(int)2, " "rate = (int)44100");
+  g_object_set (filter, "caps", caps, NULL);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_caps (caps)));
+  gst_caps_unref (caps);
+  fail_unless_equals_int (g_list_length (events), 1);
+  event = events->data;
+  fail_unless (GST_EVENT_TYPE (event) == GST_EVENT_CAPS);
+  g_list_free_full (events, (GDestroyNotify) gst_event_unref);
+  events = NULL;
+
+  /* push a segment */
+  gst_segment_init (&segment, GST_FORMAT_TIME);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_segment (&segment)));
+  fail_unless_equals_int (g_list_length (events), 1);
+  event = events->data;
+  fail_unless (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT);
+  g_list_free_full (events, (GDestroyNotify) gst_event_unref);
+  events = NULL;
+
+  /* push a buffer */
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          gst_buffer_new_wrapped (g_malloc0 (1024), 1024)), GST_FLOW_OK);
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  g_list_free_full (buffers, (GDestroyNotify) gst_buffer_unref);
+  buffers = NULL;
+
+  /* Set new incompatible caps */
+  caps = gst_caps_from_string ("audio/x-raw, "
+      "channels=(int)2, " "rate = (int)48000");
+  g_object_set (filter, "caps", caps, NULL);
+
+  /* push a buffer without updating the caps */
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          gst_buffer_new_wrapped (g_malloc0 (1024), 1024)), GST_FLOW_OK);
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  g_list_free_full (buffers, (GDestroyNotify) gst_buffer_unref);
+  buffers = NULL;
+
+  /* No caps event here, we're still at the old caps */
+  fail_unless (g_list_length (events) == 0);
+
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_caps (caps)));
+  gst_caps_unref (caps);
+  fail_unless_equals_int (g_list_length (events), 1);
+  event = events->data;
+  fail_unless (GST_EVENT_TYPE (event) == GST_EVENT_CAPS);
+  g_list_free_full (events, (GDestroyNotify) gst_event_unref);
+  events = NULL;
+
+  /* Push a new buffer, now we have the new caps */
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          gst_buffer_new_wrapped (g_malloc0 (1024), 1024)), GST_FLOW_OK);
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  g_list_free_full (buffers, (GDestroyNotify) gst_buffer_unref);
+  buffers = NULL;
+
+  /* Set back old caps */
+  caps = gst_caps_from_string ("audio/x-raw, "
+      "channels=(int)2, " "rate = (int)44100");
+  g_object_set (filter, "caps", caps, NULL);
+  gst_caps_unref (caps);
+
+  /* push a buffer without updating the caps */
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          gst_buffer_new_wrapped (g_malloc0 (1024), 1024)), GST_FLOW_OK);
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  g_list_free_full (buffers, (GDestroyNotify) gst_buffer_unref);
+  buffers = NULL;
+
+  /* Now set new caps again but the old caps are currently pushed */
+  caps = gst_caps_from_string ("audio/x-raw, "
+      "channels=(int)2, " "rate = (int)48000");
+  g_object_set (filter, "caps", caps, NULL);
+  gst_caps_unref (caps);
+  /* Race condition simulation here! */
+  caps = gst_caps_from_string ("audio/x-raw, "
+      "channels=(int)2, " "rate = (int)44100");
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_caps (caps)));
+  gst_caps_unref (caps);
+  fail_unless_equals_int (g_list_length (events), 1);
+  event = events->data;
+  fail_unless (GST_EVENT_TYPE (event) == GST_EVENT_CAPS);
+  g_list_free_full (events, (GDestroyNotify) gst_event_unref);
+  events = NULL;
+
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          gst_buffer_new_wrapped (g_malloc0 (1024), 1024)), GST_FLOW_OK);
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  g_list_free_full (buffers, (GDestroyNotify) gst_buffer_unref);
+  buffers = NULL;
+
+  /* cleanup */
+  GST_DEBUG ("cleanup");
+
+  gst_pad_set_active (mysrcpad, FALSE);
+  gst_pad_set_active (mysinkpad, FALSE);
+  gst_check_teardown_src_pad (filter);
+  gst_check_teardown_sink_pad (filter);
+  gst_check_teardown_element (filter);
+}
+
+GST_END_TEST;
+
 static Suite *
 capsfilter_suite (void)
 {
@@ -337,6 +482,7 @@ capsfilter_suite (void)
   tcase_add_test (tc_chain, test_caps_query);
   tcase_add_test (tc_chain, test_accept_caps_query);
   tcase_add_test (tc_chain, test_push_pending_events);
+  tcase_add_test (tc_chain, test_caps_change_mode_delayed);
 
   return s;
 }
-- 
2.1.1