/* GStreamer interactive test for the dvdreadsrc element
 * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

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

#include <gst/gst.h>

typedef struct _DVDReadSrcTestContext {
  GstElement *pipeline;
  GstElement *src;
  GstElement *sink;
  GstBus     *bus;
  guint       num_titles;
  guint       cur_title;
  GList      *errors;     /* list of strings */
} DVDReadSrcTestContext;

GST_DEBUG_CATEGORY_STATIC (dvdreadsrc_test_debug);
#define GST_CAT_DEFAULT dvdreadsrc_test_debug

/* for convenience */
static GstFormat title_format;   /* 0 */
static GstFormat chapter_format; /* 0 */

/* command line option stuff */
static gboolean  skip_read_test; /* FALSE */
static gboolean  skip_conv_test; /* FALSE */
static gboolean  skip_rseek_test;/* FALSE */
static gchar    *opt_device;     /* NULL */
static gint      opt_title;      /* 0 */

void
context_add_error (DVDReadSrcTestContext * ctx, const gchar *format, ...)
{
	va_list args;
	gchar *s, *s2;

	g_return_if_fail (ctx != NULL);
	g_return_if_fail (format != NULL);

	va_start (args, format);
	s = g_strdup_vprintf (format, args);
	va_end (args);


    s2 = g_strdup_printf ("[%02u] %s", ctx->cur_title, s);

    ctx->errors = g_list_append (ctx->errors, s2);
    g_warning ("%s", s2);

	g_free (s);
    g_free (s2);
}

static gboolean
got_buf (GstPad * pad, GstBuffer * buf, GstBuffer ** p_buf)
{
  gst_buffer_replace (p_buf, buf);
  return TRUE;
}

static gboolean
got_event (GstPad * pad, GstEvent * evt, GList ** p_event_list)
{
  /* not interested in seek events etc. */
  if (!GST_EVENT_IS_DOWNSTREAM (evt))
    return TRUE;

  if (GST_EVENT_TYPE (evt) == GST_EVENT_FLUSH_START ||
      GST_EVENT_TYPE (evt) == GST_EVENT_FLUSH_STOP) {
    g_list_foreach (*p_event_list, (GFunc) gst_mini_object_unref, NULL);
    g_list_free (*p_event_list);
    *p_event_list = NULL;
  } else {
    /* g_print ("*** EVENT:%s %s\n", GST_EVENT_TYPE_NAME (evt),
     *    (evt->structure) ? gst_structure_to_string (evt->structure) : ""); */
    *p_event_list = g_list_append (*p_event_list, gst_event_ref (evt));
  }
  return TRUE;
}

