/* GStreamer
 *
 * Copyright (C) 2014-2015 Sebastian Dröge <sebastian@centricular.com>
 * Copyright (C) 2015 Brijesh Singh <brijesh.ksingh@gmail.com>
 * Copyright (C) 2019-2020 Stephan Hesse <stephan@emliri.com>
 * Copyright (C) 2020 Philippe Normand <philn@igalia.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.
 */

/**
 * SECTION:gstplay
 * @title: GstPlay
 * @short_description: Player
 * @symbols:
 * - GstPlay
 *
 * The goal of the GstPlay library is to ease the integration of multimedia
 * playback features in applications. Thus, if you need to build a media player
 * from the ground-up, GstPlay provides the features you will most likely need.
 *
 * An example player is available in gst-examples/playback/player/gst-play/.
 *
 * Internally the GstPlay makes use of the `playbin3` element. The legacy
 * `playbin2` can be selected if the `GST_PLAY_USE_PLAYBIN3=0` environment
 * variable has been set.
 *
 * **Important note**: If your application relies on the GstBus to get
 * notifications from GstPlay, you need to add some explicit clean-up code in
 * order to prevent the GstPlay object from leaking. See below for the details.
 * If you use the GstPlaySignalAdapter, no special clean-up is required.
 *
 * When the GstPlaySignalAdapter is not used, the GstBus owned by GstPlay should
 * be set to flushing state before any attempt to drop the last reference of the
 * GstPlay object. An example in C:
 *
 * ```c
 * ...
 * GstBus *bus = gst_play_get_message_bus (player);
 * gst_bus_set_flushing (bus, TRUE);
 * gst_object_unref (bus);
 * gst_object_unref (player);
 * ```
 *
 * The messages managed by the player contain a reference to itself, and if the
 * bus watch is just removed together with dropping the player then the bus will
 * simply keep them around forever (and the bus never goes away because the
 * player has a strong reference to it, so there's a reference cycle as long as
 * there are messages). Setting the bus to flushing state forces it to get rid
 * of its queued messages, thus breaking any possible reference cycle.
 *
 * Since: 1.20
 */

/* TODO:
 *
 * - Equalizer
 * - Gapless playback
 * - Frame stepping
 * - Subtitle font, connection speed
 * - Deinterlacing
 * - Buffering control (-> progressive downloading)
 * - Playlist/queue object
 * - Custom video sink (e.g. embed in GL scene)
 *
 */

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

#include "gstplay.h"
#include "gstplay-video-renderer-private.h"
#include "gstplay-media-info-private.h"
#include "gstplay-message-private.h"

#include <gst/gst.h>
#include <gst/video/video.h>
#include <gst/tag/tag.h>
#include <gst/pbutils/pbutils.h>

#include <string.h>

GST_DEBUG_CATEGORY_STATIC (gst_play_debug);
#define GST_CAT_DEFAULT gst_play_debug

#define DEFAULT_URI NULL
#define DEFAULT_POSITION GST_CLOCK_TIME_NONE
#define DEFAULT_DURATION GST_CLOCK_TIME_NONE
#define DEFAULT_VOLUME 1.0
#define DEFAULT_MUTE FALSE
#define DEFAULT_RATE 1.0
#define DEFAULT_POSITION_UPDATE_INTERVAL_MS 100
#define DEFAULT_AUDIO_VIDEO_OFFSET 0
#define DEFAULT_SUBTITLE_VIDEO_OFFSET 0

/**
 * gst_play_error_quark:
 * Since: 1.20
 */
GQuark
gst_play_error_quark (void)
{
  return g_quark_from_static_string ("gst-play-error-quark");
}

enum
{
  PROP_0,
  PROP_VIDEO_RENDERER,
  PROP_URI,
  PROP_SUBURI,
  PROP_POSITION,
  PROP_DURATION,
  PROP_MEDIA_INFO,
  PROP_CURRENT_AUDIO_TRACK,
  PROP_CURRENT_VIDEO_TRACK,
  PROP_CURRENT_SUBTITLE_TRACK,
  PROP_VOLUME,
  PROP_MUTE,
  PROP_RATE,
  PROP_PIPELINE,
  PROP_VIDEO_MULTIVIEW_MODE,
  PROP_VIDEO_MULTIVIEW_FLAGS,
  PROP_AUDIO_VIDEO_OFFSET,
  PROP_SUBTITLE_VIDEO_OFFSET,
  PROP_LAST
};

enum
{
  GST_PLAY_FLAG_VIDEO = (1 << 0),
  GST_PLAY_FLAG_AUDIO = (1 << 1),
  GST_PLAY_FLAG_SUBTITLE = (1 << 2),
  GST_PLAY_FLAG_VIS = (1 << 3)
};

struct _GstPlay
{
  GstObject parent;

  GstPlayVideoRenderer *video_renderer;

  gchar *uri;
  gchar *redirect_uri;
  gchar *suburi;

  GThread *thread;
  GMutex lock;
  GCond cond;
  GMainContext *context;
  GMainLoop *loop;

  GstBus *api_bus;

  GstElement *playbin;
  GstBus *bus;
  GstState target_state, current_state;
  gboolean is_live, is_eos;
  GSource *tick_source, *ready_timeout_source;

  GstClockTime cached_duration;
  gint64 cached_position;

  gdouble rate;

  GstPlayState app_state;

  gint buffering_percent;

  GstTagList *global_tags;
  GstPlayMediaInfo *media_info;

  GstElement *current_vis_element;

  GstStructure *config;

  GList *missing_plugin_messages;

  /* Protected by lock */
  gboolean seek_pending;        /* Only set from main context */
  GstClockTime last_seek_time;  /* Only set from main context */
  GSource *seek_source;
  GstClockTime seek_position;

  GstStreamCollection *collection;
  guint32 selection_seqnum;
  GList *current_selection;
  gchar *video_sid;
  gboolean video_enabled;
  gchar *audio_sid;
  gboolean audio_enabled;
  gchar *subtitle_sid;
  gboolean subtitle_enabled;
  gulong stream_notify_id;
};

struct _GstPlayClass
{
  GstObjectClass parent_class;
};

#define parent_class gst_play_parent_class
G_DEFINE_TYPE (GstPlay, gst_play, GST_TYPE_OBJECT);

static GParamSpec *param_specs[PROP_LAST] = { NULL, };

static void gst_play_dispose (GObject * object);
static void gst_play_finalize (GObject * object);
static void gst_play_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_play_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_play_constructed (GObject * object);

static gpointer gst_play_main (gpointer data);

static void gst_play_set_playbin_video_sink (GstPlay * self);

static void gst_play_seek_internal_locked (GstPlay * self);
static void gst_play_stop_internal (GstPlay * self, gboolean transient);
static gboolean gst_play_pause_internal (gpointer user_data);
static gboolean gst_play_play_internal (gpointer user_data);
static gboolean gst_play_seek_internal (gpointer user_data);
static void gst_play_set_rate_internal (GstPlay * self);
static void change_state (GstPlay * self, GstPlayState state);

static GstPlayMediaInfo *gst_play_media_info_create (GstPlay * self);

static void gst_play_stream_info_update (GstPlay * self, GstPlayStreamInfo * s);
static GstPlayStreamInfo *gst_play_stream_info_find (GstPlayMediaInfo *
    media_info, GType type, gint stream_index);

static void gst_play_video_info_update (GstPlay * self,
    GstPlayStreamInfo * stream_info);
static void gst_play_audio_info_update (GstPlay * self,
    GstPlayStreamInfo * stream_info);
static void gst_play_subtitle_info_update (GstPlay * self,
    GstPlayStreamInfo * stream_info);

static gboolean gst_play_select_streams (GstPlay * self);

/* For playbin3 */
static void gst_play_streams_info_create_from_collection (GstPlay * self,
    GstPlayMediaInfo * media_info, GstStreamCollection * collection);
static void gst_play_stream_info_update_from_stream (GstPlay * self,
    GstPlayStreamInfo * s, GstStream * stream);
static GstPlayStreamInfo *gst_play_stream_info_find_from_stream_id
    (GstPlayMediaInfo * media_info, const gchar * stream_id);
static GstPlayStreamInfo *gst_play_stream_info_get_current_from_stream_id
    (GstPlay * self, const gchar * stream_id, GType type);
static void stream_notify_cb (GstStreamCollection * collection,
    GstStream * stream, GParamSpec * pspec, GstPlay * self);

static void on_media_info_updated (GstPlay * self);

static void *get_title (GstTagList * tags);
static void *get_container_format (GstTagList * tags);
static void *get_from_tags (GstPlay * self, GstPlayMediaInfo * media_info,
    void *(*func) (GstTagList *));
static void *get_cover_sample (GstTagList * tags);

static void remove_seek_source (GstPlay * self);

static gboolean query_position (GstPlay * self, GstClockTime * position);

static void gst_play_set_uri_details (GstPlay * self, GstStructure * details);

static void
gst_play_init (GstPlay * self)
{
  GST_TRACE_OBJECT (self, "Initializing");

  self = gst_play_get_instance_private (self);

  g_mutex_init (&self->lock);
  g_cond_init (&self->cond);

  self->context = g_main_context_new ();
  self->loop = g_main_loop_new (self->context, FALSE);
  self->api_bus = gst_bus_new ();
  gst_object_set_name (GST_OBJECT (self->api_bus), "api_bus");

  /* *INDENT-OFF* */
  self->config = gst_structure_new_static_str ("play-config",
      "position-update-interval", G_TYPE_UINT, DEFAULT_POSITION_UPDATE_INTERVAL_MS,
      "accurate-seek", G_TYPE_BOOLEAN, FALSE,
      "pipeline-dump-error-in-details", G_TYPE_BOOLEAN, FALSE,
      NULL);
  /* *INDENT-ON* */

  self->seek_pending = FALSE;
  self->seek_position = GST_CLOCK_TIME_NONE;
  self->last_seek_time = GST_CLOCK_TIME_NONE;

  self->cached_position = 0;
  self->cached_duration = GST_CLOCK_TIME_NONE;

  self->audio_enabled = TRUE;
  self->video_enabled = TRUE;
  self->subtitle_enabled = TRUE;
  self->selection_seqnum = GST_SEQNUM_INVALID;

  GST_TRACE_OBJECT (self, "Initialized");
}

/*
 * Works same as gst_structure_set to set field/type/value triplets on message data
 */
static void
api_bus_post_message (GstPlay * self, GstPlayMessage message_type,
    const gchar * firstfield, ...)
{
  GstStructure *message_data = NULL;
  GstStructure *details = NULL;
  GstMessage *msg = NULL;
  va_list varargs;

  GST_INFO ("Posting API-bus message-type: %s",
      gst_play_message_get_name (message_type));
  message_data = gst_structure_new_static_str (GST_PLAY_MESSAGE_DATA,
      GST_PLAY_MESSAGE_DATA_TYPE, GST_TYPE_PLAY_MESSAGE, message_type, NULL);

  va_start (varargs, firstfield);
  gst_structure_set_valist (message_data, firstfield, varargs);
  va_end (varargs);

  msg = gst_message_new_custom (GST_MESSAGE_APPLICATION,
      GST_OBJECT (self), message_data);

  // ERROR/WARNING messages store the details in differently named fields for
  // backwards compatibility
  if (message_type == GST_PLAY_MESSAGE_ERROR) {
    const GValue *v = gst_structure_get_value (message_data,
        GST_PLAY_MESSAGE_DATA_ERROR_DETAILS);
    details = g_value_get_boxed (v);
  } else if (message_type == GST_PLAY_MESSAGE_WARNING) {
    const GValue *v = gst_structure_get_value (message_data,
        GST_PLAY_MESSAGE_DATA_WARNING_DETAILS);
    details = g_value_get_boxed (v);
  }

  if (!details)
    details = gst_message_writable_details (msg);

  gst_play_set_uri_details (self, details);

  GST_DEBUG_OBJECT (self,
      "Created message with payload: [ %" GST_PTR_FORMAT " ]", message_data);
  gst_bus_post (self->api_bus, msg);
}

