/* GStreamer RTP base payloader unit tests
 * Copyright (C) 2014  Sebastian Rasmussen <sebras@hotmail.com>
 *
 * 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.
 */

#include <gst/gst.h>
#include <gst/check/gstcheck.h>
#include <gst/rtp/gstrtpbuffer.h>
#include <gst/rtp/gstrtpbasepayload.h>

#define DEFAULT_CLOCK_RATE (42)
#define BUFFER_BEFORE_LIST (10)

/* GstRtpDummyPay */

#define GST_TYPE_RTP_DUMMY_PAY \
  (gst_rtp_dummy_pay_get_type())
#define GST_RTP_DUMMY_PAY(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_RTP_DUMMY_PAY,GstRtpDummyPay))
#define GST_RTP_DUMMY_PAY_CLASS(klass) \
  (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_RTP_DUMMY_PAY,GstRtpDummyPayClass))
#define GST_IS_RTP_DUMMY_PAY(obj) \
  (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_RTP_DUMMY_PAY))
#define GST_IS_RTP_DUMMY_PAY_CLASS(klass) \
  (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_RTP_DUMMY_PAY))

typedef struct _GstRtpDummyPay GstRtpDummyPay;
typedef struct _GstRtpDummyPayClass GstRtpDummyPayClass;

struct _GstRtpDummyPay
{
  GstRTPBasePayload payload;
};

struct _GstRtpDummyPayClass
{
  GstRTPBasePayloadClass parent_class;
};

GType gst_rtp_dummy_pay_get_type (void);

G_DEFINE_TYPE (GstRtpDummyPay, gst_rtp_dummy_pay, GST_TYPE_RTP_BASE_PAYLOAD);

static GstFlowReturn gst_rtp_dummy_pay_handle_buffer (GstRTPBasePayload * pay,
    GstBuffer * buffer);

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

static GstStaticPadTemplate gst_rtp_dummy_pay_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/x-rtp"));

static void
gst_rtp_dummy_pay_class_init (GstRtpDummyPayClass * klass)
{
  GstElementClass *gstelement_class;
  GstRTPBasePayloadClass *gstrtpbasepayload_class;

  gstelement_class = GST_ELEMENT_CLASS (klass);
  gstrtpbasepayload_class = GST_RTP_BASE_PAYLOAD_CLASS (klass);

  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&gst_rtp_dummy_pay_sink_template));
  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&gst_rtp_dummy_pay_src_template));

  gstrtpbasepayload_class->handle_buffer = gst_rtp_dummy_pay_handle_buffer;
}

static void
gst_rtp_dummy_pay_init (GstRtpDummyPay * pay)
{
  gst_rtp_base_payload_set_options (GST_RTP_BASE_PAYLOAD (pay), "application",
      TRUE, "dummy", DEFAULT_CLOCK_RATE);
}

static GstRtpDummyPay *
rtp_dummy_pay_new (void)
{
  return g_object_new (GST_TYPE_RTP_DUMMY_PAY, NULL);
}

static GstFlowReturn
gst_rtp_dummy_pay_handle_buffer (GstRTPBasePayload * pay, GstBuffer * buffer)
{
  GstBuffer *paybuffer;

  GST_LOG ("payloading buffer pts=%" GST_TIME_FORMAT " offset=%"
      G_GUINT64_FORMAT, GST_TIME_ARGS (GST_BUFFER_PTS (buffer)),
      GST_BUFFER_OFFSET (buffer));

  if (!gst_pad_has_current_caps (GST_RTP_BASE_PAYLOAD_SRCPAD (pay))) {
    if (!gst_rtp_base_payload_set_outcaps (GST_RTP_BASE_PAYLOAD (pay),
            "custom-caps", G_TYPE_UINT, DEFAULT_CLOCK_RATE, NULL)) {
      gst_buffer_unref (buffer);
      return GST_FLOW_NOT_NEGOTIATED;
    }
  }

  paybuffer = gst_rtp_buffer_new_allocate (0, 0, 0);

  GST_BUFFER_PTS (paybuffer) = GST_BUFFER_PTS (buffer);
  GST_BUFFER_OFFSET (paybuffer) = GST_BUFFER_OFFSET (buffer);

  gst_buffer_append (paybuffer, buffer);

  GST_LOG ("payloaded buffer pts=%" GST_TIME_FORMAT " offset=%"
      G_GUINT64_FORMAT, GST_TIME_ARGS (GST_BUFFER_PTS (paybuffer)),
      GST_BUFFER_OFFSET (paybuffer));

  if (GST_BUFFER_PTS (paybuffer) < BUFFER_BEFORE_LIST) {
    return gst_rtp_base_payload_push (pay, paybuffer);
  } else {
    GstBufferList *list = gst_buffer_list_new ();
    gst_buffer_list_add (list, paybuffer);
    return gst_rtp_base_payload_push_list (pay, list);
  }
}

/* Helper functions and global state */

static GstStaticPadTemplate srctmpl = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS_ANY);

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

static GstStaticPadTemplate special_sinktmpl = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/x-rtp, payload=(int)98, ssrc=(uint)24, "
        "timestamp-offset=(uint)212, seqnum-offset=(uint)2424"));

typedef struct State State;

struct State
{
  GstElement *element;
  GstPad *sinkpad;
  GstPad *srcpad;
};

static GList *events;

static gboolean
event_func (GstPad * pad, GstObject * noparent, GstEvent * event)
{
  events = g_list_append (events, gst_event_ref (event));
  return gst_pad_event_default (pad, noparent, event);
}

static void
drop_events (void)
{
  while (events != NULL) {
    gst_event_unref (GST_EVENT (events->data));
    events = g_list_delete_link (events, events);
  }
}

static void
validate_events_received (guint received)
{
  fail_unless_equals_int (g_list_length (events), received);
}

