mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-25 01:30:38 +00:00
4934 lines
134 KiB
C
4934 lines
134 KiB
C
/* 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/video/colorbalance.h>
|
|
#include <gst/tag/tag.h>
|
|
#include <gst/pbutils/descriptions.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");
|
|
}
|
|
|
|
static GQuark QUARK_CONFIG;
|
|
|
|
/* Keep ConfigQuarkId and _config_quark_strings ordered and synced */
|
|
typedef enum
|
|
{
|
|
CONFIG_QUARK_USER_AGENT = 0,
|
|
CONFIG_QUARK_POSITION_INTERVAL_UPDATE,
|
|
CONFIG_QUARK_ACCURATE_SEEK,
|
|
CONFIG_QUARK_PIPELINE_DUMP_IN_ERROR_DETAILS,
|
|
|
|
CONFIG_QUARK_MAX
|
|
} ConfigQuarkId;
|
|
|
|
static const gchar *_config_quark_strings[] = {
|
|
"user-agent",
|
|
"position-interval-update",
|
|
"accurate-seek",
|
|
"pipeline-dump-in-error-details",
|
|
};
|
|
|
|
static GQuark _config_quark_table[CONFIG_QUARK_MAX];
|
|
|
|
#define CONFIG_QUARK(q) _config_quark_table[CONFIG_QUARK_##q]
|
|
|
|
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;
|
|
|
|
/* 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;
|
|
|
|
/* For playbin3 */
|
|
gboolean use_playbin3;
|
|
GstStreamCollection *collection;
|
|
gchar *video_sid;
|
|
gchar *audio_sid;
|
|
gchar *subtitle_sid;
|
|
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_streams_info_create (GstPlay * self,
|
|
GstPlayMediaInfo * media_info, const gchar * prop, GType type);
|
|
static void gst_play_stream_info_update (GstPlay * self, GstPlayStreamInfo * s);
|
|
static void gst_play_stream_info_update_tags_and_caps (GstPlay * self,
|
|
GstPlayStreamInfo * s);
|
|
static GstPlayStreamInfo *gst_play_stream_info_find (GstPlayMediaInfo *
|
|
media_info, GType type, gint stream_index);
|
|
static GstPlayStreamInfo *gst_play_stream_info_get_current (GstPlay *
|
|
self, const gchar * prop, GType type);
|
|
|
|
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);
|
|
|
|
/* 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_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 ();
|
|
|
|
/* *INDENT-OFF* */
|
|
self->config = gst_structure_new_id (QUARK_CONFIG,
|
|
CONFIG_QUARK (POSITION_INTERVAL_UPDATE), G_TYPE_UINT, DEFAULT_POSITION_UPDATE_INTERVAL_MS,
|
|
CONFIG_QUARK (ACCURATE_SEEK), G_TYPE_BOOLEAN, FALSE,
|
|
CONFIG_QUARK (PIPELINE_DUMP_IN_ERROR_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;
|
|
|
|
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;
|
|
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 (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);
|
|
GST_DEBUG ("Created message with payload: [ %" GST_PTR_FORMAT " ]",
|
|
message_data);
|
|
gst_bus_post (self->api_bus, msg);
|
|
}
|
|
|
|
static void
|
|
config_quark_initialize (void)
|
|
{
|
|
gint i;
|
|
|
|
QUARK_CONFIG = g_quark_from_static_string ("play-config");
|
|
|
|
if (G_N_ELEMENTS (_config_quark_strings) != CONFIG_QUARK_MAX)
|
|
g_warning ("the quark table is not consistent! %d != %d",
|
|
(int) G_N_ELEMENTS (_config_quark_strings), CONFIG_QUARK_MAX);
|
|
|
|
for (i = 0; i < CONFIG_QUARK_MAX; i++) {
|
|
_config_quark_table[i] =
|
|
g_quark_from_static_string (_config_quark_strings[i]);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
config_quark_initialize ();
|
|
}
|
|
|
|
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);
|
|
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,
|
|
¤t_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, const GstStructure * details)
|
|
{
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
GstStructure *extra_details = NULL;
|
|
gchar *dot_data = NULL;
|
|
#endif
|
|
|
|
GST_ERROR_OBJECT (self, "Error: %s (%s, %d)", err->message,
|
|
g_quark_to_string (err->domain), err->code);
|
|
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
if (details != NULL) {
|
|
extra_details = gst_structure_copy (details);
|
|
} else {
|
|
extra_details = gst_structure_new_empty ("error-details");
|
|
}
|
|
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 (extra_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,
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
extra_details
|
|
#else
|
|
details
|
|
#endif
|
|
, NULL);
|
|
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
g_free (dot_data);
|
|
gst_structure_free (extra_details);
|
|
#endif
|
|
|
|
g_error_free (err);
|
|
|
|
remove_tick_source (self);
|
|
remove_ready_timeout_source (self);
|
|
|
|
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
|
|
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;
|
|
const GstStructure *details = NULL;
|
|
|
|
dump_dot_file (self, "error");
|
|
|
|
gst_message_parse_error (msg, &err, &debug);
|
|
gst_message_parse_error_details (msg, &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);
|
|
|
|
play_err =
|
|
g_error_new_literal (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED, 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;
|
|
const GstStructure *details = NULL;
|
|
|
|
dump_dot_file (self, "warning");
|
|
|
|
gst_message_parse_warning (msg, &err, &debug);
|
|
gst_message_parse_warning_details (msg, &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);
|
|
|
|
play_err =
|
|
g_error_new_literal (GST_PLAY_ERROR, GST_PLAY_ERROR_FAILED, full_message);
|
|
|
|
GST_WARNING_OBJECT (self, "Warning: %s (%s, %d)", err->message,
|
|
g_quark_to_string (err->domain), err->code);
|
|
|
|
if (details != NULL) {
|
|
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);
|
|
} else {
|
|
api_bus_post_message (self, GST_PLAY_MESSAGE_WARNING,
|
|
GST_PLAY_MESSAGE_DATA_WARNING, G_TYPE_ERROR, play_err, NULL);
|
|
}
|
|
|
|
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_element_state_get_name (old_state),
|
|
gst_element_state_get_name (new_state),
|
|
gst_element_state_get_name (pending_state));
|
|
|
|
transition_name = g_strdup_printf ("%s_%s",
|
|
gst_element_state_get_name (old_state),
|
|
gst_element_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) {
|
|
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_element_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_element_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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
stream_collection_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
|
|
gpointer user_data)
|
|
{
|
|
GstPlay *self = GST_PLAY (user_data);
|
|
GstStreamCollection *collection = NULL;
|
|
gboolean updated = FALSE;
|
|
|
|
gst_message_parse_stream_collection (msg, &collection);
|
|
|
|
if (!collection)
|
|
return;
|
|
|
|
g_mutex_lock (&self->lock);
|
|
updated = update_stream_collection (self, collection);
|
|
gst_object_unref (collection);
|
|
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);
|
|
GstStreamCollection *collection = NULL;
|
|
gboolean updated = FALSE;
|
|
guint i, len;
|
|
|
|
gst_message_parse_streams_selected (msg, &collection);
|
|
|
|
if (!collection)
|
|
return;
|
|
|
|
g_mutex_lock (&self->lock);
|
|
updated = update_stream_collection (self, collection);
|
|
gst_object_unref (collection);
|
|
|
|
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;
|
|
|
|
len = gst_message_streams_selected_get_size (msg);
|
|
for (i = 0; i < len; i++) {
|
|
GstStream *stream;
|
|
GstStreamType stream_type;
|
|
const gchar *stream_id;
|
|
gchar **current_sid;
|
|
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)
|
|
current_sid = &self->audio_sid;
|
|
else if (stream_type & GST_STREAM_TYPE_VIDEO)
|
|
current_sid = &self->video_sid;
|
|
else if (stream_type & GST_STREAM_TYPE_TEXT)
|
|
current_sid = &self->subtitle_sid;
|
|
else {
|
|
GST_WARNING_OBJECT (self,
|
|
"Unknown stream-id %s with type 0x%x", stream_id, stream_type);
|
|
continue;
|
|
}
|
|
|
|
if (G_UNLIKELY (*current_sid)) {
|
|
GST_FIXME_OBJECT (self,
|
|
"Multiple streams are selected for type %s, choose the first one",
|
|
gst_stream_type_get_name (stream_type));
|
|
continue;
|
|
}
|
|
|
|
*current_sid = g_strdup (stream_id);
|
|
}
|
|
g_mutex_unlock (&self->lock);
|
|
|
|
if (self->media_info && updated)
|
|
on_media_info_updated (self);
|
|
}
|
|
|
|
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 GstCaps *
|
|
get_caps (GstPlay * self, gint stream_index, GType type)
|
|
{
|
|
GstPad *pad = NULL;
|
|
GstCaps *caps = NULL;
|
|
|
|
if (type == GST_TYPE_PLAY_VIDEO_INFO)
|
|
g_signal_emit_by_name (G_OBJECT (self->playbin),
|
|
"get-video-pad", stream_index, &pad);
|
|
else if (type == GST_TYPE_PLAY_AUDIO_INFO)
|
|
g_signal_emit_by_name (G_OBJECT (self->playbin),
|
|
"get-audio-pad", stream_index, &pad);
|
|
else
|
|
g_signal_emit_by_name (G_OBJECT (self->playbin),
|
|
"get-text-pad", stream_index, &pad);
|
|
|
|
if (pad) {
|
|
caps = gst_pad_get_current_caps (pad);
|
|
gst_object_unref (pad);
|
|
}
|
|
|
|
return caps;
|
|
}
|
|
|
|
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) {
|
|
gint text_index = -1;
|
|
gchar *suburi = NULL;
|
|
|
|
g_object_get (G_OBJECT (self->playbin), "current-suburi", &suburi, NULL);
|
|
if (suburi) {
|
|
if (self->use_playbin3) {
|
|
if (self->subtitle_sid &&
|
|
g_str_equal (self->subtitle_sid, stream_info->stream_id)) {
|
|
info->language = g_path_get_basename (suburi);
|
|
}
|
|
} else {
|
|
g_object_get (G_OBJECT (self->playbin), "current-text", &text_index,
|
|
NULL);
|
|
if (text_index == gst_play_stream_info_get_index (stream_info))
|
|
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 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 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;
|
|
}
|
|
|
|
static GstPlayStreamInfo *
|
|
gst_play_stream_info_get_current (GstPlay * self, const gchar * prop,
|
|
GType type)
|
|
{
|
|
gint current;
|
|
GstPlayStreamInfo *info;
|
|
|
|
if (!self->media_info)
|
|
return NULL;
|
|
|
|
g_object_get (G_OBJECT (self->playbin), prop, ¤t, NULL);
|
|
g_mutex_lock (&self->lock);
|
|
info = gst_play_stream_info_find (self->media_info, type, current);
|
|
if (info)
|
|
info = gst_play_stream_info_copy (info);
|
|
g_mutex_unlock (&self->lock);
|
|
|
|
return info;
|
|
}
|
|
|
|
static GstPlayStreamInfo *
|
|
gst_play_stream_info_get_current_from_stream_id (GstPlay * self,
|
|
const gchar * stream_id, GType type)
|
|
{
|
|
GstPlayStreamInfo *info;
|
|
|
|
if (!self->media_info || !stream_id)
|
|
return NULL;
|
|
|
|
g_mutex_lock (&self->lock);
|
|
info = gst_play_stream_info_find_from_stream_id (self->media_info, stream_id);
|
|
if (info && G_OBJECT_TYPE (info) == type)
|
|
info = gst_play_stream_info_copy (info);
|
|
else
|
|
info = NULL;
|
|
g_mutex_unlock (&self->lock);
|
|
|
|
return 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);
|
|
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) {
|
|
codec = gst_pb_utils_get_codec_description (caps);
|
|
}
|
|
}
|
|
|
|
return codec;
|
|
}
|
|
|
|
static void
|
|
gst_play_stream_info_update_tags_and_caps (GstPlay * self,
|
|
GstPlayStreamInfo * s)
|
|
{
|
|
GstTagList *tags;
|
|
gint stream_index;
|
|
|
|
stream_index = gst_play_stream_info_get_index (s);
|
|
|
|
if (GST_IS_PLAY_VIDEO_INFO (s))
|
|
g_signal_emit_by_name (self->playbin, "get-video-tags",
|
|
stream_index, &tags);
|
|
else if (GST_IS_PLAY_AUDIO_INFO (s))
|
|
g_signal_emit_by_name (self->playbin, "get-audio-tags",
|
|
stream_index, &tags);
|
|
else
|
|
g_signal_emit_by_name (self->playbin, "get-text-tags", stream_index, &tags);
|
|
|
|
if (s->tags)
|
|
gst_tag_list_unref (s->tags);
|
|
s->tags = tags;
|
|
|
|
if (s->caps)
|
|
gst_caps_unref (s->caps);
|
|
s->caps = get_caps (self, stream_index, G_OBJECT_TYPE (s));
|
|
|
|
g_free (s->codec);
|
|
s->codec = stream_info_get_codec (s);
|
|
|
|
GST_DEBUG_OBJECT (self, "%s index: %d tags: %p caps: %p",
|
|
gst_play_stream_info_get_stream_type (s), stream_index, s->tags, s->caps);
|
|
|
|
gst_play_stream_info_update (self, s);
|
|
}
|
|
|
|
static void
|
|
gst_play_streams_info_create (GstPlay * self,
|
|
GstPlayMediaInfo * media_info, const gchar * prop, GType type)
|
|
{
|
|
gint i;
|
|
gint total = -1;
|
|
GstPlayStreamInfo *s;
|
|
|
|
if (!media_info)
|
|
return;
|
|
|
|
g_object_get (G_OBJECT (self->playbin), prop, &total, NULL);
|
|
|
|
GST_DEBUG_OBJECT (self, "%s: %d", prop, total);
|
|
|
|
for (i = 0; i < total; i++) {
|
|
/* check if stream already exist in the list */
|
|
s = gst_play_stream_info_find (media_info, type, i);
|
|
|
|
if (!s) {
|
|
/* create a new stream info instance */
|
|
s = gst_play_stream_info_new (i, type);
|
|
|
|
/* 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 stream_index: %d",
|
|
gst_play_stream_info_get_stream_type (s), i);
|
|
}
|
|
|
|
gst_play_stream_info_update_tags_and_caps (self, s);
|
|
}
|
|
}
|
|
|
|
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 index: %d tags: %p caps: %p",
|
|
gst_play_stream_info_get_stream_type (s), s->stream_index,
|
|
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, GST_TYPE_PLAY_AUDIO_INFO);
|
|
n_audio++;
|
|
} else if (stream_type & GST_STREAM_TYPE_VIDEO) {
|
|
s = gst_play_stream_info_new (n_video, GST_TYPE_PLAY_VIDEO_INFO);
|
|
n_video++;
|
|
} else if (stream_type & GST_STREAM_TYPE_TEXT) {
|
|
s = gst_play_stream_info_new (n_text, GST_TYPE_PLAY_SUBTITLE_INFO);
|
|
n_text++;
|
|
} else {
|
|
GST_DEBUG_OBJECT (self, "Unknown type stream %d", i);
|
|
continue;
|
|
}
|
|
|
|
s->stream_id = g_strdup (stream_id);
|
|
|
|
/* 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 stream_index: %d",
|
|
gst_play_stream_info_get_stream_type (s), s->stream_index);
|
|
|
|
gst_play_stream_info_update_from_stream (self, s, stream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
video_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data)
|
|
{
|
|
GstPlay *self = GST_PLAY (user_data);
|
|
|
|
g_mutex_lock (&self->lock);
|
|
gst_play_streams_info_create (self, self->media_info,
|
|
"n-video", GST_TYPE_PLAY_VIDEO_INFO);
|
|
g_mutex_unlock (&self->lock);
|
|
}
|
|
|
|
static void
|
|
audio_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data)
|
|
{
|
|
GstPlay *self = GST_PLAY (user_data);
|
|
|
|
g_mutex_lock (&self->lock);
|
|
gst_play_streams_info_create (self, self->media_info,
|
|
"n-audio", GST_TYPE_PLAY_AUDIO_INFO);
|
|
g_mutex_unlock (&self->lock);
|
|
}
|
|
|
|
static void
|
|
subtitle_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data)
|
|
{
|
|
GstPlay *self = GST_PLAY (user_data);
|
|
|
|
g_mutex_lock (&self->lock);
|
|
gst_play_streams_info_create (self, self->media_info,
|
|
"n-text", GST_TYPE_PLAY_SUBTITLE_INFO);
|
|
g_mutex_unlock (&self->lock);
|
|
}
|
|
|
|
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);
|
|
|
|
if (self->use_playbin3) {
|
|
gst_play_streams_info_create_from_collection (self, media_info,
|
|
self->collection);
|
|
} else {
|
|
/* create audio/video/sub streams */
|
|
gst_play_streams_info_create (self, media_info, "n-video",
|
|
GST_TYPE_PLAY_VIDEO_INFO);
|
|
gst_play_streams_info_create (self, media_info, "n-audio",
|
|
GST_TYPE_PLAY_AUDIO_INFO);
|
|
gst_play_streams_info_create (self, media_info, "n-text",
|
|
GST_TYPE_PLAY_SUBTITLE_INFO);
|
|
}
|
|
|
|
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
|
|
tags_changed_cb (GstPlay * self, gint stream_index, GType type)
|
|
{
|
|
GstPlayStreamInfo *s;
|
|
|
|
if (!self->media_info)
|
|
return;
|
|
|
|
/* update the stream information */
|
|
g_mutex_lock (&self->lock);
|
|
s = gst_play_stream_info_find (self->media_info, type, stream_index);
|
|
gst_play_stream_info_update_tags_and_caps (self, s);
|
|
g_mutex_unlock (&self->lock);
|
|
|
|
on_media_info_updated (self);
|
|
}
|
|
|
|
static void
|
|
video_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index,
|
|
gpointer user_data)
|
|
{
|
|
tags_changed_cb (GST_PLAY (user_data), stream_index,
|
|
GST_TYPE_PLAY_VIDEO_INFO);
|
|
}
|
|
|
|
static void
|
|
audio_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index,
|
|
gpointer user_data)
|
|
{
|
|
tags_changed_cb (GST_PLAY (user_data), stream_index,
|
|
GST_TYPE_PLAY_AUDIO_INFO);
|
|
}
|
|
|
|
static void
|
|
subtitle_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index,
|
|
gpointer user_data)
|
|
{
|
|
tags_changed_cb (GST_PLAY (user_data), stream_index,
|
|
GST_TYPE_PLAY_SUBTITLE_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 gpointer
|
|
gst_play_main (gpointer data)
|
|
{
|
|
GstPlay *self = GST_PLAY (data);
|
|
GstBus *bus;
|
|
GSource *source;
|
|
GstElement *scaletempo;
|
|
const gchar *env;
|
|
|
|
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);
|
|
|
|
env = g_getenv ("GST_PLAY_USE_PLAYBIN3");
|
|
if (env && g_str_has_prefix (env, "0"))
|
|
self->use_playbin3 = FALSE;
|
|
else
|
|
self->use_playbin3 = TRUE;
|
|
|
|
if (self->use_playbin3) {
|
|
GST_DEBUG_OBJECT (self, "playbin3 enabled");
|
|
self->playbin = gst_element_factory_make ("playbin3", "playbin3");
|
|
} else {
|
|
self->playbin = gst_element_factory_make ("playbin", "playbin");
|
|
}
|
|
|
|
if (!self->playbin) {
|
|
g_error ("GstPlay: 'playbin' 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_bus_add_signal_watch (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);
|
|
|
|
if (self->use_playbin3) {
|
|
g_signal_connect (G_OBJECT (bus), "message::stream-collection",
|
|
G_CALLBACK (stream_collection_cb), self);
|
|
g_signal_connect (G_OBJECT (bus), "message::streams-selected",
|
|
G_CALLBACK (streams_selected_cb), self);
|
|
} else {
|
|
g_signal_connect (self->playbin, "video-changed",
|
|
G_CALLBACK (video_changed_cb), self);
|
|
g_signal_connect (self->playbin, "audio-changed",
|
|
G_CALLBACK (audio_changed_cb), self);
|
|
g_signal_connect (self->playbin, "text-changed",
|
|
G_CALLBACK (subtitle_changed_cb), self);
|
|
|
|
g_signal_connect (self->playbin, "video-tags-changed",
|
|
G_CALLBACK (video_tags_changed_cb), self);
|
|
g_signal_connect (self->playbin, "audio-tags-changed",
|
|
G_CALLBACK (audio_tags_changed_cb), self);
|
|
g_signal_connect (self->playbin, "text-tags-changed",
|
|
G_CALLBACK (subtitle_tags_changed_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);
|
|
|
|
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_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);
|
|
|
|
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;
|
|
}
|
|
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);
|
|
|
|
if (!is_track_enabled (self, GST_PLAY_FLAG_AUDIO))
|
|
return NULL;
|
|
|
|
if (self->use_playbin3) {
|
|
info = (GstPlayAudioInfo *)
|
|
gst_play_stream_info_get_current_from_stream_id (self,
|
|
self->audio_sid, GST_TYPE_PLAY_AUDIO_INFO);
|
|
} else {
|
|
info = (GstPlayAudioInfo *) gst_play_stream_info_get_current (self,
|
|
"current-audio", GST_TYPE_PLAY_AUDIO_INFO);
|
|
}
|
|
|
|
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);
|
|
|
|
if (!is_track_enabled (self, GST_PLAY_FLAG_VIDEO))
|
|
return NULL;
|
|
|
|
if (self->use_playbin3) {
|
|
info = (GstPlayVideoInfo *)
|
|
gst_play_stream_info_get_current_from_stream_id (self,
|
|
self->video_sid, GST_TYPE_PLAY_VIDEO_INFO);
|
|
} else {
|
|
info = (GstPlayVideoInfo *) gst_play_stream_info_get_current (self,
|
|
"current-video", GST_TYPE_PLAY_VIDEO_INFO);
|
|
}
|
|
|
|
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);
|
|
|
|
if (!is_track_enabled (self, GST_PLAY_FLAG_SUBTITLE))
|
|
return NULL;
|
|
|
|
if (self->use_playbin3) {
|
|
info = (GstPlaySubtitleInfo *)
|
|
gst_play_stream_info_get_current_from_stream_id (self,
|
|
self->subtitle_sid, GST_TYPE_PLAY_SUBTITLE_INFO);
|
|
} else {
|
|
info = (GstPlaySubtitleInfo *) gst_play_stream_info_get_current (self,
|
|
"current-text", GST_TYPE_PLAY_SUBTITLE_INFO);
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
/* Must be called with lock */
|
|
static gboolean
|
|
gst_play_select_streams (GstPlay * self)
|
|
{
|
|
GList *stream_list = NULL;
|
|
gboolean ret = FALSE;
|
|
|
|
if (self->audio_sid)
|
|
stream_list = g_list_append (stream_list, g_strdup (self->audio_sid));
|
|
if (self->video_sid)
|
|
stream_list = g_list_append (stream_list, g_strdup (self->video_sid));
|
|
if (self->subtitle_sid)
|
|
stream_list = g_list_append (stream_list, g_strdup (self->subtitle_sid));
|
|
|
|
g_mutex_unlock (&self->lock);
|
|
if (stream_list) {
|
|
ret = gst_element_send_event (self->playbin,
|
|
gst_event_new_select_streams (stream_list));
|
|
g_list_free_full (stream_list, g_free);
|
|
} else {
|
|
GST_ERROR_OBJECT (self, "No available streams for select-streams");
|
|
}
|
|
g_mutex_lock (&self->lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* gst_play_set_audio_track:
|
|
* @play: #GstPlay instance
|
|
* @stream_index: stream index
|
|
*
|
|
* Returns: %TRUE or %FALSE
|
|
*
|
|
* Sets the audio track @stream_index.
|
|
* 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), 0);
|
|
|
|
g_mutex_lock (&self->lock);
|
|
info = gst_play_stream_info_find (self->media_info,
|
|
GST_TYPE_PLAY_AUDIO_INFO, stream_index);
|
|
g_mutex_unlock (&self->lock);
|
|
if (!info) {
|
|
GST_ERROR_OBJECT (self, "invalid audio stream index %d", stream_index);
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->use_playbin3) {
|
|
g_mutex_lock (&self->lock);
|
|
g_free (self->audio_sid);
|
|
self->audio_sid = g_strdup (info->stream_id);
|
|
ret = gst_play_select_streams (self);
|
|
g_mutex_unlock (&self->lock);
|
|
} else {
|
|
g_object_set (G_OBJECT (self->playbin), "current-audio", stream_index,
|
|
NULL);
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* gst_play_set_video_track:
|
|
* @play: #GstPlay instance
|
|
* @stream_index: stream index
|
|
*
|
|
* Returns: %TRUE or %FALSE
|
|
*
|
|
* Sets the video track @stream_index.
|
|
* 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), 0);
|
|
|
|
/* 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);
|
|
g_mutex_unlock (&self->lock);
|
|
if (!info) {
|
|
GST_ERROR_OBJECT (self, "invalid video stream index %d", stream_index);
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->use_playbin3) {
|
|
g_mutex_lock (&self->lock);
|
|
g_free (self->video_sid);
|
|
self->video_sid = g_strdup (info->stream_id);
|
|
ret = gst_play_select_streams (self);
|
|
g_mutex_unlock (&self->lock);
|
|
} else {
|
|
g_object_set (G_OBJECT (self->playbin), "current-video", stream_index,
|
|
NULL);
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* gst_play_set_subtitle_track:
|
|
* @play: #GstPlay instance
|
|
* @stream_index: stream index
|
|
*
|
|
* Returns: %TRUE or %FALSE
|
|
*
|
|
* Sets the subtitle stack @stream_index.
|
|
* 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), 0);
|
|
|
|
g_mutex_lock (&self->lock);
|
|
info = gst_play_stream_info_find (self->media_info,
|
|
GST_TYPE_PLAY_SUBTITLE_INFO, stream_index);
|
|
g_mutex_unlock (&self->lock);
|
|
if (!info) {
|
|
GST_ERROR_OBJECT (self, "invalid subtitle stream index %d", stream_index);
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->use_playbin3) {
|
|
g_mutex_lock (&self->lock);
|
|
g_free (self->subtitle_sid);
|
|
self->subtitle_sid = g_strdup (info->stream_id);
|
|
ret = gst_play_select_streams (self);
|
|
g_mutex_unlock (&self->lock);
|
|
} else {
|
|
g_object_set (G_OBJECT (self->playbin), "current-text", stream_index, NULL);
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (self, "set stream index '%d'", stream_index);
|
|
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));
|
|
|
|
if (enabled)
|
|
play_set_flag (self, GST_PLAY_FLAG_AUDIO);
|
|
else
|
|
play_clear_flag (self, GST_PLAY_FLAG_AUDIO);
|
|
|
|
GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled");
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
|
|
if (enabled)
|
|
play_set_flag (self, GST_PLAY_FLAG_VIDEO);
|
|
else
|
|
play_clear_flag (self, GST_PLAY_FLAG_VIDEO);
|
|
|
|
GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled");
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
|
|
if (enabled)
|
|
play_set_flag (self, GST_PLAY_FLAG_SUBTITLE);
|
|
else
|
|
play_clear_flag (self, GST_PLAY_FLAG_SUBTITLE);
|
|
|
|
GST_DEBUG_OBJECT (self, "track is '%s'", enabled ? "Enabled" : "Disabled");
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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_id_set (config,
|
|
CONFIG_QUARK (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_id_get (config,
|
|
CONFIG_QUARK (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_id_set (config,
|
|
CONFIG_QUARK (POSITION_INTERVAL_UPDATE), 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_id_get (config,
|
|
CONFIG_QUARK (POSITION_INTERVAL_UPDATE), 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_id_set (config,
|
|
CONFIG_QUARK (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_id_get (config,
|
|
CONFIG_QUARK (ACCURATE_SEEK), G_TYPE_BOOLEAN, &accurate, NULL);
|
|
|
|
return accurate;
|
|
}
|
|
|
|
/**
|
|
* 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_id_set (config, CONFIG_QUARK (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_id_get (config, CONFIG_QUARK (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)
|
|
{
|
|
gint video_tracks = 0;
|
|
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);
|
|
|
|
if (self->use_playbin3) {
|
|
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);
|
|
}
|
|
} else {
|
|
g_object_get (self->playbin, "n-video", &video_tracks, NULL);
|
|
if (video_tracks == 0) {
|
|
GST_DEBUG_OBJECT (self, "total video track num is 0");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
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, field, value_type, value) G_STMT_START { \
|
|
const GstStructure *data = NULL; \
|
|
g_return_if_fail (gst_play_is_play_message (msg)); \
|
|
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)
|
|
{
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_TYPE,
|
|
GST_TYPE_PLAY_MESSAGE, type);
|
|
}
|
|
|
|
/**
|
|
* gst_play_message_parse_duration_updated:
|
|
* @msg: A #GstMessage
|
|
* @duration: (out) (optional): the resulting duration
|
|
*
|
|
* Parse the given duration @msg and extract the corresponding #GstClockTime
|
|
*
|
|
* Since: 1.20
|
|
*/
|
|
void
|
|
gst_play_message_parse_duration_updated (GstMessage * msg,
|
|
GstClockTime * duration)
|
|
{
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_DURATION,
|
|
GST_TYPE_CLOCK_TIME, duration);
|
|
}
|
|
|
|
/**
|
|
* gst_play_message_parse_position_updated:
|
|
* @msg: A #GstMessage
|
|
* @position: (out) (optional): the resulting position
|
|
*
|
|
* Parse the given position @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_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 @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_DATA_PLAY_STATE,
|
|
GST_TYPE_PLAY_STATE, state);
|
|
}
|
|
|
|
/**
|
|
* gst_play_message_parse_buffering_percent:
|
|
* @msg: A #GstMessage
|
|
* @percent: (out) (optional): the resulting buffering percent
|
|
*
|
|
* Parse the given buffering-percent @msg and extract the corresponding value
|
|
*
|
|
* Since: 1.20
|
|
*/
|
|
void
|
|
gst_play_message_parse_buffering_percent (GstMessage * msg, guint * percent)
|
|
{
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_BUFFERING_PERCENT,
|
|
G_TYPE_UINT, 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.20
|
|
*/
|
|
void
|
|
gst_play_message_parse_error (GstMessage * msg, GError ** error,
|
|
GstStructure ** details)
|
|
{
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_ERROR, G_TYPE_ERROR, error);
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_ERROR_DETAILS,
|
|
GST_TYPE_STRUCTURE, 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 error @msg and extract the corresponding #GError warning
|
|
*
|
|
* Since: 1.20
|
|
*/
|
|
void
|
|
gst_play_message_parse_warning (GstMessage * msg, GError ** error,
|
|
GstStructure ** details)
|
|
{
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_WARNING, G_TYPE_ERROR, error);
|
|
PARSE_MESSAGE_FIELD (msg, GST_PLAY_MESSAGE_DATA_WARNING, GST_TYPE_STRUCTURE,
|
|
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 @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_DATA_VIDEO_WIDTH,
|
|
G_TYPE_UINT, width);
|
|
PARSE_MESSAGE_FIELD (msg, 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 @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_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 @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_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 @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_DATA_IS_MUTED, G_TYPE_BOOLEAN,
|
|
muted);
|
|
}
|