static void
gst_play_class_init (GstPlayClass * klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;

  GST_DEBUG_CATEGORY_INIT (gst_play_debug, "gst-play", 0, "GstPlay");
  gst_play_error_quark ();

  gobject_class->set_property = gst_play_set_property;
  gobject_class->get_property = gst_play_get_property;
  gobject_class->dispose = gst_play_dispose;
  gobject_class->finalize = gst_play_finalize;
  gobject_class->constructed = gst_play_constructed;

  param_specs[PROP_VIDEO_RENDERER] =
      g_param_spec_object ("video-renderer",
      "Video Renderer", "Video renderer to use for rendering videos",
      GST_TYPE_PLAY_VIDEO_RENDERER, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_URI] = g_param_spec_string ("uri", "URI", "Current URI",
      DEFAULT_URI, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_SUBURI] = g_param_spec_string ("suburi", "Subtitle URI",
      "Current Subtitle URI", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_POSITION] =
      g_param_spec_uint64 ("position", "Position", "Current Position",
      0, G_MAXUINT64, DEFAULT_POSITION,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_MEDIA_INFO] =
      g_param_spec_object ("media-info", "Media Info",
      "Current media information", GST_TYPE_PLAY_MEDIA_INFO,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_CURRENT_AUDIO_TRACK] =
      g_param_spec_object ("current-audio-track", "Current Audio Track",
      "Current audio track information", GST_TYPE_PLAY_AUDIO_INFO,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_CURRENT_VIDEO_TRACK] =
      g_param_spec_object ("current-video-track", "Current Video Track",
      "Current video track information", GST_TYPE_PLAY_VIDEO_INFO,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_CURRENT_SUBTITLE_TRACK] =
      g_param_spec_object ("current-subtitle-track", "Current Subtitle Track",
      "Current audio subtitle information", GST_TYPE_PLAY_SUBTITLE_INFO,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_DURATION] =
      g_param_spec_uint64 ("duration", "Duration", "Duration",
      0, G_MAXUINT64, DEFAULT_DURATION,
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_VOLUME] =
      g_param_spec_double ("volume", "Volume", "Volume",
      0, 10.0, DEFAULT_VOLUME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_MUTE] =
      g_param_spec_boolean ("mute", "Mute", "Mute",
      DEFAULT_MUTE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_PIPELINE] =
      g_param_spec_object ("pipeline", "Pipeline",
      "GStreamer pipeline that is used",
      GST_TYPE_ELEMENT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_RATE] =
      g_param_spec_double ("rate", "rate", "Playback rate",
      -64.0, 64.0, DEFAULT_RATE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_VIDEO_MULTIVIEW_MODE] =
      g_param_spec_enum ("video-multiview-mode",
      "Multiview Mode Override",
      "Re-interpret a video stream as one of several frame-packed stereoscopic modes.",
      GST_TYPE_VIDEO_MULTIVIEW_FRAME_PACKING,
      GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE,
      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_VIDEO_MULTIVIEW_FLAGS] =
      g_param_spec_flags ("video-multiview-flags",
      "Multiview Flags Override",
      "Override details of the multiview frame layout",
      GST_TYPE_VIDEO_MULTIVIEW_FLAGS, GST_VIDEO_MULTIVIEW_FLAGS_NONE,
      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_AUDIO_VIDEO_OFFSET] =
      g_param_spec_int64 ("audio-video-offset", "Audio Video Offset",
      "The synchronisation offset between audio and video in nanoseconds",
      G_MININT64, G_MAXINT64, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  param_specs[PROP_SUBTITLE_VIDEO_OFFSET] =
      g_param_spec_int64 ("subtitle-video-offset", "Text Video Offset",
      "The synchronisation offset between text and video in nanoseconds",
      G_MININT64, G_MAXINT64, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
}

static void
gst_play_dispose (GObject * object)
{
  GstPlay *self = GST_PLAY (object);

  GST_TRACE_OBJECT (self, "Stopping main thread");

  gst_bus_set_flushing (self->api_bus, TRUE);

  if (self->loop) {
    g_main_loop_quit (self->loop);

    if (self->thread != g_thread_self ())
      g_thread_join (self->thread);
    else
      g_thread_unref (self->thread);
    self->thread = NULL;

    g_main_loop_unref (self->loop);
    self->loop = NULL;

    g_main_context_unref (self->context);
    self->context = NULL;
  }

  gst_clear_object (&self->api_bus);

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

static void
gst_play_finalize (GObject * object)
{
  GstPlay *self = GST_PLAY (object);

  GST_TRACE_OBJECT (self, "Finalizing");

  g_free (self->uri);
  g_free (self->redirect_uri);
  g_free (self->suburi);
  g_free (self->video_sid);
  g_free (self->audio_sid);
  g_free (self->subtitle_sid);
  if (self->global_tags)
    gst_tag_list_unref (self->global_tags);
  if (self->video_renderer)
    g_object_unref (self->video_renderer);
  if (self->current_vis_element)
    gst_object_unref (self->current_vis_element);
  if (self->config)
    gst_structure_free (self->config);
  if (self->collection)
    gst_object_unref (self->collection);
  g_list_free_full (self->current_selection, g_free);
  if (self->media_info)
    g_object_unref (self->media_info);
  g_mutex_clear (&self->lock);
  g_cond_clear (&self->cond);

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

static void
gst_play_constructed (GObject * object)
{
  GstPlay *self = GST_PLAY (object);

  GST_TRACE_OBJECT (self, "Constructed");

  g_mutex_lock (&self->lock);
  self->thread = g_thread_new ("GstPlay", gst_play_main, self);
  while (!self->loop || !g_main_loop_is_running (self->loop))
    g_cond_wait (&self->cond, &self->lock);

  gst_play_set_playbin_video_sink (self);
  g_mutex_unlock (&self->lock);

  G_OBJECT_CLASS (parent_class)->constructed (object);
}

static gboolean
gst_play_set_uri_internal (gpointer user_data)
{
  GstPlay *self = user_data;

  gst_play_stop_internal (self, FALSE);

  g_mutex_lock (&self->lock);

  GST_DEBUG_OBJECT (self, "Changing URI to '%s'", GST_STR_NULL (self->uri));

  g_object_set (self->playbin, "uri", self->uri, NULL);

  api_bus_post_message (self, GST_PLAY_MESSAGE_URI_LOADED,
      GST_PLAY_MESSAGE_DATA_URI, G_TYPE_STRING, self->uri, NULL);

  g_object_set (self->playbin, "suburi", NULL, NULL);

  g_mutex_unlock (&self->lock);

  return G_SOURCE_REMOVE;
}

static gboolean
gst_play_set_suburi_internal (gpointer user_data)
{
  GstPlay *self = user_data;
  GstClockTime position;
  GstState target_state;

  /* save the state and position */
  target_state = self->target_state;
  position = gst_play_get_position (self);

  gst_play_stop_internal (self, TRUE);
  g_mutex_lock (&self->lock);

  GST_DEBUG_OBJECT (self, "Changing SUBURI to '%s'",
      GST_STR_NULL (self->suburi));

  g_object_set (self->playbin, "suburi", self->suburi, NULL);

  g_mutex_unlock (&self->lock);

  /* restore state and position */
  if (position != GST_CLOCK_TIME_NONE)
    gst_play_seek (self, position);
  if (target_state == GST_STATE_PAUSED)
    gst_play_pause_internal (self);
  else if (target_state == GST_STATE_PLAYING)
    gst_play_play_internal (self);

  return G_SOURCE_REMOVE;
}

static void
gst_play_set_rate_internal (GstPlay * self)
{
  self->seek_position = gst_play_get_position (self);

  /* If there is no seek being dispatch to the main context currently do that,
   * otherwise we just updated the rate so that it will be taken by
   * the seek handler from the main context instead of the old one.
   */
  if (!self->seek_source) {
    /* If no seek is pending then create new seek source */
    if (!self->seek_pending) {
      self->seek_source = g_idle_source_new ();
      g_source_set_callback (self->seek_source,
          (GSourceFunc) gst_play_seek_internal, self, NULL);
      g_source_attach (self->seek_source, self->context);
    }
  }
}

static void
gst_play_set_playbin_video_sink (GstPlay * self)
{
  GstElement *video_sink = NULL;

  if (self->video_renderer != NULL) {
    video_sink =
        gst_play_video_renderer_create_video_sink (self->video_renderer, self);
  }

  if (video_sink) {
    gst_object_ref_sink (video_sink);
    g_object_set (self->playbin, "video-sink", video_sink, NULL);
    gst_object_unref (video_sink);
  }
}

static void
gst_play_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstPlay *self = GST_PLAY (object);

  switch (prop_id) {
    case PROP_VIDEO_RENDERER:
      g_mutex_lock (&self->lock);
      g_clear_object (&self->video_renderer);
      self->video_renderer = g_value_dup_object (value);

      // When the video_renderer is a GstPlayerWrappedVideoRenderer it cannot be set
      // at construction time because it requires a valid pipeline which is created
      // only after GstPlay has been constructed. That is why the video renderer is
      // set *after* GstPlay has been constructed.
      if (self->thread) {
        gst_play_set_playbin_video_sink (self);
      }
      g_mutex_unlock (&self->lock);
      break;
    case PROP_URI:{
      g_mutex_lock (&self->lock);
      g_free (self->uri);
      g_free (self->redirect_uri);
      self->redirect_uri = NULL;

      g_free (self->suburi);
      self->suburi = NULL;

      self->uri = g_value_dup_string (value);
      GST_DEBUG_OBJECT (self, "Set uri=%s", GST_STR_NULL (self->uri));
      g_mutex_unlock (&self->lock);

      g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
          gst_play_set_uri_internal, self, NULL);
      break;
    }
    case PROP_SUBURI:{
      g_mutex_lock (&self->lock);
      g_free (self->suburi);

      self->suburi = g_value_dup_string (value);
      GST_DEBUG_OBJECT (self, "Set suburi=%s", self->suburi);
      g_mutex_unlock (&self->lock);

      g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
          gst_play_set_suburi_internal, self, NULL);
      break;
    }
    case PROP_VOLUME:
      GST_DEBUG_OBJECT (self, "Set volume=%lf", g_value_get_double (value));
      g_object_set_property (G_OBJECT (self->playbin), "volume", value);
      break;
    case PROP_RATE:
      g_mutex_lock (&self->lock);
      self->rate = g_value_get_double (value);
      GST_DEBUG_OBJECT (self, "Set rate=%lf", g_value_get_double (value));
      gst_play_set_rate_internal (self);
      g_mutex_unlock (&self->lock);
      break;
    case PROP_MUTE:
      GST_DEBUG_OBJECT (self, "Set mute=%d", g_value_get_boolean (value));
      g_object_set_property (G_OBJECT (self->playbin), "mute", value);
      break;
    case PROP_VIDEO_MULTIVIEW_MODE:
      GST_DEBUG_OBJECT (self, "Set multiview mode=%u",
          g_value_get_enum (value));
      g_object_set_property (G_OBJECT (self->playbin), "video-multiview-mode",
          value);
      break;
    case PROP_VIDEO_MULTIVIEW_FLAGS:
      GST_DEBUG_OBJECT (self, "Set multiview flags=%x",
          g_value_get_flags (value));
      g_object_set_property (G_OBJECT (self->playbin), "video-multiview-flags",
          value);
      break;
    case PROP_AUDIO_VIDEO_OFFSET:
      g_object_set_property (G_OBJECT (self->playbin), "av-offset", value);
      break;
    case PROP_SUBTITLE_VIDEO_OFFSET:
      g_object_set_property (G_OBJECT (self->playbin), "text-offset", value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_play_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstPlay *self = GST_PLAY (object);

  switch (prop_id) {
    case PROP_URI:
      g_mutex_lock (&self->lock);
      g_value_set_string (value, self->uri);
      g_mutex_unlock (&self->lock);
      break;
    case PROP_SUBURI:
      g_mutex_lock (&self->lock);
      g_value_set_string (value, self->suburi);
      g_mutex_unlock (&self->lock);
      GST_DEBUG_OBJECT (self, "Returning suburi=%s",
          g_value_get_string (value));
      break;
    case PROP_POSITION:{
      GstClockTime position = GST_CLOCK_TIME_NONE;
      query_position (self, &position);
      g_value_set_uint64 (value, position);
      GST_TRACE_OBJECT (self, "Returning position=%" GST_TIME_FORMAT,
          GST_TIME_ARGS (g_value_get_uint64 (value)));
      break;
    }
    case PROP_DURATION:{
      g_value_set_uint64 (value, self->cached_duration);
      GST_TRACE_OBJECT (self, "Returning duration=%" GST_TIME_FORMAT,
          GST_TIME_ARGS (g_value_get_uint64 (value)));
      break;
    }
    case PROP_MEDIA_INFO:{
      GstPlayMediaInfo *media_info = gst_play_get_media_info (self);
      g_value_take_object (value, media_info);
      break;
    }
    case PROP_CURRENT_AUDIO_TRACK:{
      GstPlayAudioInfo *audio_info = gst_play_get_current_audio_track (self);
      g_value_take_object (value, audio_info);
      break;
    }
    case PROP_CURRENT_VIDEO_TRACK:{
      GstPlayVideoInfo *video_info = gst_play_get_current_video_track (self);
      g_value_take_object (value, video_info);
      break;
    }
    case PROP_CURRENT_SUBTITLE_TRACK:{
      GstPlaySubtitleInfo *subtitle_info =
          gst_play_get_current_subtitle_track (self);
      g_value_take_object (value, subtitle_info);
      break;
    }
    case PROP_VOLUME:
      g_object_get_property (G_OBJECT (self->playbin), "volume", value);
      GST_TRACE_OBJECT (self, "Returning volume=%lf",
          g_value_get_double (value));
      break;
    case PROP_RATE:
      g_mutex_lock (&self->lock);
      g_value_set_double (value, self->rate);
      g_mutex_unlock (&self->lock);
      break;
    case PROP_MUTE:
      g_object_get_property (G_OBJECT (self->playbin), "mute", value);
      GST_TRACE_OBJECT (self, "Returning mute=%d", g_value_get_boolean (value));
      break;
    case PROP_PIPELINE:
      g_value_set_object (value, self->playbin);
      break;
    case PROP_VIDEO_MULTIVIEW_MODE:{
      g_object_get_property (G_OBJECT (self->playbin), "video-multiview-mode",
          value);
      GST_TRACE_OBJECT (self, "Return multiview mode=%d",
          g_value_get_enum (value));
      break;
    }
    case PROP_VIDEO_MULTIVIEW_FLAGS:{
      g_object_get_property (G_OBJECT (self->playbin), "video-multiview-flags",
          value);
      GST_TRACE_OBJECT (self, "Return multiview flags=%x",
          g_value_get_flags (value));
      break;
    }
    case PROP_AUDIO_VIDEO_OFFSET:
      g_object_get_property (G_OBJECT (self->playbin), "av-offset", value);
      break;
    case PROP_SUBTITLE_VIDEO_OFFSET:
      g_object_get_property (G_OBJECT (self->playbin), "text-offset", value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
main_loop_running_cb (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  GST_TRACE_OBJECT (self, "Main loop running now");

  g_mutex_lock (&self->lock);
  g_cond_signal (&self->cond);
  g_mutex_unlock (&self->lock);

  return G_SOURCE_REMOVE;
}

static void
change_state (GstPlay * self, GstPlayState state)
{
  if (state == self->app_state)
    return;

  GST_DEBUG_OBJECT (self, "Changing app state from %s to %s",
      gst_play_state_get_name (self->app_state),
      gst_play_state_get_name (state));

  self->app_state = state;

  api_bus_post_message (self, GST_PLAY_MESSAGE_STATE_CHANGED,
      GST_PLAY_MESSAGE_DATA_PLAY_STATE, GST_TYPE_PLAY_STATE,
      self->app_state, NULL);
}

static gboolean
tick_cb (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstClockTime position;
  if (query_position (self, &position)) {
    api_bus_post_message (self, GST_PLAY_MESSAGE_POSITION_UPDATED,
        GST_PLAY_MESSAGE_DATA_POSITION, GST_TYPE_CLOCK_TIME, position, NULL);
  }

  return G_SOURCE_CONTINUE;
}

/*
 * Returns true when position is queried and differed from cached position.
 * Sets position to cached value, and to queried value if position can be queried
 * and different.
 */
static gboolean
query_position (GstPlay * self, GstClockTime * position)
{
  gint64 current_position;
  *position = self->cached_position;
  if (self->target_state >= GST_STATE_PAUSED
      && gst_element_query_position (self->playbin, GST_FORMAT_TIME,
          &current_position)) {
    GST_LOG_OBJECT (self, "Queried position %" GST_TIME_FORMAT,
        GST_TIME_ARGS (current_position));
    if (self->cached_position != current_position) {
      self->cached_position = current_position;
      *position = (GstClockTime) current_position;
      return TRUE;
    }
  }
  return FALSE;
}

static void
add_tick_source (GstPlay * self)
{
  guint position_update_interval_ms;

  if (self->tick_source)
    return;

  position_update_interval_ms =
      gst_play_config_get_position_update_interval (self->config);
  if (!position_update_interval_ms)
    return;

  self->tick_source = g_timeout_source_new (position_update_interval_ms);
  g_source_set_callback (self->tick_source, (GSourceFunc) tick_cb, self, NULL);
  g_source_attach (self->tick_source, self->context);
}

static void
remove_tick_source (GstPlay * self)
{
  if (!self->tick_source)
    return;

  g_source_destroy (self->tick_source);
  g_source_unref (self->tick_source);
  self->tick_source = NULL;
}

static gboolean
ready_timeout_cb (gpointer user_data)
{
  GstPlay *self = user_data;

  if (self->target_state <= GST_STATE_READY) {
    GST_DEBUG_OBJECT (self, "Setting pipeline to NULL state");
    self->target_state = GST_STATE_NULL;
    self->current_state = GST_STATE_NULL;
    gst_element_set_state (self->playbin, GST_STATE_NULL);
  }

  return G_SOURCE_REMOVE;
}

static void
add_ready_timeout_source (GstPlay * self)
{
  if (self->ready_timeout_source)
    return;

  self->ready_timeout_source = g_timeout_source_new_seconds (60);
  g_source_set_callback (self->ready_timeout_source,
      (GSourceFunc) ready_timeout_cb, self, NULL);
  g_source_attach (self->ready_timeout_source, self->context);
}

static void
remove_ready_timeout_source (GstPlay * self)
{
  if (!self->ready_timeout_source)
    return;

  g_source_destroy (self->ready_timeout_source);
  g_source_unref (self->ready_timeout_source);
  self->ready_timeout_source = NULL;
}


static void
on_error (GstPlay * self, GError * err, GstStructure * details)
{
#ifndef GST_DISABLE_GST_DEBUG
  gchar *dot_data = NULL;
#endif

  GST_ERROR_OBJECT (self, "Error: %s (%s, %d)", err->message,
      g_quark_to_string (err->domain), err->code);

  if (!details)
    details = gst_structure_new_static_str_empty ("error-details");

#ifndef GST_DISABLE_GST_DEBUG
  if (gst_play_config_get_pipeline_dump_in_error_details (self->config)) {
    dot_data = gst_debug_bin_to_dot_data (GST_BIN_CAST (self->playbin),
        GST_DEBUG_GRAPH_SHOW_ALL);
    gst_structure_set (details, "pipeline-dump", G_TYPE_STRING, dot_data, NULL);
  }
#endif
  api_bus_post_message (self, GST_PLAY_MESSAGE_ERROR,
      GST_PLAY_MESSAGE_DATA_ERROR, G_TYPE_ERROR, err,
      GST_PLAY_MESSAGE_DATA_ERROR_DETAILS, GST_TYPE_STRUCTURE, details, NULL);

#ifndef GST_DISABLE_GST_DEBUG
  g_free (dot_data);
#endif
  gst_structure_free (details);

  g_error_free (err);

  remove_tick_source (self);
  remove_ready_timeout_source (self);

  g_list_free_full (self->missing_plugin_messages,
      (GDestroyNotify) gst_message_unref);
  self->missing_plugin_messages = NULL;

  self->target_state = GST_STATE_NULL;
  self->current_state = GST_STATE_NULL;
  self->is_live = FALSE;
  self->is_eos = FALSE;
  gst_element_set_state (self->playbin, GST_STATE_NULL);
  change_state (self, GST_PLAY_STATE_STOPPED);
  self->buffering_percent = 100;

  g_mutex_lock (&self->lock);
  if (self->media_info) {
    g_object_unref (self->media_info);
    self->media_info = NULL;
  }

  if (self->global_tags) {
    gst_tag_list_unref (self->global_tags);
    self->global_tags = NULL;
  }

  self->seek_pending = FALSE;
  remove_seek_source (self);
  self->seek_position = GST_CLOCK_TIME_NONE;
  self->last_seek_time = GST_CLOCK_TIME_NONE;
  g_mutex_unlock (&self->lock);
}

static void
dump_dot_file (GstPlay * self, const gchar * name)
{
  gchar *full_name;

  full_name = g_strdup_printf ("gst-play.%p.%s", self, name);

  GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS (GST_BIN (self->playbin),
      GST_DEBUG_GRAPH_SHOW_ALL, full_name);

  g_free (full_name);
}

static void
gst_play_set_missing_plugin_details (GstPlay * self, GstStructure * details)
{
  GValue missing_plugin_details = G_VALUE_INIT;

  g_value_init (&missing_plugin_details, GST_TYPE_ARRAY);

  for (GList * l = self->missing_plugin_messages; l; l = l->next) {
    GstMessage *missing_plugin_message = l->data;
    GValue v = G_VALUE_INIT;
    GstStructure *s;
    gchar *description, *installer_details;

    description =
        gst_missing_plugin_message_get_description (missing_plugin_message);
    installer_details =
        gst_missing_plugin_message_get_installer_detail
        (missing_plugin_message);

    s = gst_structure_new_static_str ("missing-plugin-detail", "description",
        G_TYPE_STRING, description, "installer-details", G_TYPE_STRING,
        installer_details, NULL);
    g_value_init (&v, GST_TYPE_STRUCTURE);
    g_value_take_boxed (&v, s);
    gst_value_array_append_and_take_value (&missing_plugin_details, &v);

    g_free (description);
    g_free (installer_details);
  }


  gst_structure_take_value_static_str (details, "missing-plugin-details",
      &missing_plugin_details);
}

static void
gst_play_set_uri_details (GstPlay * self, GstStructure * details)
{
  if (!gst_structure_has_field (details, "uri")) {
    gchar *uri;

    g_object_get (self->playbin, "current-uri", &uri, NULL);
    if (!uri)
      g_object_get (self->playbin, "uri", &uri, NULL);
    if (!uri)
      uri = g_strdup (self->uri);
    gst_structure_set (details, "uri", G_TYPE_STRING, uri, NULL);
    g_free (uri);
  }
}

static void
gst_play_set_stream_id_details (GstPlay * self, GstMessage * msg,
    GstStructure * details)
{
  if (!gst_structure_has_field (details, "stream-id")) {
    GstPad *pad = NULL;
    gchar *stream_id;

    if (GST_IS_ELEMENT (GST_MESSAGE_SRC (msg))) {
      GstElement *element = GST_ELEMENT (GST_MESSAGE_SRC (msg));

      // If the message src has only one sinkpad (or is a source element)
      // grab the stream id from there
      GST_OBJECT_LOCK (element);
      if (element->numsinkpads == 1) {
        pad = gst_object_ref (element->sinkpads->data);
      } else if (element->numsinkpads == 0 && element->numsrcpads > 0) {
        pad = gst_object_ref (element->srcpads->data);
      }
      GST_OBJECT_UNLOCK (element);
    } else if (GST_IS_PAD (GST_MESSAGE_SRC (msg))) {
      pad = gst_object_ref (GST_PAD (GST_MESSAGE_SRC (msg)));
    }

    if (pad) {
      stream_id = gst_pad_get_stream_id (pad);
      if (stream_id)
        gst_structure_set (details, "stream-id", G_TYPE_STRING, stream_id,
            NULL);
      g_free (stream_id);
      gst_object_unref (pad);
    }
  }
}

static void
error_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GError *err, *play_err;
  gchar *name, *debug, *message, *full_message;
  GstStructure *details = NULL;
  GstPlayError play_error = GST_PLAY_ERROR_FAILED;

  dump_dot_file (self, "error");

  gst_message_parse_error (msg, &err, &debug);
  gst_message_parse_error_details (msg, (const GstStructure **) &details);
  if (details)
    details = gst_structure_copy (details);
  else
    details = gst_structure_new_static_str_empty ("message-details");

  name = gst_object_get_path_string (msg->src);
  message = gst_error_get_message (err->domain, err->code);

  if (debug)
    full_message =
        g_strdup_printf ("Error from element %s: %s\n%s\n%s", name, message,
        err->message, debug);
  else
    full_message =
        g_strdup_printf ("Error from element %s: %s\n%s", name, message,
        err->message);

  GST_ERROR_OBJECT (self, "ERROR: from element %s: %s", name, err->message);
  if (debug != NULL)
    GST_ERROR_OBJECT (self, "Additional debug info: %s", debug);

  gst_play_set_stream_id_details (self, msg, details);
  if (g_error_matches (err, GST_CORE_ERROR, GST_CORE_ERROR_MISSING_PLUGIN) ||
      g_error_matches (err, GST_STREAM_ERROR,
          GST_STREAM_ERROR_CODEC_NOT_FOUND)) {
    play_error = GST_PLAY_ERROR_MISSING_PLUGIN;
    gst_play_set_missing_plugin_details (self, details);
  }

  play_err = g_error_new_literal (GST_PLAY_ERROR, play_error, full_message);
  on_error (self, play_err, details);

  g_clear_error (&err);
  g_free (debug);
  g_free (name);
  g_free (full_message);
  g_free (message);
}

static void
warning_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GError *err, *play_err;
  gchar *name, *debug, *message, *full_message;
  GstStructure *details = NULL;
  GstPlayError play_error = GST_PLAY_ERROR_FAILED;

  dump_dot_file (self, "warning");

  gst_message_parse_warning (msg, &err, &debug);
  gst_message_parse_warning_details (msg, (const GstStructure **) &details);
  if (details)
    details = gst_structure_copy (details);
  else
    details = gst_structure_new_static_str_empty ("message-details");

  name = gst_object_get_path_string (msg->src);
  message = gst_error_get_message (err->domain, err->code);

  if (debug)
    full_message =
        g_strdup_printf ("Warning from element %s: %s\n%s\n%s", name, message,
        err->message, debug);
  else
    full_message =
        g_strdup_printf ("Warning from element %s: %s\n%s", name, message,
        err->message);

  GST_WARNING_OBJECT (self, "WARNING: from element %s: %s", name, err->message);
  if (debug != NULL)
    GST_WARNING_OBJECT (self, "Additional debug info: %s", debug);

  gst_play_set_stream_id_details (self, msg, details);
  if (g_error_matches (err, GST_CORE_ERROR, GST_CORE_ERROR_MISSING_PLUGIN) ||
      g_error_matches (err, GST_STREAM_ERROR,
          GST_STREAM_ERROR_CODEC_NOT_FOUND)) {
    play_error = GST_PLAY_ERROR_MISSING_PLUGIN;
    gst_play_set_missing_plugin_details (self, details);
  }

  play_err = g_error_new_literal (GST_PLAY_ERROR, play_error, full_message);

  GST_WARNING_OBJECT (self, "Warning: %s (%s, %d)", err->message,
      g_quark_to_string (err->domain), err->code);

  api_bus_post_message (self, GST_PLAY_MESSAGE_WARNING,
      GST_PLAY_MESSAGE_DATA_WARNING, G_TYPE_ERROR, play_err,
      GST_PLAY_MESSAGE_DATA_WARNING_DETAILS, GST_TYPE_STRUCTURE, details, NULL);

  gst_structure_free (details);
  g_clear_error (&play_err);
  g_clear_error (&err);
  g_free (debug);
  g_free (name);
  g_free (full_message);
  g_free (message);
}

static void
eos_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  GST_DEBUG_OBJECT (self, "End of stream");

  tick_cb (self);
  remove_tick_source (self);

  api_bus_post_message (self, GST_PLAY_MESSAGE_END_OF_STREAM, NULL);

  change_state (self, GST_PLAY_STATE_STOPPED);
  self->buffering_percent = 100;
  self->is_eos = TRUE;
}

static void
buffering_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  gint percent;

  if (self->target_state < GST_STATE_PAUSED)
    return;
  if (self->is_live)
    return;

  gst_message_parse_buffering (msg, &percent);
  GST_LOG_OBJECT (self, "Buffering %d%%", percent);

  if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {
    GstStateChangeReturn state_ret;

    GST_DEBUG_OBJECT (self, "Waiting for buffering to finish");
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED);

    if (state_ret == GST_STATE_CHANGE_FAILURE) {
      on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
              "Failed to handle buffering"), NULL);
      return;
    }

    change_state (self, GST_PLAY_STATE_BUFFERING);
  }

  if (self->buffering_percent != percent) {
    self->buffering_percent = percent;

    api_bus_post_message (self, GST_PLAY_MESSAGE_BUFFERING,
        GST_PLAY_MESSAGE_DATA_BUFFERING_PERCENT, G_TYPE_UINT, percent, NULL);
  }

  g_mutex_lock (&self->lock);
  if (percent == 100 && (self->seek_position != GST_CLOCK_TIME_NONE ||
          self->seek_pending)) {
    g_mutex_unlock (&self->lock);

    GST_DEBUG_OBJECT (self, "Buffering finished - seek pending");
  } else if (percent == 100 && self->target_state >= GST_STATE_PLAYING
      && self->current_state >= GST_STATE_PAUSED) {
    GstStateChangeReturn state_ret;

    g_mutex_unlock (&self->lock);

    GST_DEBUG_OBJECT (self, "Buffering finished - going to PLAYING");
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING);
    /* Application state change is happening when the state change happened */
    if (state_ret == GST_STATE_CHANGE_FAILURE)
      on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
              "Failed to handle buffering"), NULL);
  } else if (percent == 100 && self->target_state >= GST_STATE_PAUSED) {
    g_mutex_unlock (&self->lock);

    GST_DEBUG_OBJECT (self, "Buffering finished - staying PAUSED");
    change_state (self, GST_PLAY_STATE_PAUSED);
  } else {
    g_mutex_unlock (&self->lock);
  }
}