static void
test_title_chapter_positions_and_chapter_seeking (DVDReadSrcTestContext * ctx,
  GstState state)
{
  GstStateChangeReturn sret;
  GstPad *pad;
  gint64 c, num_chapters;
  gint64 last_ch_start = 0;

  if (!gst_element_query_duration (ctx->pipeline, &chapter_format,
      &num_chapters)) {
    context_add_error (ctx, "Failed to query number of chapters!");
    return;
  }

  sret = gst_element_set_state (ctx->pipeline, state);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    g_error ("Failed to go to %s", gst_element_state_get_name (state));
  }


  g_print ("============================================================\n");
  g_print ("%u chapters\n", (guint) num_chapters);
  for (c = 0; c < num_chapters; ++c) {
    GstFormat time_format = GST_FORMAT_TIME;
    gint64 ch_start_time = GST_CLOCK_TIME_NONE;

    if (gst_element_query_convert (ctx->pipeline, chapter_format, c,
        &time_format, &ch_start_time)) {
      g_print ("Chapter %2u starts at %" GST_TIME_FORMAT "\n", (guint) c + 1,
          GST_TIME_ARGS (ch_start_time));
    } else {
      g_print ("FAILED to convert chapter to time.\n");
    }    
  }
  g_print ("============================================================\n");

  pad = gst_element_get_pad (ctx->sink, "sink");
  g_assert (pad != NULL);

  for (c = 0; c < num_chapters; ++c) {
    GstBuffer *buf = NULL;
    gulong buf_id, evt_id;
    GList *event_list = NULL;

    g_print ("Seeking to chapter %2u ...", (guint) c + 1);

    buf_id = gst_pad_add_buffer_probe (pad, G_CALLBACK (got_buf), &buf);
    evt_id = gst_pad_add_event_probe (pad, G_CALLBACK (got_event), &event_list);

    if (!gst_element_seek_simple (ctx->pipeline, chapter_format,
        GST_SEEK_FLAG_FLUSH, c)) {
      g_print ("FAILED\n");
      exit (-1);
    }

    if (gst_element_get_state (ctx->pipeline, NULL, NULL, -1) != GST_STATE_CHANGE_SUCCESS) {
      g_error ("Failed to go back to to %s after seeking", gst_element_state_get_name (state));
    }

    g_assert (buf != NULL);
    g_assert (event_list != NULL);

    /* g_print ("last_ch_start = %lld, ts = %lld\n", last_ch_start,
     * GST_BUFFER_TIMESTAMP (buf)); */

    g_assert (!GST_BUFFER_TIMESTAMP_IS_VALID (buf) ||
        (GST_BUFFER_TIMESTAMP (buf) == 0 && c == 0) ||
        GST_BUFFER_TIMESTAMP (buf) > last_ch_start);

    /* there should at least be a NEWSEGMENT event */
    g_assert(event_list != NULL);

    {
      GstFormat fmt;
      GstEvent *evt;
      gboolean update;
      gint64 start;

      evt = GST_EVENT (event_list->data);

      /* g_print ("EVENT %2d: %s %s\n", i, GST_EVENT_TYPE_NAME (evt),
       *  (evt->structure) ? gst_structure_to_string (evt->structure) : ""); */

      if (GST_EVENT_TYPE (evt) != GST_EVENT_NEWSEGMENT) {
        context_add_error (ctx, "Expected NEWSEGMENT event, but got %s event",
            GST_EVENT_TYPE_NAME (evt));
        return;
      }

      gst_event_parse_new_segment (evt, &update, NULL, &fmt, &start,
          NULL, NULL);
      g_assert (update == FALSE);
      g_assert (fmt == GST_FORMAT_TIME);
      g_assert (c == 0 || start > 0);
      g_assert (c == 0 || last_ch_start < start);
      last_ch_start = start; 
    }

    g_print ("OK\n");

    gst_pad_remove_event_probe (pad, evt_id);
    gst_pad_remove_buffer_probe (pad, buf_id);

    gst_buffer_unref (buf);
    g_list_foreach (event_list, (GFunc) gst_mini_object_unref, NULL);
    g_list_free (event_list);
  }

  gst_object_unref (pad);
}

static void
do_byte_to_time_conversion (GstElement * pipeline, gint64 bytes,
    gint64 *p_last_time, gint64 total_bytes, gint64 total_time)
{
  GstFormat time_format = GST_FORMAT_TIME;
  gint64 time = GST_CLOCK_TIME_NONE;

  if (bytes > total_bytes)
    return;

  if (!gst_element_query_convert (pipeline, GST_FORMAT_BYTES, bytes,
      &time_format, &time)) {
    g_error ("FAILED to convert bytes to time.\n");
  }

  GST_LOG ("%12.2fM => %" GST_TIME_FORMAT, (gdouble) bytes / (1024.0 * 1024.0),
      GST_TIME_ARGS (time));

  g_print ("Conversion check: %.2f%%                                       \r",
      (double) bytes * 100.0 / (double) total_bytes);

  if (time < 0 || time > total_time) {
    g_error ("bad interpolation: time=%" GST_TIME_FORMAT " < 0 or > duration %"
        GST_TIME_FORMAT, GST_TIME_ARGS (time), GST_TIME_ARGS (total_time));
  }

  /* make sure it increases steadily */
  if (time <= *p_last_time)  {
//    *p_last_time = 0;
/* FIXME
    g_error ("bad interpolation: time=%" GST_TIME_FORMAT ", last_time=%"
        GST_TIME_FORMAT, GST_TIME_ARGS (time), GST_TIME_ARGS (*p_last_time));
*/
  }

  *p_last_time = time;
}

