mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-27 18:50:48 +00:00
playsink: Add audio and video converter convenience bins
These reconfigure based on the caps and plugin in converters if necessary. This also makes switching between compressed and raw streams work flawlessly without loosing the states of any element somewhere or having running time problems.
This commit is contained in:
parent
105da803ad
commit
2f8467d682
6 changed files with 1246 additions and 113 deletions
|
@ -18,6 +18,8 @@ libgstplaybin_la_SOURCES = \
|
|||
gststreaminfo.c \
|
||||
gststreamselector.c \
|
||||
gstsubtitleoverlay.c \
|
||||
gstplaysinkvideoconvert.c \
|
||||
gstplaysinkaudioconvert.c \
|
||||
gststreamsynchronizer.c
|
||||
|
||||
nodist_libgstplaybin_la_SOURCES = $(built_sources)
|
||||
|
@ -57,6 +59,8 @@ noinst_HEADERS = \
|
|||
gststreamselector.h \
|
||||
gstrawcaps.h \
|
||||
gstsubtitleoverlay.h \
|
||||
gstplaysinkvideoconvert.h \
|
||||
gstplaysinkaudioconvert.h \
|
||||
gststreamsynchronizer.h
|
||||
|
||||
BUILT_SOURCES = $(built_headers) $(built_sources)
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
|
||||
#include "gstplaysink.h"
|
||||
#include "gststreamsynchronizer.h"
|
||||
#include "gstplaysinkvideoconvert.h"
|
||||
#include "gstplaysinkaudioconvert.h"
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (gst_play_sink_debug);
|
||||
#define GST_CAT_DEFAULT gst_play_sink_debug
|
||||
|
@ -59,7 +61,6 @@ typedef struct
|
|||
GstPad *sinkpad;
|
||||
GstElement *queue;
|
||||
GstElement *conv;
|
||||
GstElement *resample;
|
||||
GstElement *volume; /* element with the volume property */
|
||||
gboolean sink_volume; /* if the volume was provided by the sink */
|
||||
GstElement *mute; /* element with the mute property */
|
||||
|
@ -81,7 +82,6 @@ typedef struct
|
|||
GstPad *sinkpad;
|
||||
GstElement *queue;
|
||||
GstElement *conv;
|
||||
GstElement *scale;
|
||||
GstElement *sink;
|
||||
gboolean async;
|
||||
GstElement *ts_offset;
|
||||
|
@ -1278,46 +1278,19 @@ gen_video_chain (GstPlaySink * playsink, gboolean raw, gboolean async)
|
|||
head = prev = chain->queue;
|
||||
}
|
||||
|
||||
if (raw && !(playsink->flags & GST_PLAY_FLAG_NATIVE_VIDEO)) {
|
||||
GST_DEBUG_OBJECT (playsink, "creating ffmpegcolorspace");
|
||||
chain->conv = gst_element_factory_make ("ffmpegcolorspace", "vconv");
|
||||
if (chain->conv == NULL) {
|
||||
post_missing_element_message (playsink, "ffmpegcolorspace");
|
||||
GST_ELEMENT_WARNING (playsink, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"ffmpegcolorspace"), ("video rendering might fail"));
|
||||
if (!(playsink->flags & GST_PLAY_FLAG_NATIVE_VIDEO)) {
|
||||
GST_DEBUG_OBJECT (playsink, "creating videoconverter");
|
||||
chain->conv =
|
||||
g_object_new (GST_TYPE_PLAY_SINK_VIDEO_CONVERT, "name", "vconv", NULL);
|
||||
gst_bin_add (bin, chain->conv);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->conv, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
gst_bin_add (bin, chain->conv);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->conv, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = chain->conv;
|
||||
}
|
||||
prev = chain->conv;
|
||||
}
|
||||
|
||||
GST_DEBUG_OBJECT (playsink, "creating videoscale");
|
||||
chain->scale = gst_element_factory_make ("videoscale", "vscale");
|
||||
if (chain->scale == NULL) {
|
||||
post_missing_element_message (playsink, "videoscale");
|
||||
GST_ELEMENT_WARNING (playsink, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"videoscale"), ("possibly a liboil version mismatch?"));
|
||||
} else {
|
||||
/* Add black borders if necessary to keep the DAR */
|
||||
g_object_set (chain->scale, "add-borders", TRUE, NULL);
|
||||
gst_bin_add (bin, chain->scale);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->scale, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = chain->scale;
|
||||
}
|
||||
prev = chain->scale;
|
||||
head = chain->conv;
|
||||
}
|
||||
prev = chain->conv;
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
|
@ -1388,8 +1361,7 @@ setup_video_chain (GstPlaySink * playsink, gboolean raw, gboolean async)
|
|||
|
||||
chain = playsink->videochain;
|
||||
|
||||
if (chain->chain.raw != raw)
|
||||
return FALSE;
|
||||
chain->chain.raw = raw;
|
||||
|
||||
/* if the chain was active we don't do anything */
|
||||
if (GST_PLAY_CHAIN (chain)->activated == TRUE)
|
||||
|
@ -1768,54 +1740,32 @@ gen_audio_chain (GstPlaySink * playsink, gboolean raw)
|
|||
chain->sink_volume = FALSE;
|
||||
}
|
||||
|
||||
if (raw && !(playsink->flags & GST_PLAY_FLAG_NATIVE_AUDIO)) {
|
||||
if (!(playsink->flags & GST_PLAY_FLAG_NATIVE_AUDIO) || (!have_volume
|
||||
&& playsink->flags & GST_PLAY_FLAG_SOFT_VOLUME)) {
|
||||
GST_DEBUG_OBJECT (playsink, "creating audioconvert");
|
||||
chain->conv = gst_element_factory_make ("audioconvert", "aconv");
|
||||
if (chain->conv == NULL) {
|
||||
post_missing_element_message (playsink, "audioconvert");
|
||||
GST_ELEMENT_WARNING (playsink, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"audioconvert"), ("possibly a liboil version mismatch?"));
|
||||
chain->conv =
|
||||
g_object_new (GST_TYPE_PLAY_SINK_AUDIO_CONVERT, "name", "aconv", NULL);
|
||||
gst_bin_add (bin, chain->conv);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->conv, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
gst_bin_add (bin, chain->conv);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->conv, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = chain->conv;
|
||||
}
|
||||
prev = chain->conv;
|
||||
head = chain->conv;
|
||||
}
|
||||
prev = chain->conv;
|
||||
|
||||
GST_DEBUG_OBJECT (playsink, "creating audioresample");
|
||||
chain->resample = gst_element_factory_make ("audioresample", "aresample");
|
||||
if (chain->resample == NULL) {
|
||||
post_missing_element_message (playsink, "audioresample");
|
||||
GST_ELEMENT_WARNING (playsink, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"audioresample"), ("possibly a liboil version mismatch?"));
|
||||
} else {
|
||||
gst_bin_add (bin, chain->resample);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->resample, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = chain->resample;
|
||||
}
|
||||
prev = chain->resample;
|
||||
}
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_CAST (chain->conv)->use_converters =
|
||||
!(playsink->flags & GST_PLAY_FLAG_NATIVE_AUDIO);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_CAST (chain->conv)->use_volume = (!have_volume
|
||||
&& playsink->flags & GST_PLAY_FLAG_SOFT_VOLUME);
|
||||
|
||||
if (!have_volume && playsink->flags & GST_PLAY_FLAG_SOFT_VOLUME) {
|
||||
GST_DEBUG_OBJECT (playsink, "creating volume");
|
||||
chain->volume = gst_element_factory_make ("volume", "volume");
|
||||
if (chain->volume == NULL) {
|
||||
post_missing_element_message (playsink, "volume");
|
||||
GST_ELEMENT_WARNING (playsink, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"volume"), ("possibly a liboil version mismatch?"));
|
||||
} else {
|
||||
GstPlaySinkAudioConvert *conv =
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_CAST (chain->conv);
|
||||
|
||||
if (conv->volume) {
|
||||
chain->volume = conv->volume;
|
||||
have_volume = TRUE;
|
||||
|
||||
g_signal_connect (chain->volume, "notify::volume",
|
||||
|
@ -1830,16 +1780,6 @@ gen_audio_chain (GstPlaySink * playsink, gboolean raw)
|
|||
g_object_set (G_OBJECT (chain->volume), "volume", playsink->volume,
|
||||
NULL);
|
||||
g_object_set (G_OBJECT (chain->mute), "mute", playsink->mute, NULL);
|
||||
gst_bin_add (bin, chain->volume);
|
||||
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", chain->volume, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = chain->volume;
|
||||
}
|
||||
prev = chain->volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1921,8 +1861,7 @@ setup_audio_chain (GstPlaySink * playsink, gboolean raw)
|
|||
|
||||
chain = playsink->audiochain;
|
||||
|
||||
if (chain->chain.raw != raw)
|
||||
return FALSE;
|
||||
chain->chain.raw = raw;
|
||||
|
||||
/* if the chain was active we don't do anything */
|
||||
if (GST_PLAY_CHAIN (chain)->activated == TRUE)
|
||||
|
@ -1967,29 +1906,35 @@ setup_audio_chain (GstPlaySink * playsink, gboolean raw)
|
|||
g_signal_connect (chain->mute, "notify::mute",
|
||||
G_CALLBACK (notify_mute_cb), playsink);
|
||||
}
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_CAST (chain->conv)->use_volume = FALSE;
|
||||
} else {
|
||||
GstPlaySinkAudioConvert *conv =
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_CAST (chain->conv);
|
||||
|
||||
/* no volume, we need to add a volume element when we can */
|
||||
conv->use_volume = TRUE;
|
||||
GST_DEBUG_OBJECT (playsink, "the sink has no volume property");
|
||||
if (!raw) {
|
||||
GST_LOG_OBJECT (playsink, "non-raw format, can't do soft volume control");
|
||||
|
||||
disconnect_chain (chain, playsink);
|
||||
chain->volume = NULL;
|
||||
chain->mute = NULL;
|
||||
} else {
|
||||
/* both last and current chain are raw audio, there should be a volume
|
||||
* element already, unless the sink changed from one with a volume
|
||||
* property to one that hasn't got a volume property, in which case we
|
||||
* re-generate the chain */
|
||||
if (chain->volume == NULL) {
|
||||
GST_DEBUG_OBJECT (playsink, "no existing volume element to re-use");
|
||||
/* undo background state change done earlier */
|
||||
gst_element_set_state (chain->sink, GST_STATE_NULL);
|
||||
return FALSE;
|
||||
}
|
||||
/* Disconnect signals */
|
||||
disconnect_chain (chain, playsink);
|
||||
|
||||
GST_DEBUG_OBJECT (playsink, "reusing existing volume element");
|
||||
if (conv->volume) {
|
||||
chain->volume = conv->volume;
|
||||
chain->mute = chain->volume;
|
||||
|
||||
g_signal_connect (chain->volume, "notify::volume",
|
||||
G_CALLBACK (notify_volume_cb), playsink);
|
||||
|
||||
g_signal_connect (chain->mute, "notify::mute",
|
||||
G_CALLBACK (notify_mute_cb), playsink);
|
||||
|
||||
/* configure with the latest volume and mute */
|
||||
g_object_set (G_OBJECT (chain->volume), "volume", playsink->volume, NULL);
|
||||
g_object_set (G_OBJECT (chain->mute), "mute", playsink->mute, NULL);
|
||||
}
|
||||
|
||||
GST_DEBUG_OBJECT (playsink, "reusing existing volume element");
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
|
522
gst/playback/gstplaysinkaudioconvert.c
Normal file
522
gst/playback/gstplaysinkaudioconvert.c
Normal file
|
@ -0,0 +1,522 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) <2011> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "gstplaysinkaudioconvert.h"
|
||||
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
#include <gst/gst-i18n-plugin.h>
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (gst_play_sink_audio_convert_debug);
|
||||
#define GST_CAT_DEFAULT gst_play_sink_audio_convert_debug
|
||||
|
||||
#define parent_class gst_play_sink_audio_convert_parent_class
|
||||
|
||||
G_DEFINE_TYPE (GstPlaySinkAudioConvert, gst_play_sink_audio_convert,
|
||||
GST_TYPE_BIN);
|
||||
|
||||
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
|
||||
GST_PAD_SRC,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS_ANY);
|
||||
|
||||
static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
|
||||
GST_PAD_SINK,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS_ANY);
|
||||
|
||||
static gboolean
|
||||
is_raw_caps (GstCaps * caps)
|
||||
{
|
||||
gint i, n;
|
||||
GstStructure *s;
|
||||
const gchar *name;
|
||||
|
||||
n = gst_caps_get_size (caps);
|
||||
for (i = 0; i < n; i++) {
|
||||
s = gst_caps_get_structure (caps, i);
|
||||
name = gst_structure_get_name (s);
|
||||
if (!g_str_has_prefix (name, "audio/x-raw"))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
post_missing_element_message (GstPlaySinkAudioConvert * self,
|
||||
const gchar * name)
|
||||
{
|
||||
GstMessage *msg;
|
||||
|
||||
msg = gst_missing_element_message_new (GST_ELEMENT_CAST (self), name);
|
||||
gst_element_post_message (GST_ELEMENT_CAST (self), msg);
|
||||
}
|
||||
|
||||
static void
|
||||
distribute_running_time (GstElement * element, const GstSegment * segment)
|
||||
{
|
||||
GstEvent *event;
|
||||
GstPad *pad;
|
||||
|
||||
pad = gst_element_get_static_pad (element, "sink");
|
||||
|
||||
if (segment->accum) {
|
||||
event = gst_event_new_new_segment_full (FALSE, segment->rate,
|
||||
segment->applied_rate, segment->format, 0, segment->accum, 0);
|
||||
gst_pad_push_event (pad, event);
|
||||
}
|
||||
|
||||
event = gst_event_new_new_segment_full (FALSE, segment->rate,
|
||||
segment->applied_rate, segment->format,
|
||||
segment->start, segment->stop, segment->time);
|
||||
gst_pad_push_event (pad, event);
|
||||
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
static void
|
||||
pad_blocked_cb (GstPad * pad, gboolean blocked, GstPlaySinkAudioConvert * self)
|
||||
{
|
||||
GstPad *peer;
|
||||
GstCaps *caps;
|
||||
gboolean raw;
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
self->sink_proxypad_blocked = blocked;
|
||||
GST_DEBUG_OBJECT (self, "Pad blocked: %d", blocked);
|
||||
if (!blocked)
|
||||
goto done;
|
||||
|
||||
/* There must be a peer at this point */
|
||||
peer = gst_pad_get_peer (self->sinkpad);
|
||||
caps = gst_pad_get_negotiated_caps (peer);
|
||||
if (!caps)
|
||||
caps = gst_pad_get_caps_reffed (peer);
|
||||
gst_object_unref (peer);
|
||||
|
||||
raw = is_raw_caps (caps);
|
||||
GST_DEBUG_OBJECT (self, "Caps %" GST_PTR_FORMAT " are raw: %d", caps, raw);
|
||||
gst_caps_unref (caps);
|
||||
|
||||
if (raw == self->raw)
|
||||
goto unblock;
|
||||
self->raw = raw;
|
||||
|
||||
if (raw) {
|
||||
GstBin *bin = GST_BIN_CAST (self);
|
||||
GstElement *head = NULL, *prev = NULL;
|
||||
GstPad *pad;
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Creating raw conversion pipeline");
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
|
||||
if (self->use_converters) {
|
||||
self->conv = gst_element_factory_make ("audioconvert", "conv");
|
||||
if (self->conv == NULL) {
|
||||
post_missing_element_message (self, "audioconvert");
|
||||
GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"audioconvert"), ("audio rendering might fail"));
|
||||
} else {
|
||||
gst_bin_add (bin, self->conv);
|
||||
gst_element_sync_state_with_parent (self->conv);
|
||||
distribute_running_time (self->conv, &self->segment);
|
||||
prev = head = self->conv;
|
||||
}
|
||||
|
||||
self->resample = gst_element_factory_make ("audioresample", "resample");
|
||||
if (self->resample == NULL) {
|
||||
post_missing_element_message (self, "audioresample");
|
||||
GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"audioresample"), ("possibly a liboil version mismatch?"));
|
||||
} else {
|
||||
gst_bin_add (bin, self->resample);
|
||||
gst_element_sync_state_with_parent (self->resample);
|
||||
distribute_running_time (self->resample, &self->segment);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", self->resample, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = self->resample;
|
||||
}
|
||||
prev = self->resample;
|
||||
}
|
||||
}
|
||||
|
||||
if (self->use_volume && self->volume) {
|
||||
gst_bin_add (bin, gst_object_ref (self->volume));
|
||||
gst_element_sync_state_with_parent (self->volume);
|
||||
distribute_running_time (self->volume, &self->segment);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", self->volume, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = self->volume;
|
||||
}
|
||||
prev = self->volume;
|
||||
}
|
||||
|
||||
if (head) {
|
||||
pad = gst_element_get_static_pad (head, "sink");
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), pad);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
pad = gst_element_get_static_pad (prev, "src");
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), pad);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
if (!head && !prev) {
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
}
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Raw conversion pipeline created");
|
||||
} else {
|
||||
GstBin *bin = GST_BIN_CAST (self);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Removing raw conversion pipeline");
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
|
||||
if (self->conv) {
|
||||
gst_element_set_state (self->conv, GST_STATE_NULL);
|
||||
gst_bin_remove (bin, self->conv);
|
||||
self->conv = NULL;
|
||||
}
|
||||
if (self->resample) {
|
||||
gst_element_set_state (self->resample, GST_STATE_NULL);
|
||||
gst_bin_remove (bin, self->resample);
|
||||
self->resample = NULL;
|
||||
}
|
||||
if (self->volume) {
|
||||
gst_element_set_state (self->volume, GST_STATE_NULL);
|
||||
if (GST_OBJECT_PARENT (self->volume) == GST_OBJECT_CAST (self)) {
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->volume);
|
||||
}
|
||||
}
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Raw conversion pipeline removed");
|
||||
}
|
||||
|
||||
unblock:
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
|
||||
done:
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
return;
|
||||
|
||||
link_failed:
|
||||
{
|
||||
GST_ELEMENT_ERROR (self, CORE, PAD,
|
||||
(NULL), ("Failed to configure the audio converter."));
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_play_sink_audio_convert_sink_event (GstPad * pad, GstEvent * event)
|
||||
{
|
||||
GstPlaySinkAudioConvert *self =
|
||||
GST_PLAY_SINK_AUDIO_CONVERT (gst_pad_get_parent (pad));
|
||||
gboolean ret;
|
||||
|
||||
ret = self->sink_event (pad, gst_event_ref (event));
|
||||
|
||||
if (GST_EVENT_TYPE (event) == GST_EVENT_NEWSEGMENT) {
|
||||
gboolean update;
|
||||
gdouble rate, applied_rate;
|
||||
GstFormat format;
|
||||
gint64 start, stop, position;
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
gst_event_parse_new_segment_full (event, &update, &rate, &applied_rate,
|
||||
&format, &start, &stop, &position);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Segment before %" GST_SEGMENT_FORMAT,
|
||||
&self->segment);
|
||||
gst_segment_set_newsegment_full (&self->segment, update, rate, applied_rate,
|
||||
format, start, stop, position);
|
||||
GST_DEBUG_OBJECT (self, "Segment after %" GST_SEGMENT_FORMAT,
|
||||
&self->segment);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
} else if (GST_EVENT_TYPE (event) == GST_EVENT_FLUSH_STOP) {
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
GST_DEBUG_OBJECT (self, "Resetting segment");
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
}
|
||||
|
||||
gst_event_unref (event);
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_play_sink_audio_convert_sink_setcaps (GstPad * pad, GstCaps * caps)
|
||||
{
|
||||
GstPlaySinkAudioConvert *self =
|
||||
GST_PLAY_SINK_AUDIO_CONVERT (gst_pad_get_parent (pad));
|
||||
gboolean ret;
|
||||
GstStructure *s;
|
||||
const gchar *name;
|
||||
gboolean reconfigure = FALSE;
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
s = gst_caps_get_structure (caps, 0);
|
||||
name = gst_structure_get_name (s);
|
||||
|
||||
if (g_str_has_prefix (name, "audio/x-raw-")) {
|
||||
if (!self->raw && !gst_pad_is_blocked (self->sink_proxypad)) {
|
||||
GST_DEBUG_OBJECT (self, "Changing caps from non-raw to raw");
|
||||
reconfigure = TRUE;
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
}
|
||||
} else {
|
||||
if (self->raw && !gst_pad_is_blocked (self->sink_proxypad)) {
|
||||
GST_DEBUG_OBJECT (self, "Changing caps from raw to non-raw");
|
||||
reconfigure = TRUE;
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise the setcaps below fails */
|
||||
if (reconfigure) {
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
}
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
ret = self->sink_setcaps (pad, caps);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Setting sink caps %" GST_PTR_FORMAT ": %d", caps,
|
||||
ret);
|
||||
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static GstCaps *
|
||||
gst_play_sink_audio_convert_getcaps (GstPad * pad)
|
||||
{
|
||||
GstPlaySinkAudioConvert *self =
|
||||
GST_PLAY_SINK_AUDIO_CONVERT (gst_pad_get_parent (pad));
|
||||
GstCaps *ret;
|
||||
GstPad *otherpad, *peer;
|
||||
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
if (pad == self->srcpad)
|
||||
otherpad = gst_object_ref (self->sinkpad);
|
||||
else
|
||||
otherpad = gst_object_ref (self->srcpad);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
|
||||
peer = gst_pad_get_peer (otherpad);
|
||||
if (peer) {
|
||||
ret = gst_pad_get_caps_reffed (peer);
|
||||
gst_object_unref (peer);
|
||||
} else {
|
||||
ret = gst_caps_new_any ();
|
||||
}
|
||||
|
||||
gst_object_unref (otherpad);
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_audio_convert_finalize (GObject * object)
|
||||
{
|
||||
GstPlaySinkAudioConvert *self = GST_PLAY_SINK_AUDIO_CONVERT_CAST (object);
|
||||
|
||||
if (self->volume)
|
||||
gst_object_unref (self->volume);
|
||||
|
||||
gst_object_unref (self->sink_proxypad);
|
||||
g_mutex_free (self->lock);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_play_sink_audio_convert_change_state (GstElement * element,
|
||||
GstStateChange transition)
|
||||
{
|
||||
GstStateChangeReturn ret;
|
||||
GstPlaySinkAudioConvert *self = GST_PLAY_SINK_AUDIO_CONVERT_CAST (element);
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
if (gst_pad_is_blocked (self->sink_proxypad))
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
return ret;
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
if (self->conv) {
|
||||
gst_element_set_state (self->conv, GST_STATE_NULL);
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->conv);
|
||||
self->conv = NULL;
|
||||
}
|
||||
if (self->resample) {
|
||||
gst_element_set_state (self->resample, GST_STATE_NULL);
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->resample);
|
||||
self->resample = NULL;
|
||||
}
|
||||
if (self->volume) {
|
||||
gst_element_set_state (self->volume, GST_STATE_NULL);
|
||||
if (GST_OBJECT_PARENT (self->volume) == GST_OBJECT_CAST (self)) {
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->volume);
|
||||
}
|
||||
}
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
self->raw = FALSE;
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
break;
|
||||
case GST_STATE_CHANGE_READY_TO_PAUSED:
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_LOCK (self);
|
||||
if (!gst_pad_is_blocked (self->sink_proxypad))
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK (self);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_audio_convert_class_init (GstPlaySinkAudioConvertClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class;
|
||||
GstElementClass *gstelement_class;
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT (gst_play_sink_audio_convert_debug,
|
||||
"playsinkaudioconvert", 0, "play bin");
|
||||
|
||||
gobject_class = (GObjectClass *) klass;
|
||||
gstelement_class = (GstElementClass *) klass;
|
||||
|
||||
gobject_class->finalize = gst_play_sink_audio_convert_finalize;
|
||||
|
||||
gst_element_class_add_pad_template (gstelement_class,
|
||||
gst_static_pad_template_get (&srctemplate));
|
||||
gst_element_class_add_pad_template (gstelement_class,
|
||||
gst_static_pad_template_get (&sinktemplate));
|
||||
gst_element_class_set_details_simple (gstelement_class,
|
||||
"Player Sink Audio Converter", "Audio/Bin/Converter",
|
||||
"Convenience bin for audio conversion",
|
||||
"Sebastian Dröge <sebastian.droege@collabora.co.uk>");
|
||||
|
||||
gstelement_class->change_state =
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_audio_convert_change_state);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_audio_convert_init (GstPlaySinkAudioConvert * self)
|
||||
{
|
||||
GstPadTemplate *templ;
|
||||
GstIterator *it;
|
||||
|
||||
self->lock = g_mutex_new ();
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
|
||||
templ = gst_static_pad_template_get (&sinktemplate);
|
||||
self->sinkpad = gst_ghost_pad_new_no_target_from_template ("sink", templ);
|
||||
self->sink_event = GST_PAD_EVENTFUNC (self->sinkpad);
|
||||
gst_pad_set_event_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_audio_convert_sink_event));
|
||||
self->sink_setcaps = GST_PAD_SETCAPSFUNC (self->sinkpad);
|
||||
gst_pad_set_setcaps_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_audio_convert_sink_setcaps));
|
||||
gst_pad_set_getcaps_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_audio_convert_getcaps));
|
||||
|
||||
it = gst_pad_iterate_internal_links (self->sinkpad);
|
||||
g_assert (it);
|
||||
gst_iterator_next (it, (gpointer *) & self->sink_proxypad);
|
||||
g_assert (self->sink_proxypad);
|
||||
gst_iterator_free (it);
|
||||
|
||||
gst_element_add_pad (GST_ELEMENT_CAST (self), self->sinkpad);
|
||||
gst_object_unref (templ);
|
||||
|
||||
templ = gst_static_pad_template_get (&srctemplate);
|
||||
self->srcpad = gst_ghost_pad_new_no_target_from_template ("src", templ);
|
||||
gst_pad_set_getcaps_function (self->srcpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_audio_convert_getcaps));
|
||||
gst_element_add_pad (GST_ELEMENT_CAST (self), self->srcpad);
|
||||
gst_object_unref (templ);
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
|
||||
/* FIXME: Only create this on demand but for now we need
|
||||
* it to always exist because of playsink's volume proxying
|
||||
* logic.
|
||||
*/
|
||||
self->volume = gst_element_factory_make ("volume", "volume");
|
||||
if (self->volume)
|
||||
gst_object_ref_sink (self->volume);
|
||||
}
|
91
gst/playback/gstplaysinkaudioconvert.h
Normal file
91
gst/playback/gstplaysinkaudioconvert.h
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) <2011> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#ifndef __GST_PLAY_SINK_AUDIO_CONVERT_H__
|
||||
#define __GST_PLAY_SINK_AUDIO_CONVERT_H__
|
||||
|
||||
G_BEGIN_DECLS
|
||||
#define GST_TYPE_PLAY_SINK_AUDIO_CONVERT \
|
||||
(gst_play_sink_audio_convert_get_type())
|
||||
#define GST_PLAY_SINK_AUDIO_CONVERT(obj) \
|
||||
(G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_PLAY_SINK_AUDIO_CONVERT, GstPlaySinkAudioConvert))
|
||||
#define GST_PLAY_SINK_AUDIO_CONVERT_CAST(obj) \
|
||||
((GstPlaySinkAudioConvert *) obj)
|
||||
#define GST_PLAY_SINK_AUDIO_CONVERT_CLASS(klass) \
|
||||
(G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_PLAY_SINK_AUDIO_CONVERT, GstPlaySinkAudioConvertClass))
|
||||
#define GST_IS_PLAY_SINK_AUDIO_CONVERT(obj) \
|
||||
(G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_PLAY_SINK_AUDIO_CONVERT))
|
||||
#define GST_IS_PLAY_SINK_AUDIO_CONVERT_CLASS(klass) \
|
||||
(G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_PLAY_SINK_AUDIO_CONVERT))
|
||||
|
||||
#define GST_PLAY_SINK_AUDIO_CONVERT_LOCK(obj) G_STMT_START { \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"locking from thread %p", \
|
||||
g_thread_self ()); \
|
||||
g_mutex_lock (GST_PLAY_SINK_AUDIO_CONVERT_CAST(obj)->lock); \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"locked from thread %p", \
|
||||
g_thread_self ()); \
|
||||
} G_STMT_END
|
||||
|
||||
#define GST_PLAY_SINK_AUDIO_CONVERT_UNLOCK(obj) G_STMT_START { \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"unlocking from thread %p", \
|
||||
g_thread_self ()); \
|
||||
g_mutex_unlock (GST_PLAY_SINK_AUDIO_CONVERT_CAST(obj)->lock); \
|
||||
} G_STMT_END
|
||||
|
||||
typedef struct _GstPlaySinkAudioConvert GstPlaySinkAudioConvert;
|
||||
typedef struct _GstPlaySinkAudioConvertClass GstPlaySinkAudioConvertClass;
|
||||
|
||||
struct _GstPlaySinkAudioConvert
|
||||
{
|
||||
GstBin parent;
|
||||
|
||||
/* < private > */
|
||||
GMutex *lock;
|
||||
|
||||
GstPad *sinkpad, *sink_proxypad;
|
||||
GstPadEventFunction sink_event;
|
||||
GstPadSetCapsFunction sink_setcaps;
|
||||
gboolean sink_proxypad_blocked;
|
||||
GstSegment segment;
|
||||
|
||||
GstPad *srcpad;
|
||||
|
||||
gboolean raw;
|
||||
GstElement *conv, *resample;
|
||||
|
||||
/* < pseudo public > */
|
||||
GstElement *volume;
|
||||
gboolean use_volume;
|
||||
gboolean use_converters;
|
||||
};
|
||||
|
||||
struct _GstPlaySinkAudioConvertClass
|
||||
{
|
||||
GstBinClass parent;
|
||||
};
|
||||
|
||||
GType gst_play_sink_audio_convert_get_type (void);
|
||||
|
||||
G_END_DECLS
|
||||
#endif /* __GST_PLAY_SINK_AUDIO_CONVERT_H__ */
|
485
gst/playback/gstplaysinkvideoconvert.c
Normal file
485
gst/playback/gstplaysinkvideoconvert.c
Normal file
|
@ -0,0 +1,485 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) <2011> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "gstplaysinkvideoconvert.h"
|
||||
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
#include <gst/gst-i18n-plugin.h>
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (gst_play_sink_video_convert_debug);
|
||||
#define GST_CAT_DEFAULT gst_play_sink_video_convert_debug
|
||||
|
||||
#define parent_class gst_play_sink_video_convert_parent_class
|
||||
|
||||
G_DEFINE_TYPE (GstPlaySinkVideoConvert, gst_play_sink_video_convert,
|
||||
GST_TYPE_BIN);
|
||||
|
||||
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
|
||||
GST_PAD_SRC,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS_ANY);
|
||||
|
||||
static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
|
||||
GST_PAD_SINK,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS_ANY);
|
||||
|
||||
static gboolean
|
||||
is_raw_caps (GstCaps * caps)
|
||||
{
|
||||
gint i, n;
|
||||
GstStructure *s;
|
||||
const gchar *name;
|
||||
|
||||
n = gst_caps_get_size (caps);
|
||||
for (i = 0; i < n; i++) {
|
||||
s = gst_caps_get_structure (caps, i);
|
||||
name = gst_structure_get_name (s);
|
||||
if (!g_str_has_prefix (name, "video/x-raw"))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
post_missing_element_message (GstPlaySinkVideoConvert * self,
|
||||
const gchar * name)
|
||||
{
|
||||
GstMessage *msg;
|
||||
|
||||
msg = gst_missing_element_message_new (GST_ELEMENT_CAST (self), name);
|
||||
gst_element_post_message (GST_ELEMENT_CAST (self), msg);
|
||||
}
|
||||
|
||||
static void
|
||||
distribute_running_time (GstElement * element, const GstSegment * segment)
|
||||
{
|
||||
GstEvent *event;
|
||||
GstPad *pad;
|
||||
|
||||
pad = gst_element_get_static_pad (element, "sink");
|
||||
|
||||
if (segment->accum) {
|
||||
event = gst_event_new_new_segment_full (FALSE, segment->rate,
|
||||
segment->applied_rate, segment->format, 0, segment->accum, 0);
|
||||
gst_pad_push_event (pad, event);
|
||||
}
|
||||
|
||||
event = gst_event_new_new_segment_full (FALSE, segment->rate,
|
||||
segment->applied_rate, segment->format,
|
||||
segment->start, segment->stop, segment->time);
|
||||
gst_pad_push_event (pad, event);
|
||||
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
static void
|
||||
pad_blocked_cb (GstPad * pad, gboolean blocked, GstPlaySinkVideoConvert * self)
|
||||
{
|
||||
GstPad *peer;
|
||||
GstCaps *caps;
|
||||
gboolean raw;
|
||||
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
self->sink_proxypad_blocked = blocked;
|
||||
GST_DEBUG_OBJECT (self, "Pad blocked: %d", blocked);
|
||||
if (!blocked)
|
||||
goto done;
|
||||
|
||||
/* There must be a peer at this point */
|
||||
peer = gst_pad_get_peer (self->sinkpad);
|
||||
caps = gst_pad_get_negotiated_caps (peer);
|
||||
if (!caps)
|
||||
caps = gst_pad_get_caps_reffed (peer);
|
||||
gst_object_unref (peer);
|
||||
|
||||
raw = is_raw_caps (caps);
|
||||
GST_DEBUG_OBJECT (self, "Caps %" GST_PTR_FORMAT " are raw: %d", caps, raw);
|
||||
gst_caps_unref (caps);
|
||||
|
||||
if (raw == self->raw)
|
||||
goto unblock;
|
||||
self->raw = raw;
|
||||
|
||||
if (raw) {
|
||||
GstBin *bin = GST_BIN_CAST (self);
|
||||
GstElement *head = NULL, *prev = NULL;
|
||||
GstPad *pad;
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Creating raw conversion pipeline");
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
|
||||
self->conv = gst_element_factory_make ("ffmpegcolorspace", "conv");
|
||||
if (self->conv == NULL) {
|
||||
post_missing_element_message (self, "ffmpegcolorspace");
|
||||
GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"ffmpegcolorspace"), ("video rendering might fail"));
|
||||
} else {
|
||||
gst_bin_add (bin, self->conv);
|
||||
gst_element_sync_state_with_parent (self->conv);
|
||||
distribute_running_time (self->conv, &self->segment);
|
||||
prev = head = self->conv;
|
||||
}
|
||||
|
||||
self->scale = gst_element_factory_make ("videoscale", "scale");
|
||||
if (self->scale == NULL) {
|
||||
post_missing_element_message (self, "videoscale");
|
||||
GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN,
|
||||
(_("Missing element '%s' - check your GStreamer installation."),
|
||||
"videoscale"), ("possibly a liboil version mismatch?"));
|
||||
} else {
|
||||
/* Add black borders if necessary to keep the DAR */
|
||||
g_object_set (self->scale, "add-borders", TRUE, NULL);
|
||||
gst_bin_add (bin, self->scale);
|
||||
gst_element_sync_state_with_parent (self->scale);
|
||||
distribute_running_time (self->scale, &self->segment);
|
||||
if (prev) {
|
||||
if (!gst_element_link_pads_full (prev, "src", self->scale, "sink",
|
||||
GST_PAD_LINK_CHECK_TEMPLATE_CAPS))
|
||||
goto link_failed;
|
||||
} else {
|
||||
head = self->scale;
|
||||
}
|
||||
prev = self->scale;
|
||||
}
|
||||
|
||||
if (head) {
|
||||
pad = gst_element_get_static_pad (head, "sink");
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), pad);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
pad = gst_element_get_static_pad (prev, "src");
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), pad);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
|
||||
if (!head && !prev) {
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
}
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Raw conversion pipeline created");
|
||||
} else {
|
||||
GstBin *bin = GST_BIN_CAST (self);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Removing raw conversion pipeline");
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
|
||||
if (self->conv) {
|
||||
gst_element_set_state (self->conv, GST_STATE_NULL);
|
||||
gst_bin_remove (bin, self->conv);
|
||||
self->conv = NULL;
|
||||
}
|
||||
if (self->scale) {
|
||||
gst_element_set_state (self->scale, GST_STATE_NULL);
|
||||
gst_bin_remove (bin, self->scale);
|
||||
self->scale = NULL;
|
||||
}
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Raw conversion pipeline removed");
|
||||
}
|
||||
|
||||
unblock:
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
|
||||
done:
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
return;
|
||||
|
||||
link_failed:
|
||||
{
|
||||
GST_ELEMENT_ERROR (self, CORE, PAD,
|
||||
(NULL), ("Failed to configure the video converter."));
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_play_sink_video_convert_sink_event (GstPad * pad, GstEvent * event)
|
||||
{
|
||||
GstPlaySinkVideoConvert *self =
|
||||
GST_PLAY_SINK_VIDEO_CONVERT (gst_pad_get_parent (pad));
|
||||
gboolean ret;
|
||||
|
||||
ret = self->sink_event (pad, gst_event_ref (event));
|
||||
|
||||
if (GST_EVENT_TYPE (event) == GST_EVENT_NEWSEGMENT) {
|
||||
gboolean update;
|
||||
gdouble rate, applied_rate;
|
||||
GstFormat format;
|
||||
gint64 start, stop, position;
|
||||
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
gst_event_parse_new_segment_full (event, &update, &rate, &applied_rate,
|
||||
&format, &start, &stop, &position);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Segment before %" GST_SEGMENT_FORMAT,
|
||||
&self->segment);
|
||||
gst_segment_set_newsegment_full (&self->segment, update, rate, applied_rate,
|
||||
format, start, stop, position);
|
||||
GST_DEBUG_OBJECT (self, "Segment after %" GST_SEGMENT_FORMAT,
|
||||
&self->segment);
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
} else if (GST_EVENT_TYPE (event) == GST_EVENT_FLUSH_STOP) {
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
GST_DEBUG_OBJECT (self, "Resetting segment");
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
}
|
||||
|
||||
gst_event_unref (event);
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_play_sink_video_convert_sink_setcaps (GstPad * pad, GstCaps * caps)
|
||||
{
|
||||
GstPlaySinkVideoConvert *self =
|
||||
GST_PLAY_SINK_VIDEO_CONVERT (gst_pad_get_parent (pad));
|
||||
gboolean ret;
|
||||
GstStructure *s;
|
||||
const gchar *name;
|
||||
gboolean reconfigure = FALSE;
|
||||
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
s = gst_caps_get_structure (caps, 0);
|
||||
name = gst_structure_get_name (s);
|
||||
|
||||
if (g_str_has_prefix (name, "video/x-raw-")) {
|
||||
if (!self->raw && !gst_pad_is_blocked (self->sink_proxypad)) {
|
||||
GST_DEBUG_OBJECT (self, "Changing caps from non-raw to raw");
|
||||
reconfigure = TRUE;
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
}
|
||||
} else {
|
||||
if (self->raw && !gst_pad_is_blocked (self->sink_proxypad)) {
|
||||
GST_DEBUG_OBJECT (self, "Changing caps from raw to non-raw");
|
||||
reconfigure = TRUE;
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise the setcaps below fails */
|
||||
if (reconfigure) {
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->sinkpad), NULL);
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL);
|
||||
}
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
|
||||
ret = self->sink_setcaps (pad, caps);
|
||||
|
||||
GST_DEBUG_OBJECT (self, "Setting sink caps %" GST_PTR_FORMAT ": %d", caps,
|
||||
ret);
|
||||
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static GstCaps *
|
||||
gst_play_sink_video_convert_getcaps (GstPad * pad)
|
||||
{
|
||||
GstPlaySinkVideoConvert *self =
|
||||
GST_PLAY_SINK_VIDEO_CONVERT (gst_pad_get_parent (pad));
|
||||
GstCaps *ret;
|
||||
GstPad *otherpad, *peer;
|
||||
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
if (pad == self->srcpad)
|
||||
otherpad = gst_object_ref (self->sinkpad);
|
||||
else
|
||||
otherpad = gst_object_ref (self->srcpad);
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
|
||||
peer = gst_pad_get_peer (otherpad);
|
||||
if (peer) {
|
||||
ret = gst_pad_get_caps_reffed (peer);
|
||||
gst_object_unref (peer);
|
||||
} else {
|
||||
ret = gst_caps_new_any ();
|
||||
}
|
||||
|
||||
gst_object_unref (otherpad);
|
||||
gst_object_unref (self);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_video_convert_finalize (GObject * object)
|
||||
{
|
||||
GstPlaySinkVideoConvert *self = GST_PLAY_SINK_VIDEO_CONVERT_CAST (object);
|
||||
|
||||
gst_object_unref (self->sink_proxypad);
|
||||
g_mutex_free (self->lock);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_play_sink_video_convert_change_state (GstElement * element,
|
||||
GstStateChange transition)
|
||||
{
|
||||
GstStateChangeReturn ret;
|
||||
GstPlaySinkVideoConvert *self = GST_PLAY_SINK_VIDEO_CONVERT_CAST (element);
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
if (gst_pad_is_blocked (self->sink_proxypad))
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, FALSE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
return ret;
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
if (self->conv) {
|
||||
gst_element_set_state (self->conv, GST_STATE_NULL);
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->conv);
|
||||
self->conv = NULL;
|
||||
}
|
||||
if (self->scale) {
|
||||
gst_element_set_state (self->scale, GST_STATE_NULL);
|
||||
gst_bin_remove (GST_BIN_CAST (self), self->scale);
|
||||
self->scale = NULL;
|
||||
}
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
self->raw = FALSE;
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
break;
|
||||
case GST_STATE_CHANGE_READY_TO_PAUSED:
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_LOCK (self);
|
||||
if (!gst_pad_is_blocked (self->sink_proxypad))
|
||||
gst_pad_set_blocked_async_full (self->sink_proxypad, TRUE,
|
||||
(GstPadBlockCallback) pad_blocked_cb, gst_object_ref (self),
|
||||
(GDestroyNotify) gst_object_unref);
|
||||
GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK (self);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_video_convert_class_init (GstPlaySinkVideoConvertClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class;
|
||||
GstElementClass *gstelement_class;
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT (gst_play_sink_video_convert_debug,
|
||||
"playsinkvideoconvert", 0, "play bin");
|
||||
|
||||
gobject_class = (GObjectClass *) klass;
|
||||
gstelement_class = (GstElementClass *) klass;
|
||||
|
||||
gobject_class->finalize = gst_play_sink_video_convert_finalize;
|
||||
|
||||
gst_element_class_add_pad_template (gstelement_class,
|
||||
gst_static_pad_template_get (&srctemplate));
|
||||
gst_element_class_add_pad_template (gstelement_class,
|
||||
gst_static_pad_template_get (&sinktemplate));
|
||||
gst_element_class_set_details_simple (gstelement_class,
|
||||
"Player Sink Video Converter", "Video/Bin/Converter",
|
||||
"Convenience bin for video conversion",
|
||||
"Sebastian Dröge <sebastian.droege@collabora.co.uk>");
|
||||
|
||||
gstelement_class->change_state =
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_video_convert_change_state);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_play_sink_video_convert_init (GstPlaySinkVideoConvert * self)
|
||||
{
|
||||
GstPadTemplate *templ;
|
||||
GstIterator *it;
|
||||
|
||||
self->lock = g_mutex_new ();
|
||||
gst_segment_init (&self->segment, GST_FORMAT_UNDEFINED);
|
||||
|
||||
templ = gst_static_pad_template_get (&sinktemplate);
|
||||
self->sinkpad = gst_ghost_pad_new_no_target_from_template ("sink", templ);
|
||||
self->sink_event = GST_PAD_EVENTFUNC (self->sinkpad);
|
||||
gst_pad_set_event_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_video_convert_sink_event));
|
||||
self->sink_setcaps = GST_PAD_SETCAPSFUNC (self->sinkpad);
|
||||
gst_pad_set_setcaps_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_video_convert_sink_setcaps));
|
||||
gst_pad_set_getcaps_function (self->sinkpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_video_convert_getcaps));
|
||||
|
||||
it = gst_pad_iterate_internal_links (self->sinkpad);
|
||||
g_assert (it);
|
||||
gst_iterator_next (it, (gpointer *) & self->sink_proxypad);
|
||||
g_assert (self->sink_proxypad);
|
||||
gst_iterator_free (it);
|
||||
|
||||
gst_element_add_pad (GST_ELEMENT_CAST (self), self->sinkpad);
|
||||
gst_object_unref (templ);
|
||||
|
||||
templ = gst_static_pad_template_get (&srctemplate);
|
||||
self->srcpad = gst_ghost_pad_new_no_target_from_template ("src", templ);
|
||||
gst_pad_set_getcaps_function (self->srcpad,
|
||||
GST_DEBUG_FUNCPTR (gst_play_sink_video_convert_getcaps));
|
||||
gst_element_add_pad (GST_ELEMENT_CAST (self), self->srcpad);
|
||||
gst_object_unref (templ);
|
||||
|
||||
gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad),
|
||||
self->sink_proxypad);
|
||||
}
|
86
gst/playback/gstplaysinkvideoconvert.h
Normal file
86
gst/playback/gstplaysinkvideoconvert.h
Normal file
|
@ -0,0 +1,86 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) <2011> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#ifndef __GST_PLAY_SINK_VIDEO_CONVERT_H__
|
||||
#define __GST_PLAY_SINK_VIDEO_CONVERT_H__
|
||||
|
||||
G_BEGIN_DECLS
|
||||
#define GST_TYPE_PLAY_SINK_VIDEO_CONVERT \
|
||||
(gst_play_sink_video_convert_get_type())
|
||||
#define GST_PLAY_SINK_VIDEO_CONVERT(obj) \
|
||||
(G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_PLAY_SINK_VIDEO_CONVERT, GstPlaySinkVideoConvert))
|
||||
#define GST_PLAY_SINK_VIDEO_CONVERT_CAST(obj) \
|
||||
((GstPlaySinkVideoConvert *) obj)
|
||||
#define GST_PLAY_SINK_VIDEO_CONVERT_CLASS(klass) \
|
||||
(G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_PLAY_SINK_VIDEO_CONVERT, GstPlaySinkVideoConvertClass))
|
||||
#define GST_IS_PLAY_SINK_VIDEO_CONVERT(obj) \
|
||||
(G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_PLAY_SINK_VIDEO_CONVERT))
|
||||
#define GST_IS_PLAY_SINK_VIDEO_CONVERT_CLASS(klass) \
|
||||
(G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_PLAY_SINK_VIDEO_CONVERT))
|
||||
|
||||
#define GST_PLAY_SINK_VIDEO_CONVERT_LOCK(obj) G_STMT_START { \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"locking from thread %p", \
|
||||
g_thread_self ()); \
|
||||
g_mutex_lock (GST_PLAY_SINK_VIDEO_CONVERT_CAST(obj)->lock); \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"locked from thread %p", \
|
||||
g_thread_self ()); \
|
||||
} G_STMT_END
|
||||
|
||||
#define GST_PLAY_SINK_VIDEO_CONVERT_UNLOCK(obj) G_STMT_START { \
|
||||
GST_LOG_OBJECT (obj, \
|
||||
"unlocking from thread %p", \
|
||||
g_thread_self ()); \
|
||||
g_mutex_unlock (GST_PLAY_SINK_VIDEO_CONVERT_CAST(obj)->lock); \
|
||||
} G_STMT_END
|
||||
|
||||
typedef struct _GstPlaySinkVideoConvert GstPlaySinkVideoConvert;
|
||||
typedef struct _GstPlaySinkVideoConvertClass GstPlaySinkVideoConvertClass;
|
||||
|
||||
struct _GstPlaySinkVideoConvert
|
||||
{
|
||||
GstBin parent;
|
||||
|
||||
/* < private > */
|
||||
GMutex *lock;
|
||||
|
||||
GstPad *sinkpad, *sink_proxypad;
|
||||
GstPadEventFunction sink_event;
|
||||
GstPadSetCapsFunction sink_setcaps;
|
||||
gboolean sink_proxypad_blocked;
|
||||
GstSegment segment;
|
||||
|
||||
GstPad *srcpad;
|
||||
|
||||
gboolean raw;
|
||||
GstElement *conv, *scale;
|
||||
};
|
||||
|
||||
struct _GstPlaySinkVideoConvertClass
|
||||
{
|
||||
GstBinClass parent;
|
||||
};
|
||||
|
||||
GType gst_play_sink_video_convert_get_type (void);
|
||||
|
||||
G_END_DECLS
|
||||
#endif /* __GST_PLAY_SINK_VIDEO_CONVERT_H__ */
|
Loading…
Reference in a new issue