static void
clock_lost_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstStateChangeReturn state_ret;

  GST_DEBUG_OBJECT (self, "Clock lost");
  if (self->target_state >= GST_STATE_PLAYING) {
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED);
    if (state_ret != GST_STATE_CHANGE_FAILURE)
      state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING);

    if (state_ret == GST_STATE_CHANGE_FAILURE)
      on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
              "Failed to handle clock loss"), NULL);
  }
}


static void
check_video_dimensions_changed (GstPlay * self)
{
  GstElement *video_sink;
  GstPad *video_sink_pad;
  GstCaps *caps;
  GstVideoInfo info;
  guint width = 0, height = 0;

  g_object_get (self->playbin, "video-sink", &video_sink, NULL);
  if (!video_sink)
    goto out;

  video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
  if (!video_sink_pad) {
    gst_object_unref (video_sink);
    goto out;
  }

  caps = gst_pad_get_current_caps (video_sink_pad);

  if (caps) {
    if (gst_video_info_from_caps (&info, caps)) {
      info.width = info.width * info.par_n / info.par_d;

      GST_DEBUG_OBJECT (self, "Video dimensions changed: %dx%d", info.width,
          info.height);
      width = info.width;
      height = info.height;
    }

    gst_caps_unref (caps);
  }
  gst_object_unref (video_sink_pad);
  gst_object_unref (video_sink);

out:
  api_bus_post_message (self, GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED,
      GST_PLAY_MESSAGE_DATA_VIDEO_WIDTH, G_TYPE_UINT, width,
      GST_PLAY_MESSAGE_DATA_VIDEO_HEIGHT, G_TYPE_UINT, height, NULL);
}

static void
notify_caps_cb (G_GNUC_UNUSED GObject * object,
    G_GNUC_UNUSED GParamSpec * pspec, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  check_video_dimensions_changed (self);
}

static void
on_duration_changed (GstPlay * self, GstClockTime duration)
{
  gboolean updated = FALSE;

  if (self->cached_duration == duration)
    return;

  GST_DEBUG_OBJECT (self, "Duration changed %" GST_TIME_FORMAT,
      GST_TIME_ARGS (duration));

  g_mutex_lock (&self->lock);
  self->cached_duration = duration;
  if (self->media_info) {
    self->media_info->duration = duration;
    updated = TRUE;
  }
  g_mutex_unlock (&self->lock);

  api_bus_post_message (self, GST_PLAY_MESSAGE_DURATION_CHANGED,
      GST_PLAY_MESSAGE_DATA_DURATION, GST_TYPE_CLOCK_TIME,
      gst_play_get_duration (self), NULL);

  if (updated) {
    on_media_info_updated (self);
  }
}

static void
on_seek_done (GstPlay * self)
{
  api_bus_post_message (self, GST_PLAY_MESSAGE_SEEK_DONE,
      GST_PLAY_MESSAGE_DATA_POSITION, GST_TYPE_CLOCK_TIME,
      gst_play_get_position (self), NULL);
}

static void
state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstState old_state, new_state, pending_state;

  gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);

  if (GST_MESSAGE_SRC (msg) == GST_OBJECT (self->playbin)) {
    gchar *transition_name;

    GST_DEBUG_OBJECT (self, "Changed state old: %s new: %s pending: %s",
        gst_state_get_name (old_state),
        gst_state_get_name (new_state), gst_state_get_name (pending_state));

    transition_name = g_strdup_printf ("%s_%s",
        gst_state_get_name (old_state), gst_state_get_name (new_state));
    dump_dot_file (self, transition_name);
    g_free (transition_name);

    self->current_state = new_state;

    if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED
        && pending_state == GST_STATE_VOID_PENDING) {
      GstElement *video_sink;
      GstPad *video_sink_pad;
      gint64 duration = -1;

      GST_DEBUG_OBJECT (self, "Initial PAUSED - pre-rolled");

      g_mutex_lock (&self->lock);
      if (self->media_info)
        g_object_unref (self->media_info);
      self->media_info = gst_play_media_info_create (self);
      g_mutex_unlock (&self->lock);
      on_media_info_updated (self);

      g_object_get (self->playbin, "video-sink", &video_sink, NULL);

      if (video_sink) {
        video_sink_pad = gst_element_get_static_pad (video_sink, "sink");

        if (video_sink_pad) {
          g_signal_connect (video_sink_pad, "notify::caps",
              (GCallback) notify_caps_cb, self);
          gst_object_unref (video_sink_pad);
        }
        gst_object_unref (video_sink);
      }

      check_video_dimensions_changed (self);
      if (gst_element_query_duration (self->playbin, GST_FORMAT_TIME,
              &duration)) {
        on_duration_changed (self, duration);
      } else {
        self->cached_duration = GST_CLOCK_TIME_NONE;
      }
    }

    if (new_state == GST_STATE_PAUSED
        && pending_state == GST_STATE_VOID_PENDING) {
      remove_tick_source (self);

      g_mutex_lock (&self->lock);
      if (self->seek_pending) {
        self->seek_pending = FALSE;

        if (!self->media_info->seekable) {
          GST_DEBUG_OBJECT (self, "Media is not seekable");
          remove_seek_source (self);
          self->seek_position = GST_CLOCK_TIME_NONE;
          self->last_seek_time = GST_CLOCK_TIME_NONE;
        } else if (self->seek_source) {
          GST_DEBUG_OBJECT (self, "Seek finished but new seek is pending");
          gst_play_seek_internal_locked (self);
        } else {
          GST_DEBUG_OBJECT (self, "Seek finished");
          on_seek_done (self);
        }
      }

      if (self->seek_position != GST_CLOCK_TIME_NONE) {
        if (!self->media_info->seekable) {
          GST_DEBUG_OBJECT (self, "Media is not seekable");
        } else {
          GST_DEBUG_OBJECT (self, "Seeking now that we reached PAUSED state");
          gst_play_seek_internal_locked (self);
        }
        g_mutex_unlock (&self->lock);
      } else if (!self->seek_pending) {
        g_mutex_unlock (&self->lock);

        tick_cb (self);

        if (self->target_state >= GST_STATE_PLAYING
            && self->buffering_percent == 100) {
          GstStateChangeReturn state_ret;

          state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING);
          if (state_ret == GST_STATE_CHANGE_FAILURE)
            on_error (self, g_error_new (GST_PLAY_ERROR,
                    GST_PLAY_ERROR_FAILED, "Failed to play"), NULL);
        } else if (self->buffering_percent == 100) {
          change_state (self, GST_PLAY_STATE_PAUSED);
        }
      } else {
        g_mutex_unlock (&self->lock);
      }
    } else if (new_state == GST_STATE_PLAYING
        && pending_state == GST_STATE_VOID_PENDING) {
      /* Try to query duration again if needed */
      if (self->cached_duration == GST_CLOCK_TIME_NONE) {
        gint64 duration = -1;

        if (gst_element_query_duration (self->playbin, GST_FORMAT_TIME,
                &duration)) {
          on_duration_changed (self, duration);
        }
      }

      /* Try to query seek information again for live stream */
      if (self->is_live) {
        gboolean seekable = FALSE;
        gboolean updated = FALSE;

        GstQuery *query = gst_query_new_seeking (GST_FORMAT_TIME);
        if (gst_element_query (self->playbin, query))
          gst_query_parse_seeking (query, NULL, &seekable, NULL, NULL);
        gst_query_unref (query);

        g_mutex_lock (&self->lock);
        if (self->media_info && seekable != self->media_info->seekable) {
          self->media_info->seekable = seekable;
          updated = TRUE;
        }
        g_mutex_unlock (&self->lock);

        if (updated) {
          on_media_info_updated (self);
        }
      }
      /* api_bus_post_message (self, GST_PLAY_MESSAGE_POSITION_UPDATED, */
      /*     GST_PLAY_MESSAGE_DATA_POSITION, GST_TYPE_CLOCK_TIME, 0, NULL); */

      /* If no seek is currently pending, add the tick source. This can happen
       * if we seeked already but the state-change message was still queued up */
      if (!self->seek_pending) {
        add_tick_source (self);
        change_state (self, GST_PLAY_STATE_PLAYING);
      }
    } else if (new_state == GST_STATE_READY && old_state > GST_STATE_READY) {
      change_state (self, GST_PLAY_STATE_STOPPED);
    } else {
      /* Otherwise we neither reached PLAYING nor PAUSED, so must
       * wait for something to happen... i.e. are BUFFERING now */
      change_state (self, GST_PLAY_STATE_BUFFERING);
    }
  }
}

static void
duration_changed_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  gint64 duration = GST_CLOCK_TIME_NONE;

  if (gst_element_query_duration (self->playbin, GST_FORMAT_TIME, &duration)) {
    on_duration_changed (self, duration);
  }
}

static void
latency_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  GST_DEBUG_OBJECT (self, "Latency changed");

  gst_bin_recalculate_latency (GST_BIN (self->playbin));
}

static void
request_state_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstState state;
  GstStateChangeReturn state_ret;

  gst_message_parse_request_state (msg, &state);

  GST_DEBUG_OBJECT (self, "State %s requested", gst_state_get_name (state));

  self->target_state = state;
  state_ret = gst_element_set_state (self->playbin, state);
  if (state_ret == GST_STATE_CHANGE_FAILURE)
    on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
            "Failed to change to requested state %s",
            gst_state_get_name (state)), NULL);
}

static void
media_info_update (GstPlay * self, GstPlayMediaInfo * info)
{
  g_free (info->title);
  info->title = get_from_tags (self, info, get_title);

  g_free (info->container);
  info->container = get_from_tags (self, info, get_container_format);

  if (info->image_sample)
    gst_sample_unref (info->image_sample);
  info->image_sample = get_from_tags (self, info, get_cover_sample);

  GST_DEBUG_OBJECT (self, "title: %s, container: %s "
      "image_sample: %p", info->title, info->container, info->image_sample);
}

static void
tags_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstTagList *tags = NULL;

  gst_message_parse_tag (msg, &tags);

  GST_DEBUG_OBJECT (self, "received %s tags",
      gst_tag_list_get_scope (tags) ==
      GST_TAG_SCOPE_GLOBAL ? "global" : "stream");

  if (gst_tag_list_get_scope (tags) == GST_TAG_SCOPE_GLOBAL) {
    g_mutex_lock (&self->lock);
    if (self->media_info) {
      if (self->media_info->tags)
        gst_tag_list_unref (self->media_info->tags);
      self->media_info->tags = gst_tag_list_ref (tags);
      media_info_update (self, self->media_info);
      g_mutex_unlock (&self->lock);
      on_media_info_updated (self);
    } else {
      if (self->global_tags)
        gst_tag_list_unref (self->global_tags);
      self->global_tags = gst_tag_list_ref (tags);
      g_mutex_unlock (&self->lock);
    }
  }

  gst_tag_list_unref (tags);
}

static void
element_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  const GstStructure *s;

  s = gst_message_get_structure (msg);
  if (gst_structure_has_name (s, "redirect")) {
    const gchar *new_location;

    new_location = gst_structure_get_string (s, "new-location");
    if (!new_location) {
      const GValue *locations_list, *location_val;
      guint i, size;

      locations_list = gst_structure_get_value (s, "locations");
      size = gst_value_list_get_size (locations_list);
      for (i = 0; i < size; ++i) {
        const GstStructure *location_s;

        location_val = gst_value_list_get_value (locations_list, i);
        if (!GST_VALUE_HOLDS_STRUCTURE (location_val))
          continue;

        location_s = (const GstStructure *) g_value_get_boxed (location_val);
        if (!gst_structure_has_name (location_s, "redirect"))
          continue;

        new_location = gst_structure_get_string (location_s, "new-location");
        if (new_location)
          break;
      }
    }

    if (new_location) {
      GstState target_state;

      GST_DEBUG_OBJECT (self, "Redirect to '%s'", new_location);

      /* Remember target state and restore after setting the URI */
      target_state = self->target_state;

      gst_play_stop_internal (self, TRUE);

      g_mutex_lock (&self->lock);
      g_free (self->redirect_uri);
      self->redirect_uri = g_strdup (new_location);
      g_object_set (self->playbin, "uri", self->redirect_uri, NULL);
      g_mutex_unlock (&self->lock);

      if (target_state == GST_STATE_PAUSED)
        gst_play_pause_internal (self);
      else if (target_state == GST_STATE_PLAYING)
        gst_play_play_internal (self);
    }
  } else if (gst_is_missing_plugin_message (msg)) {
    self->missing_plugin_messages =
        g_list_prepend (self->missing_plugin_messages, gst_message_ref (msg));
  }
}

/* Must be called with lock */
static gboolean
update_stream_collection (GstPlay * self, GstStreamCollection * collection)
{
  if (self->collection && self->collection == collection)
    return FALSE;

  if (self->collection && self->stream_notify_id)
    g_signal_handler_disconnect (self->collection, self->stream_notify_id);

  gst_object_replace ((GstObject **) & self->collection,
      (GstObject *) collection);
  if (self->media_info) {
    gst_object_unref (self->media_info);
    self->media_info = gst_play_media_info_create (self);
  }

  self->stream_notify_id =
      g_signal_connect (self->collection, "stream-notify",
      G_CALLBACK (stream_notify_cb), self);

  // If no stream selected then we don't have to search
  gboolean found_audio = self->audio_sid == NULL;
  gboolean found_video = self->video_sid == NULL;
  gboolean found_subtitle = self->subtitle_sid == NULL;

  guint len = gst_stream_collection_get_size (collection);
  for (guint i = 0; i < len; i++) {
    GstStream *stream = gst_stream_collection_get_stream (collection, i);
    GstStreamType stream_type = gst_stream_get_stream_type (stream);
    const gchar *stream_id = gst_stream_get_stream_id (stream);

    if ((stream_type & GST_STREAM_TYPE_AUDIO)
        && g_strcmp0 (self->audio_sid, stream_id) == 0) {
      found_audio = TRUE;
    }
    if ((stream_type & GST_STREAM_TYPE_VIDEO)
        && g_strcmp0 (self->video_sid, stream_id) == 0) {
      found_video = TRUE;
    }
    if ((stream_type & GST_STREAM_TYPE_TEXT)
        && g_strcmp0 (self->subtitle_sid, stream_id) == 0) {
      found_subtitle = TRUE;
    }
  }

  if (!found_audio) {
    GST_WARNING_OBJECT (self, "Didn't find selected audio stream id '%s'",
        self->audio_sid);
    g_free (self->audio_sid);
    self->audio_sid = NULL;
  }

  if (!found_video) {
    GST_WARNING_OBJECT (self, "Didn't find selected video stream id '%s'",
        self->video_sid);
    g_free (self->video_sid);
    self->video_sid = NULL;
  }

  if (!found_subtitle) {
    GST_WARNING_OBJECT (self, "Didn't find selected subtitle stream id '%s'",
        self->subtitle_sid);
    g_free (self->subtitle_sid);
    self->subtitle_sid = NULL;
  }

  return self->media_info != NULL;
}

static void
stream_collection_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstStreamCollection *collection = NULL;

  gst_message_parse_stream_collection (msg, &collection);

  if (!collection)
    return;

  g_mutex_lock (&self->lock);
  gboolean updated = update_stream_collection (self, collection);
  gst_object_unref (collection);

  // Select a default stream if it is enabled and none was selected by the application
  gboolean select_audio = self->audio_enabled && !self->audio_sid;
  gboolean select_video = self->video_enabled && !self->video_sid;
  gboolean select_subtitle = self->subtitle_enabled && !self->subtitle_sid;

  if (select_audio || select_video || select_subtitle) {
    GST_DEBUG_OBJECT (self,
        "Do default selection: audio %d video %d subtitle %d", select_audio,
        select_video, select_subtitle);

    guint len = gst_stream_collection_get_size (collection);
    for (guint i = 0; i < len; i++) {
      GstStream *stream = gst_stream_collection_get_stream (collection, i);
      GstStreamType stream_type = gst_stream_get_stream_type (stream);
      const gchar *stream_id = gst_stream_get_stream_id (stream);

      if ((stream_type & GST_STREAM_TYPE_AUDIO) && select_audio) {
        g_free (self->audio_sid);
        self->audio_sid = g_strdup (stream_id);
        select_audio = FALSE;
        updated = TRUE;
      } else if ((stream_type & GST_STREAM_TYPE_VIDEO) && select_video) {
        g_free (self->video_sid);
        self->video_sid = g_strdup (stream_id);
        select_video = FALSE;
        updated = TRUE;
      } else if ((stream_type & GST_STREAM_TYPE_TEXT) && select_subtitle) {
        g_free (self->subtitle_sid);
        self->subtitle_sid = g_strdup (stream_id);
        select_subtitle = FALSE;
        updated = TRUE;
      }
    }
  }

  if (updated)
    gst_play_select_streams (self);

  g_mutex_unlock (&self->lock);

  if (self->media_info && updated)
    on_media_info_updated (self);
}

static void
streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
    gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  guint32 seqnum = gst_message_get_seqnum (msg);

  g_mutex_lock (&self->lock);
  // Ignore selections for previous select-streams events
  if (self->selection_seqnum != seqnum) {
    g_mutex_unlock (&self->lock);
    return;
  }

  gboolean found_audio = self->audio_sid == NULL;
  gboolean found_video = self->video_sid == NULL;
  gboolean found_subtitle = self->subtitle_sid == NULL;

  guint len = gst_message_streams_selected_get_size (msg);
  for (guint i = 0; i < len; i++) {
    GstStream *stream;
    GstStreamType stream_type;
    const gchar *stream_id;

    stream = gst_message_streams_selected_get_stream (msg, i);
    stream_type = gst_stream_get_stream_type (stream);
    stream_id = gst_stream_get_stream_id (stream);

    if ((stream_type & GST_STREAM_TYPE_AUDIO)) {
      GST_DEBUG_OBJECT (self, "Selected audio track %s", stream_id);
      if (g_strcmp0 (self->audio_sid, stream_id) == 0 && self->audio_enabled) {
        found_audio = TRUE;
      } else {
        GST_WARNING_OBJECT (self, "Unexpected audio stream id '%s' selected",
            stream_id);
      }
    }

    if ((stream_type & GST_STREAM_TYPE_VIDEO)) {
      GST_DEBUG_OBJECT (self, "Selected video track %s", stream_id);
      if (g_strcmp0 (self->video_sid, stream_id) == 0 && self->video_enabled) {
        found_video = TRUE;
      } else {
        GST_WARNING_OBJECT (self, "Unexpected video stream id '%s' selected",
            stream_id);
      }
    }

    if ((stream_type & GST_STREAM_TYPE_TEXT)) {
      GST_DEBUG_OBJECT (self, "Selected subtitle track %s", stream_id);
      if (g_strcmp0 (self->subtitle_sid, stream_id) == 0
          && self->subtitle_enabled) {
        found_subtitle = TRUE;
      } else {
        GST_WARNING_OBJECT (self, "Unexpected subtitle stream id '%s' selected",
            stream_id);
      }
    }
  }

  if (!found_audio && self->audio_enabled) {
    GST_WARNING_OBJECT (self, "Didn't find selected audio stream id '%s'",
        self->audio_sid);
  }

  if (!found_video && self->video_enabled) {
    GST_WARNING_OBJECT (self, "Didn't find selected video stream id '%s'",
        self->video_sid);
  }

  if (!found_subtitle && self->subtitle_enabled) {
    GST_WARNING_OBJECT (self, "Didn't find selected subtitle stream id '%s'",
        self->subtitle_sid);
  }
  g_mutex_unlock (&self->lock);
}

