/* GStreamer
 * Copyright (C) 1999,2000 Erik Walthinsen <omega@cse.ogi.edu>
 *                    2005 Wim Taymans <wim@fluendo.com>
 *                    2006 Thomas Vander Stichele <thomas at apestaart dot org>
 *                    2014 Tim-Philipp Müller <tim centricular com>
 *               2015-2017 YouView TV Ltd, Vincent Penquerc'h <vincent.penquerch@collabora.co.uk>
 *
 * gstipcpipelinesink.c:
 *
 * 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-ipcpipelinesink
 * @see_also: #GstIpcPipelineSrc, #GstIpcSlavePipeline
 *
 * Communicates with an ipcpipelinesrc element in another process via a socket.
 *
 * This element, together with ipcpipelinesrc and ipcslavepipeline form a
 * mechanism that allows splitting a single pipeline in different processes.
 * The main use-case for it is a playback pipeline split in two parts, where the
 * first part contains the networking, parsing and demuxing and the second part
 * contains the decoding and display. The intention of this split is to improve
 * security of an application, by letting the networking, parsing and demuxing
 * parts run in a less privileged process than the process that accesses the
 * decoder and display.
 *
 * Once the pipelines in those different processes have been created, the
 * playback can be controlled entirely from the first pipeline, which is the
 * one that contains ipcpipelinesink. We call this pipeline the “master”.
 * All relevant events and queries sent from the application are sent to
 * the master pipeline and messages to the application are sent from the master
 * pipeline. The second pipeline, in the other process, is transparently slaved.
 *
 * ipcpipelinesink can work only in push mode and does not synchronize buffers
 * to the clock. Synchronization is meant to happen either at the real sink at
 * the end of the remote slave pipeline, or not to happen at all, if the
 * pipeline is live.
 *
 * A master pipeline may contain more than one ipcpipelinesink elements, which
 * can be connected either to the same slave pipeline or to different ones.
 *
 * Communication with ipcpipelinesrc on the slave happens via a socket, using a
 * custom protocol. Each buffer, event, query, message or state change is
 * serialized in a "packet" and sent over the socket. The sender then
 * performs a blocking wait for a reply, if a return code is needed.
 *
 * All objects that contain a GstStructure (messages, queries, events) are
 * serialized by serializing the GstStructure to a string
 * (gst_structure_to_string). This implies some limitations, of course.
 * All fields of this structures that are not serializable to strings (ex.
 * object pointers) are ignored, except for some cases where custom
 * serialization may occur (ex error/warning/info messages that contain a
 * GError are serialized differently).
 *
 * Buffers are transported by writing their content directly on the socket.
 * More efficient ways for memory sharing could be implemented in the future.
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "gstipcpipelineelements.h"
#include "gstipcpipelinesink.h"

static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS_ANY);

GST_DEBUG_CATEGORY_STATIC (gst_ipc_pipeline_sink_debug);
#define GST_CAT_DEFAULT gst_ipc_pipeline_sink_debug

enum
{
  SIGNAL_DISCONNECT,
  /* FILL ME */
  LAST_SIGNAL
};
static guint gst_ipc_pipeline_sink_signals[LAST_SIGNAL] = { 0 };

enum
{
  PROP_0,
  PROP_FDIN,
  PROP_FDOUT,
  PROP_READ_CHUNK_SIZE,
  PROP_ACK_TIME,
};


#define DEFAULT_READ_CHUNK_SIZE 4096
#define DEFAULT_ACK_TIME (10 * G_TIME_SPAN_SECOND)

#define _do_init \
    GST_DEBUG_CATEGORY_INIT (gst_ipc_pipeline_sink_debug, "ipcpipelinesink", 0, "ipcpipelinesink element");
#define gst_ipc_pipeline_sink_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstIpcPipelineSink, gst_ipc_pipeline_sink,
    GST_TYPE_ELEMENT, _do_init);
GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (ipcpipelinesink, "ipcpipelinesink",
    GST_RANK_NONE, GST_TYPE_IPC_PIPELINE_SINK,
    icepipeline_element_init (plugin));

static void gst_ipc_pipeline_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_ipc_pipeline_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_ipc_pipeline_sink_dispose (GObject * obj);
static void gst_ipc_pipeline_sink_finalize (GObject * obj);
static gboolean gst_ipc_pipeline_sink_start_reader_thread (GstIpcPipelineSink *
    sink);