static void
validate_event (guint index, const gchar * name, const gchar * field, ...)
{
  GstEvent *event;
  va_list var_args;

  fail_if (index >= g_list_length (events));
  event = GST_EVENT (g_list_nth_data (events, index));
  fail_if (event == NULL);

  GST_TRACE ("%" GST_PTR_FORMAT, event);

  fail_unless_equals_string (GST_EVENT_TYPE_NAME (event), name);

  va_start (var_args, field);
  while (field) {
    if (!g_strcmp0 (field, "timestamp")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      GstClockTime timestamp, duration;
      gst_event_parse_gap (event, &timestamp, &duration);
      fail_unless_equals_uint64 (timestamp, expected);
    } else if (!g_strcmp0 (field, "duration")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      GstClockTime timestamp, duration;
      gst_event_parse_gap (event, &timestamp, &duration);
      fail_unless_equals_uint64 (duration, expected);
    } else if (!g_strcmp0 (field, "time")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      const GstSegment *segment;
      gst_event_parse_segment (event, &segment);
      fail_unless_equals_uint64 (segment->time, expected);
    } else if (!g_strcmp0 (field, "start")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      const GstSegment *segment;
      gst_event_parse_segment (event, &segment);
      fail_unless_equals_uint64 (segment->start, expected);
    } else if (!g_strcmp0 (field, "stop")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      const GstSegment *segment;
      gst_event_parse_segment (event, &segment);
      fail_unless_equals_uint64 (segment->stop, expected);
    } else if (!g_strcmp0 (field, "applied-rate")) {
      gdouble expected = va_arg (var_args, gdouble);
      const GstSegment *segment;
      gst_event_parse_segment (event, &segment);
      fail_unless_equals_uint64 (segment->applied_rate, expected);
    } else if (!g_strcmp0 (field, "rate")) {
      gdouble expected = va_arg (var_args, gdouble);
      const GstSegment *segment;
      gst_event_parse_segment (event, &segment);
      fail_unless_equals_uint64 (segment->rate, expected);
    } else if (!g_strcmp0 (field, "media-type")) {
      const gchar *expected = va_arg (var_args, const gchar *);
      GstCaps *caps;
      const gchar *media_type;
      gst_event_parse_caps (event, &caps);
      media_type = gst_structure_get_name (gst_caps_get_structure (caps, 0));
      fail_unless_equals_string (media_type, expected);
    } else if (!g_strcmp0 (field, "npt-start")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      GstCaps *caps;
      GstClockTime start;
      gst_event_parse_caps (event, &caps);
      fail_unless (gst_structure_get_clock_time (gst_caps_get_structure (caps,
                  0), "npt-start", &start));
      fail_unless_equals_uint64 (start, expected);
    } else if (!g_strcmp0 (field, "npt-stop")) {
      GstClockTime expected = va_arg (var_args, GstClockTime);
      GstCaps *caps;
      GstClockTime stop;
      gst_event_parse_caps (event, &caps);
      fail_unless (gst_structure_get_clock_time (gst_caps_get_structure (caps,
                  0), "npt-stop", &stop));
      fail_unless_equals_uint64 (stop, expected);
    } else if (!g_strcmp0 (field, "play-speed")) {
      gdouble expected = va_arg (var_args, gdouble);
      GstCaps *caps;
      gdouble speed;
      gst_event_parse_caps (event, &caps);
      fail_unless (gst_structure_get_double (gst_caps_get_structure (caps, 0),
              "play-speed", &speed));
      fail_unless (speed == expected);
    } else if (!g_strcmp0 (field, "play-scale")) {
      gdouble expected = va_arg (var_args, gdouble);
      GstCaps *caps;
      gdouble scale;
      gst_event_parse_caps (event, &caps);
      fail_unless (gst_structure_get_double (gst_caps_get_structure (caps, 0),
              "play-scale", &scale));
      fail_unless (scale == expected);
    } else if (!g_strcmp0 (field, "ssrc")) {
      guint expected = va_arg (var_args, guint);
      GstCaps *caps;
      guint ssrc;
      gst_event_parse_caps (event, &caps);
      fail_unless (gst_structure_get_uint (gst_caps_get_structure (caps, 0),
              "ssrc", &ssrc));
      fail_unless_equals_int (ssrc, expected);
    } else if (!g_strcmp0 (field, "a-framerate")) {
      const gchar *expected = va_arg (var_args, const gchar *);
      GstCaps *caps;
      const gchar *framerate;
      gst_event_parse_caps (event, &caps);
      framerate = gst_structure_get_string (gst_caps_get_structure (caps, 0),
          "a-framerate");
      fail_unless_equals_string (framerate, expected);
    } else {
      fail ("test cannot validate unknown event field '%s'", field);
    }
    field = va_arg (var_args, const gchar *);
  }
  va_end (var_args);
}

static void
validate_normal_start_events (uint index)
{
  validate_event (index, "stream-start", NULL);

  validate_event (index + 1, "caps", "media-type", "application/x-rtp", NULL);

  validate_event (index + 2, "segment",
      "time", G_GUINT64_CONSTANT (0),
      "start", G_GUINT64_CONSTANT (0), "stop", G_MAXUINT64, NULL);
}

#define push_buffer(state, field, ...) \
	push_buffer_full ((state), GST_FLOW_OK, (field), __VA_ARGS__)
#define push_buffer_fails(state, field, ...) \
	push_buffer_full ((state), GST_FLOW_FLUSHING, (field), __VA_ARGS__)

static void
push_buffer_full (State * state, GstFlowReturn expected,
    const gchar * field, ...)
{
  GstBuffer *buf = gst_rtp_buffer_new_allocate (0, 0, 0);
  GstRTPBuffer rtp = { NULL };
  gboolean mapped = FALSE;
  va_list var_args;

  va_start (var_args, field);
  while (field) {
    if (!g_strcmp0 (field, "pts")) {
      GstClockTime pts = va_arg (var_args, GstClockTime);
      GST_BUFFER_PTS (buf) = pts;
    } else if (!g_strcmp0 (field, "offset")) {
      guint64 offset = va_arg (var_args, guint64);
      GST_BUFFER_OFFSET (buf) = offset;
    } else if (!g_strcmp0 (field, "discont")) {
      gboolean discont = va_arg (var_args, gboolean);
      if (discont) {
        GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);
      } else {
        GST_BUFFER_FLAG_UNSET (buf, GST_BUFFER_FLAG_DISCONT);
      }
    } else {
      if (!mapped) {
        gst_rtp_buffer_map (buf, GST_MAP_WRITE, &rtp);
        mapped = TRUE;
      }
      if (!g_strcmp0 (field, "rtptime")) {
        guint32 rtptime = va_arg (var_args, guint);
        gst_rtp_buffer_set_timestamp (&rtp, rtptime);
      } else if (!g_strcmp0 (field, "payload-type")) {
        guint payload_type = va_arg (var_args, guint);
        gst_rtp_buffer_set_payload_type (&rtp, payload_type);
      } else if (!g_strcmp0 (field, "seq")) {
        guint seq = va_arg (var_args, guint);
        gst_rtp_buffer_set_seq (&rtp, seq);
      } else if (!g_strcmp0 (field, "ssrc")) {
        guint32 ssrc = va_arg (var_args, guint);
        gst_rtp_buffer_set_ssrc (&rtp, ssrc);
      } else {
        fail ("test cannot set unknown buffer field '%s'", field);
      }
    }
    field = va_arg (var_args, const gchar *);
  }
  va_end (var_args);

  if (mapped) {
    gst_rtp_buffer_unmap (&rtp);
  }

  fail_unless_equals_int (gst_pad_push (state->srcpad, buf), expected);
}

static void
push_buffer_list (State * state, const gchar * field, ...)
{
  GstBuffer *buf = gst_rtp_buffer_new_allocate (0, 0, 0);
  GstRTPBuffer rtp = { NULL };
  gboolean mapped = FALSE;
  GstBufferList *list;
  va_list var_args;

  va_start (var_args, field);
  while (field) {
    if (!g_strcmp0 (field, "pts")) {
      GstClockTime pts = va_arg (var_args, GstClockTime);
      GST_BUFFER_PTS (buf) = pts;
    } else if (!g_strcmp0 (field, "offset")) {
      guint64 offset = va_arg (var_args, guint64);
      GST_BUFFER_OFFSET (buf) = offset;
    } else if (!g_strcmp0 (field, "discont")) {
      gboolean discont = va_arg (var_args, gboolean);
      if (discont) {
        GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);
      } else {
        GST_BUFFER_FLAG_UNSET (buf, GST_BUFFER_FLAG_DISCONT);
      }
    } else {
      if (!mapped) {
        gst_rtp_buffer_map (buf, GST_MAP_WRITE, &rtp);
        mapped = TRUE;
      }
      if (!g_strcmp0 (field, "rtptime")) {
        guint32 rtptime = va_arg (var_args, guint);
        gst_rtp_buffer_set_timestamp (&rtp, rtptime);
      } else if (!g_strcmp0 (field, "payload-type")) {
        guint payload_type = va_arg (var_args, guint);
        gst_rtp_buffer_set_payload_type (&rtp, payload_type);
      } else if (!g_strcmp0 (field, "seq")) {
        guint seq = va_arg (var_args, guint);
        gst_rtp_buffer_set_seq (&rtp, seq);
      } else if (!g_strcmp0 (field, "ssrc")) {
        guint32 ssrc = va_arg (var_args, guint);
        gst_rtp_buffer_set_ssrc (&rtp, ssrc);
      } else {
        fail ("test cannot set unknown buffer field '%s'", field);
      }
    }
    field = va_arg (var_args, const gchar *);
  }
  va_end (var_args);

  if (mapped) {
    gst_rtp_buffer_unmap (&rtp);
  }

  list = gst_buffer_list_new ();
  gst_buffer_list_add (list, buf);
  fail_unless_equals_int (gst_pad_push_list (state->srcpad, list), GST_FLOW_OK);
}