static void
play_set_flag (GstPlay * self, gint pos)
{
  gint flags;

  g_object_get (self->playbin, "flags", &flags, NULL);
  flags |= pos;
  g_object_set (self->playbin, "flags", flags, NULL);

  GST_DEBUG_OBJECT (self, "setting flags=%#x", flags);
}

static void
play_clear_flag (GstPlay * self, gint pos)
{
  gint flags;

  g_object_get (self->playbin, "flags", &flags, NULL);
  flags &= ~pos;
  g_object_set (self->playbin, "flags", flags, NULL);

  GST_DEBUG_OBJECT (self, "setting flags=%#x", flags);
}

/*
 * on_media_info_updated:
 *
 * create a new copy of self->media_info object and post it to the user
 * application.
 */
static void
on_media_info_updated (GstPlay * self)
{
  GstPlayMediaInfo *media_info_copy;

  g_mutex_lock (&self->lock);
  media_info_copy = gst_play_media_info_copy (self->media_info);
  g_mutex_unlock (&self->lock);

  api_bus_post_message (self, GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED,
      GST_PLAY_MESSAGE_DATA_MEDIA_INFO, GST_TYPE_PLAY_MEDIA_INFO,
      media_info_copy, NULL);
  g_object_unref (media_info_copy);
}

static void
gst_play_subtitle_info_update (GstPlay * self, GstPlayStreamInfo * stream_info)
{
  GstPlaySubtitleInfo *info = (GstPlaySubtitleInfo *) stream_info;

  if (stream_info->tags) {

    /* free the old language info */
    g_free (info->language);
    info->language = NULL;

    /* First try to get the language full name from tag, if name is not
     * available then try language code. If we find the language code
     * then use gstreamer api to translate code to full name.
     */
    gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_NAME,
        &info->language);
    if (!info->language) {
      gchar *lang_code = NULL;

      gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_CODE,
          &lang_code);
      if (lang_code) {
        info->language = g_strdup (gst_tag_get_language_name (lang_code));
        g_free (lang_code);
      }
    }

    /* If we are still failed to find language name then check if external
     * subtitle is loaded and compare the stream index between current sub
     * stream index with our stream index and if matches then declare it as
     * external subtitle and use the filename.
     */
    if (!info->language) {
      gchar *suburi = NULL;

      g_object_get (G_OBJECT (self->playbin), "current-suburi", &suburi, NULL);
      if (suburi) {
        if (self->subtitle_sid &&
            g_str_equal (self->subtitle_sid, stream_info->stream_id)) {
          info->language = g_path_get_basename (suburi);
        }
        g_free (suburi);
      }
    }

  } else {
    g_free (info->language);
    info->language = NULL;
  }

  GST_DEBUG_OBJECT (self, "language=%s", info->language);
}

static void
gst_play_video_info_update (GstPlay * self, GstPlayStreamInfo * stream_info)
{
  GstPlayVideoInfo *info = (GstPlayVideoInfo *) stream_info;

  if (stream_info->caps) {
    GstStructure *s;

    s = gst_caps_get_structure (stream_info->caps, 0);
    if (s) {
      gint width, height;
      gint fps_n, fps_d;
      gint par_n, par_d;

      if (gst_structure_get_int (s, "width", &width))
        info->width = width;
      else
        info->width = -1;

      if (gst_structure_get_int (s, "height", &height))
        info->height = height;
      else
        info->height = -1;

      if (gst_structure_get_fraction (s, "framerate", &fps_n, &fps_d)) {
        info->framerate_num = fps_n;
        info->framerate_denom = fps_d;
      } else {
        info->framerate_num = 0;
        info->framerate_denom = 1;
      }


      if (gst_structure_get_fraction (s, "pixel-aspect-ratio", &par_n, &par_d)) {
        info->par_num = par_n;
        info->par_denom = par_d;
      } else {
        info->par_num = 1;
        info->par_denom = 1;
      }
    }
  } else {
    info->width = info->height = -1;
    info->par_num = info->par_denom = 1;
    info->framerate_num = 0;
    info->framerate_denom = 1;
  }

  if (stream_info->tags) {
    guint bitrate, max_bitrate;

    if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_BITRATE, &bitrate))
      info->bitrate = bitrate;
    else
      info->bitrate = -1;

    if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_MAXIMUM_BITRATE,
            &max_bitrate) || gst_tag_list_get_uint (stream_info->tags,
            GST_TAG_NOMINAL_BITRATE, &max_bitrate))
      info->max_bitrate = max_bitrate;
    else
      info->max_bitrate = -1;
  } else {
    info->bitrate = info->max_bitrate = -1;
  }

  GST_DEBUG_OBJECT (self, "width=%d height=%d fps=%.2f par=%d:%d "
      "bitrate=%d max_bitrate=%d", info->width, info->height,
      (gdouble) info->framerate_num / info->framerate_denom,
      info->par_num, info->par_denom, info->bitrate, info->max_bitrate);
}

static void
gst_play_audio_info_update (GstPlay * self, GstPlayStreamInfo * stream_info)
{
  GstPlayAudioInfo *info = (GstPlayAudioInfo *) stream_info;

  if (stream_info->caps) {
    GstStructure *s;

    s = gst_caps_get_structure (stream_info->caps, 0);
    if (s) {
      gint rate, channels;

      if (gst_structure_get_int (s, "rate", &rate))
        info->sample_rate = rate;
      else
        info->sample_rate = -1;

      if (gst_structure_get_int (s, "channels", &channels))
        info->channels = channels;
      else
        info->channels = 0;
    }
  } else {
    info->sample_rate = -1;
    info->channels = 0;
  }

  if (stream_info->tags) {
    guint bitrate, max_bitrate;

    if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_BITRATE, &bitrate))
      info->bitrate = bitrate;
    else
      info->bitrate = -1;

    if (gst_tag_list_get_uint (stream_info->tags, GST_TAG_MAXIMUM_BITRATE,
            &max_bitrate) || gst_tag_list_get_uint (stream_info->tags,
            GST_TAG_NOMINAL_BITRATE, &max_bitrate))
      info->max_bitrate = max_bitrate;
    else
      info->max_bitrate = -1;

    /* if we have old language the free it */
    g_free (info->language);
    info->language = NULL;

    /* First try to get the language full name from tag, if name is not
     * available then try language code. If we find the language code
     * then use gstreamer api to translate code to full name.
     */
    gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_NAME,
        &info->language);
    if (!info->language) {
      gchar *lang_code = NULL;

      gst_tag_list_get_string (stream_info->tags, GST_TAG_LANGUAGE_CODE,
          &lang_code);
      if (lang_code) {
        info->language = g_strdup (gst_tag_get_language_name (lang_code));
        g_free (lang_code);
      }
    }
  } else {
    g_free (info->language);
    info->language = NULL;
    info->max_bitrate = info->bitrate = -1;
  }

  GST_DEBUG_OBJECT (self, "language=%s rate=%d channels=%d bitrate=%d "
      "max_bitrate=%d", info->language, info->sample_rate, info->channels,
      info->bitrate, info->max_bitrate);
}

static GstPlayStreamInfo *
gst_play_stream_info_find (GstPlayMediaInfo * media_info,
    GType type, gint stream_index)
{
  GList *list, *l;
  GstPlayStreamInfo *info = NULL;

  if (!media_info)
    return NULL;

  list = gst_play_media_info_get_stream_list (media_info);
  for (l = list; l != NULL; l = l->next) {
    info = (GstPlayStreamInfo *) l->data;
    if ((G_OBJECT_TYPE (info) == type) && (info->stream_index == stream_index)) {
      return g_object_ref (info);
    }
  }

  return NULL;
}

static GstPlayStreamInfo *
gst_play_stream_info_find_from_stream_id (GstPlayMediaInfo * media_info,
    const gchar * stream_id)
{
  GList *list, *l;
  GstPlayStreamInfo *info = NULL;

  if (!media_info)
    return NULL;

  list = gst_play_media_info_get_stream_list (media_info);
  for (l = list; l != NULL; l = l->next) {
    info = (GstPlayStreamInfo *) l->data;
    if (g_str_equal (info->stream_id, stream_id)) {
      return g_object_ref (info);
    }
  }

  return NULL;
}

static gboolean
is_track_enabled (GstPlay * self, gint pos)
{
  gint flags;

  g_object_get (G_OBJECT (self->playbin), "flags", &flags, NULL);

  if ((flags & pos))
    return TRUE;

  return FALSE;
}

/* Must be called with lock */
static GstPlayStreamInfo *
gst_play_stream_info_get_current_from_stream_id (GstPlay * self,
    const gchar * stream_id, GType type)
{
  GstPlayStreamInfo *info;
  GstPlayStreamInfo *result_info = NULL;

  if (!self->media_info || !stream_id)
    return NULL;

  info = gst_play_stream_info_find_from_stream_id (self->media_info, stream_id);
  if (info) {
    if (G_OBJECT_TYPE (info) == type)
      result_info = gst_play_stream_info_copy (info);
    g_object_unref (info);
  }

  return result_info;
}

static void
stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
    GParamSpec * pspec, GstPlay * self)
{
  GstPlayStreamInfo *info;
  const gchar *stream_id;
  gboolean emit_signal = FALSE;

  if (!self->media_info)
    return;

  if (G_PARAM_SPEC_VALUE_TYPE (pspec) != GST_TYPE_CAPS &&
      G_PARAM_SPEC_VALUE_TYPE (pspec) != GST_TYPE_TAG_LIST)
    return;

  stream_id = gst_stream_get_stream_id (stream);
  g_mutex_lock (&self->lock);
  info = gst_play_stream_info_find_from_stream_id (self->media_info, stream_id);
  if (info) {
    gst_play_stream_info_update_from_stream (self, info, stream);
    g_object_unref (info);
    emit_signal = TRUE;
  }
  g_mutex_unlock (&self->lock);

  if (emit_signal)
    on_media_info_updated (self);
}

static void
gst_play_stream_info_update (GstPlay * self, GstPlayStreamInfo * s)
{
  if (GST_IS_PLAY_VIDEO_INFO (s))
    gst_play_video_info_update (self, s);
  else if (GST_IS_PLAY_AUDIO_INFO (s))
    gst_play_audio_info_update (self, s);
  else
    gst_play_subtitle_info_update (self, s);
}

static gchar *
stream_info_get_codec (GstPlayStreamInfo * s)
{
  const gchar *type;
  GstTagList *tags;
  gchar *codec = NULL;

  if (GST_IS_PLAY_VIDEO_INFO (s))
    type = GST_TAG_VIDEO_CODEC;
  else if (GST_IS_PLAY_AUDIO_INFO (s))
    type = GST_TAG_AUDIO_CODEC;
  else
    type = GST_TAG_SUBTITLE_CODEC;

  tags = gst_play_stream_info_get_tags (s);
  if (tags) {
    gst_tag_list_get_string (tags, type, &codec);
    if (!codec)
      gst_tag_list_get_string (tags, GST_TAG_CODEC, &codec);
  }

  if (!codec) {
    GstCaps *caps;
    caps = gst_play_stream_info_get_caps (s);
    if (caps && gst_caps_is_fixed (caps)) {
      codec = gst_pb_utils_get_codec_description (caps);
    }
  }

  return codec;
}

static void
gst_play_stream_info_update_from_stream (GstPlay * self,
    GstPlayStreamInfo * s, GstStream * stream)
{
  if (s->tags)
    gst_tag_list_unref (s->tags);
  s->tags = gst_stream_get_tags (stream);

  if (s->caps)
    gst_caps_unref (s->caps);
  s->caps = gst_stream_get_caps (stream);

  g_free (s->codec);
  s->codec = stream_info_get_codec (s);

  GST_DEBUG_OBJECT (self, "%s stream id: %s tags: %p caps: %p",
      gst_play_stream_info_get_stream_type (s), s->stream_id, s->tags, s->caps);

  gst_play_stream_info_update (self, s);
}

static void
gst_play_streams_info_create_from_collection (GstPlay * self,
    GstPlayMediaInfo * media_info, GstStreamCollection * collection)
{
  guint i;
  guint total;
  GstPlayStreamInfo *s;
  guint n_audio = 0;
  guint n_video = 0;
  guint n_text = 0;

  if (!media_info || !collection)
    return;

  total = gst_stream_collection_get_size (collection);

  for (i = 0; i < total; i++) {
    GstStream *stream = gst_stream_collection_get_stream (collection, i);
    GstStreamType stream_type = gst_stream_get_stream_type (stream);
    const gchar *stream_id = gst_stream_get_stream_id (stream);

    if (stream_type & GST_STREAM_TYPE_AUDIO) {
      s = gst_play_stream_info_new (n_audio, stream_id,
          GST_TYPE_PLAY_AUDIO_INFO);
      n_audio++;
    } else if (stream_type & GST_STREAM_TYPE_VIDEO) {
      s = gst_play_stream_info_new (n_video, stream_id,
          GST_TYPE_PLAY_VIDEO_INFO);
      n_video++;
    } else if (stream_type & GST_STREAM_TYPE_TEXT) {
      s = gst_play_stream_info_new (n_text, stream_id,
          GST_TYPE_PLAY_SUBTITLE_INFO);
      n_text++;
    } else {
      GST_DEBUG_OBJECT (self, "Unknown type stream %d", i);
      continue;
    }

    /* add the object in stream list */
    media_info->stream_list = g_list_append (media_info->stream_list, s);

    /* based on type, add the object in its corresponding stream_ list */
    if (GST_IS_PLAY_AUDIO_INFO (s))
      media_info->audio_stream_list = g_list_append
          (media_info->audio_stream_list, s);
    else if (GST_IS_PLAY_VIDEO_INFO (s))
      media_info->video_stream_list = g_list_append
          (media_info->video_stream_list, s);
    else
      media_info->subtitle_stream_list = g_list_append
          (media_info->subtitle_stream_list, s);

    GST_DEBUG_OBJECT (self, "create %s stream id %s",
        gst_play_stream_info_get_stream_type (s), s->stream_id);

    gst_play_stream_info_update_from_stream (self, s, stream);
  }
}

static void *
get_title (GstTagList * tags)
{
  gchar *title = NULL;

  gst_tag_list_get_string (tags, GST_TAG_TITLE, &title);
  if (!title)
    gst_tag_list_get_string (tags, GST_TAG_TITLE_SORTNAME, &title);

  return title;
}

static void *
get_container_format (GstTagList * tags)
{
  gchar *container = NULL;

  gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container);

  /* TODO: If container is not available then maybe consider
   * parsing caps or file extension to guess the container format.
   */

  return container;
}

static void *
get_from_tags (GstPlay * self, GstPlayMediaInfo * media_info,
    void *(*func) (GstTagList *))
{
  GList *l;
  void *ret = NULL;

  if (media_info->tags) {
    ret = func (media_info->tags);
    if (ret)
      return ret;
  }

  /* if global tag does not exit then try video and audio streams */
  GST_DEBUG_OBJECT (self, "trying video tags");
  for (l = gst_play_media_info_get_video_streams (media_info); l != NULL;
      l = l->next) {
    GstTagList *tags;

    tags = gst_play_stream_info_get_tags ((GstPlayStreamInfo *) l->data);
    if (tags)
      ret = func (tags);

    if (ret)
      return ret;
  }

  GST_DEBUG_OBJECT (self, "trying audio tags");
  for (l = gst_play_media_info_get_audio_streams (media_info); l != NULL;
      l = l->next) {
    GstTagList *tags;

    tags = gst_play_stream_info_get_tags ((GstPlayStreamInfo *) l->data);
    if (tags)
      ret = func (tags);

    if (ret)
      return ret;
  }

  GST_DEBUG_OBJECT (self, "failed to get the information from tags");
  return NULL;
}

static void *
get_cover_sample (GstTagList * tags)
{
  GstSample *cover_sample = NULL;

  gst_tag_list_get_sample (tags, GST_TAG_IMAGE, &cover_sample);
  if (!cover_sample)
    gst_tag_list_get_sample (tags, GST_TAG_PREVIEW_IMAGE, &cover_sample);

  return cover_sample;
}

static GstPlayMediaInfo *
gst_play_media_info_create (GstPlay * self)
{
  GstPlayMediaInfo *media_info;
  GstQuery *query;

  GST_DEBUG_OBJECT (self, "begin");
  media_info = gst_play_media_info_new (self->uri);
  media_info->duration = gst_play_get_duration (self);
  media_info->tags = self->global_tags;
  media_info->is_live = self->is_live;
  self->global_tags = NULL;

  query = gst_query_new_seeking (GST_FORMAT_TIME);
  if (gst_element_query (self->playbin, query))
    gst_query_parse_seeking (query, NULL, &media_info->seekable, NULL, NULL);
  gst_query_unref (query);

  gst_play_streams_info_create_from_collection (self, media_info,
      self->collection);

  media_info->title = get_from_tags (self, media_info, get_title);
  media_info->container =
      get_from_tags (self, media_info, get_container_format);
  media_info->image_sample = get_from_tags (self, media_info, get_cover_sample);

  GST_DEBUG_OBJECT (self, "uri: %s title: %s duration: %" GST_TIME_FORMAT
      " seekable: %s live: %s container: %s image_sample %p",
      media_info->uri, media_info->title, GST_TIME_ARGS (media_info->duration),
      media_info->seekable ? "yes" : "no", media_info->is_live ? "yes" : "no",
      media_info->container, media_info->image_sample);

  GST_DEBUG_OBJECT (self, "end");
  return media_info;
}

static void
volume_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
    GstPlay * self)
{
  api_bus_post_message (self, GST_PLAY_MESSAGE_VOLUME_CHANGED,
      GST_PLAY_MESSAGE_DATA_VOLUME, G_TYPE_DOUBLE,
      gst_play_get_volume (self), NULL);
}

static void
mute_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
    GstPlay * self)
{

  api_bus_post_message (self, GST_PLAY_MESSAGE_MUTE_CHANGED,
      GST_PLAY_MESSAGE_DATA_IS_MUTED, G_TYPE_BOOLEAN,
      gst_play_get_mute (self), NULL);
}

static void
source_setup_cb (GstElement * playbin, GstElement * source, GstPlay * self)
{
  gchar *user_agent;

  user_agent = gst_play_config_get_user_agent (self->config);
  if (user_agent) {
    GParamSpec *prop;

    prop = g_object_class_find_property (G_OBJECT_GET_CLASS (source),
        "user-agent");
    if (prop && prop->value_type == G_TYPE_STRING) {
      GST_INFO_OBJECT (self, "Setting source user-agent: %s", user_agent);
      g_object_set (source, "user-agent", user_agent, NULL);
    }

    g_free (user_agent);
  }
}

static void
about_to_finish_cb (GstElement * playbin, GstPlay * self)
{
  GstPlayLoop loop;
  gchar *uri = NULL;

  g_mutex_lock (&self->lock);
  loop = gst_play_config_get_loop (self->config);
  uri = g_strdup (self->uri);
  g_mutex_unlock (&self->lock);

  switch (loop) {
    case GST_PLAY_LOOP_NONE:
      break;
    case GST_PLAY_LOOP_TRACK:
      GST_DEBUG_OBJECT (self, "Resetting URI to '%s'", GST_STR_NULL (uri));

      g_object_set (self->playbin, "uri", uri, NULL);
      break;
  }

  g_free (uri);
}