static void gst_ipc_pipeline_sink_stop_reader_thread (GstIpcPipelineSink *
    sink);

static GstStateChangeReturn gst_ipc_pipeline_sink_change_state (GstElement *
    element, GstStateChange transition);

static GstFlowReturn gst_ipc_pipeline_sink_chain (GstPad * pad,
    GstObject * parent, GstBuffer * buffer);
static gboolean gst_ipc_pipeline_sink_event (GstPad * pad, GstObject * parent,
    GstEvent * event);
static gboolean gst_ipc_pipeline_sink_element_query (GstElement * element,
    GstQuery * query);
static gboolean gst_ipc_pipeline_sink_send_event (GstElement * element,
    GstEvent * event);
static gboolean gst_ipc_pipeline_sink_query (GstPad * pad, GstObject * parent,
    GstQuery * query);
static gboolean gst_ipc_pipeline_sink_pad_activate_mode (GstPad * pad,
    GstObject * parent, GstPadMode mode, gboolean active);


static void gst_ipc_pipeline_sink_disconnect (GstIpcPipelineSink * sink);
static void pusher (gpointer data, gpointer user_data);


static void
gst_ipc_pipeline_sink_class_init (GstIpcPipelineSinkClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gstelement_class = GST_ELEMENT_CLASS (klass);

  gobject_class->set_property = gst_ipc_pipeline_sink_set_property;
  gobject_class->get_property = gst_ipc_pipeline_sink_get_property;
  gobject_class->dispose = gst_ipc_pipeline_sink_dispose;
  gobject_class->finalize = gst_ipc_pipeline_sink_finalize;

  g_object_class_install_property (gobject_class, PROP_FDIN,
      g_param_spec_int ("fdin", "Input file descriptor",
          "File descriptor to received data from",
          -1, 0xffff, -1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_FDOUT,
      g_param_spec_int ("fdout", "Output file descriptor",
          "File descriptor to send data through",
          -1, 0xffff, -1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_READ_CHUNK_SIZE,
      g_param_spec_uint ("read-chunk-size", "Read chunk size",
          "Read chunk size",
          1, 1 << 24, DEFAULT_READ_CHUNK_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_ACK_TIME,
      g_param_spec_uint64 ("ack-time", "Ack time",
          "Maximum time to wait for a response to a message",
          0, G_MAXUINT64, DEFAULT_ACK_TIME,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gst_ipc_pipeline_sink_signals[SIGNAL_DISCONNECT] =
      g_signal_new ("disconnect",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      G_STRUCT_OFFSET (GstIpcPipelineSinkClass, disconnect),
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  gst_element_class_set_static_metadata (gstelement_class,
      "Inter-process Pipeline Sink",
      "Sink",
      "Allows splitting and continuing a pipeline in another process",
      "Vincent Penquerc'h <vincent.penquerch@collabora.co.uk>");
  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&sinktemplate));

  gstelement_class->change_state =
      GST_DEBUG_FUNCPTR (gst_ipc_pipeline_sink_change_state);
  gstelement_class->query =
      GST_DEBUG_FUNCPTR (gst_ipc_pipeline_sink_element_query);
  gstelement_class->send_event =
      GST_DEBUG_FUNCPTR (gst_ipc_pipeline_sink_send_event);

  klass->disconnect = GST_DEBUG_FUNCPTR (gst_ipc_pipeline_sink_disconnect);
}

static void
gst_ipc_pipeline_sink_init (GstIpcPipelineSink * sink)
{
  GstPadTemplate *pad_template;

  GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_SINK);

  gst_ipc_pipeline_comm_init (&sink->comm, GST_ELEMENT (sink));
  sink->comm.read_chunk_size = DEFAULT_READ_CHUNK_SIZE;
  sink->comm.ack_time = DEFAULT_ACK_TIME;
  sink->comm.fdin = -1;
  sink->comm.fdout = -1;
  sink->threads = g_thread_pool_new (pusher, sink, -1, FALSE, NULL);
  gst_ipc_pipeline_sink_start_reader_thread (sink);

  pad_template =
      gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (sink), "sink");
  g_return_if_fail (pad_template != NULL);

  sink->sinkpad = gst_pad_new_from_template (pad_template, "sink");

  gst_pad_set_activatemode_function (sink->sinkpad,
      gst_ipc_pipeline_sink_pad_activate_mode);
  gst_pad_set_query_function (sink->sinkpad, gst_ipc_pipeline_sink_query);
  gst_pad_set_event_function (sink->sinkpad, gst_ipc_pipeline_sink_event);
  gst_pad_set_chain_function (sink->sinkpad, gst_ipc_pipeline_sink_chain);
  gst_element_add_pad (GST_ELEMENT_CAST (sink), sink->sinkpad);

}

static void
gst_ipc_pipeline_sink_dispose (GObject * obj)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (obj);

  gst_ipc_pipeline_sink_stop_reader_thread (sink);
  gst_ipc_pipeline_comm_cancel (&sink->comm, TRUE);

  G_OBJECT_CLASS (parent_class)->dispose (obj);
}

static void
gst_ipc_pipeline_sink_finalize (GObject * obj)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (obj);

  gst_ipc_pipeline_comm_clear (&sink->comm);
  g_thread_pool_free (sink->threads, TRUE, TRUE);

  G_OBJECT_CLASS (parent_class)->finalize (obj);
}