static void
validate_buffers_received (guint received_buffers)
{
  fail_unless_equals_int (g_list_length (buffers), received_buffers);
}

static void
validate_buffer (guint index, const gchar * field, ...)
{
  GstBuffer *buf;
  GstRTPBuffer rtp = { NULL };
  gboolean mapped = FALSE;
  va_list var_args;

  fail_if (index >= g_list_length (buffers));
  buf = GST_BUFFER (g_list_nth_data (buffers, index));
  fail_if (buf == NULL);

  GST_TRACE ("%" GST_PTR_FORMAT, buf);

  va_start (var_args, field);
  while (field) {
    if (!g_strcmp0 (field, "pts")) {
      GstClockTime pts = va_arg (var_args, GstClockTime);
      fail_unless_equals_uint64 (GST_BUFFER_PTS (buf), pts);
    } else if (!g_strcmp0 (field, "offset")) {
      guint64 offset = va_arg (var_args, guint64);
      fail_unless_equals_uint64 (GST_BUFFER_OFFSET (buf), offset);
    } else if (!g_strcmp0 (field, "discont")) {
      gboolean discont = va_arg (var_args, gboolean);
      if (discont) {
        fail_unless (GST_BUFFER_FLAG_IS_SET (buf, GST_BUFFER_FLAG_DISCONT));
      } else {
        fail_if (GST_BUFFER_FLAG_IS_SET (buf, GST_BUFFER_FLAG_DISCONT));
      }
    } else {
      if (!mapped) {
        gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp);
        mapped = TRUE;
      }
      if (!g_strcmp0 (field, "rtptime")) {
        guint32 rtptime = va_arg (var_args, guint);
        fail_unless_equals_int (gst_rtp_buffer_get_timestamp (&rtp), rtptime);
      } else if (!g_strcmp0 (field, "payload-type")) {
        guint pt = va_arg (var_args, guint);
        fail_unless_equals_int (gst_rtp_buffer_get_payload_type (&rtp), pt);
      } else if (!g_strcmp0 (field, "seq")) {
        guint seq = va_arg (var_args, guint);
        fail_unless_equals_int (gst_rtp_buffer_get_seq (&rtp), seq);
      } else if (!g_strcmp0 (field, "ssrc")) {
        guint32 ssrc = va_arg (var_args, guint);
        fail_unless_equals_int (gst_rtp_buffer_get_ssrc (&rtp), ssrc);
      } else {
        fail ("test cannot validate unknown buffer field '%s'", field);
      }
    }
    field = va_arg (var_args, const gchar *);
  }
  va_end (var_args);

  if (mapped) {
    gst_rtp_buffer_unmap (&rtp);
  }
}

static void
get_buffer_field (guint index, const gchar * field, ...)
{
  GstBuffer *buf;
  GstRTPBuffer rtp = { NULL };
  gboolean mapped = FALSE;
  va_list var_args;

  fail_if (index >= g_list_length (buffers));
  buf = GST_BUFFER (g_list_nth_data (buffers, (index)));
  fail_if (buf == NULL);

  va_start (var_args, field);
  while (field) {
    if (!g_strcmp0 (field, "pts")) {
      GstClockTime *pts = va_arg (var_args, GstClockTime *);
      *pts = GST_BUFFER_PTS (buf);
    } else if (!g_strcmp0 (field, "offset")) {
      guint64 *offset = va_arg (var_args, guint64 *);
      *offset = GST_BUFFER_OFFSET (buf);
    } else if (!g_strcmp0 (field, "discont")) {
      gboolean *discont = va_arg (var_args, gboolean *);
      *discont = GST_BUFFER_FLAG_IS_SET (buf, GST_BUFFER_FLAG_DISCONT);
    } else {
      if (!mapped) {
        gst_rtp_buffer_map (buf, GST_MAP_READ, &rtp);
        mapped = TRUE;
      }
      if (!g_strcmp0 (field, "rtptime")) {
        guint32 *rtptime = va_arg (var_args, guint32 *);
        *rtptime = gst_rtp_buffer_get_timestamp (&rtp);
      } else if (!g_strcmp0 (field, "payload-type")) {
        guint *pt = va_arg (var_args, guint *);
        *pt = gst_rtp_buffer_get_payload_type (&rtp);
      } else if (!g_strcmp0 (field, "seq")) {
        guint16 *seq = va_arg (var_args, guint16 *);
        *seq = gst_rtp_buffer_get_seq (&rtp);
      } else if (!g_strcmp0 (field, "ssrc")) {
        guint32 *ssrc = va_arg (var_args, guint32 *);
        *ssrc = gst_rtp_buffer_get_ssrc (&rtp);
      } else {
        fail ("test retrieve validate unknown buffer field '%s'", field);
      }
    }
    field = va_arg (var_args, const gchar *);
  }
  va_end (var_args);

  if (mapped)
    gst_rtp_buffer_unmap (&rtp);
}

static State *
create_payloader (const gchar * caps_str,
    GstStaticPadTemplate * sinktmpl, const gchar * property, ...)
{
  va_list var_args;
  GstCaps *caps;
  State *state;

  state = g_new0 (State, 1);

  state->element = GST_ELEMENT (rtp_dummy_pay_new ());
  fail_unless (GST_IS_RTP_DUMMY_PAY (state->element));

  va_start (var_args, property);
  g_object_set_valist (G_OBJECT (state->element), property, var_args);
  va_end (var_args);

  state->srcpad = gst_check_setup_src_pad (state->element, &srctmpl);
  state->sinkpad = gst_check_setup_sink_pad (state->element, sinktmpl);

  fail_unless (gst_pad_set_active (state->srcpad, TRUE));
  fail_unless (gst_pad_set_active (state->sinkpad, TRUE));

  caps = gst_caps_from_string (caps_str);
  gst_check_setup_events (state->srcpad, state->element, caps, GST_FORMAT_TIME);
  gst_caps_unref (caps);

  gst_pad_set_chain_function (state->sinkpad, gst_check_chain_func);
  gst_pad_set_event_function (state->sinkpad, event_func);

  return state;
}

static void
set_state (State * state, GstState new_state)
{
  fail_unless_equals_int (gst_element_set_state (state->element, new_state),
      GST_STATE_CHANGE_SUCCESS);
}

static void
validate_would_not_be_filled (State * state, guint size, GstClockTime duration)
{
  GstRTPBasePayload *basepay;
  basepay = GST_RTP_BASE_PAYLOAD (state->element);
  fail_if (gst_rtp_base_payload_is_filled (basepay, size, duration));
}

static void
validate_would_be_filled (State * state, guint size, GstClockTime duration)
{
  GstRTPBasePayload *basepay;
  basepay = GST_RTP_BASE_PAYLOAD (state->element);
  fail_unless (gst_rtp_base_payload_is_filled (basepay, size, duration));
}