#define CHECK_BYTE_TO_TIME_BYTE_INTERVAL (1024*1024)

static void
test_byte_to_time_conversions (DVDReadSrcTestContext * ctx)
{
  GstStateChangeReturn sret;
  GstFormat byte_format = GST_FORMAT_BYTES;
  GstFormat time_format = GST_FORMAT_TIME;
  gint64 b, total_bytes, total_time, last_time = -1;

  if (!gst_element_query_duration (ctx->pipeline, &byte_format, &total_bytes)) {
    context_add_error (ctx, "Failed to query length in bytes!");
    return;
  }

  if (!gst_element_query_duration (ctx->pipeline, &time_format, &total_time)) {
    context_add_error (ctx, "Failed to query length in TIME!");
    return;
  }

  sret = gst_element_set_state (ctx->pipeline, GST_STATE_PAUSED);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    g_error ("Failed to go to PAUSED");
  }

  g_print ("[%02u/%02u] Title length in bytes: %.2fM\n", ctx->cur_title,
      ctx->num_titles, (gdouble) total_bytes / (1024.0 * 1024.0));
  g_print ("[%02u/%02u] Title length in bytes: %" GST_TIME_FORMAT "\n",
      ctx->cur_title, ctx->num_titles, GST_TIME_ARGS (total_time));

  g_print ("Testing BYTE => TIME conversion ...\n");

  for (b = 0; b < total_bytes; b += CHECK_BYTE_TO_TIME_BYTE_INTERVAL) {
    /* this checks the code path for 'round' byte values */
    do_byte_to_time_conversion (ctx->pipeline,
        b, &last_time, total_bytes, total_time);
    /* this will probably require some interpolation */
    do_byte_to_time_conversion (ctx->pipeline,
        b + (CHECK_BYTE_TO_TIME_BYTE_INTERVAL / 3),
        &last_time, total_bytes, total_time);
    do_byte_to_time_conversion (ctx->pipeline,
        b + (2 * CHECK_BYTE_TO_TIME_BYTE_INTERVAL / 3),
        &last_time, total_bytes, total_time);
  }
  g_print ("Conversion check: 100%%      \r");
  g_print ("\n");
}

static void
print_position (DVDReadSrcTestContext * ctx)
{
  GstFormat time_format = GST_FORMAT_TIME;
  gint64 pos, dur;

  if (gst_element_query_duration (ctx->pipeline, &time_format, &dur) &&
      gst_element_query_position (ctx->pipeline, &time_format, &pos)) {
    g_print ("[%02u/%02u] Reading title %" GST_TIME_FORMAT "/%" GST_TIME_FORMAT
        " (%.1f%%)\r", ctx->cur_title, ctx->num_titles, GST_TIME_ARGS (pos),
        GST_TIME_ARGS (dur), (double) pos * 100.0 / (double) dur);
  } else {
    g_print ("[%02u/%02u] failed to query position and/or duration\n",
        ctx->cur_title, ctx->num_titles);
  }
}

static void
parse_error_message (DVDReadSrcTestContext * ctx, GstMessage * err_msg)
{
  GError *err = NULL;
  gchar *dbg = NULL;

  if (err_msg == NULL)
    return;

  gst_message_parse_error (err_msg, &err, &dbg);

  context_add_error (ctx, "ERROR reading title: %s [%s]",
      err->message, GST_STR_NULL (dbg));

  g_free (dbg);
  g_error_free (err);
}

#define RANDOM_SEEK_ITERATIONS 100
#define RANDOM_SEEK_MAX_DELTA  (15 * GST_SECOND)