static void
gst_ipc_pipeline_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (object);

  switch (prop_id) {
    case PROP_FDIN:
      sink->comm.fdin = g_value_get_int (value);
      break;
    case PROP_FDOUT:
      sink->comm.fdout = g_value_get_int (value);
      break;
    case PROP_READ_CHUNK_SIZE:
      sink->comm.read_chunk_size = g_value_get_uint (value);
      break;
    case PROP_ACK_TIME:
      sink->comm.ack_time = g_value_get_uint64 (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_ipc_pipeline_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (object);

  switch (prop_id) {
    case PROP_FDIN:
      g_value_set_int (value, sink->comm.fdin);
      break;
    case PROP_FDOUT:
      g_value_set_int (value, sink->comm.fdout);
      break;
    case PROP_READ_CHUNK_SIZE:
      g_value_set_uint (value, sink->comm.read_chunk_size);
      break;
    case PROP_ACK_TIME:
      g_value_set_uint64 (value, sink->comm.ack_time);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_ipc_pipeline_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (parent);
  gboolean ret;

  GST_DEBUG_OBJECT (sink, "received event %p of type %s (%d)",
      event, gst_event_type_get_name (event->type), event->type);

  ret = gst_ipc_pipeline_comm_write_event_to_fd (&sink->comm, FALSE, event);
  gst_event_unref (event);
  return ret;
}

static GstFlowReturn
gst_ipc_pipeline_sink_chain (GstPad * pad, GstObject * parent,
    GstBuffer * buffer)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (parent);
  GstFlowReturn ret;

  GST_DEBUG_OBJECT (sink, "Rendering buffer %" GST_PTR_FORMAT, buffer);

  ret = gst_ipc_pipeline_comm_write_buffer_to_fd (&sink->comm, buffer);
  if (ret != GST_FLOW_OK)
    GST_DEBUG_OBJECT (sink, "Peer result was %s", gst_flow_get_name (ret));

  gst_buffer_unref (buffer);
  return ret;
}

static gboolean
gst_ipc_pipeline_sink_query (GstPad * pad, GstObject * parent, GstQuery * query)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (parent);
  gboolean ret;

  GST_DEBUG_OBJECT (sink, "Got query %s: %" GST_PTR_FORMAT,
      GST_QUERY_TYPE_NAME (query), query);

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_ALLOCATION:
      GST_DEBUG_OBJECT (sink, "Rejecting ALLOCATION query");
      return FALSE;
    case GST_QUERY_CAPS:
    {
      /* caps queries occur even while linking the pipeline.
       * It is possible that the ipcpipelinesrc may not be connected at this
       * point, so let's avoid a couple of errors... */
      GstState state;
      GST_OBJECT_LOCK (sink);
      state = GST_STATE (sink);
      GST_OBJECT_UNLOCK (sink);
      if (state == GST_STATE_NULL)
        return FALSE;
    }
    default:
      break;
  }
  ret = gst_ipc_pipeline_comm_write_query_to_fd (&sink->comm, FALSE, query);

  return ret;
}