static gpointer
gst_play_main (gpointer data)
{
  GstPlay *self = GST_PLAY (data);
  GstBus *bus;
  GSource *source;
  GstElement *scaletempo;

  GST_TRACE_OBJECT (self, "Starting main thread");

  g_main_context_push_thread_default (self->context);

  source = g_idle_source_new ();
  g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, self,
      NULL);
  g_source_attach (source, self->context);
  g_source_unref (source);

  self->playbin = gst_element_factory_make ("playbin3", "playbin3");
  if (!self->playbin) {
    g_error ("GstPlay: 'playbin3' element not found, please check your setup");
    g_assert_not_reached ();
  }

  gst_object_ref_sink (self->playbin);

  if (self->video_renderer) {
    gst_play_set_playbin_video_sink (self);
  }

  scaletempo = gst_element_factory_make ("scaletempo", NULL);
  if (scaletempo) {
    g_object_set (self->playbin, "audio-filter", scaletempo, NULL);
  } else {
    g_warning ("GstPlay: scaletempo element not available. Audio pitch "
        "will not be preserved during trick modes");
  }

  self->bus = bus = gst_element_get_bus (self->playbin);
  gst_object_set_name (GST_OBJECT (self->bus), "playbin_bus");
  gst_bus_add_signal_watch (bus);
  gst_bus_enable_sync_message_emission (bus);

  g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb),
      self);
  g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb),
      self);
  g_signal_connect (G_OBJECT (bus), "message::eos", G_CALLBACK (eos_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::state-changed",
      G_CALLBACK (state_changed_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::buffering",
      G_CALLBACK (buffering_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::clock-lost",
      G_CALLBACK (clock_lost_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::duration-changed",
      G_CALLBACK (duration_changed_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::latency",
      G_CALLBACK (latency_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::request-state",
      G_CALLBACK (request_state_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::element",
      G_CALLBACK (element_cb), self);
  g_signal_connect (G_OBJECT (bus), "message::tag", G_CALLBACK (tags_cb), self);
  g_signal_connect (G_OBJECT (bus), "sync-message::stream-collection",
      G_CALLBACK (stream_collection_cb), self);
  g_signal_connect (G_OBJECT (bus), "sync-message::streams-selected",
      G_CALLBACK (streams_selected_cb), self);
  g_signal_connect (self->playbin, "notify::volume",
      G_CALLBACK (volume_notify_cb), self);
  g_signal_connect (self->playbin, "notify::mute",
      G_CALLBACK (mute_notify_cb), self);
  g_signal_connect (self->playbin, "source-setup",
      G_CALLBACK (source_setup_cb), self);
  g_signal_connect (self->playbin, "about-to-finish",
      G_CALLBACK (about_to_finish_cb), self);

  self->target_state = GST_STATE_NULL;
  self->current_state = GST_STATE_NULL;
  change_state (self, GST_PLAY_STATE_STOPPED);
  self->buffering_percent = 100;
  self->is_eos = FALSE;
  self->is_live = FALSE;
  self->rate = 1.0;

  GST_TRACE_OBJECT (self, "Starting main loop");
  g_main_loop_run (self->loop);
  GST_TRACE_OBJECT (self, "Stopped main loop");

  gst_bus_remove_signal_watch (bus);
  gst_object_unref (bus);

  remove_tick_source (self);
  remove_ready_timeout_source (self);

  g_list_free_full (self->missing_plugin_messages,
      (GDestroyNotify) gst_message_unref);
  self->missing_plugin_messages = NULL;

  g_mutex_lock (&self->lock);
  if (self->media_info) {
    g_object_unref (self->media_info);
    self->media_info = NULL;
  }

  remove_seek_source (self);
  g_mutex_unlock (&self->lock);

  g_main_context_pop_thread_default (self->context);

  self->target_state = GST_STATE_NULL;
  self->current_state = GST_STATE_NULL;
  if (self->playbin) {
    gst_element_set_state (self->playbin, GST_STATE_NULL);
    gst_object_unref (self->playbin);
    self->playbin = NULL;
  }

  GST_TRACE_OBJECT (self, "Stopped main thread");

  return NULL;
}

static gpointer
gst_play_init_once (G_GNUC_UNUSED gpointer user_data)
{
  gst_init (NULL, NULL);

  return NULL;
}

/**
 * gst_play_new:
 * @video_renderer: (transfer full) (allow-none): GstPlayVideoRenderer to use
 *
 * Creates a new #GstPlay instance.
 *
 * Video is going to be rendered by @video_renderer, or if %NULL is provided
 * no special video set up will be done and some default handling will be
 * performed.
 *
 * This also initializes GStreamer via `gst_init()` on the first call if this
 * didn't happen before.
 *
 * Returns: (transfer full): a new #GstPlay instance
 * Since: 1.20
 */
GstPlay *
gst_play_new (GstPlayVideoRenderer * video_renderer)
{
  static GOnce once = G_ONCE_INIT;
  GstPlay *self;

  g_once (&once, gst_play_init_once, NULL);

  self = g_object_new (GST_TYPE_PLAY, "video-renderer", video_renderer, NULL);

  gst_object_ref_sink (self);

  if (video_renderer)
    g_object_unref (video_renderer);

  return self;
}

/**
 * gst_play_get_message_bus:
 * @play: #GstPlay instance
 *
 * GstPlay API exposes a #GstBus instance which purpose is to provide data
 * structures representing play-internal events in form of #GstMessage<!-- -->s of
 * type GST_MESSAGE_APPLICATION.
 *
 * Each message carries a "play-message" field of type #GstPlayMessage.
 * Further fields of the message data are specific to each possible value of
 * that enumeration.
 *
 * Applications can consume the messages asynchronously within their own
 * event-loop / UI-thread etc. Note that in case the application does not
 * consume the messages, the bus will accumulate these internally and eventually
 * fill memory. To avoid that, the bus has to be set "flushing".
 *
 * Returns: (transfer full): The play message bus instance
 *
 * Since: 1.20
 */
GstBus *
gst_play_get_message_bus (GstPlay * self)
{
  return g_object_ref (self->api_bus);
}

static gboolean
gst_play_play_internal (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstStateChangeReturn state_ret;

  GST_DEBUG_OBJECT (self, "Play");

  g_mutex_lock (&self->lock);
  if (!self->uri) {
    g_mutex_unlock (&self->lock);
    return G_SOURCE_REMOVE;
  }
  g_mutex_unlock (&self->lock);

  remove_ready_timeout_source (self);
  self->target_state = GST_STATE_PLAYING;

  if (self->current_state < GST_STATE_PAUSED)
    change_state (self, GST_PLAY_STATE_BUFFERING);

  if (self->current_state >= GST_STATE_PAUSED && !self->is_eos
      && self->buffering_percent >= 100
      && !(self->seek_position != GST_CLOCK_TIME_NONE || self->seek_pending)) {
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING);
  } else {
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED);
  }

  if (state_ret == GST_STATE_CHANGE_FAILURE) {
    on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
            "Failed to play"), NULL);
    return G_SOURCE_REMOVE;
  } else if (state_ret == GST_STATE_CHANGE_NO_PREROLL) {
    self->is_live = TRUE;
    GST_DEBUG_OBJECT (self, "Pipeline is live");
  }

  if (self->is_eos) {
    gboolean ret;

    GST_DEBUG_OBJECT (self, "Was EOS, seeking to beginning");
    self->is_eos = FALSE;
    ret =
        gst_element_seek_simple (self->playbin, GST_FORMAT_TIME,
        GST_SEEK_FLAG_FLUSH, 0);
    if (!ret) {
      GST_ERROR_OBJECT (self, "Seek to beginning failed");
      gst_play_stop_internal (self, TRUE);
      gst_play_play_internal (self);
    }
  }

  return G_SOURCE_REMOVE;
}

/**
 * gst_play_play:
 * @play: #GstPlay instance
 *
 * Request to play the loaded stream.
 * Since: 1.20
 */
void
gst_play_play (GstPlay * self)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
      gst_play_play_internal, self, NULL);
}

static gboolean
gst_play_pause_internal (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);
  GstStateChangeReturn state_ret;

  GST_DEBUG_OBJECT (self, "Pause");

  g_mutex_lock (&self->lock);
  if (!self->uri) {
    g_mutex_unlock (&self->lock);
    return G_SOURCE_REMOVE;
  }
  g_mutex_unlock (&self->lock);

  tick_cb (self);
  remove_tick_source (self);
  remove_ready_timeout_source (self);

  self->target_state = GST_STATE_PAUSED;

  if (self->current_state < GST_STATE_PAUSED)
    change_state (self, GST_PLAY_STATE_BUFFERING);

  state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED);
  if (state_ret == GST_STATE_CHANGE_FAILURE) {
    on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
            "Failed to pause"), NULL);
    return G_SOURCE_REMOVE;
  } else if (state_ret == GST_STATE_CHANGE_NO_PREROLL) {
    self->is_live = TRUE;
    GST_DEBUG_OBJECT (self, "Pipeline is live");
  }

  if (self->is_eos) {
    gboolean ret;

    GST_DEBUG_OBJECT (self, "Was EOS, seeking to beginning");
    self->is_eos = FALSE;
    ret =
        gst_element_seek_simple (self->playbin, GST_FORMAT_TIME,
        GST_SEEK_FLAG_FLUSH, 0);
    if (!ret) {
      GST_ERROR_OBJECT (self, "Seek to beginning failed");
      gst_play_stop_internal (self, TRUE);
      gst_play_pause_internal (self);
    }
  }

  return G_SOURCE_REMOVE;
}

/**
 * gst_play_pause:
 * @play: #GstPlay instance
 *
 * Pauses the current stream.
 * Since: 1.20
 */
void
gst_play_pause (GstPlay * self)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
      gst_play_pause_internal, self, NULL);
}

static void
gst_play_stop_internal (GstPlay * self, gboolean transient)
{
  /* directly return if we're already stopped */
  if (self->current_state <= GST_STATE_READY &&
      self->target_state <= GST_STATE_READY)
    return;

  GST_DEBUG_OBJECT (self, "Stop (transient %d)", transient);

  tick_cb (self);
  remove_tick_source (self);

  g_list_free_full (self->missing_plugin_messages,
      (GDestroyNotify) gst_message_unref);
  self->missing_plugin_messages = NULL;

  add_ready_timeout_source (self);

  self->target_state = GST_STATE_NULL;
  self->current_state = GST_STATE_READY;
  self->is_live = FALSE;
  self->is_eos = FALSE;
  gst_bus_set_flushing (self->bus, TRUE);
  gst_element_set_state (self->playbin, GST_STATE_READY);
  gst_bus_set_flushing (self->bus, FALSE);
  change_state (self, transient
      && self->app_state !=
      GST_PLAY_STATE_STOPPED ? GST_PLAY_STATE_BUFFERING :
      GST_PLAY_STATE_STOPPED);
  self->buffering_percent = 100;
  self->cached_duration = GST_CLOCK_TIME_NONE;
  g_mutex_lock (&self->lock);
  if (self->media_info) {
    g_object_unref (self->media_info);
    self->media_info = NULL;
  }
  if (self->global_tags) {
    gst_tag_list_unref (self->global_tags);
    self->global_tags = NULL;
  }
  self->seek_pending = FALSE;
  remove_seek_source (self);
  self->seek_position = GST_CLOCK_TIME_NONE;
  self->last_seek_time = GST_CLOCK_TIME_NONE;
  self->rate = 1.0;
  if (self->collection) {
    if (self->stream_notify_id)
      g_signal_handler_disconnect (self->collection, self->stream_notify_id);
    self->stream_notify_id = 0;
    gst_object_unref (self->collection);
    self->collection = NULL;
  }
  self->selection_seqnum = GST_SEQNUM_INVALID;
  g_list_free_full (self->current_selection, g_free);
  self->current_selection = NULL;
  g_free (self->video_sid);
  g_free (self->audio_sid);
  g_free (self->subtitle_sid);
  self->video_sid = NULL;
  self->audio_sid = NULL;
  self->subtitle_sid = NULL;
  g_mutex_unlock (&self->lock);
}

static gboolean
gst_play_stop_internal_dispatch (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  gst_play_stop_internal (self, FALSE);

  return G_SOURCE_REMOVE;
}


/**
 * gst_play_stop:
 * @play: #GstPlay instance
 *
 * Stops playing the current stream and resets to the first position
 * in the stream.
 * Since: 1.20
 */
void
gst_play_stop (GstPlay * self)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
      gst_play_stop_internal_dispatch, self, NULL);
}

/* Must be called with lock from main context, releases lock! */
static void
gst_play_seek_internal_locked (GstPlay * self)
{
  gboolean ret;
  GstClockTime position;
  gdouble rate;
  GstStateChangeReturn state_ret;
  GstEvent *s_event;
  GstSeekFlags flags = 0;
  gboolean accurate = FALSE;

  remove_seek_source (self);

  /* Only seek in PAUSED */
  if (self->current_state < GST_STATE_PAUSED) {
    return;
  } else if (self->current_state != GST_STATE_PAUSED) {
    g_mutex_unlock (&self->lock);
    state_ret = gst_element_set_state (self->playbin, GST_STATE_PAUSED);
    if (state_ret == GST_STATE_CHANGE_FAILURE) {
      on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
              "Failed to seek"), NULL);
      g_mutex_lock (&self->lock);
      return;
    }
    g_mutex_lock (&self->lock);
    return;
  }

  self->last_seek_time = gst_util_get_timestamp ();
  position = self->seek_position;
  self->seek_position = GST_CLOCK_TIME_NONE;
  self->seek_pending = TRUE;
  rate = self->rate;
  g_mutex_unlock (&self->lock);

  remove_tick_source (self);
  self->is_eos = FALSE;

  flags |= GST_SEEK_FLAG_FLUSH;

  accurate = gst_play_config_get_seek_accurate (self->config);

  if (accurate) {
    flags |= GST_SEEK_FLAG_ACCURATE;
  } else {
    flags &= ~GST_SEEK_FLAG_ACCURATE;
  }

  if (rate != 1.0) {
    flags |= GST_SEEK_FLAG_TRICKMODE;
  }

  if (rate >= 0.0) {
    s_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags,
        GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE);
  } else {
    s_event = gst_event_new_seek (rate, GST_FORMAT_TIME, flags,
        GST_SEEK_TYPE_SET, G_GINT64_CONSTANT (0), GST_SEEK_TYPE_SET, position);
  }

  GST_DEBUG_OBJECT (self, "Seek with rate %.2lf to %" GST_TIME_FORMAT,
      rate, GST_TIME_ARGS (position));

  ret = gst_element_send_event (self->playbin, s_event);
  if (!ret)
    on_error (self, g_error_new (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED,
            "Failed to seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (position)),
        NULL);

  g_mutex_lock (&self->lock);
}

static gboolean
gst_play_seek_internal (gpointer user_data)
{
  GstPlay *self = GST_PLAY (user_data);

  g_mutex_lock (&self->lock);
  gst_play_seek_internal_locked (self);
  g_mutex_unlock (&self->lock);

  return G_SOURCE_REMOVE;
}

/**
 * gst_play_set_rate:
 * @play: #GstPlay instance
 * @rate: playback rate
 *
 * Playback at specified rate
 * Since: 1.20
 */
void
gst_play_set_rate (GstPlay * self, gdouble rate)
{
  g_return_if_fail (GST_IS_PLAY (self));
  g_return_if_fail (rate != 0.0);

  g_object_set (self, "rate", rate, NULL);
}

/**
 * gst_play_get_rate:
 * @play: #GstPlay instance
 *
 * Returns: current playback rate
 * Since: 1.20
 */
gdouble
gst_play_get_rate (GstPlay * self)
{
  gdouble val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_RATE);

  g_object_get (self, "rate", &val, NULL);

  return val;
}

/**
 * gst_play_seek:
 * @play: #GstPlay instance
 * @position: position to seek in nanoseconds
 *
 * Seeks the currently-playing stream to the absolute @position time
 * in nanoseconds.
 * Since: 1.20
 */
void
gst_play_seek (GstPlay * self, GstClockTime position)
{
  g_return_if_fail (GST_IS_PLAY (self));
  g_return_if_fail (GST_CLOCK_TIME_IS_VALID (position));

  g_mutex_lock (&self->lock);
  if (self->media_info && !self->media_info->seekable) {
    GST_DEBUG_OBJECT (self, "Media is not seekable");
    g_mutex_unlock (&self->lock);
    return;
  }

  self->seek_position = position;

  /* If there is no seek being dispatch to the main context currently do that,
   * otherwise we just updated the seek position so that it will be taken by
   * the seek handler from the main context instead of the old one.
   */
  if (!self->seek_source) {
    GstClockTime now = gst_util_get_timestamp ();

    /* If no seek is pending or it was started more than 250 mseconds ago seek
     * immediately, otherwise wait until the 250 mseconds have passed */
    if (!self->seek_pending || (now - self->last_seek_time > 250 * GST_MSECOND)) {
      self->seek_source = g_idle_source_new ();
      g_source_set_callback (self->seek_source,
          (GSourceFunc) gst_play_seek_internal, self, NULL);
      GST_TRACE_OBJECT (self, "Dispatching seek to position %" GST_TIME_FORMAT,
          GST_TIME_ARGS (position));
      g_source_attach (self->seek_source, self->context);
    } else {
      guint delay = 250000 - (now - self->last_seek_time) / 1000;

      /* Note that last_seek_time must be set to something at this point and
       * it must be smaller than 250 mseconds */
      self->seek_source = g_timeout_source_new (delay);
      g_source_set_callback (self->seek_source,
          (GSourceFunc) gst_play_seek_internal, self, NULL);

      GST_TRACE_OBJECT (self,
          "Delaying seek to position %" GST_TIME_FORMAT " by %u us",
          GST_TIME_ARGS (position), delay);
      g_source_attach (self->seek_source, self->context);
    }
  }
  g_mutex_unlock (&self->lock);
}

static void
remove_seek_source (GstPlay * self)
{
  if (!self->seek_source)
    return;

  g_source_destroy (self->seek_source);
  g_source_unref (self->seek_source);
  self->seek_source = NULL;
}

/**
 * gst_play_get_uri:
 * @play: #GstPlay instance
 *
 * Gets the URI of the currently-playing stream.
 *
 * Returns: (transfer full) (nullable): a string containing the URI of the
 * currently-playing stream. g_free() after usage.
 * Since: 1.20
 */
gchar *
gst_play_get_uri (GstPlay * self)
{
  gchar *val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_URI);

  g_object_get (self, "uri", &val, NULL);

  return val;
}

/**
 * gst_play_set_uri:
 * @play: #GstPlay instance
 * @uri: (nullable): next URI to play.
 *
 * Sets the next URI to play.
 * Since: 1.20
 */
void
gst_play_set_uri (GstPlay * self, const gchar * val)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "uri", val, NULL);
}

/**
 * gst_play_set_subtitle_uri:
 * @play: #GstPlay instance
 * @uri: (nullable): subtitle URI
 *
 * Sets the external subtitle URI. This should be combined with a call to
 * gst_play_set_subtitle_track_enabled(@play, TRUE) so the subtitles are actually
 * rendered.
 * Since: 1.20
 */
void
gst_play_set_subtitle_uri (GstPlay * self, const gchar * suburi)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "suburi", suburi, NULL);
}

/**
 * gst_play_get_subtitle_uri:
 * @play: #GstPlay instance
 *
 * Current subtitle URI
 *
 * Returns: (transfer full) (nullable): URI of the current external subtitle.
 *   g_free() after usage.
 * Since: 1.20
 */
gchar *
gst_play_get_subtitle_uri (GstPlay * self)
{
  gchar *val = NULL;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_object_get (self, "suburi", &val, NULL);

  return val;
}

/**
 * gst_play_get_position:
 * @play: #GstPlay instance
 *
 * Returns: the absolute position time, in nanoseconds, of the
 * currently-playing stream.
 * Since: 1.20
 */
GstClockTime
gst_play_get_position (GstPlay * self)
{
  GstClockTime val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_POSITION);

  g_object_get (self, "position", &val, NULL);

  return val;
}

/**
 * gst_play_get_duration:
 * @play: #GstPlay instance
 *
 * Retrieves the duration of the media stream that self represents.
 *
 * Returns: the duration of the currently-playing media stream, in
 * nanoseconds.
 * Since: 1.20
 */
GstClockTime
gst_play_get_duration (GstPlay * self)
{
  GstClockTime val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_DURATION);

  g_object_get (self, "duration", &val, NULL);

  return val;
}

/**
 * gst_play_get_volume:
 * @play: #GstPlay instance
 *
 * Returns the current volume level, as a percentage between 0 and 1.
 *
 * Returns: the volume as percentage between 0 and 1.
 * Since: 1.20
 */
gdouble
gst_play_get_volume (GstPlay * self)
{
  gdouble val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_VOLUME);

  g_object_get (self, "volume", &val, NULL);

  return val;
}

/**
 * gst_play_set_volume:
 * @play: #GstPlay instance
 * @val: the new volume level, as a percentage between 0 and 1
 *
 * Sets the volume level of the stream as a percentage between 0 and 1.
 * Since: 1.20
 */
void
gst_play_set_volume (GstPlay * self, gdouble val)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "volume", val, NULL);
}