static void
ssrc_collision (State * state, guint ssrc,
    gboolean have_new_ssrc, guint new_ssrc)
{
  GstStructure *s;
  GstEvent *event;
  if (have_new_ssrc) {
    s = gst_structure_new ("GstRTPCollision",
        "ssrc", G_TYPE_UINT, ssrc,
        "suggested-ssrc", G_TYPE_UINT, new_ssrc, NULL);
  } else {
    s = gst_structure_new ("GstRTPCollision", "ssrc", G_TYPE_UINT, ssrc, NULL);
  }
  event = gst_event_new_custom (GST_EVENT_CUSTOM_UPSTREAM, s);
  fail_unless (gst_pad_push_event (state->sinkpad, event));
}

static void
reconfigure (State * state)
{
  GstEvent *event;
  event = gst_event_new_reconfigure ();
  fail_unless (gst_pad_push_event (state->sinkpad, event));
}

static void
validate_stats (State * state, guint clock_rate,
    GstClockTime running_time, guint16 seq, guint32 rtptime)
{
  GstStructure *stats;

  g_object_get (state->element, "stats", &stats, NULL);

  fail_unless_equals_int (g_value_get_uint (gst_structure_get_value (stats,
              "clock-rate")), clock_rate);
  fail_unless_equals_uint64 (g_value_get_uint64 (gst_structure_get_value (stats,
              "running-time")), running_time);
  fail_unless_equals_int (g_value_get_uint (gst_structure_get_value (stats,
              "seqnum")), seq);
  fail_unless_equals_int (g_value_get_uint (gst_structure_get_value (stats,
              "timestamp")), rtptime);

  gst_structure_free (stats);
}

static void
destroy_payloader (State * state)
{
  gst_check_teardown_sink_pad (state->element);
  gst_check_teardown_src_pad (state->element);

  gst_check_drop_buffers ();
  drop_events ();

  g_object_unref (state->element);

  g_free (state);
}

/* Tests */

/* push two buffers to the payloader which should successfully payload them
 * into RTP packets. the first packet will have a random rtptime and sequence
 * number, but the last packet should have an rtptime incremented by
 * DEFAULT_CLOCK_RATE and a sequence number incremented by one becuase the
 * packets are sequential. besides the two payloaded RTP packets there should
 * be the three events initial events: stream-start, caps and segment.
 */