static gboolean
gst_ipc_pipeline_sink_element_query (GstElement * element, GstQuery * query)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (element);
  gboolean ret;

  GST_DEBUG_OBJECT (sink, "Got element query %s: %" GST_PTR_FORMAT,
      GST_QUERY_TYPE_NAME (query), query);

  ret = gst_ipc_pipeline_comm_write_query_to_fd (&sink->comm, TRUE, query);
  GST_DEBUG_OBJECT (sink, "Got query reply: %d: %" GST_PTR_FORMAT, ret, query);
  return ret;
}

static gboolean
gst_ipc_pipeline_sink_send_event (GstElement * element, GstEvent * event)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (element);
  gboolean ret;

  GST_DEBUG_OBJECT (sink, "Got element event %s: %" GST_PTR_FORMAT,
      GST_EVENT_TYPE_NAME (event), event);

  ret = gst_ipc_pipeline_comm_write_event_to_fd (&sink->comm, TRUE, event);
  GST_DEBUG_OBJECT (sink, "Got event reply: %d: %" GST_PTR_FORMAT, ret, event);

  gst_event_unref (event);
  return ret;
}


static gboolean
gst_ipc_pipeline_sink_pad_activate_mode (GstPad * pad,
    GstObject * parent, GstPadMode mode, gboolean active)
{
  if (mode == GST_PAD_MODE_PULL)
    return FALSE;
  return TRUE;
}

static void
on_buffer (guint32 id, GstBuffer * buffer, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);
  GST_ERROR_OBJECT (sink,
      "Got buffer id %u! I never knew buffers could go upstream...", id);
  gst_buffer_unref (buffer);
}

static void
pusher (gpointer data, gpointer user_data)
{
  GstIpcPipelineSink *sink = user_data;
  gboolean ret;
  guint32 id;

  id = GPOINTER_TO_INT (gst_mini_object_get_qdata (GST_MINI_OBJECT (data),
          QUARK_ID));

  if (GST_IS_EVENT (data)) {
    GstEvent *event = GST_EVENT (data);
    GST_DEBUG_OBJECT (sink, "Pushing event async: %" GST_PTR_FORMAT, event);
    ret = gst_pad_push_event (sink->sinkpad, event);
    GST_DEBUG_OBJECT (sink, "Event pushed, return %d", ret);
    gst_ipc_pipeline_comm_write_boolean_ack_to_fd (&sink->comm, id, ret);
  } else if (GST_IS_QUERY (data)) {
    GstQuery *query = GST_QUERY (data);
    GST_DEBUG_OBJECT (sink, "Pushing query async: %" GST_PTR_FORMAT, query);
    ret = gst_pad_peer_query (sink->sinkpad, query);
    GST_DEBUG_OBJECT (sink, "Query pushed, return %d", ret);
    gst_ipc_pipeline_comm_write_query_result_to_fd (&sink->comm, id, ret,
        query);
    gst_query_unref (query);
  } else {
    GST_ERROR_OBJECT (sink, "Unsupported object type");
  }
  gst_object_unref (sink);
}

static void
on_event (guint32 id, GstEvent * event, gboolean upstream, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);

  if (!upstream) {
    GST_ERROR_OBJECT (sink, "Got downstream event id %u! Not supposed to...",
        id);
    gst_ipc_pipeline_comm_write_boolean_ack_to_fd (&sink->comm, id, FALSE);
    gst_event_unref (event);
    return;
  }

  GST_DEBUG_OBJECT (sink, "Got event id %u: %" GST_PTR_FORMAT, id, event);
  gst_object_ref (sink);
  g_thread_pool_push (sink->threads, event, NULL);
}

static void
on_query (guint32 id, GstQuery * query, gboolean upstream, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);

  if (!upstream) {
    GST_ERROR_OBJECT (sink, "Got downstream query id %u! Not supposed to...",
        id);
    gst_ipc_pipeline_comm_write_query_result_to_fd (&sink->comm, id, FALSE,
        query);
    gst_query_unref (query);
    return;
  }

  GST_DEBUG_OBJECT (sink, "Got query id %u: %" GST_PTR_FORMAT, id, query);
  gst_object_ref (sink);
  g_thread_pool_push (sink->threads, query, NULL);
}

static void
on_state_change (guint32 id, GstStateChange transition, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);
  GST_ERROR_OBJECT (sink, "Got state change id %u! Not supposed to...", id);
}