/**
 * gst_play_get_mute:
 * @play: #GstPlay instance
 *
 * Returns: %TRUE if the currently-playing stream is muted.
 * Since: 1.20
 */
gboolean
gst_play_get_mute (GstPlay * self)
{
  gboolean val;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_MUTE);

  g_object_get (self, "mute", &val, NULL);

  return val;
}

/**
 * gst_play_set_mute:
 * @play: #GstPlay instance
 * @val: Mute state the should be set
 *
 * %TRUE if the currently-playing stream should be muted.
 * Since: 1.20
 */
void
gst_play_set_mute (GstPlay * self, gboolean val)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "mute", val, NULL);
}

/**
 * gst_play_get_pipeline:
 * @play: #GstPlay instance
 *
 * Returns: (transfer full): The internal playbin instance.
 *
 * The caller should free it with g_object_unref()
 * Since: 1.20
 */
GstElement *
gst_play_get_pipeline (GstPlay * self)
{
  GstElement *val;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_object_get (self, "pipeline", &val, NULL);

  return val;
}

/**
 * gst_play_get_media_info:
 * @play: #GstPlay instance
 *
 * A Function to get the current media info #GstPlayMediaInfo instance.
 *
 * Returns: (transfer full) (nullable): media info instance.
 *
 * The caller should free it with g_object_unref()
 * Since: 1.20
 */
GstPlayMediaInfo *
gst_play_get_media_info (GstPlay * self)
{
  GstPlayMediaInfo *info;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  if (!self->media_info)
    return NULL;

  g_mutex_lock (&self->lock);
  info = gst_play_media_info_copy (self->media_info);
  g_mutex_unlock (&self->lock);

  return info;
}

/**
 * gst_play_get_current_audio_track:
 * @play: #GstPlay instance
 *
 * A Function to get current audio #GstPlayAudioInfo instance.
 *
 * Returns: (transfer full) (nullable): current audio track.
 *
 * The caller should free it with g_object_unref()
 * Since: 1.20
 */
GstPlayAudioInfo *
gst_play_get_current_audio_track (GstPlay * self)
{
  GstPlayAudioInfo *info;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_mutex_lock (&self->lock);
  if (!self->audio_enabled) {
    g_mutex_unlock (&self->lock);
    return NULL;
  }

  info = (GstPlayAudioInfo *)
      gst_play_stream_info_get_current_from_stream_id (self,
      self->audio_sid, GST_TYPE_PLAY_AUDIO_INFO);
  g_mutex_unlock (&self->lock);

  return info;
}

/**
 * gst_play_get_current_video_track:
 * @play: #GstPlay instance
 *
 * A Function to get current video #GstPlayVideoInfo instance.
 *
 * Returns: (transfer full) (nullable): current video track.
 *
 * The caller should free it with g_object_unref()
 * Since: 1.20
 */
GstPlayVideoInfo *
gst_play_get_current_video_track (GstPlay * self)
{
  GstPlayVideoInfo *info;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_mutex_lock (&self->lock);
  if (!self->video_enabled) {
    g_mutex_unlock (&self->lock);
    return NULL;
  }

  info = (GstPlayVideoInfo *)
      gst_play_stream_info_get_current_from_stream_id (self,
      self->video_sid, GST_TYPE_PLAY_VIDEO_INFO);
  g_mutex_unlock (&self->lock);

  return info;
}

/**
 * gst_play_get_current_subtitle_track:
 * @play: #GstPlay instance
 *
 * A Function to get current subtitle #GstPlaySubtitleInfo instance.
 *
 * Returns: (transfer full) (nullable): current subtitle track.
 *
 * The caller should free it with g_object_unref()
 * Since: 1.20
 */
GstPlaySubtitleInfo *
gst_play_get_current_subtitle_track (GstPlay * self)
{
  GstPlaySubtitleInfo *info;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_mutex_lock (&self->lock);
  if (!self->subtitle_enabled) {
    g_mutex_unlock (&self->lock);
    return NULL;
  }

  info = (GstPlaySubtitleInfo *)
      gst_play_stream_info_get_current_from_stream_id (self,
      self->subtitle_sid, GST_TYPE_PLAY_SUBTITLE_INFO);
  g_mutex_unlock (&self->lock);

  return info;
}

static gboolean
is_same_stream_selection (GList * a, GList * b)
{
  // We always create the list in the same order so
  // checking both lists linearly is sufficient
  while (a && b) {
    if (!g_str_equal (a->data, b->data))
      return FALSE;

    a = a->next;
    b = b->next;
  }

  // If both lists are at the end now then they were equal
  return a == b;
}

/* Must be called with lock */
static gboolean
gst_play_select_streams (GstPlay * self)
{
  GList *stream_list = NULL;
  gboolean ret = FALSE;

  if (!self->collection)
    return FALSE;

  if (self->audio_sid && self->audio_enabled) {
    GST_DEBUG_OBJECT (self, "Selecting audio track %s", self->audio_sid);
    stream_list = g_list_append (stream_list, g_strdup (self->audio_sid));
  }
  if (self->video_sid && self->video_enabled) {
    GST_DEBUG_OBJECT (self, "Selecting video track %s", self->video_sid);
    stream_list = g_list_append (stream_list, g_strdup (self->video_sid));
  }
  if (self->subtitle_sid && self->subtitle_enabled) {
    GST_DEBUG_OBJECT (self, "Selecting subtitle track %s", self->subtitle_sid);
    stream_list = g_list_append (stream_list, g_strdup (self->subtitle_sid));
  }

  if (stream_list) {
    if (is_same_stream_selection (self->current_selection, stream_list)) {
      GST_DEBUG_OBJECT (self, "Stream selection did not change");
      g_list_free_full (stream_list, g_free);
    } else {
      GstEvent *ev = gst_event_new_select_streams (stream_list);
      g_list_free_full (self->current_selection, g_free);
      self->current_selection = stream_list;
      self->selection_seqnum = gst_event_get_seqnum (ev);
      g_mutex_unlock (&self->lock);
      ret = gst_element_send_event (self->playbin, ev);
      g_mutex_lock (&self->lock);
      if (!ret) {
        GST_WARNING_OBJECT (self, "Stream selection failed");
      }
    }
  } else {
    GST_ERROR_OBJECT (self, "No available streams for select-streams");
  }

  return ret;
}

/**
 * gst_play_set_audio_track:
 * @play: #GstPlay instance
 * @stream_index: stream index
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the audio track @stream_index.
 *
 * Deprecated: 1.26: Use gst_play_set_audio_track_id() instead.
 *
 * Since: 1.20
 */
gboolean
gst_play_set_audio_track (GstPlay * self, gint stream_index)
{
  GstPlayStreamInfo *info;
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  info = gst_play_stream_info_find (self->media_info,
      GST_TYPE_PLAY_AUDIO_INFO, stream_index);
  if (!info) {
    GST_ERROR_OBJECT (self, "invalid audio stream index %d", stream_index);
    g_mutex_unlock (&self->lock);
    return FALSE;
  }

  g_free (self->audio_sid);
  self->audio_sid = g_strdup (info->stream_id);
  GST_DEBUG_OBJECT (self, "Selecting audio stream id '%s'", info->stream_id);

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
  g_object_unref (info);

  return ret;
}

/**
 * gst_play_set_video_track:
 * @play: #GstPlay instance
 * @stream_index: stream index
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the video track @stream_index.
 *
 * Deprecated: 1.26: Use gst_play_set_video_track_id() instead.
 *
 * Since: 1.20
 */
gboolean
gst_play_set_video_track (GstPlay * self, gint stream_index)
{
  GstPlayStreamInfo *info;
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  /* check if stream_index exist in our internal media_info list */
  g_mutex_lock (&self->lock);
  info = gst_play_stream_info_find (self->media_info,
      GST_TYPE_PLAY_VIDEO_INFO, stream_index);
  if (!info) {
    GST_ERROR_OBJECT (self, "invalid video stream index %d", stream_index);
    g_mutex_unlock (&self->lock);
    return FALSE;
  }

  g_free (self->video_sid);
  self->video_sid = g_strdup (info->stream_id);
  GST_DEBUG_OBJECT (self, "Selecting video stream id '%s'", info->stream_id);

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
  g_object_unref (info);

  return ret;
}

/**
 * gst_play_set_subtitle_track:
 * @play: #GstPlay instance
 * @stream_index: stream index
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the subtitle stack @stream_index.
 *
 * Deprecated: 1.26: Use gst_play_set_subtitle_track_id() instead.
 *
 * Since: 1.20
 */
gboolean
gst_play_set_subtitle_track (GstPlay * self, gint stream_index)
{
  GstPlayStreamInfo *info;
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  info = gst_play_stream_info_find (self->media_info,
      GST_TYPE_PLAY_SUBTITLE_INFO, stream_index);
  if (!info) {
    GST_ERROR_OBJECT (self, "invalid subtitle stream index %d", stream_index);
    g_mutex_unlock (&self->lock);
    return FALSE;
  }

  g_free (self->subtitle_sid);
  self->subtitle_sid = g_strdup (info->stream_id);
  GST_DEBUG_OBJECT (self, "Selecting subtitle stream id '%s'", info->stream_id);

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
  g_object_unref (info);

  return ret;
}

/**
 * gst_play_set_audio_track_id:
 * @play: #GstPlay instance
 * @stream_id: (nullable): stream id
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the audio track @stream_id.
 *
 * Since: 1.26
 */
gboolean
gst_play_set_audio_track_id (GstPlay * self, const gchar * stream_id)
{
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  if (stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid audio stream id %s", stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_AUDIO_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid audio stream id %s", stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  g_free (self->audio_sid);
  self->audio_sid = g_strdup (stream_id);
  GST_DEBUG_OBJECT (self, "Selecting audio stream id '%s'",
      GST_STR_NULL (stream_id));

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);

  return ret;
}

/**
 * gst_play_set_video_track_id:
 * @play: #GstPlay instance
 * @stream_id: (nullable): stream id
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the video track @stream_id.
 *
 * Since: 1.26
 */
gboolean
gst_play_set_video_track_id (GstPlay * self, const gchar * stream_id)
{
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  if (stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid video stream index %s", stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_VIDEO_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid video stream id %s", stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  g_free (self->video_sid);
  self->video_sid = g_strdup (stream_id);
  GST_DEBUG_OBJECT (self, "Selecting video stream id '%s'",
      GST_STR_NULL (stream_id));

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);

  return ret;
}

/**
 * gst_play_set_subtitle_track_id:
 * @play: #GstPlay instance
 * @stream_id: (nullable): stream id
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the subtitle track @stream_id.
 *
 * Since: 1.26
 */
gboolean
gst_play_set_subtitle_track_id (GstPlay * self, const gchar * stream_id)
{
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  if (stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid subtitle stream index %s", stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_SUBTITLE_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid subtile stream id %s", stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  g_free (self->subtitle_sid);
  self->subtitle_sid = g_strdup (stream_id);
  GST_DEBUG_OBJECT (self, "Selecting subtitle stream id '%s'",
      GST_STR_NULL (stream_id));

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);

  return ret;
}

/**
 * gst_play_set_track_ids:
 * @play: #GstPlay instance
 * @audio_stream_id: (nullable): audio stream id
 * @video_stream_id: (nullable): video stream id
 * @subtitle_stream_id: (nullable): subtitle stream id
 *
 * Returns: %TRUE or %FALSE
 *
 * Sets the selected track stream ids. Setting %NULL as stream id disables the
 * corresponding track.
 *
 * Since: 1.26
 */
gboolean
gst_play_set_track_ids (GstPlay * self, const gchar * audio_stream_id,
    const gchar * video_stream_id, const gchar * subtitle_stream_id)
{
  gboolean ret = TRUE;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  if (audio_stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        audio_stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid audio stream id %s", audio_stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_AUDIO_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid audio stream id %s", audio_stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  if (video_stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        video_stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid video stream index %s", video_stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_VIDEO_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid video stream id %s", video_stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  if (subtitle_stream_id) {
    GstPlayStreamInfo *info;

    info = gst_play_stream_info_find_from_stream_id (self->media_info,
        subtitle_stream_id);
    if (!info) {
      GST_ERROR_OBJECT (self, "invalid subtitle stream index %s",
          subtitle_stream_id);
      g_mutex_unlock (&self->lock);
      return FALSE;
    }

    if (!GST_IS_PLAY_SUBTITLE_INFO (info)) {
      GST_ERROR_OBJECT (self, "invalid subtile stream id %s",
          subtitle_stream_id);
      g_mutex_unlock (&self->lock);
      g_object_unref (info);
      return FALSE;
    }
    g_object_unref (info);
  }

  g_free (self->audio_sid);
  self->audio_sid = g_strdup (audio_stream_id);
  g_free (self->video_sid);
  self->video_sid = g_strdup (video_stream_id);
  g_free (self->subtitle_sid);
  self->subtitle_sid = g_strdup (subtitle_stream_id);

  GST_DEBUG_OBJECT (self, "set stream ids audio '%s' video '%s' subtitle '%s'",
      GST_STR_NULL (audio_stream_id), GST_STR_NULL (video_stream_id),
      GST_STR_NULL (subtitle_stream_id));

  ret = gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);

  return ret;
}

/**
 * gst_play_set_audio_track_enabled:
 * @play: #GstPlay instance
 * @enabled: TRUE or FALSE
 *
 * Enable or disable the current audio track.
 * Since: 1.20
 */
void
gst_play_set_audio_track_enabled (GstPlay * self, gboolean enabled)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_mutex_lock (&self->lock);
  self->audio_enabled = enabled;
  GST_DEBUG_OBJECT (self, "Audio track is %s",
      enabled ? "enabled" : "disabled");
  gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
}

/**
 * gst_play_set_video_track_enabled:
 * @play: #GstPlay instance
 * @enabled: TRUE or FALSE
 *
 * Enable or disable the current video track.
 * Since: 1.20
 */
void
gst_play_set_video_track_enabled (GstPlay * self, gboolean enabled)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_mutex_lock (&self->lock);
  self->video_enabled = enabled;
  GST_DEBUG_OBJECT (self, "Video track is %s",
      enabled ? "enabled" : "disabled");
  gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
}

/**
 * gst_play_set_subtitle_track_enabled:
 * @play: #GstPlay instance
 * @enabled: TRUE or FALSE
 *
 * Enable or disable the current subtitle track.
 * Since: 1.20
 */
void
gst_play_set_subtitle_track_enabled (GstPlay * self, gboolean enabled)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_mutex_lock (&self->lock);
  self->subtitle_enabled = enabled;
  GST_DEBUG_OBJECT (self, "Subtitle track is %s",
      enabled ? "enabled" : "disabled");
  gst_play_select_streams (self);
  g_mutex_unlock (&self->lock);
}

/**
 * gst_play_set_visualization:
 * @play: #GstPlay instance
 * @name: (nullable): visualization element obtained from
 * #gst_play_visualizations_get()
 *
 * Returns: %TRUE if the visualization was set correctly. Otherwise,
 * %FALSE.
 * Since: 1.20
 */
gboolean
gst_play_set_visualization (GstPlay * self, const gchar * name)
{
  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  g_mutex_lock (&self->lock);
  if (self->current_vis_element) {
    gst_object_unref (self->current_vis_element);
    self->current_vis_element = NULL;
  }

  if (name) {
    self->current_vis_element = gst_element_factory_make (name, NULL);
    if (!self->current_vis_element)
      goto error_no_element;
    gst_object_ref_sink (self->current_vis_element);
  }
  g_object_set (self->playbin, "vis-plugin", self->current_vis_element, NULL);

  g_mutex_unlock (&self->lock);
  GST_DEBUG_OBJECT (self, "set vis-plugin to '%s'", name);

  return TRUE;

error_no_element:
  g_mutex_unlock (&self->lock);
  GST_WARNING_OBJECT (self, "could not find visualization '%s'", name);
  return FALSE;
}

/**
 * gst_play_get_current_visualization:
 * @play: #GstPlay instance
 *
 * Returns: (transfer full) (nullable): Name of the currently enabled
 *   visualization.
 *   g_free() after usage.
 * Since: 1.20
 */
gchar *
gst_play_get_current_visualization (GstPlay * self)
{
  gchar *name = NULL;
  GstElement *vis_plugin = NULL;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  if (!is_track_enabled (self, GST_PLAY_FLAG_VIS))
    return NULL;

  g_object_get (self->playbin, "vis-plugin", &vis_plugin, NULL);

  if (vis_plugin) {
    GstElementFactory *factory = gst_element_get_factory (vis_plugin);
    if (factory)
      name = g_strdup (gst_plugin_feature_get_name (factory));
    gst_object_unref (vis_plugin);
  }

  GST_DEBUG_OBJECT (self, "vis-plugin '%s' %p", name, vis_plugin);

  return name;
}

/**
 * gst_play_set_visualization_enabled:
 * @play: #GstPlay instance
 * @enabled: TRUE or FALSE
 *
 * Enable or disable the visualization.
 * Since: 1.20
 */
void
gst_play_set_visualization_enabled (GstPlay * self, gboolean enabled)
{
  g_return_if_fail (GST_IS_PLAY (self));

  if (enabled)
    play_set_flag (self, GST_PLAY_FLAG_VIS);
  else
    play_clear_flag (self, GST_PLAY_FLAG_VIS);

  GST_DEBUG_OBJECT (self, "visualization is '%s'",
      enabled ? "Enabled" : "Disabled");
}

struct CBChannelMap
{
  const gchar *label;           /* channel label name */
  const gchar *name;            /* get_name () */
};

static const struct CBChannelMap cb_channel_map[] = {
  /* GST_PLAY_COLOR_BALANCE_BRIGHTNESS */ {"BRIGHTNESS", "brightness"},
  /* GST_PLAY_COLOR_BALANCE_CONTRAST   */ {"CONTRAST", "contrast"},
  /* GST_PLAY_COLOR_BALANCE_SATURATION */ {"SATURATION", "saturation"},
  /* GST_PLAY_COLOR_BALANCE_HUE        */ {"HUE", "hue"},
};

static GstColorBalanceChannel *
gst_play_color_balance_find_channel (GstPlay * self,
    GstPlayColorBalanceType type)
{
  GstColorBalanceChannel *channel;
  const GList *l, *channels;

  if (type < GST_PLAY_COLOR_BALANCE_BRIGHTNESS ||
      type > GST_PLAY_COLOR_BALANCE_HUE)
    return NULL;

  channels =
      gst_color_balance_list_channels (GST_COLOR_BALANCE (self->playbin));
  for (l = channels; l; l = l->next) {
    channel = l->data;
    if (g_strrstr (channel->label, cb_channel_map[type].label))
      return channel;
  }

  return NULL;
}

/**
 * gst_play_has_color_balance:
 * @play:#GstPlay instance
 *
 * Checks whether the @play has color balance support available.
 *
 * Returns: %TRUE if @play has color balance support. Otherwise,
 *   %FALSE.
 * Since: 1.20
 */
gboolean
gst_play_has_color_balance (GstPlay * self)
{
  const GList *channels;

  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);

  if (!GST_IS_COLOR_BALANCE (self->playbin))
    return FALSE;

  channels =
      gst_color_balance_list_channels (GST_COLOR_BALANCE (self->playbin));
  return (channels != NULL);
}

/**
 * gst_play_set_color_balance:
 * @play: #GstPlay instance
 * @type: #GstPlayColorBalanceType
 * @value: The new value for the @type, ranged [0,1]
 *
 * Sets the current value of the indicated channel @type to the passed
 * value.
 * Since: 1.20
 */
void
gst_play_set_color_balance (GstPlay * self, GstPlayColorBalanceType type,
    gdouble value)
{
  GstColorBalanceChannel *channel;
  gdouble new_val;

  g_return_if_fail (GST_IS_PLAY (self));
  g_return_if_fail (value >= 0.0 && value <= 1.0);

  if (!GST_IS_COLOR_BALANCE (self->playbin))
    return;

  channel = gst_play_color_balance_find_channel (self, type);
  if (!channel)
    return;

  value = CLAMP (value, 0.0, 1.0);

  /* Convert to channel range */
  new_val = channel->min_value + value * ((gdouble) channel->max_value -
      (gdouble) channel->min_value);

  gst_color_balance_set_value (GST_COLOR_BALANCE (self->playbin), channel,
      new_val);
}

/**
 * gst_play_get_color_balance:
 * @play: #GstPlay instance
 * @type: #GstPlayColorBalanceType
 *
 * Retrieve the current value of the indicated @type.
 *
 * Returns: The current value of @type, between [0,1]. In case of
 *   error -1 is returned.
 * Since: 1.20
 */