static void
test_title_random_seek (DVDReadSrcTestContext * ctx)
{
  GstStateChangeReturn sret;
  GstClockTime delta_sums;
  GstBuffer *buf = NULL;
  GstFormat tformat;
  GstPad *pad;
  gulong buf_id;
  gint64 length = -1;
  gint i;

  sret = gst_element_set_state (ctx->pipeline, GST_STATE_PAUSED);
  if (sret == GST_STATE_CHANGE_FAILURE) {
    context_add_error (ctx, "DVD source does not want to go to PAUSED");
    return;
  }

  sret = gst_element_get_state (ctx->pipeline, NULL, NULL, -1);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    context_add_error (ctx, "DVD source failed to reach PAUSED state");
    return;
  }

  tformat = GST_FORMAT_TIME;
  if (!gst_element_query_duration (ctx->pipeline, &tformat, &length)) {
    context_add_error (ctx, "Failed to query length of title in TIME format");
    return;
  }

  pad = gst_element_get_pad (ctx->sink, "sink");
  g_assert (pad != NULL);

  buf_id = gst_pad_add_buffer_probe (pad, G_CALLBACK (got_buf), &buf);

  delta_sums = 0;

  for (i = 0; i < RANDOM_SEEK_ITERATIONS; ++i) {
    GstClockTime delta;
    GstBuffer **p_buf = &buf;
    gdouble to_percent = g_random_double ();
    gint64 to_time = (gint64) ((gdouble) length * to_percent);

    gst_buffer_replace ((GstMiniObject **) p_buf, NULL);

    g_print ("Random seek %3d/%3d to %" GST_TIME_FORMAT " ... ",
        i, RANDOM_SEEK_ITERATIONS, GST_TIME_ARGS (to_time));

    if (!gst_element_seek_simple (ctx->pipeline, GST_FORMAT_TIME,
        GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, to_time)) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "Failed to seek to %" GST_TIME_FORMAT,
          GST_TIME_ARGS (to_time));
      return;
    }

    sret = gst_element_get_state (ctx->pipeline, NULL, NULL, -1);
    if (sret != GST_STATE_CHANGE_SUCCESS) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "Failed to finish seek to %" GST_TIME_FORMAT,
          GST_TIME_ARGS (to_time));
      return;
    }

    if (buf == NULL) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "No buffer after seeking to %" GST_TIME_FORMAT,
          GST_TIME_ARGS (to_time));
      return;
    }

    if (!GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "No timestamp on buffer after seeking to %"
          GST_TIME_FORMAT, GST_TIME_ARGS (to_time));
      return;
    }

    if (!GST_BUFFER_TIMESTAMP (buf) > to_time) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "Timestamp on buffer is after %" GST_TIME_FORMAT
          ", namely %" GST_TIME_FORMAT, GST_TIME_ARGS (to_time),
          GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf)));
      return;
    }

    delta = to_time - GST_BUFFER_TIMESTAMP (buf);

    if (delta > RANDOM_SEEK_MAX_DELTA) {
      g_print ("FAILED                                                  \n");
      context_add_error (ctx, "Timestamp on buffer is %" GST_TIME_FORMAT
          ", which is more than %u secs from seek position %" GST_TIME_FORMAT,
          GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf)),
          (guint) (RANDOM_SEEK_MAX_DELTA / GST_SECOND), GST_TIME_ARGS (to_time));
      return;
    }

    delta_sums += delta;

    g_print ("ok                                                      \r");
  }

  gst_pad_remove_buffer_probe (pad, buf_id);
  gst_object_unref (pad);

  g_print ("                                                              \r");
  g_print ("Random seek test: OK (average delta = %" GST_TIME_FORMAT ")\n",
      GST_TIME_ARGS (delta_sums / RANDOM_SEEK_ITERATIONS));
}