static void
on_state_lost (gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);

  GST_DEBUG_OBJECT (sink, "Got state lost notification, losing state");

  GST_OBJECT_LOCK (sink);
  sink->pass_next_async_done = TRUE;
  GST_OBJECT_UNLOCK (sink);

  gst_element_lost_state (GST_ELEMENT (sink));
}

static void
do_async_done (GstElement * element, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (element);
  GstMessage *message = user_data;

  GST_STATE_LOCK (sink);
  GST_OBJECT_LOCK (sink);
  if (sink->pass_next_async_done) {
    sink->pass_next_async_done = FALSE;
    GST_OBJECT_UNLOCK (sink);
    gst_element_continue_state (element, GST_STATE_CHANGE_SUCCESS);
    GST_STATE_UNLOCK (sink);
    gst_element_post_message (element, gst_message_ref (message));

  } else {
    GST_OBJECT_UNLOCK (sink);
    GST_STATE_UNLOCK (sink);
  }

  gst_message_unref (message);
}

static void
on_message (guint32 id, GstMessage * message, gpointer user_data)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (user_data);

  GST_DEBUG_OBJECT (sink, "Got message id %u: %" GST_PTR_FORMAT, id, message);

  switch (GST_MESSAGE_TYPE (message)) {
    case GST_MESSAGE_ASYNC_DONE:
      GST_OBJECT_LOCK (sink);
      if (sink->pass_next_async_done) {
        GST_OBJECT_UNLOCK (sink);
        gst_object_call_async (GST_OBJECT (sink),
            (GstObjectCallAsyncFunc) do_async_done, message);
      } else {
        GST_OBJECT_UNLOCK (sink);
        gst_message_unref (message);
      }
      return;
    default:
      break;
  }

  gst_element_post_message (GST_ELEMENT (sink), message);
}

static gboolean
gst_ipc_pipeline_sink_start_reader_thread (GstIpcPipelineSink * sink)
{
  if (!gst_ipc_pipeline_comm_start_reader_thread (&sink->comm, on_buffer,
          on_event, on_query, on_state_change, on_state_lost, on_message,
          sink)) {
    GST_ERROR_OBJECT (sink, "Failed to start reader thread");
    return FALSE;
  }
  return TRUE;
}

static void
gst_ipc_pipeline_sink_stop_reader_thread (GstIpcPipelineSink * sink)
{
  gst_ipc_pipeline_comm_stop_reader_thread (&sink->comm);
}


static void
gst_ipc_pipeline_sink_disconnect (GstIpcPipelineSink * sink)
{
  GST_DEBUG_OBJECT (sink, "Disconnecting");
  gst_ipc_pipeline_sink_stop_reader_thread (sink);
  sink->comm.fdin = -1;
  sink->comm.fdout = -1;
  gst_ipc_pipeline_comm_cancel (&sink->comm, FALSE);
  gst_ipc_pipeline_sink_start_reader_thread (sink);
}