gdouble
gst_play_get_color_balance (GstPlay * self, GstPlayColorBalanceType type)
{
  GstColorBalanceChannel *channel;
  gint value;

  g_return_val_if_fail (GST_IS_PLAY (self), -1);

  if (!GST_IS_COLOR_BALANCE (self->playbin))
    return -1;

  channel = gst_play_color_balance_find_channel (self, type);
  if (!channel)
    return -1;

  value = gst_color_balance_get_value (GST_COLOR_BALANCE (self->playbin),
      channel);

  return ((gdouble) value -
      (gdouble) channel->min_value) / ((gdouble) channel->max_value -
      (gdouble) channel->min_value);
}

/**
 * gst_play_get_multiview_mode:
 * @play: #GstPlay instance
 *
 * Retrieve the current value of the indicated @type.
 *
 * Returns: The current value of @type, Default: -1 "none"
 *
 * Since: 1.20
 */
GstVideoMultiviewFramePacking
gst_play_get_multiview_mode (GstPlay * self)
{
  GstVideoMultiviewFramePacking val = GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE;

  g_return_val_if_fail (GST_IS_PLAY (self),
      GST_VIDEO_MULTIVIEW_FRAME_PACKING_NONE);

  g_object_get (self, "video-multiview-mode", &val, NULL);

  return val;
}

/**
 * gst_play_set_multiview_mode:
 * @play: #GstPlay instance
 * @mode: The new value for the @type
 *
 * Sets the current value of the indicated mode @type to the passed
 * value.
 *
 * Since: 1.20
 */
void
gst_play_set_multiview_mode (GstPlay * self, GstVideoMultiviewFramePacking mode)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "video-multiview-mode", mode, NULL);
}

/**
 * gst_play_get_multiview_flags:
 * @play: #GstPlay instance
 *
 * Retrieve the current value of the indicated @type.
 *
 * Returns: The current value of @type, Default: 0x00000000 "none
 *
 * Since: 1.20
 */
GstVideoMultiviewFlags
gst_play_get_multiview_flags (GstPlay * self)
{
  GstVideoMultiviewFlags val = GST_VIDEO_MULTIVIEW_FLAGS_NONE;

  g_return_val_if_fail (GST_IS_PLAY (self), val);

  g_object_get (self, "video-multiview-flags", &val, NULL);

  return val;
}

/**
 * gst_play_set_multiview_flags:
 * @play: #GstPlay instance
 * @flags: The new value for the @type
 *
 * Sets the current value of the indicated mode @type to the passed
 * value.
 *
 * Since: 1.20
 */
void
gst_play_set_multiview_flags (GstPlay * self, GstVideoMultiviewFlags flags)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "video-multiview-flags", flags, NULL);
}

/**
 * gst_play_get_audio_video_offset:
 * @play: #GstPlay instance
 *
 * Retrieve the current value of audio-video-offset property
 *
 * Returns: The current value of audio-video-offset in nanoseconds
 *
 * Since: 1.20
 */
gint64
gst_play_get_audio_video_offset (GstPlay * self)
{
  gint64 val = 0;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_AUDIO_VIDEO_OFFSET);

  g_object_get (self, "audio-video-offset", &val, NULL);

  return val;
}

/**
 * gst_play_set_audio_video_offset:
 * @play: #GstPlay instance
 * @offset: #gint64 in nanoseconds
 *
 * Sets audio-video-offset property by value of @offset
 *
 * Since: 1.20
 */
void
gst_play_set_audio_video_offset (GstPlay * self, gint64 offset)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "audio-video-offset", offset, NULL);
}

/**
 * gst_play_get_subtitle_video_offset:
 * @play: #GstPlay instance
 *
 * Retrieve the current value of subtitle-video-offset property
 *
 * Returns: The current value of subtitle-video-offset in nanoseconds
 *
 * Since: 1.20
 */
gint64
gst_play_get_subtitle_video_offset (GstPlay * self)
{
  gint64 val = 0;

  g_return_val_if_fail (GST_IS_PLAY (self), DEFAULT_SUBTITLE_VIDEO_OFFSET);

  g_object_get (self, "subtitle-video-offset", &val, NULL);

  return val;
}

/**
 * gst_play_set_subtitle_video_offset:
 * @play: #GstPlay instance
 * @offset: #gint64 in nanoseconds
 *
 * Sets subtitle-video-offset property by value of @offset
 *
 * Since: 1.20
 */
void
gst_play_set_subtitle_video_offset (GstPlay * self, gint64 offset)
{
  g_return_if_fail (GST_IS_PLAY (self));

  g_object_set (self, "subtitle-video-offset", offset, NULL);
}


#define C_ENUM(v) ((gint) v)
#define C_FLAGS(v) ((guint) v)

GType
gst_play_color_balance_type_get_type (void)
{
  static gsize id = 0;
  static const GEnumValue values[] = {
    {C_ENUM (GST_PLAY_COLOR_BALANCE_HUE), "GST_PLAY_COLOR_BALANCE_HUE",
        "hue"},
    {C_ENUM (GST_PLAY_COLOR_BALANCE_BRIGHTNESS),
        "GST_PLAY_COLOR_BALANCE_BRIGHTNESS", "brightness"},
    {C_ENUM (GST_PLAY_COLOR_BALANCE_SATURATION),
        "GST_PLAY_COLOR_BALANCE_SATURATION", "saturation"},
    {C_ENUM (GST_PLAY_COLOR_BALANCE_CONTRAST),
        "GST_PLAY_COLOR_BALANCE_CONTRAST", "contrast"},
    {0, NULL, NULL}
  };

  if (g_once_init_enter (&id)) {
    GType tmp = g_enum_register_static ("GstPlayColorBalanceType", values);
    g_once_init_leave (&id, tmp);
  }

  return (GType) id;
}

/**
 * gst_play_color_balance_type_get_name:
 * @type: a #GstPlayColorBalanceType
 *
 * Gets a string representing the given color balance type.
 *
 * Returns: (transfer none): a string with the name of the color
 *   balance type.
 * Since: 1.20
 */
const gchar *
gst_play_color_balance_type_get_name (GstPlayColorBalanceType type)
{
  g_return_val_if_fail (type >= GST_PLAY_COLOR_BALANCE_BRIGHTNESS &&
      type <= GST_PLAY_COLOR_BALANCE_HUE, NULL);

  return cb_channel_map[type].name;
}

GType
gst_play_state_get_type (void)
{
  static gsize id = 0;
  static const GEnumValue values[] = {
    {C_ENUM (GST_PLAY_STATE_STOPPED), "GST_PLAY_STATE_STOPPED", "stopped"},
    {C_ENUM (GST_PLAY_STATE_BUFFERING), "GST_PLAY_STATE_BUFFERING",
        "buffering"},
    {C_ENUM (GST_PLAY_STATE_PAUSED), "GST_PLAY_STATE_PAUSED", "paused"},
    {C_ENUM (GST_PLAY_STATE_PLAYING), "GST_PLAY_STATE_PLAYING", "playing"},
    {0, NULL, NULL}
  };

  if (g_once_init_enter (&id)) {
    GType tmp = g_enum_register_static ("GstPlayState", values);
    g_once_init_leave (&id, tmp);
  }

  return (GType) id;
}

GType
gst_play_message_get_type (void)
{
  static gsize id = 0;
  static const GEnumValue values[] = {
    {C_ENUM (GST_PLAY_MESSAGE_URI_LOADED), "GST_PLAY_MESSAGE_URI_LOADED",
        "uri-loaded"},
    {C_ENUM (GST_PLAY_MESSAGE_POSITION_UPDATED),
        "GST_PLAY_MESSAGE_POSITION_UPDATED", "position-updated"},
    {C_ENUM (GST_PLAY_MESSAGE_DURATION_CHANGED),
        "GST_PLAY_MESSAGE_DURATION_CHANGED", "duration-changed"},
    {C_ENUM (GST_PLAY_MESSAGE_STATE_CHANGED),
        "GST_PLAY_MESSAGE_STATE_CHANGED", "state-changed"},
    {C_ENUM (GST_PLAY_MESSAGE_BUFFERING), "GST_PLAY_MESSAGE_BUFFERING",
        "buffering"},
    {C_ENUM (GST_PLAY_MESSAGE_END_OF_STREAM),
        "GST_PLAY_MESSAGE_END_OF_STREAM", "end-of-stream"},
    {C_ENUM (GST_PLAY_MESSAGE_ERROR), "GST_PLAY_MESSAGE_ERROR", "error"},
    {C_ENUM (GST_PLAY_MESSAGE_WARNING), "GST_PLAY_MESSAGE_WARNING",
        "warning"},
    {C_ENUM (GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED),
          "GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED",
        "video-dimensions-changed"},
    {C_ENUM (GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED),
        "GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED", "media-info-updated"},
    {C_ENUM (GST_PLAY_MESSAGE_VOLUME_CHANGED),
        "GST_PLAY_MESSAGE_VOLUME_CHANGED", "volume-changed"},
    {C_ENUM (GST_PLAY_MESSAGE_MUTE_CHANGED),
        "GST_PLAY_MESSAGE_MUTE_CHANGED", "mute-changed"},
    {C_ENUM (GST_PLAY_MESSAGE_SEEK_DONE), "GST_PLAY_MESSAGE_SEEK_DONE",
        "seek-done"},
    {0, NULL, NULL}
  };

  if (g_once_init_enter (&id)) {
    GType tmp = g_enum_register_static ("GstPlayMessage", values);
    g_once_init_leave (&id, tmp);
  }

  return (GType) id;
}

GType
gst_play_loop_get_type (void)
{
  static gsize id = 0;
  static const GEnumValue values[] = {
    {C_ENUM (GST_PLAY_LOOP_NONE), "GST_PLAY_LOOP_NONE", "none"},
    {C_ENUM (GST_PLAY_LOOP_TRACK), "GST_PLAY_LOOP_TRACK", "track"},
    {0, NULL, NULL}
  };

  if (g_once_init_enter (&id)) {
    GType tmp = g_enum_register_static ("GstPlayLoop", values);
    g_once_init_leave (&id, tmp);
  }

  return (GType) id;
}

/**
 * gst_play_state_get_name:
 * @state: a #GstPlayState
 *
 * Gets a string representing the given state.
 *
 * Returns: (transfer none): a string with the name of the state.
 * Since: 1.20
 */
const gchar *
gst_play_state_get_name (GstPlayState state)
{
  switch (state) {
    case GST_PLAY_STATE_STOPPED:
      return "stopped";
    case GST_PLAY_STATE_BUFFERING:
      return "buffering";
    case GST_PLAY_STATE_PAUSED:
      return "paused";
    case GST_PLAY_STATE_PLAYING:
      return "playing";
  }

  g_assert_not_reached ();
  return NULL;
}

/**
 * gst_play_message_get_name:
 * @message_type: a #GstPlayMessage
 *
 * Returns: (transfer none): a string with the name of the message.
 * Since: 1.20
 */
const gchar *
gst_play_message_get_name (GstPlayMessage message_type)
{
  GEnumClass *enum_class;
  GEnumValue *enum_value;
  enum_class = g_type_class_ref (GST_TYPE_PLAY_MESSAGE);
  enum_value = g_enum_get_value (enum_class, message_type);
  g_assert (enum_value != NULL);
  g_type_class_unref (enum_class);
  return enum_value->value_name;
}

/**
 * gst_play_loop_get_name:
 * @loop: a #GstPlayLoop
 *
 * Returns: (transfer none): a string with the name of the loop.
 * Since: 1.28
 */
const gchar *
gst_play_loop_get_name (GstPlayLoop loop)
{
  GEnumClass *enum_class;
  GEnumValue *enum_value;
  enum_class = g_type_class_ref (GST_TYPE_PLAY_LOOP);
  enum_value = g_enum_get_value (enum_class, loop);
  g_assert (enum_value != NULL);
  g_type_class_unref (enum_class);
  return enum_value->value_name;
}

GType
gst_play_error_get_type (void)
{
  static gsize id = 0;
  static const GEnumValue values[] = {
    {C_ENUM (GST_PLAY_ERROR_FAILED), "GST_PLAY_ERROR_FAILED", "failed"},
    {0, NULL, NULL}
  };

  if (g_once_init_enter (&id)) {
    GType tmp = g_enum_register_static ("GstPlayError", values);
    g_once_init_leave (&id, tmp);
  }

  return (GType) id;
}

/**
 * gst_play_error_get_name:
 * @error: a #GstPlayError
 *
 * Gets a string representing the given error.
 *
 * Returns: (transfer none): a string with the given error.
 * Since: 1.20
 */
const gchar *
gst_play_error_get_name (GstPlayError error)
{
  switch (error) {
    case GST_PLAY_ERROR_FAILED:
      return "failed";
    case GST_PLAY_ERROR_MISSING_PLUGIN:
      return "missing-plugin";
  }

  g_assert_not_reached ();
  return NULL;
}

/**
 * gst_play_set_config:
 * @play: #GstPlay instance
 * @config: (transfer full): a #GstStructure
 *
 * Set the configuration of the play. If the play is already configured, and
 * the configuration hasn't changed, this function will return %TRUE. If the
 * play is not in the GST_PLAY_STATE_STOPPED, this method will return %FALSE
 * and active configuration will remain.
 *
 * @config is a #GstStructure that contains the configuration parameters for
 * the play.
 *
 * This function takes ownership of @config.
 *
 * Returns: %TRUE when the configuration could be set.
 * Since: 1.20
 */
gboolean
gst_play_set_config (GstPlay * self, GstStructure * config)
{
  g_return_val_if_fail (GST_IS_PLAY (self), FALSE);
  g_return_val_if_fail (config != NULL, FALSE);

  g_mutex_lock (&self->lock);

  if (self->app_state != GST_PLAY_STATE_STOPPED) {
    GST_INFO_OBJECT (self, "can't change config while play is %s",
        gst_play_state_get_name (self->app_state));
    g_mutex_unlock (&self->lock);
    return FALSE;
  }

  if (self->config)
    gst_structure_free (self->config);
  self->config = config;
  g_mutex_unlock (&self->lock);

  return TRUE;
}

/**
 * gst_play_get_config:
 * @play: #GstPlay instance
 *
 * Get a copy of the current configuration of the play. This configuration
 * can either be modified and used for the gst_play_set_config() call
 * or it must be freed after usage.
 *
 * Returns: (transfer full): a copy of the current configuration of @play. Use
 * gst_structure_free() after usage or gst_play_set_config().
 *
 * Since: 1.20
 */
GstStructure *
gst_play_get_config (GstPlay * self)
{
  GstStructure *ret;

  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  g_mutex_lock (&self->lock);
  ret = gst_structure_copy (self->config);
  g_mutex_unlock (&self->lock);

  return ret;
}

/**
 * gst_play_config_set_user_agent:
 * @config: a #GstPlay configuration
 * @agent: (nullable): the string to use as user agent
 *
 * Set the user agent to pass to the server if @play needs to connect
 * to a server during playback. This is typically used when playing HTTP
 * or RTSP streams.
 *
 * Since: 1.20
 */
void
gst_play_config_set_user_agent (GstStructure * config, const gchar * agent)
{
  g_return_if_fail (config != NULL);
  g_return_if_fail (agent != NULL);

  gst_structure_set (config, "user-agent", G_TYPE_STRING, agent, NULL);
}

/**
 * gst_play_config_get_user_agent:
 * @config: a #GstPlay configuration
 *
 * Return the user agent which has been configured using
 * gst_play_config_set_user_agent() if any.
 *
 * Returns: (transfer full) (nullable): the configured agent, or %NULL
 * Since: 1.20
 */
gchar *
gst_play_config_get_user_agent (const GstStructure * config)
{
  gchar *agent = NULL;

  g_return_val_if_fail (config != NULL, NULL);

  gst_structure_get (config, "user-agent", G_TYPE_STRING, &agent, NULL);

  return agent;
}

/**
 * gst_play_config_set_position_update_interval:
 * @config: a #GstPlay configuration
 * @interval: interval in ms
 *
 * Set desired interval in milliseconds between two position-updated messages.
 * Pass 0 to stop updating the position.
 * Since: 1.20
 */
void
gst_play_config_set_position_update_interval (GstStructure * config,
    guint interval)
{
  g_return_if_fail (config != NULL);
  g_return_if_fail (interval <= 10000);

  gst_structure_set (config,
      "position-update-interval", G_TYPE_UINT, interval, NULL);
}

/**
 * gst_play_config_get_position_update_interval:
 * @config: a #GstPlay configuration
 *
 * Returns: current position update interval in milliseconds
 *
 * Since: 1.20
 */
guint
gst_play_config_get_position_update_interval (const GstStructure * config)
{
  guint interval = DEFAULT_POSITION_UPDATE_INTERVAL_MS;

  g_return_val_if_fail (config != NULL, DEFAULT_POSITION_UPDATE_INTERVAL_MS);

  gst_structure_get (config,
      "position-update-interval", G_TYPE_UINT, &interval, NULL);

  return interval;
}

/**
 * gst_play_config_set_seek_accurate:
 * @config: a #GstPlay configuration
 * @accurate: accurate seek or not
 *
 * Enable or disable accurate seeking. When enabled, elements will try harder
 * to seek as accurately as possible to the requested seek position. Generally
 * it will be slower especially for formats that don't have any indexes or
 * timestamp markers in the stream.
 *
 * If accurate seeking is disabled, elements will seek as close as the request
 * position without slowing down seeking too much.
 *
 * Accurate seeking is disabled by default.
 *
 * Since: 1.20
 */
void
gst_play_config_set_seek_accurate (GstStructure * config, gboolean accurate)
{
  g_return_if_fail (config != NULL);

  gst_structure_set (config, "accurate-seek", G_TYPE_BOOLEAN, accurate, NULL);
}

/**
 * gst_play_config_get_seek_accurate:
 * @config: a #GstPlay configuration
 *
 * Returns: %TRUE if accurate seeking is enabled
 *
 * Since: 1.20
 */
gboolean
gst_play_config_get_seek_accurate (const GstStructure * config)
{
  gboolean accurate = FALSE;

  g_return_val_if_fail (config != NULL, FALSE);

  gst_structure_get (config, "accurate-seek", G_TYPE_BOOLEAN, &accurate, NULL);

  return accurate;
}

/**
 * gst_play_config_set_loop:
 * @config: a #GstPlay configuration
 * @loop: #GstPlayLoop
 *
 * Sets the looping mode.
 *
 * Looping is disabled by default.
 *
 * Since: 1.28
 */
void
gst_play_config_set_loop (GstStructure * config, GstPlayLoop loop)
{
  g_return_if_fail (config != NULL);

  gst_structure_set (config, "loop", GST_TYPE_PLAY_LOOP, loop, NULL);
}

/**
 * gst_play_config_get_loop:
 * @config: a #GstPlay configuration
 *
 * Returns: The looping mode.
 *
 * Since: 1.28
 */
GstPlayLoop
gst_play_config_get_loop (const GstStructure * config)
{
  GstPlayLoop loop = GST_PLAY_LOOP_NONE;

  g_return_val_if_fail (config != NULL, GST_PLAY_LOOP_NONE);

  gst_structure_get (config, "loop", GST_TYPE_PLAY_LOOP, &loop, NULL);

  return loop;
}

/**
 * gst_play_config_set_pipeline_dump_in_error_details:
 * @config: a #GstPlay configuration
 * @value: Include pipeline dumps in error details, or not.
 *
 * When enabled, the error message emitted by #GstPlay will include a pipeline
 * dump (in Graphviz DOT format) in the error details #GstStructure. The field
 * name is `pipeline-dump`.
 *
 * This option is disabled by default.
 *
 * Since: 1.24
 */
void
gst_play_config_set_pipeline_dump_in_error_details (GstStructure * config,
    gboolean value)
{
  g_return_if_fail (config != NULL);

  gst_structure_set (config, "pipeline-dump-in-error-details",
      G_TYPE_BOOLEAN, value, NULL);
}

/**
 * gst_play_config_get_pipeline_dump_in_error_details:
 * @config: a #GstPlay configuration
 *
 * Returns: %TRUE if pipeline dumps are included in #GstPlay error message
 * details.
 *
 * Since: 1.24
 */
gboolean
gst_play_config_get_pipeline_dump_in_error_details (const GstStructure * config)
{
  gboolean value = FALSE;

  g_return_val_if_fail (config != NULL, FALSE);

  gst_structure_get (config, "pipeline-dump-in-error-details",
      G_TYPE_BOOLEAN, &value, NULL);

  return value;
}