static void
test_title_read_from_start_to_end (DVDReadSrcTestContext * ctx)
{
  GstStateChangeReturn sret;
  GstMessageType msg_mask;
  GstMessage *msg;

  gst_element_set_state (ctx->pipeline, GST_STATE_NULL);

  sret = gst_element_set_state (ctx->pipeline, GST_STATE_PLAYING);
  if (sret == GST_STATE_CHANGE_FAILURE) {
    context_add_error (ctx, "DVD source does not want to go to PLAYING");
    return;
  }

  sret = gst_element_get_state (ctx->pipeline, NULL, NULL, -1);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    context_add_error (ctx, "DVD source failed to reach PLAYING state");
    return;
  }

  msg_mask = GST_MESSAGE_ERROR | GST_MESSAGE_EOS;
  while ((msg = gst_bus_poll (ctx->bus, msg_mask, GST_SECOND / 2)) == NULL) {
    print_position (ctx);
  }

  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_ERROR: {
      parse_error_message (ctx, msg);
      break;
    }
    case GST_MESSAGE_EOS: {
      print_position (ctx);
      g_print ("Finished reading title %u.                \n", ctx->cur_title);
      break;
    }
    default:
      g_assert_not_reached ();
  }
  gst_message_unref (msg);
}

static void
test_title (DVDReadSrcTestContext * ctx, guint title)
{
  GstStateChangeReturn sret;

  g_assert (title > 0 && title <= ctx->num_titles);

  gst_element_set_state (ctx->pipeline, GST_STATE_NULL);

  g_print ("Reading title %u ..\n", title);
  g_object_set (ctx->src, "title", title, NULL);

  sret = gst_element_set_state (ctx->pipeline, GST_STATE_PLAYING);
  if (sret == GST_STATE_CHANGE_FAILURE) {
    context_add_error (ctx, "Failed to set DVD source to PLAYING");
    return;
  }

  sret = gst_element_get_state (ctx->pipeline, NULL, NULL, -1);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    context_add_error (ctx, "DVD source failed to go to PLAYING");
    return;
  }

  ctx->cur_title = title;

  g_print ("Playing title %u ...\n", title);

  test_title_chapter_positions_and_chapter_seeking (ctx, GST_STATE_PAUSED);

  if (!skip_rseek_test) {
    test_title_random_seek (ctx);
  }

  if (!skip_read_test) {
//    g_object_set (ctx->sink, "sync", TRUE, NULL);
    test_title_read_from_start_to_end (ctx);
//    g_object_set (ctx->sink, "sync", FALSE, NULL);
  }

  if (!skip_conv_test) {
    test_byte_to_time_conversions (ctx);
  }

  gst_element_set_state (ctx->pipeline, GST_STATE_NULL);
}

static void
dvdreadsrc_test_setup_context (DVDReadSrcTestContext * ctx,
    const gchar * device)
{
  GstStateChangeReturn sret;
  gint64 num_titles;

  ctx->pipeline = gst_pipeline_new ("pipeline");
  g_assert (ctx->pipeline != NULL);

  ctx->src = gst_element_factory_make ("dvdreadsrc", "dvdreadsrc");
  g_assert (ctx->src != NULL);

  if (device) {
    g_print ("Setting DVD device to '%s'\n", device);
    g_object_set (ctx->src, "device", device, NULL);
  }

  ctx->sink = gst_element_factory_make ("fakesink", "sink");
  g_assert (ctx->sink != NULL);

  if (!gst_bin_add (GST_BIN (ctx->pipeline), ctx->src))
    g_error ("Failed to add source to pipeline!");

  if (!gst_bin_add (GST_BIN (ctx->pipeline), ctx->sink))
    g_error ("Failed to add sink to pipeline!");

  if (!gst_element_link (ctx->src, ctx->sink)) {
    g_error ("Failed to link source to sink!");
  }

  title_format = gst_format_get_by_nick ("title");
  g_assert (title_format != 0);

  chapter_format = gst_format_get_by_nick ("chapter");
  g_assert (chapter_format != 0);

  sret = gst_element_set_state (ctx->pipeline, GST_STATE_PAUSED);
  if (sret == GST_STATE_CHANGE_FAILURE) {
    /* TODO: print error from bus if there is one */
    g_error ("Failed to start up DVD source");
  }

  sret = gst_element_get_state (ctx->pipeline, NULL, NULL, -1);
  if (sret != GST_STATE_CHANGE_SUCCESS) {
    g_error ("DVD source failed to go into PAUSED state");
  }

  if (!gst_element_query_duration (ctx->src, &title_format, &num_titles)) {
    g_error ("Failed to query number of titles on DVD");
  }

  ctx->num_titles = (guint) num_titles;

  gst_element_set_state (ctx->pipeline, GST_STATE_NULL);

  ctx->bus = gst_pipeline_get_bus (GST_PIPELINE (ctx->pipeline));

  g_print ("==================================\n");
  g_print ("DVD has %u titles\n", ctx->num_titles);
  g_print ("==================================\n");

  ctx->errors = NULL;
}