static GstStateChangeReturn
gst_ipc_pipeline_sink_change_state (GstElement * element,
    GstStateChange transition)
{
  GstIpcPipelineSink *sink = GST_IPC_PIPELINE_SINK (element);
  GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
  GstStateChangeReturn peer_ret = GST_STATE_CHANGE_SUCCESS;
  gboolean async = FALSE;
  gboolean down = FALSE;

  GST_DEBUG_OBJECT (sink, "Got state change request: %s -> %s",
      gst_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
      gst_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      if (sink->comm.fdin < 0) {
        GST_ERROR_OBJECT (element, "Invalid fdin: %d", sink->comm.fdin);
        return GST_STATE_CHANGE_FAILURE;
      }
      if (sink->comm.fdout < 0) {
        GST_ERROR_OBJECT (element, "Invalid fdout: %d", sink->comm.fdout);
        return GST_STATE_CHANGE_FAILURE;
      }
      if (!sink->comm.reader_thread) {
        GST_ERROR_OBJECT (element, "Failed to start reader thread");
        return GST_STATE_CHANGE_FAILURE;
      }
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      /* In these transitions, it is possible that the peer returns ASYNC.
       * We don't know that in advance, but we post async-start anyway because
       * it needs to be delivered *before* async-done, and async-done may
       * arrive at any point in time after we've set the state of the peer.
       * In case the peer doesn't return ASYNC, we just post async-done
       * ourselves and the parent GstBin takes care of matching and deleting
       * them, so the app never gets any of these. */
      async = TRUE;
      break;
    default:
      break;
  }

  /* downwards state change */
  down = (GST_STATE_TRANSITION_CURRENT (transition) >=
      GST_STATE_TRANSITION_NEXT (transition));

  if (async) {
    GST_DEBUG_OBJECT (sink,
        "Posting async-start for %s, will need state-change-done",
        gst_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));

    gst_element_post_message (GST_ELEMENT (sink),
        gst_message_new_async_start (GST_OBJECT (sink)));

    GST_OBJECT_LOCK (sink);
    sink->pass_next_async_done = TRUE;
    GST_OBJECT_UNLOCK (sink);
  }

  /* change the state of the peer first */
  /* If the fd out is -1, we do not actually call the peer. This will happen
     when we explicitly disconnected, and in that case we want to be able
     to bring the element down to NULL, so it can be restarted with a new
     slave pipeline. */
  if (sink->comm.fdout >= 0) {
    GST_DEBUG_OBJECT (sink, "Calling peer with state change");
    peer_ret = gst_ipc_pipeline_comm_write_state_change_to_fd (&sink->comm,
        transition);
    if (peer_ret == GST_STATE_CHANGE_FAILURE && down) {
      GST_WARNING_OBJECT (sink, "Peer returned state change failure, "
          "but ignoring because we are going down");
      peer_ret = GST_STATE_CHANGE_SUCCESS;
    }
  } else {
    if (down) {
      GST_WARNING_OBJECT (sink, "Not calling peer (fdout %d)",
          sink->comm.fdout);
      peer_ret = GST_STATE_CHANGE_SUCCESS;
    } else {
      GST_ERROR_OBJECT (sink, "Not calling peer (fdout %d) and failing",
          sink->comm.fdout);
      peer_ret = GST_STATE_CHANGE_FAILURE;
    }
  }

  /* chain up to the parent class to change our state, if the peer succeeded */
  if (peer_ret != GST_STATE_CHANGE_FAILURE) {
    ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

    if (G_UNLIKELY (ret == GST_STATE_CHANGE_FAILURE && down)) {
      GST_WARNING_OBJECT (sink, "Parent returned state change failure, "
          "but ignoring because we are going down");
      ret = GST_STATE_CHANGE_SUCCESS;
    }
  }

  GST_DEBUG_OBJECT (sink, "For %s -> %s: Peer ret: %s, parent ret: %s",
      gst_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
      gst_state_get_name (GST_STATE_TRANSITION_NEXT (transition)),
      gst_state_change_return_get_name (peer_ret),
      gst_state_change_return_get_name (ret));

  /* now interpret the return codes */
  if (async && peer_ret != GST_STATE_CHANGE_ASYNC) {
    GST_DEBUG_OBJECT (sink, "Posting async-done for %s; peer wasn't ASYNC",
        gst_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));

    GST_OBJECT_LOCK (sink);
    sink->pass_next_async_done = FALSE;
    GST_OBJECT_UNLOCK (sink);

    gst_element_post_message (GST_ELEMENT (sink),
        gst_message_new_async_done (GST_OBJECT (sink), GST_CLOCK_TIME_NONE));
  } else if (G_UNLIKELY (!async && peer_ret == GST_STATE_CHANGE_ASYNC)) {
    GST_WARNING_OBJECT (sink, "Transition not async but peer returned ASYNC");
    peer_ret = GST_STATE_CHANGE_SUCCESS;
  }

  if (peer_ret == GST_STATE_CHANGE_FAILURE || ret == GST_STATE_CHANGE_FAILURE) {
    if (peer_ret != GST_STATE_CHANGE_FAILURE && sink->comm.fdout >= 0) {
      /* only the parent's ret was FAILURE - revert remote changes */
      GST_DEBUG_OBJECT (sink, "Reverting remote state change because parent "
          "returned failure");
      gst_ipc_pipeline_comm_write_state_change_to_fd (&sink->comm,
          GST_STATE_TRANSITION (GST_STATE_TRANSITION_NEXT (transition),
              GST_STATE_TRANSITION_CURRENT (transition)));
    }
    return GST_STATE_CHANGE_FAILURE;
  }

  /* the parent's (GstElement) state change func won't return ASYNC or
   * NO_PREROLL, so unless it has returned FAILURE, which we have caught above,
   * we are not interested in its return code... just return the peer's */
  return peer_ret;
}