/**
 * gst_play_get_video_snapshot:
 * @play: #GstPlay instance
 * @format: output format of the video snapshot
 * @config: (allow-none): Additional configuration
 *
 * Get a snapshot of the currently selected video stream, if any. The format can be
 * selected with @format and optional configuration is possible with @config.
 * Currently supported settings are:
 * - width, height of type G_TYPE_INT
 * - pixel-aspect-ratio of type GST_TYPE_FRACTION
 *  Except for GST_PLAY_THUMBNAIL_RAW_NATIVE format, if no config is set, pixel-aspect-ratio would be 1/1
 *
 * Returns: (transfer full) (nullable):  Current video snapshot sample or %NULL on failure
 *
 * Since: 1.20
 */
GstSample *
gst_play_get_video_snapshot (GstPlay * self,
    GstPlaySnapshotFormat format, const GstStructure * config)
{
  GstPlayVideoInfo *video_info = NULL;
  GstSample *sample = NULL;
  GstCaps *caps = NULL;
  gint width = -1;
  gint height = -1;
  gint par_n = 1;
  gint par_d = 1;
  g_return_val_if_fail (GST_IS_PLAY (self), NULL);

  video_info = gst_play_get_current_video_track (self);
  if (video_info == NULL) {
    GST_DEBUG_OBJECT (self, "no current video track");
    return NULL;
  } else {
    g_object_unref (video_info);
  }

  switch (format) {
    case GST_PLAY_THUMBNAIL_RAW_xRGB:
      caps = gst_caps_new_simple ("video/x-raw",
          "format", G_TYPE_STRING, "xRGB", NULL);
      break;
    case GST_PLAY_THUMBNAIL_RAW_BGRx:
      caps = gst_caps_new_simple ("video/x-raw",
          "format", G_TYPE_STRING, "BGRx", NULL);
      break;
    case GST_PLAY_THUMBNAIL_JPG:
      caps = gst_caps_new_empty_simple ("image/jpeg");
      break;
    case GST_PLAY_THUMBNAIL_PNG:
      caps = gst_caps_new_empty_simple ("image/png");
      break;
    case GST_PLAY_THUMBNAIL_RAW_NATIVE:
    default:
      caps = gst_caps_new_empty_simple ("video/x-raw");
      break;
  }

  if (NULL != config) {
    if (!gst_structure_get_int (config, "width", &width))
      width = -1;
    if (!gst_structure_get_int (config, "height", &height))
      height = -1;
    if (!gst_structure_get_fraction (config, "pixel-aspect-ratio", &par_n,
            &par_d)) {
      if (format != GST_PLAY_THUMBNAIL_RAW_NATIVE) {
        par_n = 1;
        par_d = 1;
      } else {
        par_n = 0;
        par_d = 0;
      }
    }
  }

  if (width > 0 && height > 0) {
    gst_caps_set_simple (caps, "width", G_TYPE_INT, width,
        "height", G_TYPE_INT, height, NULL);
  }

  if (format != GST_PLAY_THUMBNAIL_RAW_NATIVE) {
    gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION,
        par_n, par_d, NULL);
  } else if (NULL != config && par_n != 0 && par_d != 0) {
    gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION,
        par_n, par_d, NULL);
  }

  g_signal_emit_by_name (self->playbin, "convert-sample", caps, &sample);
  gst_caps_unref (caps);
  if (!sample) {
    GST_WARNING_OBJECT (self, "Failed to retrieve or convert video frame");
    return NULL;
  }

  return sample;
}

/**
 * gst_play_is_play_message:
 * @msg: A #GstMessage
 *
 * Returns: A #gboolean indicating whether the passed message represents a #GstPlay message or not.
 *
 * Since: 1.20
 */
gboolean
gst_play_is_play_message (GstMessage * msg)
{
  const GstStructure *data = NULL;
  g_return_val_if_fail (GST_IS_MESSAGE (msg), FALSE);

  data = gst_message_get_structure (msg);
  g_return_val_if_fail (data, FALSE);

  return g_str_equal (gst_structure_get_name (data), GST_PLAY_MESSAGE_DATA);
}

#define PARSE_MESSAGE_FIELD(msg, expected_msg_type, field, value_type, value) G_STMT_START { \
    const GstStructure *data = NULL;                                      \
    GstPlayMessage msg_type;                                              \
    g_return_if_fail (gst_play_is_play_message (msg));                    \
    gst_play_message_parse_type (msg, &msg_type);                         \
    g_return_if_fail (msg_type == expected_msg_type);                     \
    data = gst_message_get_structure (msg);                               \
    gst_structure_get (data, field, value_type, value, NULL);             \
} G_STMT_END

/**
 * gst_play_message_parse_type:
 * @msg: A #GstMessage
 * @type: (out) (optional): the resulting message type
 *
 * Parse the given @msg and extract its #GstPlayMessage type.
 *
 * Since: 1.20
 */
void
gst_play_message_parse_type (GstMessage * msg, GstPlayMessage * type)
{
  const GstStructure *data = NULL;
  g_return_if_fail (gst_play_is_play_message (msg));
  data = gst_message_get_structure (msg);
  gst_structure_get (data, GST_PLAY_MESSAGE_DATA_TYPE, GST_TYPE_PLAY_MESSAGE,
      type, NULL);
}

/**
 * gst_play_message_get_uri:
 * @msg: A #GstMessage
 *
 * Reads the URI the play message @msg applies to.
 *
 * Returns: (transfer none): The URI this message applies to
 *
 * Since: 1.26
 */
const gchar *
gst_play_message_get_uri (GstMessage * msg)
{
  const GstStructure *details = NULL;
  const gchar *uri;
  GstPlayMessage msg_type;

  g_return_val_if_fail (gst_play_is_play_message (msg), NULL);

  gst_play_message_parse_type (msg, &msg_type);

  // ERROR/WARNING messages store the details in differently named fields for
  // backwards compatibility
  if (msg_type == GST_PLAY_MESSAGE_ERROR) {
    const GstStructure *s = gst_message_get_structure (msg);
    const GValue *v =
        gst_structure_get_value (s, GST_PLAY_MESSAGE_DATA_ERROR_DETAILS);
    details = g_value_get_boxed (v);
  } else if (msg_type == GST_PLAY_MESSAGE_WARNING) {
    const GstStructure *s = gst_message_get_structure (msg);
    const GValue *v =
        gst_structure_get_value (s, GST_PLAY_MESSAGE_DATA_WARNING_DETAILS);
    details = g_value_get_boxed (v);
  }

  if (!details)
    details = gst_message_get_details (msg);

  g_return_val_if_fail (details, NULL);
  uri = gst_structure_get_string (details, "uri");
  g_return_val_if_fail (uri, NULL);

  return uri;
}

/**
 * gst_play_message_get_stream_id:
 * @msg: A #GstMessage
 *
 * Reads the stream ID the play message @msg applies to, if any.
 *
 * Returns: (transfer none) (nullable): The stream ID this message applies to
 *
 * Since: 1.26
 */
const gchar *
gst_play_message_get_stream_id (GstMessage * msg)
{
  const GstStructure *details = NULL;
  const gchar *stream_id;
  GstPlayMessage msg_type;

  g_return_val_if_fail (gst_play_is_play_message (msg), NULL);

  gst_play_message_parse_type (msg, &msg_type);

  // ERROR/WARNING messages store the details in differently named fields for
  // backwards compatibility
  if (msg_type == GST_PLAY_MESSAGE_ERROR) {
    const GstStructure *s = gst_message_get_structure (msg);
    const GValue *v =
        gst_structure_get_value (s, GST_PLAY_MESSAGE_DATA_ERROR_DETAILS);
    details = g_value_get_boxed (v);
  } else if (msg_type == GST_PLAY_MESSAGE_WARNING) {
    const GstStructure *s = gst_message_get_structure (msg);
    const GValue *v =
        gst_structure_get_value (s, GST_PLAY_MESSAGE_DATA_WARNING_DETAILS);
    details = g_value_get_boxed (v);
  }

  if (!details)
    details = gst_message_get_details (msg);
  g_return_val_if_fail (details, NULL);
  stream_id = gst_structure_get_string (details, "stream-id");

  return stream_id;
}

/**
 * gst_play_message_parse_uri_loaded:
 * @msg: A #GstMessage
 * @uri: (out) (optional) (transfer full): the resulting URI
 *
 * Parse the given uri-loaded @msg and extract the corresponding value
 *
 * Since: 1.26
 */
void
gst_play_message_parse_uri_loaded (GstMessage * msg, gchar ** uri)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_URI_LOADED,
      GST_PLAY_MESSAGE_DATA_URI, G_TYPE_STRING, uri);
}

/**
 * gst_play_message_parse_duration_changed:
 * @msg: A #GstMessage
 * @duration: (out) (optional): the resulting duration
 *
 * Parse the given duration-changed @msg and extract the corresponding #GstClockTime
 *
 * Since: 1.26
 */
void
gst_play_message_parse_duration_changed (GstMessage * msg,
    GstClockTime * duration)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DURATION_CHANGED,
      GST_PLAY_MESSAGE_DATA_DURATION, GST_TYPE_CLOCK_TIME, duration);
}

/**
 * gst_play_message_parse_duration_updated:
 * @msg: A #GstMessage
 * @duration: (out) (optional): the resulting duration
 *
 * Parse the given duration-changed @msg and extract the corresponding #GstClockTime
 *
 * Since: 1.20
 *
 * Deprecated: 1.26: Use gst_play_message_parse_duration_changed().
 */
void
gst_play_message_parse_duration_updated (GstMessage * msg,
    GstClockTime * duration)
{
  gst_play_message_parse_duration_changed (msg, duration);
}

/**
 * gst_play_message_parse_position_updated:
 * @msg: A #GstMessage
 * @position: (out) (optional): the resulting position
 *
 * Parse the given position-updated @msg and extract the corresponding #GstClockTime
 *
 * Since: 1.20
 */
void
gst_play_message_parse_position_updated (GstMessage * msg,
    GstClockTime * position)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_POSITION_UPDATED,
      GST_PLAY_MESSAGE_DATA_POSITION, GST_TYPE_CLOCK_TIME, position);
}

/**
 * gst_play_message_parse_state_changed:
 * @msg: A #GstMessage
 * @state: (out) (optional): the resulting play state
 *
 * Parse the given state-changed @msg and extract the corresponding #GstPlayState
 *
 * Since: 1.20
 */
void
gst_play_message_parse_state_changed (GstMessage * msg, GstPlayState * state)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_STATE_CHANGED,
      GST_PLAY_MESSAGE_DATA_PLAY_STATE, GST_TYPE_PLAY_STATE, state);
}

/**
 * gst_play_message_parse_buffering:
 * @msg: A #GstMessage
 * @percent: (out) (optional): the resulting buffering percent
 *
 * Parse the given buffering @msg and extract the corresponding value
 *
 * Since: 1.26
 */
void
gst_play_message_parse_buffering (GstMessage * msg, guint * percent)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_BUFFERING,
      GST_PLAY_MESSAGE_DATA_BUFFERING_PERCENT, G_TYPE_UINT, percent);
}

/**
 * gst_play_message_parse_buffering_percent:
 * @msg: A #GstMessage
 * @percent: (out) (optional): the resulting buffering percent
 *
 * Parse the given buffering @msg and extract the corresponding value
 *
 * Since: 1.20
 *
 * Deprecated: 1.26: Use gst_play_message_parse_buffering().
 */
void
gst_play_message_parse_buffering_percent (GstMessage * msg, guint * percent)
{
  gst_play_message_parse_buffering (msg, percent);
}

/**
 * gst_play_message_parse_error:
 * @msg: A #GstMessage
 * @error: (out) (optional) (transfer full): the resulting error
 * @details: (out) (optional) (nullable) (transfer full): A #GstStructure containing additional details about the error
 *
 * Parse the given error @msg and extract the corresponding #GError.
 *
 * Since 1.26 the details will always contain the URI this refers to in an
 * "uri" field of type string, and (if known) the string "stream-id" it is
 * referring to.
 *
 * Since: 1.20
 */
void
gst_play_message_parse_error (GstMessage * msg, GError ** error,
    GstStructure ** details)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_ERROR, GST_PLAY_MESSAGE_DATA_ERROR,
      G_TYPE_ERROR, error);
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_ERROR,
      GST_PLAY_MESSAGE_DATA_ERROR_DETAILS, GST_TYPE_STRUCTURE, details);
}

static gboolean
gst_play_message_parse_missing_plugin (GstMessage * msg,
    GstPlayMessage msg_type, gchar *** descriptions,
    gchar *** installer_details)
{
  const GError *err;
  const GValue *v, *details_array;
  const GstStructure *s, *details;
  guint n_details;

  if (descriptions)
    *descriptions = NULL;
  if (installer_details)
    *installer_details = NULL;

  s = gst_message_get_structure (msg);

  v = gst_structure_get_value (s,
      msg_type ==
      GST_PLAY_MESSAGE_ERROR ? GST_PLAY_MESSAGE_DATA_ERROR :
      GST_PLAY_MESSAGE_DATA_WARNING);
  if (!v)
    return FALSE;
  err = g_value_get_boxed (v);
  if (!err)
    return FALSE;

  if (!g_error_matches (err, GST_PLAY_ERROR, GST_PLAY_ERROR_MISSING_PLUGIN))
    return FALSE;

  v = gst_structure_get_value (s,
      msg_type ==
      GST_PLAY_MESSAGE_ERROR ? GST_PLAY_MESSAGE_DATA_ERROR_DETAILS :
      GST_PLAY_MESSAGE_DATA_WARNING_DETAILS);
  if (!v)
    return FALSE;
  details = g_value_get_boxed (v);
  if (!details)
    return FALSE;

  details_array = gst_structure_get_value (details, "missing-plugin-details");

  n_details = gst_value_array_get_size (details_array);
  if (descriptions)
    *descriptions = g_new0 (gchar *, n_details + 1);
  if (installer_details)
    *installer_details = g_new0 (gchar *, n_details + 1);

  for (guint i = 0; i < n_details; i++) {
    const GValue *details_v = gst_value_array_get_value (details_array, i);
    const GstStructure *details_s = g_value_get_boxed (details_v);
    gchar *str;

    if (descriptions) {
      gst_structure_get (details_s, "description", G_TYPE_STRING, &str, NULL);
      (*descriptions)[i] = str;
    }

    if (installer_details) {
      gst_structure_get (details_s, "installer-details", G_TYPE_STRING, &str,
          NULL);
      (*installer_details)[i] = str;
    }
  }

  return TRUE;

}

/**
 * gst_play_message_parse_error_missing_plugin:
 * @msg: A #GstMessage
 * @descriptions: (out) (optional) (transfer full) (array zero-terminated=1): a %NULL-terminated array of descriptions
 * @installer_details: (out) (optional) (transfer full) (array zero-terminated=1): a %NULL-terminated array of installer details
 *
 * Parses missing plugin descriptions and installer details from a
 * GST_PLAY_ERROR_MISSING_PLUGIN error message.
 *
 * Both arrays will have the same length, and strings at the same index
 * correspond to each other.
 *
 * The installer details can be passed to gst_install_plugins_sync() or
 * gst_install_plugins_async().
 *
 * Returns: %TRUE if the message contained a missing-plugin error.
 *
 * Since: 1.26
 */
gboolean
gst_play_message_parse_error_missing_plugin (GstMessage * msg,
    gchar *** descriptions, gchar *** installer_details)
{
  GstPlayMessage msg_type;

  gst_play_message_parse_type (msg, &msg_type);
  g_return_val_if_fail (msg_type == GST_PLAY_MESSAGE_ERROR, FALSE);

  return gst_play_message_parse_missing_plugin (msg, msg_type, descriptions,
      installer_details);
}

/**
 * gst_play_message_parse_warning:
 * @msg: A #GstMessage
 * @error: (out) (optional) (transfer full): the resulting warning
 * @details: (out) (optional) (nullable) (transfer full): A #GstStructure containing additional details about the warning
 *
 * Parse the given warning @msg and extract the corresponding #GError.
 *
 * Since 1.26 the details will always contain the URI this refers to in an
 * "uri" field of type string, and (if known) the string "stream-id" it is
 * referring to.
 *
 * Since: 1.20
 */
void
gst_play_message_parse_warning (GstMessage * msg, GError ** error,
    GstStructure ** details)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_WARNING,
      GST_PLAY_MESSAGE_DATA_WARNING, G_TYPE_ERROR, error);
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_WARNING,
      GST_PLAY_MESSAGE_DATA_WARNING_DETAILS, GST_TYPE_STRUCTURE, details);
}

/**
 * gst_play_message_parse_warning_missing_plugin:
 * @msg: A #GstMessage
 * @descriptions: (out) (optional) (transfer full) (array zero-terminated=1): a %NULL-terminated array of descriptions
 * @installer_details: (out) (optional) (transfer full) (array zero-terminated=1): a %NULL-terminated array of installer details
 *
 * Parses missing plugin descriptions and installer details from a
 * GST_PLAY_ERROR_MISSING_PLUGIN warning message.
 *
 * Both arrays will have the same length, and strings at the same index
 * correspond to each other.
 *
 * The installer details can be passed to gst_install_plugins_sync() or
 * gst_install_plugins_async().
 *
 * Returns: %TRUE if the message contained a missing-plugin error.
 *
 * Since: 1.26
 */
gboolean
gst_play_message_parse_warning_missing_plugin (GstMessage * msg,
    gchar *** descriptions, gchar *** installer_details)
{
  GstPlayMessage msg_type;

  gst_play_message_parse_type (msg, &msg_type);
  g_return_val_if_fail (msg_type == GST_PLAY_MESSAGE_WARNING, FALSE);

  return gst_play_message_parse_missing_plugin (msg, msg_type, descriptions,
      installer_details);
}

/**
 * gst_play_message_parse_video_dimensions_changed:
 * @msg: A #GstMessage
 * @width: (out) (optional): the resulting video width
 * @height: (out) (optional): the resulting video height
 *
 * Parse the given video-dimensions-changed @msg and extract the corresponding video dimensions
 *
 * Since: 1.20
 */
void
gst_play_message_parse_video_dimensions_changed (GstMessage * msg,
    guint * width, guint * height)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED,
      GST_PLAY_MESSAGE_DATA_VIDEO_WIDTH, G_TYPE_UINT, width);
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_VIDEO_DIMENSIONS_CHANGED,
      GST_PLAY_MESSAGE_DATA_VIDEO_HEIGHT, G_TYPE_UINT, height);
}

/**
 * gst_play_message_parse_media_info_updated:
 * @msg: A #GstMessage
 * @info: (out) (optional) (transfer full): the resulting media info
 *
 * Parse the given media-info-updated @msg and extract the corresponding media information
 *
 * Since: 1.20
 */
void
gst_play_message_parse_media_info_updated (GstMessage * msg,
    GstPlayMediaInfo ** info)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_MEDIA_INFO_UPDATED,
      GST_PLAY_MESSAGE_DATA_MEDIA_INFO, GST_TYPE_PLAY_MEDIA_INFO, info);
}

/**
 * gst_play_message_parse_volume_changed:
 * @msg: A #GstMessage
 * @volume: (out) (optional): the resulting audio volume
 *
 * Parse the given volume-changed @msg and extract the corresponding audio volume
 *
 * Since: 1.20
 */
void
gst_play_message_parse_volume_changed (GstMessage * msg, gdouble * volume)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_VOLUME_CHANGED,
      GST_PLAY_MESSAGE_DATA_VOLUME, G_TYPE_DOUBLE, volume);
}

/**
 * gst_play_message_parse_muted_changed:
 * @msg: A #GstMessage
 * @muted: (out) (optional): the resulting audio muted state
 *
 * Parse the given mute-changed @msg and extract the corresponding audio muted state
 *
 * Since: 1.20
 */
void
gst_play_message_parse_muted_changed (GstMessage * msg, gboolean * muted)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_MUTE_CHANGED,
      GST_PLAY_MESSAGE_DATA_IS_MUTED, G_TYPE_BOOLEAN, muted);
}

/**
 * gst_play_message_parse_seek_done:
 * @msg: A #GstMessage
 * @position: (out) (optional): the resulting position
 *
 * Parse the given seek-done @msg and extract the corresponding #GstClockTime
 *
 * Since: 1.26
 */
void
gst_play_message_parse_seek_done (GstMessage * msg, GstClockTime * position)
{
  PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_SEEK_DONE,
      GST_PLAY_MESSAGE_DATA_POSITION, GST_TYPE_CLOCK_TIME, position);
}
