/* GStreamer
 * Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.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 <stdio.h>

#include <gst/gst.h>
#include <gst/rtsp/rtsp.h>

/* Uncomment to enable audio output */
// #define OUTPUT_AUDIO

typedef struct
{
  gchar *range;
  gdouble speed;
  gchar *frames;
  gchar *rate_control;
  gboolean reverse;
} SeekParameters;

typedef struct
{
  GstElement *src;
  GstElement *vconv;            /* decodebin -> vconv */

  GstElement *sink;
  GstElement *pipe;
  SeekParameters *seek_params;
  GMainLoop *loop;
  GIOChannel *io;
  gboolean new_range;
  guint io_watch_id;
  gboolean reset_sync;
} Context;

typedef struct
{
  const gchar *name;
  gboolean has_argument;
  const gchar *help;
    gboolean (*func) (Context * ctx, gchar * arg, gboolean * async);
} Command;

static gboolean cmd_help (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_pause (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_play (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_reverse (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_range (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_speed (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_frames (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_rate_control (Context * ctx, gchar * arg, gboolean * async);
static gboolean cmd_step_forward (Context * ctx, gchar * arg, gboolean * async);

static Command commands[] = {
  {"help", FALSE, "Display list of valid commands", cmd_help},
  {"pause", FALSE, "Pause playback", cmd_pause},
  {"play", FALSE, "Resume playback", cmd_play},
  {"reverse", FALSE, "Reverse playback direction", cmd_reverse},
  {"range", TRUE,
        "Seek to the specified range, example: \"range: 19000101T000000Z-19000101T000200Z\"",
      cmd_range},
  {"speed", TRUE, "Set the playback speed, example: \"speed: 1.0\"", cmd_speed},
  {"frames", TRUE,
        "Set the frames trickmode, example: \"frames: intra\", \"frames: predicted\", \"frames: intra/1000\"",
      cmd_frames},
  {"rate-control", TRUE,
        "Set the rate control mode, example: \"rate-control: no\"",
      cmd_rate_control},
  {"s", FALSE, "Step to the following frame (in current playback direction)",
      cmd_step_forward},
  {NULL},
};

static gchar *rtsp_address;

#define MAKE_AND_ADD(var, pipe, name, label, elem_name) \
G_STMT_START { \
  if (G_UNLIKELY (!(var = (gst_element_factory_make (name, elem_name))))) { \
    GST_ERROR ("Could not create element %s", name); \
    goto label; \
  } \
  if (G_UNLIKELY (!gst_bin_add (GST_BIN_CAST (pipe), var))) { \
    GST_ERROR ("Could not add element %s", name); \
    goto label; \
  } \
} G_STMT_END

#define DEFAULT_RANGE "19000101T000000Z-19000101T000200Z"
#define DEFAULT_SPEED 1.0
#define DEFAULT_FRAMES "none"
#define DEFAULT_RATE_CONTROL "yes"
#define DEFAULT_REVERSE FALSE

static void
decode_pad_added_cb (GstElement * src, GstPad * srcpad, Context * ctx)
{
  /* Try to link to video output */
  GstCaps *caps = gst_pad_get_current_caps (srcpad);
  if (caps == NULL)
    goto no_caps;

  const GstStructure *s = gst_caps_get_structure (caps, 0);
  if (s == NULL) {
    goto no_caps;
  }

  GstElement *peer = NULL;
  if (gst_structure_has_name (s, "video/x-raw")) {
    GST_INFO ("Have decoded video stream");
    peer = ctx->vconv;
  }
#if defined(OUTPUT_AUDIO)
  else if (gst_structure_has_name (s, "audio/x-raw")) {
    GstElement *aconv, *asink;

    GST_INFO ("Have decoded audio stream");
    MAKE_AND_ADD (aconv, ctx->pipe, "audioconvert", fail, NULL);
    MAKE_AND_ADD (asink, ctx->pipe, "autoaudiosink", fail, NULL);

    gst_element_set_state (aconv, GST_STATE_PLAYING);
    gst_element_set_state (asink, GST_STATE_PLAYING);

    if (!gst_element_link (aconv, asink)) {
      goto fail;
    }
    peer = aconv;
  }
#endif

  gst_caps_replace (&caps, NULL);

  if (peer != NULL) {
    GstPad *sinkpad = gst_element_get_static_pad (peer, "sink");
    if (gst_pad_link (srcpad, sinkpad) != GST_PAD_LINK_OK) {
      goto not_negotiated;
    }
    gst_object_unref (sinkpad);
  }

  return;

no_caps:
  GST_ELEMENT_ERROR (ctx->pipe, CORE, NEGOTIATION, (NULL),
      ("decoded pad %" GST_PTR_FORMAT " did not have caps", srcpad));
  gst_caps_replace (&caps, NULL);
  return;
not_negotiated:
  GST_ELEMENT_ERROR (ctx->pipe, CORE, NEGOTIATION, (NULL),
      ("Failed to link decoded pad %" GST_PTR_FORMAT " to output", srcpad));
  return;
#if defined(OUTPUT_AUDIO)
fail:
  GST_ELEMENT_ERROR (ctx->pipe, CORE, NEGOTIATION, (NULL),
      ("Failed to connect %" GST_PTR_FORMAT " to output", srcpad));
  return;
#endif
}

static void
pad_added_cb (GstElement * src, GstPad * srcpad, Context * ctx)
{
  GstCaps *caps = gst_pad_get_current_caps (srcpad);
  if (caps == NULL)
    goto no_caps;

  const GstStructure *s = gst_caps_get_structure (caps, 0);
  if (s == NULL) {
    goto no_caps;
  }

  /* If the media is not audio or video, ignore it. (Could put ONVIF metadata to an appsink here though */
  const gchar *media = gst_structure_get_string (s, "media");
  if (media == NULL || (!g_str_equal (media, "video") &&
          !g_str_equal (media, "audio"))) {
    GST_INFO ("Ignoring rtsp pad %" GST_PTR_FORMAT " with caps %"
        GST_PTR_FORMAT, srcpad, caps);
    return;
  }

  GstElement *queue, *onvifparse, *dec;

  /* Create decode chain */
  MAKE_AND_ADD (queue, ctx->pipe, "queue", fail, NULL);
  MAKE_AND_ADD (onvifparse, ctx->pipe, "rtponvifparse", fail, NULL);
  MAKE_AND_ADD (dec, ctx->pipe, "decodebin", fail, NULL);

  gst_element_set_state (queue, GST_STATE_PLAYING);
  gst_element_set_state (onvifparse, GST_STATE_PLAYING);
  gst_element_set_state (dec, GST_STATE_PLAYING);

  g_signal_connect (dec, "pad-added", G_CALLBACK (decode_pad_added_cb), ctx);

  if (!gst_element_link_many (queue, onvifparse, dec, NULL)) {
    goto fail;
  }

  /* Connect srcpad to decode chain */
  GstPad *sinkpad = gst_element_get_static_pad (queue, "sink");
  if (gst_pad_link (srcpad, sinkpad) != GST_PAD_LINK_OK) {
    goto fail;
  }
  gst_object_unref (sinkpad);

  return;

no_caps:
  GST_ELEMENT_ERROR (ctx->pipe, CORE, NEGOTIATION, (NULL),
      ("decoded pad %" GST_PTR_FORMAT " did not have caps", srcpad));
  return;
fail:
  GST_ELEMENT_ERROR (ctx->pipe, CORE, NEGOTIATION, (NULL),
      ("Could not link input stream to decoders handling pad %" GST_PTR_FORMAT,
          srcpad));
  return;
}

static gboolean
setup (Context * ctx)
{
  GstElement *toverlay, *tee, *vqueue;
  gboolean ret = FALSE;

  MAKE_AND_ADD (ctx->src, ctx->pipe, "rtspsrc", done, NULL);

  MAKE_AND_ADD (ctx->vconv, ctx->pipe, "videoconvert", done, NULL);
  MAKE_AND_ADD (toverlay, ctx->pipe, "timeoverlay", done, NULL);
  MAKE_AND_ADD (tee, ctx->pipe, "tee", done, NULL);
  MAKE_AND_ADD (vqueue, ctx->pipe, "queue", done, NULL);
  MAKE_AND_ADD (ctx->sink, ctx->pipe, "autovideosink", done, NULL);

  g_object_set (ctx->src, "location", rtsp_address, NULL);
  g_object_set (ctx->src, "onvif-mode", TRUE, NULL);
  g_object_set (ctx->src, "tcp-timeout", 0, NULL);
  g_object_set (toverlay, "show-times-as-dates", TRUE, NULL);

  g_object_set (toverlay, "datetime-format", "%a %d, %b %Y - %T", NULL);

  g_signal_connect (ctx->src, "pad-added", G_CALLBACK (pad_added_cb), ctx);

  if (!gst_element_link_many (ctx->vconv, toverlay, tee, vqueue, ctx->sink,
          NULL)) {
    goto done;
  }

  g_object_set (ctx->src, "onvif-rate-control", FALSE, "is-live", FALSE, NULL);

  if (!g_strcmp0 (ctx->seek_params->rate_control, "no")) {
    g_object_set (ctx->sink, "sync", FALSE, NULL);
  }

  ret = TRUE;

done:
  return ret;
}

/* Retrieve the actual sink from inside an auto*sink */
static GstElement *
get_actual_sink (GstElement * autosink)
{
  GstPad *sinkpad = gst_element_get_static_pad (autosink, "sink");
  GstPad *actual_sinkpad = gst_ghost_pad_get_target (GST_GHOST_PAD (sinkpad));
  gst_object_unref (GST_OBJECT (sinkpad));

  if (actual_sinkpad == NULL) {
    return NULL;
  }

  GstElement *actual_sink =
      (GstElement *) gst_object_get_parent (GST_OBJECT (actual_sinkpad));
  gst_object_unref (GST_OBJECT (actual_sinkpad));

  return actual_sink;
}

static GstClockTime
get_current_position (Context * ctx, gboolean reverse)
{
  GstSample *sample = NULL;
  GstBuffer *buffer;
  GstClockTime ret;

  GstElement *sink = get_actual_sink (ctx->sink);
  if (sink == NULL) {
    return 0;
  }

  g_object_get (sink, "last-sample", &sample, NULL);
  gst_object_unref (sink);

  if (sample == NULL) {
    return 0;
  }

  buffer = gst_sample_get_buffer (sample);

  ret = GST_BUFFER_PTS (buffer);

  if (reverse && GST_CLOCK_TIME_IS_VALID (GST_BUFFER_DURATION (buffer)))
    ret += GST_BUFFER_DURATION (buffer);

  gst_sample_unref (sample);

  return ret;
}

static GstEvent *
translate_seek_parameters (Context * ctx, SeekParameters * seek_params)
{
  GstEvent *ret = NULL;
  gchar *range_str = NULL;
  GstRTSPTimeRange *rtsp_range;
  GstSeekType start_type, stop_type;
  GstClockTime start, stop;
  gdouble rate;
  GstSeekFlags flags;
  gchar **split = NULL;
  GstClockTime trickmode_interval = 0;

  range_str = g_strdup_printf ("clock=%s", seek_params->range);

  if (gst_rtsp_range_parse (range_str, &rtsp_range) != GST_RTSP_OK) {
    GST_ERROR ("Failed to parse range %s", range_str);
    goto done;
  }

  gst_rtsp_range_get_times (rtsp_range, &start, &stop);

  if (start > stop) {
    GST_ERROR ("Invalid range, start > stop: %s", seek_params->range);
    goto done;
  }

  start_type = GST_SEEK_TYPE_SET;
  stop_type = GST_SEEK_TYPE_SET;

  if (!ctx->new_range) {
    GstClockTime current_position =
        get_current_position (ctx, seek_params->reverse);

    if (seek_params->reverse) {
      stop_type = GST_SEEK_TYPE_SET;
      stop = current_position;
    } else {
      start_type = GST_SEEK_TYPE_SET;
      start = current_position;
    }
  }

  ctx->new_range = FALSE;

  flags = GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE;

  split = g_strsplit (seek_params->frames, "/", 2);

  if (!g_strcmp0 (split[0], "intra")) {
    if (split[1]) {
      guint64 interval;
      gchar *end;

      interval = g_ascii_strtoull (split[1], &end, 10);

      if (!end || *end != '\0') {
        GST_ERROR ("Unexpected interval value %s", split[1]);
        goto done;
      }

      trickmode_interval = interval * GST_MSECOND;
    }
    flags |= GST_SEEK_FLAG_TRICKMODE_KEY_UNITS;
  } else if (!g_strcmp0 (split[0], "predicted")) {
    if (split[1]) {
      GST_ERROR ("Predicted frames mode does not allow an interval (%s)",
          seek_params->frames);
      goto done;
    }
    flags |= GST_SEEK_FLAG_TRICKMODE_FORWARD_PREDICTED;
  } else if (g_strcmp0 (split[0], "none")) {
    GST_ERROR ("Invalid frames mode (%s)", seek_params->frames);
    goto done;
  }

  if (seek_params->reverse) {
    rate = -1.0 * seek_params->speed;
  } else {
    rate = 1.0 * seek_params->speed;
  }

  ret = gst_event_new_seek (rate, GST_FORMAT_TIME, flags,
      start_type, start, stop_type, stop);

  if (trickmode_interval)
    gst_event_set_seek_trickmode_interval (ret, trickmode_interval);

done:
  if (split)
    g_strfreev (split);
  g_free (range_str);
  return ret;
}

static void prompt_on (Context * ctx);
static void prompt_off (Context * ctx);

static gboolean
cmd_help (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = TRUE;
  guint i;

  *async = FALSE;

  for (i = 0; commands[i].name; i++) {
    g_print ("%s: %s\n", commands[i].name, commands[i].help);
  }

  return ret;
}

static gboolean
cmd_pause (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret;
  GstStateChangeReturn state_ret;

  g_print ("Pausing\n");

  state_ret = gst_element_set_state (ctx->pipe, GST_STATE_PAUSED);

  *async = state_ret == GST_STATE_CHANGE_ASYNC;
  ret = state_ret != GST_STATE_CHANGE_FAILURE;

  return ret;
}

static gboolean
cmd_play (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret;
  GstStateChangeReturn state_ret;

  g_print ("Playing\n");

  state_ret = gst_element_set_state (ctx->pipe, GST_STATE_PLAYING);

  *async = state_ret == GST_STATE_CHANGE_ASYNC;
  ret = state_ret != GST_STATE_CHANGE_FAILURE;

  return ret;
}

static gboolean
do_seek (Context * ctx)
{
  gboolean ret = FALSE;
  GstEvent *event;

  if (!(event = translate_seek_parameters (ctx, ctx->seek_params))) {
    GST_ERROR ("Failed to create seek event");
    goto done;
  }

  if (ctx->seek_params->reverse)
    g_object_set (ctx->src, "onvif-rate-control", FALSE, NULL);

  if (ctx->reset_sync) {
    g_object_set (ctx->sink, "sync", TRUE, NULL);
    ctx->reset_sync = FALSE;
  }

  if (!gst_element_send_event (ctx->src, event)) {
    GST_ERROR ("Failed to seek rtspsrc");
    g_main_loop_quit (ctx->loop);
    goto done;
  }

  ret = TRUE;

done:
  return ret;
}

static gboolean
cmd_reverse (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = TRUE;

  g_print ("Reversing playback direction\n");

  ctx->seek_params->reverse = !ctx->seek_params->reverse;

  ret = do_seek (ctx);

  *async = ret == TRUE;

  return ret;
}

static gboolean
cmd_range (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = TRUE;

  g_print ("Switching to new range\n");

  g_free (ctx->seek_params->range);
  ctx->seek_params->range = g_strdup (arg);
  ctx->new_range = TRUE;

  ret = do_seek (ctx);

  *async = ret == TRUE;

  return ret;
}

static gboolean
cmd_speed (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = FALSE;
  gchar *endptr = NULL;
  gdouble new_speed;

  new_speed = g_ascii_strtod (arg, &endptr);

  g_print ("Switching gears\n");

  if (endptr == NULL || *endptr != '\0' || new_speed <= 0.0) {
    GST_ERROR ("Invalid value for speed: %s", arg);
    goto done;
  }

  ctx->seek_params->speed = new_speed;
  ret = do_seek (ctx);

done:
  *async = ret == TRUE;
  return ret;
}

static gboolean
cmd_frames (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = TRUE;

  g_print ("Changing Frames trickmode\n");

  g_free (ctx->seek_params->frames);
  ctx->seek_params->frames = g_strdup (arg);
  ret = do_seek (ctx);
  *async = ret == TRUE;

  return ret;
}

static gboolean
cmd_rate_control (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = FALSE;

  *async = FALSE;

  if (!g_strcmp0 (arg, "no")) {
    g_object_set (ctx->sink, "sync", FALSE, NULL);
    ret = TRUE;
  } else if (!g_strcmp0 (arg, "yes")) {
    /* TODO: there probably is a solution that doesn't involve sending
     * a request to the server to reset our position */
    ctx->reset_sync = TRUE;
    ret = do_seek (ctx);
    *async = TRUE;
  } else {
    GST_ERROR ("Invalid rate-control: %s", arg);
    goto done;
  }

  ret = TRUE;

done:
  return ret;
}

static gboolean
cmd_step_forward (Context * ctx, gchar * arg, gboolean * async)
{
  gboolean ret = FALSE;
  GstEvent *event;

  event = gst_event_new_step (GST_FORMAT_BUFFERS, 1, 1.0, TRUE, FALSE);

  g_print ("Stepping\n");

  if (!gst_element_send_event (ctx->sink, event)) {
    GST_ERROR ("Failed to step forward");
    goto done;
  }

  ret = TRUE;

done:
  *async = ret == TRUE;
  return ret;
}

static void
handle_command (Context * ctx, gchar * cmd)
{
  gchar **split;
  guint i;
  gboolean valid_command = FALSE;

  split = g_strsplit (cmd, ":", 0);

  cmd = g_strstrip (split[0]);

  if (cmd == NULL || *cmd == '\0') {
    g_print ("> ");
    goto done;
  }

  for (i = 0; commands[i].name; i++) {
    if (!g_strcmp0 (commands[i].name, cmd)) {
      valid_command = TRUE;
      if (commands[i].has_argument && g_strv_length (split) != 2) {
        g_print ("Command %s expects exactly one argument:\n%s: %s\n", cmd,
            commands[i].name, commands[i].help);
      } else if (!commands[i].has_argument && g_strv_length (split) != 1) {
        g_print ("Command %s expects no argument:\n%s: %s\n", cmd,
            commands[i].name, commands[i].help);
      } else {
        gboolean async = FALSE;

        if (commands[i].func (ctx,
                commands[i].has_argument ? g_strstrip (split[1]) : NULL, &async)
            && async)
          prompt_off (ctx);
        else
          g_print ("> ");
      }
      break;
    }
  }

  if (!valid_command) {
    g_print ("Invalid command %s\n> ", cmd);
  }

done:
  g_strfreev (split);
}

static gboolean
io_callback (GIOChannel * io, GIOCondition condition, Context * ctx)
{
  gboolean ret = TRUE;
  gchar *str;
  GError *error = NULL;

  switch (condition) {
    case G_IO_PRI:
    case G_IO_IN:
      switch (g_io_channel_read_line (io, &str, NULL, NULL, &error)) {
        case G_IO_STATUS_ERROR:
          GST_ERROR ("Failed to read commands from stdin: %s", error->message);
          g_clear_error (&error);
          g_main_loop_quit (ctx->loop);
          break;
        case G_IO_STATUS_EOF:
          g_print ("EOF received, bye\n");
          g_main_loop_quit (ctx->loop);
          break;
        case G_IO_STATUS_AGAIN:
          break;
        case G_IO_STATUS_NORMAL:
          handle_command (ctx, str);
          g_free (str);
          break;
      }
      break;
    case G_IO_ERR:
    case G_IO_HUP:
      GST_ERROR ("Failed to read commands from stdin");
      g_main_loop_quit (ctx->loop);
      break;
    case G_IO_OUT:
    default:
      break;
  }

  return ret;
}

#ifndef STDIN_FILENO
#ifdef G_OS_WIN32
#define STDIN_FILENO _fileno(stdin)
#else /* !G_OS_WIN32 */
#define STDIN_FILENO 0
#endif /* G_OS_WIN32 */
#endif /* STDIN_FILENO */

static void
prompt_on (Context * ctx)
{
  g_assert (!ctx->io);
  ctx->io = g_io_channel_unix_new (STDIN_FILENO);
  ctx->io_watch_id =
      g_io_add_watch (ctx->io, G_IO_IN, (GIOFunc) io_callback, ctx);
  g_print ("> ");
}

static void
prompt_off (Context * ctx)
{
  g_assert (ctx->io);
  g_source_remove (ctx->io_watch_id);
  g_io_channel_unref (ctx->io);
  ctx->io = NULL;
}

static gboolean
bus_message_cb (GstBus * bus, GstMessage * message, Context * ctx)
{
  switch (GST_MESSAGE_TYPE (message)) {
    case GST_MESSAGE_STATE_CHANGED:{
      GstState olds, news, pendings;

      if (GST_MESSAGE_SRC (message) == GST_OBJECT (ctx->pipe)) {
        gst_message_parse_state_changed (message, &olds, &news, &pendings);
        GST_DEBUG_BIN_TO_DOT_FILE (GST_BIN (ctx->pipe),
            GST_DEBUG_GRAPH_SHOW_ALL, "playing");
      }
      break;
    }
    case GST_MESSAGE_ERROR:{
      GError *error = NULL;
      gchar *debug;

      gst_message_parse_error (message, &error, &debug);

      gst_printerr ("Error: %s (%s)\n", error->message, debug);
      g_clear_error (&error);
      g_free (debug);
      g_main_loop_quit (ctx->loop);
      break;
    }
    case GST_MESSAGE_LATENCY:{
      gst_bin_recalculate_latency (GST_BIN (ctx->pipe));
      break;
    }
    case GST_MESSAGE_ASYNC_DONE:{
      prompt_on (ctx);
    }
    default:
      break;
  }

  return TRUE;
}

int
main (int argc, char **argv)
{
  GOptionContext *optctx;
  Context ctx = { 0, };
  GstBus *bus;
  gint ret = 1;
  GError *error = NULL;
  const gchar *range = NULL;
  const gchar *frames = NULL;
  const gchar *rate_control = NULL;
  gchar *default_speed =
      g_strdup_printf ("Speed to request (default: %.1f)", DEFAULT_SPEED);
  SeekParameters seek_params =
      { NULL, DEFAULT_SPEED, NULL, NULL, DEFAULT_REVERSE };
  GOptionEntry entries[] = {
    {"range", 0, 0, G_OPTION_ARG_STRING, &range,
        "Range to seek (default: " DEFAULT_RANGE ")", "RANGE"},
    {"speed", 0, 0, G_OPTION_ARG_DOUBLE, &seek_params.speed,
        default_speed, "SPEED"},
    {"frames", 0, 0, G_OPTION_ARG_STRING, &frames,
        "Frames to request (default: " DEFAULT_FRAMES ")", "FRAMES"},
    {"rate-control", 0, 0, G_OPTION_ARG_STRING, &rate_control,
        "Apply rate control on the client side (default: "
          DEFAULT_RATE_CONTROL ")", "RATE_CONTROL"},
    {"reverse", 0, 0, G_OPTION_ARG_NONE, &seek_params.reverse,
        "Playback direction", ""},
    {NULL}
  };

  optctx = g_option_context_new ("<rtsp-url> - ONVIF RTSP Client");
  g_option_context_add_main_entries (optctx, entries, NULL);
  g_option_context_add_group (optctx, gst_init_get_option_group ());
  if (!g_option_context_parse (optctx, &argc, &argv, &error)) {
    g_printerr ("Error parsing options: %s\n", error->message);
    g_option_context_free (optctx);
    g_clear_error (&error);
    return -1;
  }
  if (argc < 2) {
    g_print ("%s\n", g_option_context_get_help (optctx, TRUE, NULL));
    return 1;
  }
  rtsp_address = argv[1];
  g_option_context_free (optctx);

  seek_params.range = g_strdup (range ? range : DEFAULT_RANGE);
  seek_params.frames = g_strdup (frames ? frames : DEFAULT_FRAMES);
  seek_params.rate_control =
      g_strdup (rate_control ? rate_control : DEFAULT_RATE_CONTROL);

  if (seek_params.speed <= 0.0) {
    GST_ERROR ("SPEED must be a positive number");
    return 1;
  }

  ctx.seek_params = &seek_params;
  ctx.new_range = TRUE;
  ctx.reset_sync = FALSE;

  ctx.pipe = gst_pipeline_new (NULL);
  if (!setup (&ctx)) {
    g_printerr ("Damn\n");
    goto done;
  }

  g_print ("Type help for the list of available commands\n");

  do_seek (&ctx);

  ctx.loop = g_main_loop_new (NULL, FALSE);

  bus = gst_pipeline_get_bus (GST_PIPELINE (ctx.pipe));
  gst_bus_add_watch (bus, (GstBusFunc) bus_message_cb, &ctx);

  /* This will make rtspsrc progress to the OPEN state, at which point we can seek it */
  if (!gst_element_set_state (ctx.pipe, GST_STATE_PLAYING))
    goto done;

  g_main_loop_run (ctx.loop);

  g_main_loop_unref (ctx.loop);

  gst_bus_remove_watch (bus);
  gst_object_unref (bus);
  gst_element_set_state (ctx.pipe, GST_STATE_NULL);
  gst_object_unref (ctx.pipe);

  ret = 0;

done:
  g_free (seek_params.range);
  g_free (seek_params.frames);
  g_free (seek_params.rate_control);
  g_free (default_speed);
  return ret;
}