static void
dvdreadsrc_test_teardown_context (DVDReadSrcTestContext * ctx)
{
  gst_element_set_state (ctx->pipeline, GST_STATE_NULL);
  gst_object_unref (ctx->pipeline);
  gst_object_unref (ctx->bus);
  g_list_foreach (ctx->errors, (GFunc) g_free, NULL);
  g_list_free (ctx->errors);
}

int
main (int argc, char ** argv)
{
  static const GOptionEntry test_goptions[] = {
    { "device", 'd', 0, G_OPTION_ARG_STRING, &opt_device,
      "DVD device (default: /dev/dvd)", NULL },
    { "title", 't', 0, G_OPTION_ARG_INT, &opt_title,
      "DVD title number to run the test on (default: test all titles)", NULL },
    { "skip-random-seek-test", '\0', 0, G_OPTION_ARG_NONE, &skip_rseek_test,
      "Skip the random seek test", NULL },
    { "skip-read-test", '\0', 0, G_OPTION_ARG_NONE, &skip_read_test,
      "Skip the read test (which reads each title from start to end)", NULL },
    { "skip-convert-test", '\0', 0, G_OPTION_ARG_NONE, &skip_conv_test,
      "Skip the byte to time conversion test (which takes a while)", NULL },
    { NULL, '\0', 0, 0, NULL, NULL, NULL }
  };
  GOptionContext *option_ctx;
  GError *option_err = NULL;

  DVDReadSrcTestContext ctx;
  guint num_errors = 0;

  /* command line option parsing */
  option_ctx = g_option_context_new ("");
  g_option_context_add_group (option_ctx, gst_init_get_option_group ());
  g_option_context_add_main_entries (option_ctx, test_goptions, NULL);

  if (!g_option_context_parse (option_ctx, &argc, &argv, &option_err)) {
    g_error ("Error parsing command line options: %s", option_err->message);
    return -1;
  }
  g_option_context_free (option_ctx);

  gst_init (&argc, &argv);

  GST_DEBUG_CATEGORY_INIT (dvdreadsrc_test_debug, "dvdreadsrctest", 0,
      "dvdreadsrctest");

  dvdreadsrc_test_setup_context (&ctx, opt_device);

  if (opt_title > 0 && opt_title <= ctx.num_titles) {
    test_title (&ctx, opt_title);
  } else {
    gint i;

    for (i = 1; i <= ctx.num_titles; ++i) {
      test_title (&ctx, i);
    }
  }

  num_errors = g_list_length (ctx.errors);

  g_print ("=================================================\n");
  if (num_errors == 0) {
    if (skip_read_test) {
      g_print ("Finished checking DVD titles (w/o reading) without any errors.\n");
    } else {
      g_print ("Finished reading DVD titles without any errors.\n");
    }
  } else {
    GList *l;

    g_print ("Finished reading all DVD titles, got %u errors:\n", num_errors);
    for (l = ctx.errors; l != NULL; l = l->next) {
      g_print ("  * %s\n", (gchar *) l->data);
    }
  }
  g_print ("=================================================\n");

  dvdreadsrc_test_teardown_context (&ctx);

  /* free stuff wherever possible to make leak detection with valgrind easier */
  gst_deinit ();

  return num_errors;
}