GST_START_TEST (rtp_base_payload_buffer_test)
{
  State *state;
  guint32 rtptime;
  guint16 seq;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "perfect-rtptime", FALSE, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);
  get_buffer_field (0, "rtptime", &rtptime, "seq", &seq, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND,
      "rtptime", rtptime + 1 * DEFAULT_CLOCK_RATE, "seq", seq + 1, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* push single buffers in buffer lists to the payloader to be payloaded into
 * RTP packets. the dummy payloader will start pushing buffer lists itself
 * after BUFFER_BEFORE_LIST payloaded RTP packets. any RTP packets included in
 * buffer lists should have rtptime and sequence numbers incrementting in the
 * same way as for separate RTP packets.
 */
GST_START_TEST (rtp_base_payload_buffer_list_test)
{
  State *state;
  guint32 rtptime;
  guint16 seq;
  guint i;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  for (i = 0; i < BUFFER_BEFORE_LIST + 1; i++) {
    push_buffer_list (state, "pts", i * GST_SECOND, NULL);
  }

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (11);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);
  get_buffer_field (0, "rtptime", &rtptime, "seq", &seq, NULL);

  for (i = 1; i < BUFFER_BEFORE_LIST + 1; i++) {
    validate_buffer (i,
        "pts", i * GST_SECOND,
        "rtptime", rtptime + i * DEFAULT_CLOCK_RATE, "seq", seq + i, NULL);
  }

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* push two buffers. because the payloader is using non-perfect rtptime the
 * second buffer will be timestamped with the default clock and ignore any
 * offset set on the buffers being payloaded.
 */
GST_START_TEST (rtp_base_payload_normal_rtptime_test)
{
  guint32 rtptime;
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "perfect-rtptime", FALSE, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state,
      "pts", 0 * GST_SECOND, "offset", GST_BUFFER_OFFSET_NONE, NULL);

  push_buffer (state,
      "pts", 1 * GST_SECOND, "offset", GST_BUFFER_OFFSET_NONE, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0,
      "pts", 0 * GST_SECOND, "offset", GST_BUFFER_OFFSET_NONE, NULL);
  get_buffer_field (0, "rtptime", &rtptime, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND,
      "offset", GST_BUFFER_OFFSET_NONE,
      "rtptime", rtptime + DEFAULT_CLOCK_RATE, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* push two buffers. because the payloader is using perfect rtptime the
 * second buffer will be timestamped with a timestamp incremented with the
 * difference in offset between the first and second buffer. the pts will be
 * ignored for any buffer after the first buffer.
 */
GST_START_TEST (rtp_base_payload_perfect_rtptime_test)
{
  guint32 rtptime;
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "perfect-rtptime", TRUE, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, "offset", G_GINT64_CONSTANT (0),
      NULL);

  push_buffer (state, "pts", GST_CLOCK_TIME_NONE, "offset",
      G_GINT64_CONSTANT (21), NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0, "pts", 0 * GST_SECOND, "offset", G_GINT64_CONSTANT (0),
      NULL);
  get_buffer_field (0, "rtptime", &rtptime, NULL);

  validate_buffer (1,
      "pts", GST_CLOCK_TIME_NONE, "offset", G_GINT64_CONSTANT (21), "rtptime",
      rtptime + 21, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that a payloader will re-use the last used timestamp when a buffer
 * is using perfect rtptime and both the pushed buffers timestamp and the offset
 * is NONE. the payloader is configuered to start with a specific timestamp.
 * then a buffer is sent with a valid timestamp but without any offset. the
 * payloded RTP packet is expected to use the specific timestamp. next another
 * buffer is pushed with a normal timestamp set to illustrate that the payloaded
 * RTP packet will have an increased timestamp. finally a buffer without any
 * timestamp or offset is pushed. in this case the payloaded RTP packet is
 * expected to have the same timestamp as the previously payloaded RTP packet.
 */
GST_START_TEST (rtp_base_payload_no_pts_no_offset_test)
{
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "timestamp-offset", 0x42, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state,
      "pts", 0 * GST_SECOND, "offset", GST_BUFFER_OFFSET_NONE, NULL);

  push_buffer (state,
      "pts", 1 * GST_SECOND, "offset", GST_BUFFER_OFFSET_NONE, NULL);

  push_buffer (state,
      "pts", GST_CLOCK_TIME_NONE, "offset", GST_BUFFER_OFFSET_NONE, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (3);

  validate_buffer (0,
      "pts", 0 * GST_SECOND,
      "offset", GST_BUFFER_OFFSET_NONE, "rtptime", 0x42, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND,
      "offset", GST_BUFFER_OFFSET_NONE,
      "rtptime", 0x42 + 1 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (2,
      "pts", GST_CLOCK_TIME_NONE,
      "offset", GST_BUFFER_OFFSET_NONE,
      "rtptime", 0x42 + 1 * DEFAULT_CLOCK_RATE, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that a downstream element with caps on its sink pad can effectively
 * configure the payloader's payload-type, ssrc, timestamp-offset and
 * seqnum-offset properties and therefore also affect the payloaded RTP packets.
 * this is done by connecting to a sink pad with template caps setting the
 * relevant fields and then pushing a buffer and making sure that the payloaded
 * RTP packet has the expected properties.
 */
GST_START_TEST (rtp_base_payload_downstream_caps_test)
{
  State *state;

  state = create_payloader ("application/x-rtp", &special_sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (1);

  validate_buffer (0,
      "pts", 0 * GST_SECOND,
      "seq", 2424, "payload-type", 98, "ssrc", 24, "rtptime", 212, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* when a payloader receives a GstRTPCollision upstream event it should try to
 * switch to a new ssrc for the next payloaded RTP packets. GstRTPCollision can
 * supply a suggested new ssrc. if a suggested new ssrc is supplied then the
 * payloaded is supposed to use this new ssrc, otherwise it should generate a
 * new random ssrc which is not identical to the one that collided.
 *
 * this is tested by first setting the ssrc to a specific value and pushing a
 * buffer. the payloaded RTP packet is validate to have the set ssrc. then a
 * GstRTPCollision event is generated to instruct the payloader that the
 * previously set ssrc collided. this event suggests a new ssrc and it is
 * verified that a pushed buffer results in a payloaded RTP packet that actually
 * uses this new ssrc. finally a new GstRTPCollision event is generated to
 * indicate another ssrc collision. this time the event does not suggest a new
 * ssrc. the payloaded RTP packet is then expected to have a new random ssrc
 * different from the collided one.
 */
GST_START_TEST (rtp_base_payload_ssrc_collision_test)
{
  State *state;
  guint32 ssrc;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  g_object_set (state->element, "ssrc", 0x4242, NULL);
  g_object_get (state->element, "ssrc", &ssrc, NULL);
  fail_unless_equals_int (ssrc, 0x4242);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  ssrc_collision (state, 0x4242, TRUE, 0x4343);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  ssrc_collision (state, 0x4343, FALSE, 0);

  push_buffer (state, "pts", 2 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (3);

  validate_buffer (0, "pts", 0 * GST_SECOND, "ssrc", 0x4242, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, "ssrc", 0x4343, NULL);

  validate_buffer (2, "pts", 2 * GST_SECOND, NULL);
  get_buffer_field (2, "ssrc", &ssrc, NULL);
  fail_if (ssrc == 0x4343);

  validate_events_received (5);

  validate_normal_start_events (0);

  validate_event (3, "caps",
      "media-type", "application/x-rtp", "ssrc", 0x4343, NULL);

  validate_event (4, "caps",
      "media-type", "application/x-rtp", "ssrc", ssrc, NULL);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that an upstream event different from GstRTPCollision is succesfully
 * forwarded to upstream elements. in this test a caps reconfiguration event is
 * pushed upstream to validate the behaviour.
 */
GST_START_TEST (rtp_base_payload_reconfigure_test)
{
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  reconfigure (state);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, NULL);

  validate_events_received (4);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that changing the mtu actually affects whether buffers are
 * considered to be filled. first detect the default mtu and check that having
 * buffers slightly less or equal to the size will not be considered to be
 * filled, and that going over this size will be filling the buffers. then
 * change the mtu slightly and validate that the boundary actually changed.
 * lastly try the boundary values and make sure that they work as expected.
 */
GST_START_TEST (rtp_base_payload_property_mtu_test)
{
  State *state;
  guint mtu, check;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  g_object_get (state->element, "mtu", &mtu, NULL);
  validate_would_not_be_filled (state, mtu - 1, GST_CLOCK_TIME_NONE);
  validate_would_not_be_filled (state, mtu, GST_CLOCK_TIME_NONE);
  validate_would_be_filled (state, mtu + 1, GST_CLOCK_TIME_NONE);

  g_object_set (state->element, "mtu", mtu - 1, NULL);
  g_object_get (state->element, "mtu", &check, NULL);
  fail_unless_equals_int (check, mtu - 1);
  validate_would_not_be_filled (state, mtu - 1, GST_CLOCK_TIME_NONE);
  validate_would_be_filled (state, mtu, GST_CLOCK_TIME_NONE);
  validate_would_be_filled (state, mtu + 1, GST_CLOCK_TIME_NONE);

  g_object_set (state->element, "mtu", 28, NULL);
  g_object_get (state->element, "mtu", &check, NULL);
  fail_unless_equals_int (check, 28);
  validate_would_not_be_filled (state, 28, GST_CLOCK_TIME_NONE);
  validate_would_be_filled (state, 29, GST_CLOCK_TIME_NONE);

  g_object_set (state->element, "mtu", G_MAXUINT, NULL);
  g_object_get (state->element, "mtu", &check, NULL);
  fail_unless_equals_int (check, G_MAXUINT);
  validate_would_not_be_filled (state, G_MAXUINT - 1, GST_CLOCK_TIME_NONE);
  validate_would_not_be_filled (state, G_MAXUINT, GST_CLOCK_TIME_NONE);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that changing the payload-type will actually affect the
 * payload-type of the payloaded RTP packets. first get the default, then send
 * a buffer with this payload-type. increment the payload-type and send another
 * buffer. then test the boundary values for the payload-type and make sure
 * that these are all carried over to the payloaded RTP packets.
 */
GST_START_TEST (rtp_base_payload_property_pt_test)
{
  State *state;
  guint payload_type, check;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  g_object_get (state->element, "pt", &payload_type, NULL);
  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  g_object_set (state->element, "pt", payload_type + 1, NULL);
  g_object_get (state->element, "pt", &check, NULL);
  fail_unless_equals_int (check, payload_type + 1);
  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  g_object_set (state->element, "pt", 0, NULL);
  g_object_get (state->element, "pt", &check, NULL);
  fail_unless_equals_int (check, 0);
  push_buffer (state, "pts", 2 * GST_SECOND, NULL);

  g_object_set (state->element, "pt", 0x7f, NULL);
  g_object_get (state->element, "pt", &check, NULL);
  fail_unless_equals_int (check, 0x7f);
  push_buffer (state, "pts", 3 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (4);

  validate_buffer (0,
      "pts", 0 * GST_SECOND, "payload-type", payload_type, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND, "payload-type", payload_type + 1, NULL);

  validate_buffer (2, "pts", 2 * GST_SECOND, "payload-type", 0, NULL);

  validate_buffer (3, "pts", 3 * GST_SECOND, "payload-type", 0x7f, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that changing the ssrc will actually affect the ssrc of the
 * payloaded RTP packets. first get the current ssrc which should indicate
 * random ssrcs. send two buffers and expect their ssrcs to be random but
 * identical. since setting the ssrc will only take effect when the pipeline
 * goes READY->PAUSED, bring the pipeline to NULL state, set the ssrc to a given
 * value and make sure that this is carried over to the payloaded RTP packets.
 * the last step is to test the boundary values.
 */
GST_START_TEST (rtp_base_payload_property_ssrc_test)
{
  State *state;
  guint32 ssrc;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  g_object_get (state->element, "ssrc", &ssrc, NULL);
  fail_unless_equals_int (ssrc, -1);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "ssrc", 0x4242, NULL);
  g_object_get (state->element, "ssrc", &ssrc, NULL);
  fail_unless_equals_int (ssrc, 0x4242);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 2 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "ssrc", 0, NULL);
  g_object_get (state->element, "ssrc", &ssrc, NULL);
  fail_unless_equals_int (ssrc, 0);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 3 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "ssrc", G_MAXUINT32, NULL);
  g_object_get (state->element, "ssrc", &ssrc, NULL);
  fail_unless_equals_int (ssrc, G_MAXUINT32);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 4 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (5);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);
  get_buffer_field (0, "ssrc", &ssrc, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, "ssrc", ssrc, NULL);

  validate_buffer (2, "pts", 2 * GST_SECOND, "ssrc", 0x4242, NULL);

  validate_buffer (3, "pts", 3 * GST_SECOND, "ssrc", 0, NULL);

  validate_buffer (4, "pts", 4 * GST_SECOND, "ssrc", G_MAXUINT32, NULL);

  validate_events_received (12);

  validate_normal_start_events (0);

  validate_normal_start_events (3);

  validate_normal_start_events (6);

  validate_normal_start_events (9);

  destroy_payloader (state);
}

GST_END_TEST;

/* validate that changing the timestamp-offset will actually effect the rtptime
 * of the payloaded RTP packets. unfortunately setting the timestamp-offset
 * property will only take effect when the payloader goes from READY to PAUSED.
 * so the test starts by making sure that the default timestamp-offset indicates
 * random timestamps. then a buffer is pushed which is expected to be payloaded
 * as an RTP packet with a random timestamp. then the timestamp-offset is
 * modified without changing the state of the pipeline. therefore the next
 * buffer pushed is expected to result in an RTP packet with a timestamp equal
 * to the previous RTP packet incremented by DEFAULT_CLOCK_RATE. next the
 * pipeline is brought to NULL state and the timestamp-offset is set to a
 * specific value, the pipeline is then brought back to PLAYING state and the
 * two buffers pushed are expected to result in payloaded RTP packets that have
 * timestamps based on the set timestamp-offset incremented by multiples of
 * DEFAULT_CLOCK_RATE. next the boundary values of the timestamp-offset are
 * tested. again the pipeline state needs to be modified and buffers are pushed
 * and the resulting payloaded RTP packets' timestamps are validated. note that
 * the maximum timestamp-offset value will wrap around for the very last
 * payloaded RTP packet.
 */
GST_START_TEST (rtp_base_payload_property_timestamp_offset_test)
{
  guint32 rtptime;
  guint32 offset;
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  g_object_get (state->element, "timestamp-offset", &offset, NULL);
  fail_unless_equals_int (offset, -1);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  g_object_set (state->element, "timestamp-offset", 0x42, NULL);
  g_object_get (state->element, "timestamp-offset", &offset, NULL);
  fail_unless_equals_int (offset, 0x42);
  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "timestamp-offset", 0x4242, NULL);
  g_object_get (state->element, "timestamp-offset", &offset, NULL);
  fail_unless_equals_int (offset, 0x4242);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 2 * GST_SECOND, NULL);

  push_buffer (state, "pts", 3 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "timestamp-offset", 0, NULL);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 4 * GST_SECOND, NULL);

  push_buffer (state, "pts", 5 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "timestamp-offset", G_MAXUINT32, NULL);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 6 * GST_SECOND, NULL);

  push_buffer (state, "pts", 7 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (8);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);
  get_buffer_field (0, "rtptime", &rtptime, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND, "rtptime", rtptime + 1 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (2,
      "pts", 2 * GST_SECOND, "rtptime", 0x4242 + 2 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (3,
      "pts", 3 * GST_SECOND, "rtptime", 0x4242 + 3 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (4,
      "pts", 4 * GST_SECOND, "rtptime", 0 + 4 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (5,
      "pts", 5 * GST_SECOND, "rtptime", 0 + 5 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (6,
      "pts", 6 * GST_SECOND,
      "rtptime", G_MAXUINT32 + 6 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (7,
      "pts", 7 * GST_SECOND, "rtptime", 7 * DEFAULT_CLOCK_RATE - 1, NULL);

  validate_events_received (12);

  validate_normal_start_events (0);

  validate_normal_start_events (3);

  validate_normal_start_events (6);

  validate_normal_start_events (9);

  destroy_payloader (state);
}

GST_END_TEST;

/* as for timestamp-offset above setting the seqnum-offset property of a
 * payloader will only take effect when the payloader goes from READY to PAUSED
 * state. this test starts by validating that seqnum-offset indicates random
 * sequence numbers and that the random sequence numbers increment by one for
 * each payloaded RTP packet. also it is verified that setting seqnum-offset
 * without bringing the pipeline to READY will not affect the payloaded RTP
 * packets' sequence numbers. next the pipeline is brought to NULL state,
 * seqnum-offset is set to a specific value before bringing the pipeline back to
 * PLAYING state. the next two buffers pushed are expected to resulting in
 * payloaded RTP packets that start with sequence numbers relating to the set
 * seqnum-offset value, and that again increment by one for each packet. finally
 * the boundary values of seqnum-offset are tested. this means bringing the
 * pipeline to NULL state, setting the seqnum-offset and bringing the pipeline
 * back to PLAYING state. note that for the very last payloded RTP packet the
 * sequence number will have wrapped around because the previous packet is
 * expected to have the maximum sequence number value.
 */
GST_START_TEST (rtp_base_payload_property_seqnum_offset_test)
{
  State *state;
  guint16 seq;
  gint offset;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  set_state (state, GST_STATE_PLAYING);

  g_object_get (state->element, "seqnum-offset", &offset, NULL);
  fail_unless_equals_int (offset, -1);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  g_object_set (state->element, "seqnum-offset", 0x42, NULL);
  g_object_get (state->element, "seqnum-offset", &offset, NULL);
  fail_unless_equals_int (offset, 0x42);
  push_buffer (state, "pts", 1 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "seqnum-offset", 0x4242, NULL);
  g_object_get (state->element, "seqnum-offset", &offset, NULL);
  fail_unless_equals_int (offset, 0x4242);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 2 * GST_SECOND, NULL);

  push_buffer (state, "pts", 3 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "seqnum-offset", -1, NULL);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 4 * GST_SECOND, NULL);

  push_buffer (state, "pts", 5 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "seqnum-offset", G_MAXUINT16, NULL);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 6 * GST_SECOND, NULL);

  push_buffer (state, "pts", 7 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (8);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);
  get_buffer_field (0, "seq", &seq, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, "seq", seq + 1, NULL);

  validate_buffer (2, "pts", 2 * GST_SECOND, "seq", 0x4242, NULL);

  validate_buffer (3, "pts", 3 * GST_SECOND, "seq", 0x4242 + 1, NULL);

  validate_buffer (4, "pts", 4 * GST_SECOND, NULL);
  get_buffer_field (4, "seq", &seq, NULL);

  validate_buffer (5, "pts", 5 * GST_SECOND, "seq", seq + 1, NULL);

  validate_buffer (6, "pts", 6 * GST_SECOND, "seq", G_MAXUINT16, NULL);

  validate_buffer (7, "pts", 7 * GST_SECOND, "seq", 0, NULL);

  validate_events_received (12);

  validate_normal_start_events (0);

  validate_normal_start_events (3);

  validate_normal_start_events (6);

  validate_normal_start_events (9);

  destroy_payloader (state);
}

GST_END_TEST;

/* a payloader's max-ptime property is linked to its MTU property. whenever a
 * packet is larger than MTU or has a duration longer than max-ptime it will be
 * considered to be full. so this test first validates that the default value of
 * max-ptime is unspecified. then it retrieves the MTU and validates that a
 * packet of size MTU will not be considered full even if the duration is at its
 * maximum value. however incrementing the size to exceed the MTU will result in
 * the packet being full. next max-ptime is set to a value and it is verified
 * that only if both the size and duration are below the allowed values then the
 * packet will be considered not to be full, otherwise it will be reported as
 * being full. finally the boundary values of the property are tested in a
 * similar fashion.
 */
GST_START_TEST (rtp_base_payload_property_max_ptime_test)
{
  gint64 max_ptime;
  State *state;
  guint mtu;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  g_object_get (state->element, "max-ptime", &max_ptime, NULL);
  fail_unless_equals_int64 (max_ptime, -1);
  g_object_get (state->element, "mtu", &mtu, NULL);
  validate_would_not_be_filled (state, mtu, G_MAXINT64 - 1);
  validate_would_be_filled (state, mtu + 1, G_MAXINT64 - 1);

  g_object_set (state->element, "max-ptime", GST_SECOND, NULL);
  g_object_get (state->element, "max-ptime", &max_ptime, NULL);
  fail_unless_equals_int64 (max_ptime, GST_SECOND);
  validate_would_not_be_filled (state, mtu, GST_SECOND - 1);
  validate_would_be_filled (state, mtu, GST_SECOND);
  validate_would_be_filled (state, mtu + 1, GST_SECOND - 1);
  validate_would_be_filled (state, mtu + 1, GST_SECOND);

  g_object_set (state->element, "max-ptime", G_GUINT64_CONSTANT (-1), NULL);
  g_object_get (state->element, "max-ptime", &max_ptime, NULL);
  fail_unless_equals_int64 (max_ptime, G_GUINT64_CONSTANT (-1));
  validate_would_not_be_filled (state, mtu, G_MAXINT64 - 1);
  validate_would_be_filled (state, mtu + 1, G_MAXINT64 - 1);

  g_object_set (state->element, "max-ptime", G_MAXINT64, NULL);
  g_object_get (state->element, "max-ptime", &max_ptime, NULL);
  fail_unless_equals_int64 (max_ptime, G_MAXINT64);
  validate_would_be_filled (state, mtu, G_MAXINT64);

  destroy_payloader (state);
}

GST_END_TEST;

/* a basepayloader has a min-ptime property with an allowed range, the property
 * itself is never checked by the payloader but is meant to be used by
 * inheriting classes. therefore this test only validates that setting the
 * property will mean that retrieveing the property results in the value
 * previously being set. first the default value is validated, then a new
 * specific value, before finally testing the boundary values.
 */
GST_START_TEST (rtp_base_payload_property_min_ptime_test)
{
  State *state;
  guint64 reference, min_ptime;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  g_object_get (state->element, "min-ptime", &reference, NULL);
  fail_unless_equals_int (reference, 0);

  g_object_set (state->element, "min-ptime", reference + 1, NULL);
  g_object_get (state->element, "min-ptime", &min_ptime, NULL);
  fail_unless_equals_int (min_ptime, reference + 1);

  g_object_set (state->element, "min-ptime", G_GUINT64_CONSTANT (0), NULL);
  g_object_get (state->element, "min-ptime", &min_ptime, NULL);
  fail_unless_equals_int (min_ptime, 0);

  g_object_set (state->element, "min-ptime", G_MAXINT64, NULL);
  g_object_get (state->element, "min-ptime", &min_ptime, NULL);
  fail_unless_equals_int64 (min_ptime, G_MAXINT64);

  destroy_payloader (state);
}

GST_END_TEST;

/* paylaoders have a timestamp property that reflects the timestamp of the last
 * payloaded RTP packet. in this test the timestamp-offset is set to a specific
 * value so that when the first buffer is pushed its timestamp can be predicted
 * and thus that the timestamp property also has this value. (if
 * timestamp-offset was not set the timestamp would be random). another buffer
 * is then pushed and its timestamp is expected to increment by
 * DEFAULT_CLOCK_RATE.
 */
GST_START_TEST (rtp_base_payload_property_timestamp_test)
{
  State *state;
  guint32 timestamp;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "timestamp-offset", 0, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);
  g_object_get (state->element, "timestamp", &timestamp, NULL);
  fail_unless_equals_int (timestamp, 0);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);
  g_object_get (state->element, "timestamp", &timestamp, NULL);
  fail_unless_equals_int (timestamp, DEFAULT_CLOCK_RATE);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0, "pts", 0 * GST_SECOND, "rtptime", 0, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND, "rtptime", DEFAULT_CLOCK_RATE, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* basepayloaders have a seqnum property that is supposed to contain the
 * sequence number of the last payloaded RTP packet. so therefore this test
 * initializes the seqnum-offset property to a know value and pushes a buffer.
 * the payloaded RTP packet is expected to have a sequence number equal to the
 * set seqnum-offset, as is the seqnum property. next another buffer is pushed
 * and then both the payloaded RTP packet and the seqnum property value are
 * expected to increment by one compared to the previous packet.
 */
GST_START_TEST (rtp_base_payload_property_seqnum_test)
{
  State *state;
  guint seq;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "seqnum-offset", 0, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);
  g_object_get (state->element, "seqnum", &seq, NULL);
  fail_unless_equals_int (seq, 0);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);
  g_object_get (state->element, "seqnum", &seq, NULL);
  fail_unless_equals_int (seq, 1);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (2);

  validate_buffer (0, "pts", 0 * GST_SECOND, "seq", 0, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, "seq", 1, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* basepayloader has a perfect-rtptime property when it is set to FALSE
 * the timestamps of payloaded RTP packets will determined by initial
 * timestamp-offset (usually random) as well as the clock-rate. when
 * perfect-rtptime is set to TRUE the timestamps of payloaded RTP packets are
 * instead determined by the timestamp of the first packet and then the
 * difference in offset of the input buffers.
 *
 * to verify that this test starts by setting the timestamp-offset to a specific
 * value to prevent random timestamps of the RTP packets. next perfect-rtptime
 * is set to FALSE. the two buffers pushed will result in two payloaded RTP
 * packets whose timestamps differ based on the current clock-rate
 * DEFAULT_CLOCK_RATE. the next step is to set perfect-rtptime to TRUE. the two
 * buffers that are pushed will result in two payloaded RTP packets. the first
 * of these RTP packets has a timestamp that relates to the previous packet and
 * the difference in offset between the middle two input buffers. the latter of
 * the two RTP packets has a timestamp that instead relates to the offset of the
 * last two input buffers.
 */
GST_START_TEST (rtp_base_payload_property_perfect_rtptime_test)
{
  State *state;
  guint32 timestamp_base = 0;
  gboolean perfect;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "timestamp-offset", timestamp_base, NULL);

  set_state (state, GST_STATE_PLAYING);

  g_object_set (state->element, "perfect-rtptime", FALSE, NULL);
  g_object_get (state->element, "perfect-rtptime", &perfect, NULL);
  fail_unless (!perfect);

  push_buffer (state, "pts", 0 * GST_SECOND, "offset", G_GINT64_CONSTANT (0),
      NULL);

  push_buffer (state, "pts", 1 * GST_SECOND, "offset", G_GINT64_CONSTANT (17),
      NULL);

  g_object_set (state->element, "perfect-rtptime", TRUE, NULL);
  g_object_get (state->element, "perfect-rtptime", &perfect, NULL);
  fail_unless (perfect);

  push_buffer (state, "pts", 2 * GST_SECOND, "offset", G_GINT64_CONSTANT (31),
      NULL);

  push_buffer (state, "pts", 3 * GST_SECOND, "offset", G_GINT64_CONSTANT (67),
      NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (4);

  validate_buffer (0,
      "pts", 0 * GST_SECOND, "offset", G_GINT64_CONSTANT (0), "rtptime",
      timestamp_base, NULL);

  validate_buffer (1,
      "pts", 1 * GST_SECOND,
      "offset", G_GINT64_CONSTANT (17), "rtptime",
      timestamp_base + 1 * DEFAULT_CLOCK_RATE, NULL);

  validate_buffer (2,
      "pts", 2 * GST_SECOND,
      "offset", G_GINT64_CONSTANT (31),
      "rtptime", timestamp_base + 1 * DEFAULT_CLOCK_RATE + (31 - 17), NULL);

  validate_buffer (3,
      "pts", 3 * GST_SECOND,
      "offset", G_GINT64_CONSTANT (67),
      "rtptime", timestamp_base + 1 * DEFAULT_CLOCK_RATE + (67 - 17), NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  destroy_payloader (state);
}

GST_END_TEST;

/* basepayloaders have a ptime-multiple property but its value does not affect
 * any payloaded RTP packets as this is supposed to be done by inherited
 * classes. therefore this test only validates the default value of the
 * property, makes sure that a set value actually sticks and that the boundary
 * values are indeed allowed to be set.
 */
GST_START_TEST (rtp_base_payload_property_ptime_multiple_test)
{
  State *state;
  gint64 multiple;

  state = create_payloader ("application/x-rtp", &sinktmpl, NULL);

  g_object_get (state->element, "ptime-multiple", &multiple, NULL);
  fail_unless_equals_int64 (multiple, 0);

  g_object_set (state->element, "ptime-multiple", G_GINT64_CONSTANT (42), NULL);
  g_object_get (state->element, "ptime-multiple", &multiple, NULL);
  fail_unless_equals_int64 (multiple, 42);

  g_object_set (state->element, "ptime-multiple", G_GINT64_CONSTANT (0), NULL);
  g_object_get (state->element, "ptime-multiple", &multiple, NULL);
  fail_unless_equals_int64 (multiple, 0);

  g_object_set (state->element, "ptime-multiple", G_MAXINT64, NULL);
  g_object_get (state->element, "ptime-multiple", &multiple, NULL);
  fail_unless_equals_int64 (multiple, G_MAXINT64);

  destroy_payloader (state);
}

GST_END_TEST;

/* basepayloaders have a property called stats that is used to atomically
 * retrieve several values (clock-rate, running-time, seqnum and timestamp) that
 * relate to the stream and its current progress. this test is meant to test
 * retrieval of these values.
 *
 * first of all perfect-rtptime is set to TRUE, next the the test starts out by
 * setting seqnum-offset and timestamp-offset to known values to prevent that
 * sequence numbers and timestamps of payloaded RTP packets are random. next the
 * stats property is retrieved. the clock-rate must be at the default
 * DEFAULT_CLOCK_RATE, while running-time must be equal to the first buffers
 * PTS. the sequence number should be equal to the initialized value of
 * seqnum-offset and the timestamp should be equal to the initialized value of
 * timestamp-offset. after pushing a second buffer the stats property is
 * validate again. this time running-time, seqnum and timestamp should have
 * advanced as expected. next the pipeline is brought to NULL state to be able
 * to change the perfect-rtptime property to FALSE before going back to PLAYING
 * state. this is done to validate that the stats values reflect normal
 * timestamp updates that are not based on input buffer offsets as expected.
 * lastly two buffers are pushed and the stats property retrieved after each
 * time. here it is expected that the sequence numbers values are restarted at
 * the inital value while the timestamps and running-time reflect the input
 * buffers.
 */
GST_START_TEST (rtp_base_payload_property_stats_test)
{
  State *state;

  state = create_payloader ("application/x-rtp", &sinktmpl,
      "perfect-rtptime", TRUE, "seqnum-offset", 0, "timestamp-offset", 0, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);
  validate_stats (state,
      DEFAULT_CLOCK_RATE, 0 * GST_SECOND, 0, 0 * DEFAULT_CLOCK_RATE);

  push_buffer (state, "pts", 1 * GST_SECOND, NULL);
  validate_stats (state,
      DEFAULT_CLOCK_RATE, 1 * DEFAULT_CLOCK_RATE, 1, 1 * DEFAULT_CLOCK_RATE);

  set_state (state, GST_STATE_NULL);
  g_object_set (state->element, "perfect-rtptime", FALSE, NULL);
  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 2 * GST_SECOND, NULL);
  validate_stats (state,
      DEFAULT_CLOCK_RATE, 2 * GST_SECOND, 0, 2 * DEFAULT_CLOCK_RATE);

  push_buffer (state, "pts", 3 * GST_SECOND, NULL);
  validate_stats (state,
      DEFAULT_CLOCK_RATE, 3 * GST_SECOND, 1, 3 * DEFAULT_CLOCK_RATE);
  set_state (state, GST_STATE_NULL);

  validate_buffers_received (4);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);

  validate_buffer (1, "pts", 1 * GST_SECOND, NULL);

  validate_buffer (2, "pts", 2 * GST_SECOND, NULL);

  validate_buffer (3, "pts", 3 * GST_SECOND, NULL);

  validate_events_received (6);

  validate_normal_start_events (0);

  validate_normal_start_events (3);

  destroy_payloader (state);
}

GST_END_TEST;

/* push a single buffer to the payloader which should successfully payload it
 * into an RTP packet. besides the payloaded RTP packet there should be the
 * three events initial events: stream-start, caps and segment. because of that
 * the input caps has framerate this will be propagated to an a-framerate field
 * on the output caps.
 */
GST_START_TEST (rtp_base_payload_framerate_attribute)
{
  State *state;

  state = create_payloader ("video/x-raw,framerate=(fraction)1/4", &sinktmpl,
      "perfect-rtptime", FALSE, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (1);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  validate_event (1, "caps", "a-framerate", "0.25", NULL);

  destroy_payloader (state);
}

GST_END_TEST;

/* push a single buffer to the payloader which should successfully payload it
 * into an RTP packet. besides the payloaded RTP packet there should be the
 * three events initial events: stream-start, caps and segment. because of that
 * the input caps has both framerate and max-framerate set the a-framerate field
 * on the output caps will correspond to the value of the max-framerate field.
 */
GST_START_TEST (rtp_base_payload_max_framerate_attribute)
{
  State *state;

  state =
      create_payloader
      ("video/x-raw,framerate=(fraction)0/1,max-framerate=(fraction)1/8",
      &sinktmpl, "perfect-rtptime", FALSE, NULL);

  set_state (state, GST_STATE_PLAYING);

  push_buffer (state, "pts", 0 * GST_SECOND, NULL);

  set_state (state, GST_STATE_NULL);

  validate_buffers_received (1);

  validate_buffer (0, "pts", 0 * GST_SECOND, NULL);

  validate_events_received (3);

  validate_normal_start_events (0);

  validate_event (1, "caps", "a-framerate", "0.125", NULL);

  destroy_payloader (state);
}

GST_END_TEST;

static Suite *
rtp_basepayloading_suite (void)
{
  Suite *s = suite_create ("rtp_base_payloading_test");
  TCase *tc_chain = tcase_create ("payloading tests");

  tcase_set_timeout (tc_chain, 60);

  suite_add_tcase (s, tc_chain);
  tcase_add_test (tc_chain, rtp_base_payload_buffer_test);
  tcase_add_test (tc_chain, rtp_base_payload_buffer_list_test);

  tcase_add_test (tc_chain, rtp_base_payload_normal_rtptime_test);
  tcase_add_test (tc_chain, rtp_base_payload_perfect_rtptime_test);
  tcase_add_test (tc_chain, rtp_base_payload_no_pts_no_offset_test);

  tcase_add_test (tc_chain, rtp_base_payload_downstream_caps_test);

  tcase_add_test (tc_chain, rtp_base_payload_ssrc_collision_test);
  tcase_add_test (tc_chain, rtp_base_payload_reconfigure_test);

  tcase_add_test (tc_chain, rtp_base_payload_property_mtu_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_pt_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_ssrc_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_timestamp_offset_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_seqnum_offset_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_max_ptime_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_min_ptime_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_timestamp_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_seqnum_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_perfect_rtptime_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_ptime_multiple_test);
  tcase_add_test (tc_chain, rtp_base_payload_property_stats_test);

  tcase_add_test (tc_chain, rtp_base_payload_framerate_attribute);
  tcase_add_test (tc_chain, rtp_base_payload_max_framerate_attribute);

  return s;
}

GST_CHECK_MAIN (rtp_basepayloading)
