gstreamer/subprojects/gst-plugins-bad/sys/dwrite/gstdwritetextoverlay.cpp
Seungha Yang 1c4de219e4 dwrite: Add dwritesubtitleoverlay element
Adding new subtitle overlay element. It's a bin which is wrapping
two internal elements dwritesubtitlemux and dwritetextoverlay.

* dwritesubtitlemux: A new internal element to aggregate subtitle
buffers and to attach the aggregated subtitle buffers on
video buffer as meta.
* dwritetextoverlay: Extracts/renders the subtitle meta and
discard the meta after rendering.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4934>
2023-06-28 20:15:31 +00:00

730 lines
21 KiB
C++

/* GStreamer
* Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com>
* Copyright (C) 2023 Seungha Yang <seungha@centricular.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstdwritetextoverlay.h"
#include <gst/base/base.h>
#include <caption.h>
#include <mutex>
GST_DEBUG_CATEGORY_STATIC (dwrite_text_overlay_debug);
#define GST_CAT_DEFAULT dwrite_text_overlay_debug
enum
{
PROP_0,
PROP_ENABLE_CC,
PROP_CC_FIELD,
PROP_CC_TIMEOUT,
PROP_REMOVE_CC_META,
};
/* *INDENT-OFF* */
static std::vector<GParamSpec *> _pspec;
/* *INDENT-ON* */
#define DEFAULT_ENABLE_CC TRUE
#define DEFAULT_CC_FIELD -1
#define DEFAULT_CC_TIMEOUT GST_CLOCK_TIME_NONE
#define DEFAULT_REMOVE_CC_META FALSE
/* *INDENT-OFF* */
struct GstDWriteTextOverlayPrivate
{
std::mutex lock;
caption_frame_t frame;
GstClockTime caption_running_time;
GstClockTime running_time;
guint8 selected_field;
std::string closed_caption;
std::string text;
/* properties */
gboolean enable_cc = DEFAULT_ENABLE_CC;
gint field = DEFAULT_CC_FIELD;
GstClockTime timeout = DEFAULT_CC_TIMEOUT;
gboolean remove_caption_meta = DEFAULT_REMOVE_CC_META;
};
/* *INDENT-ON* */
struct _GstDWriteTextOverlay
{
GstDWriteBaseOverlay parent;
GstDWriteTextOverlayPrivate *priv;
};
static void gst_dwrite_text_overlay_finalize (GObject * object);
static void gst_dwrite_text_overlay_set_property (GObject * object,
guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_dwrite_text_overlay_get_property (GObject * object,
guint prop_id, GValue * value, GParamSpec * pspec);
static gboolean gst_dwrite_text_overlay_start (GstBaseTransform * trans);
static gboolean gst_dwrite_text_overlay_sink_event (GstBaseTransform * trans,
GstEvent * event);
static WString gst_dwrite_text_overlay_get_text (GstDWriteBaseOverlay * overlay,
const WString & default_text, GstBuffer * buffer);
static void
gst_dwrite_text_overlay_after_transform (GstDWriteBaseOverlay * overlay,
GstBuffer * buffer);
#define gst_dwrite_text_overlay_parent_class parent_class
G_DEFINE_TYPE (GstDWriteTextOverlay, gst_dwrite_text_overlay,
GST_TYPE_DWRITE_BASE_OVERLAY);
static void
gst_dwrite_text_overlay_class_init (GstDWriteTextOverlayClass * klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS (klass);
GstDWriteBaseOverlayClass *overlay_class =
GST_DWRITE_BASE_OVERLAY_CLASS (klass);
object_class->finalize = gst_dwrite_text_overlay_finalize;
object_class->set_property = gst_dwrite_text_overlay_set_property;
object_class->get_property = gst_dwrite_text_overlay_get_property;
gst_dwrite_text_overlay_build_param_specs (_pspec);
for (guint i = 0; i < _pspec.size (); i++)
g_object_class_install_property (object_class, i + 1, _pspec[i]);
gst_element_class_set_static_metadata (element_class,
"DirectWrite Text Overlay", "Filter/Editor/Video",
"Adds text strings on top of a video buffer",
"Seungha Yang <seungha@centricular.com>");
trans_class->start = GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_start);
trans_class->sink_event =
GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_sink_event);
overlay_class->get_text =
GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_get_text);
overlay_class->after_transform =
GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_after_transform);
GST_DEBUG_CATEGORY_INIT (dwrite_text_overlay_debug,
"dwritetextoverlay", 0, "dwritetextoverlay");
}
static void
gst_dwrite_text_overlay_init (GstDWriteTextOverlay * self)
{
self->priv = new GstDWriteTextOverlayPrivate ();
self->priv->closed_caption.reserve (CAPTION_FRAME_TEXT_BYTES);
g_object_set (self, "text-alignment", DWRITE_TEXT_ALIGNMENT_CENTER,
"paragraph-alignment", DWRITE_PARAGRAPH_ALIGNMENT_FAR,
"font-size", 20.0, nullptr);
}
static void
gst_dwrite_text_overlay_finalize (GObject * object)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object);
delete self->priv;
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_dwrite_text_overlay_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object);
GstDWriteTextOverlayPrivate *priv = self->priv;
std::lock_guard < std::mutex > lk (priv->lock);
switch (prop_id) {
case PROP_ENABLE_CC:
priv->enable_cc = g_value_get_boolean (value);
break;
case PROP_CC_FIELD:
priv->field = g_value_get_int (value);
if (priv->field == -1) {
priv->selected_field = 0xff;
} else {
priv->selected_field = (guint) priv->field;
}
break;
case PROP_CC_TIMEOUT:
priv->timeout = g_value_get_uint64 (value);
break;
case PROP_REMOVE_CC_META:
priv->remove_caption_meta = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_dwrite_text_overlay_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object);
GstDWriteTextOverlayPrivate *priv = self->priv;
std::lock_guard < std::mutex > lk (priv->lock);
switch (prop_id) {
case PROP_ENABLE_CC:
g_value_set_boolean (value, priv->enable_cc);
break;
case PROP_CC_FIELD:
g_value_set_int (value, priv->field);
break;
case PROP_CC_TIMEOUT:
g_value_set_uint64 (value, priv->timeout);
break;
case PROP_REMOVE_CC_META:
g_value_set_boolean (value, priv->remove_caption_meta);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static gboolean
gst_dwrite_text_overlay_start (GstBaseTransform * trans)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (trans);
GstDWriteTextOverlayPrivate *priv = self->priv;
caption_frame_init (&priv->frame);
priv->running_time = GST_CLOCK_TIME_NONE;
priv->caption_running_time = GST_CLOCK_TIME_NONE;
priv->selected_field = 0xff;
priv->closed_caption.clear ();
return GST_BASE_TRANSFORM_CLASS (parent_class)->start (trans);
}
static gboolean
gst_dwrite_text_overlay_sink_event (GstBaseTransform * trans, GstEvent * event)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (trans);
GstDWriteTextOverlayPrivate *priv = self->priv;
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_SEGMENT:
priv->caption_running_time = GST_CLOCK_TIME_NONE;
priv->running_time = GST_CLOCK_TIME_NONE;
break;
default:
break;
}
return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event);
}
static guint
gst_dwrite_text_overlay_extract_cdp (GstDWriteTextOverlay * self,
const guint8 * cdp, guint cdp_len, guint * pos)
{
GstByteReader br;
guint16 u16;
guint8 u8;
guint8 flags;
guint len = 0;
GST_TRACE_OBJECT (self, "Extracting CDP");
/* Header + footer length */
if (cdp_len < 11) {
GST_WARNING_OBJECT (self, "cdp packet too short (%u). expected at "
"least %u", cdp_len, 11);
return 0;
}
gst_byte_reader_init (&br, cdp, cdp_len);
u16 = gst_byte_reader_get_uint16_be_unchecked (&br);
if (u16 != 0x9669) {
GST_WARNING_OBJECT (self, "cdp packet does not have initial magic bytes "
"of 0x9669");
return 0;
}
u8 = gst_byte_reader_get_uint8_unchecked (&br);
if (u8 != cdp_len) {
GST_WARNING_OBJECT (self, "cdp packet length (%u) does not match passed "
"in value (%u)", u8, cdp_len);
return 0;
}
/* skip framerate value */
gst_byte_reader_skip_unchecked (&br, 1);
flags = gst_byte_reader_get_uint8_unchecked (&br);
/* No cc_data? */
if ((flags & 0x40) == 0) {
GST_DEBUG_OBJECT (self, "cdp packet does have any cc_data");
return 0;
}
/* cdp_hdr_sequence_cntr */
gst_byte_reader_skip_unchecked (&br, 2);
/* skip timecode */
if (flags & 0x80) {
if (gst_byte_reader_get_remaining (&br) < 5) {
GST_WARNING_OBJECT (self, "cdp packet does not have enough data to "
"contain a timecode (%u). Need at least 5 bytes",
gst_byte_reader_get_remaining (&br));
return 0;
}
gst_byte_reader_skip_unchecked (&br, 5);
}
/* ccdata_present */
if (flags & 0x40) {
guint8 cc_count;
if (gst_byte_reader_get_remaining (&br) < 2) {
GST_WARNING_OBJECT (self, "not enough data to contain valid cc_data");
return 0;
}
u8 = gst_byte_reader_get_uint8_unchecked (&br);
if (u8 != 0x72) {
GST_WARNING_OBJECT (self, "missing cc_data start code of 0x72, "
"found 0x%02x", u8);
return 0;
}
cc_count = gst_byte_reader_get_uint8_unchecked (&br);
if ((cc_count & 0xe0) != 0xe0) {
GST_WARNING_OBJECT (self, "reserved bits are not 0xe0, found 0x%02x", u8);
return 0;
}
cc_count &= 0x1f;
len = 3 * cc_count;
if (gst_byte_reader_get_remaining (&br) < len) {
GST_WARNING_OBJECT (self, "not enough bytes (%u) left for the "
"number of byte triples (%u)", gst_byte_reader_get_remaining (&br),
cc_count);
return 0;
}
*pos = gst_byte_reader_get_pos (&br);
}
/* skip everything else we don't care about */
return len;
}
static void
gst_dwrite_text_overlay_decode_cc_data (GstDWriteTextOverlay * self,
const guint8 * data, guint len, GstClockTime running_time)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
GstByteReader br;
GST_TRACE_OBJECT (self, "Decoding CC data");
gst_byte_reader_init (&br, data, len);
while (gst_byte_reader_get_remaining (&br) >= 3) {
guint8 cc_type;
guint16 cc_data;
cc_type = gst_byte_reader_get_uint8_unchecked (&br);
cc_data = gst_byte_reader_get_uint16_be_unchecked (&br);
if ((cc_type & 0x04) != 0x04)
continue;
cc_type = cc_type & 0x03;
if (cc_type != 0x00 && cc_type != 0x01)
continue;
if (priv->selected_field == 0xff) {
GST_INFO_OBJECT (self, "Selected field %d", cc_type);
priv->selected_field = cc_type;
}
if (cc_type != priv->selected_field)
continue;
auto status = caption_frame_decode (&priv->frame, cc_data, 0.0);
switch (status) {
case LIBCAPTION_READY:
{
auto len = caption_frame_to_text (&priv->frame,
&priv->closed_caption[0], TRUE);
priv->closed_caption.resize (len);
break;
}
case LIBCAPTION_CLEAR:
priv->closed_caption.clear ();
break;
default:
break;
}
priv->caption_running_time = running_time;
}
}
static void
gst_dwrite_text_overlay_decode_s334_1a (GstDWriteTextOverlay * self,
const guint8 * data, guint len, GstClockTime running_time)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
GstByteReader br;
GST_TRACE_OBJECT (self, "Decoding S334-1A");
gst_byte_reader_init (&br, data, len);
while (gst_byte_reader_get_remaining (&br) >= 3) {
guint8 cc_type;
guint16 cc_data;
cc_type = gst_byte_reader_get_uint8_unchecked (&br);
cc_data = gst_byte_reader_get_uint16_be_unchecked (&br);
cc_type = cc_type & 0x01;
if (priv->selected_field == 0xff) {
GST_INFO_OBJECT (self, "Selected field %d", cc_type);
priv->selected_field = cc_type;
}
if (cc_type != priv->selected_field)
continue;
auto status = caption_frame_decode (&priv->frame, cc_data, 0.0);
switch (status) {
case LIBCAPTION_READY:
{
auto len = caption_frame_to_text (&priv->frame,
&priv->closed_caption[0], TRUE);
priv->closed_caption.resize (len);
break;
}
case LIBCAPTION_CLEAR:
priv->closed_caption.clear ();
break;
default:
break;
}
priv->caption_running_time = running_time;
}
}
static void
gst_dwrite_text_overlay_decode_raw (GstDWriteTextOverlay * self,
const guint8 * data, guint len, GstClockTime running_time)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
GstByteReader br;
GST_TRACE_OBJECT (self, "Decoding CEA608 RAW");
gst_byte_reader_init (&br, data, len);
while (gst_byte_reader_get_remaining (&br) >= 2) {
guint16 cc_data;
cc_data = gst_byte_reader_get_uint16_be_unchecked (&br);
auto status = caption_frame_decode (&priv->frame, cc_data, 0.0);
switch (status) {
case LIBCAPTION_READY:
{
auto len = caption_frame_to_text (&priv->frame,
&priv->closed_caption[0], TRUE);
priv->closed_caption.resize (len);
break;
}
case LIBCAPTION_CLEAR:
priv->closed_caption.clear ();
break;
default:
break;
}
priv->caption_running_time = running_time;
}
}
static void
xml_text (GMarkupParseContext * context, const gchar * text, gsize text_len,
gpointer user_data, GError ** error)
{
gchar **accum = (gchar **) user_data;
gchar *concat;
if (*accum) {
concat = g_strconcat (*accum, text, NULL);
g_free (*accum);
*accum = concat;
} else {
*accum = g_strdup (text);
}
}
static gchar *
gst_dwrite_text_overlay_strip_markup (GstDWriteTextOverlay * self,
const gchar * markup)
{
GMarkupParser parser = { 0, };
GMarkupParseContext *context;
gchar *accum = nullptr;
parser.text = xml_text;
context = g_markup_parse_context_new (&parser,
(GMarkupParseFlags) 0, &accum, nullptr);
if (!g_markup_parse_context_parse (context, "<root>", 6, nullptr))
goto error;
if (!g_markup_parse_context_parse (context, markup, strlen (markup), nullptr))
goto error;
if (!g_markup_parse_context_parse (context, "</root>", 7, nullptr))
goto error;
if (!g_markup_parse_context_end_parse (context, nullptr))
goto error;
done:
g_markup_parse_context_free (context);
return accum;
error:
g_free (accum);
accum = nullptr;
goto done;
}
static void
gst_dwrite_text_overlay_extract_meta (GstDWriteTextOverlay * self,
GstDWriteSubtitleMeta * meta)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
GstCaps *caps = nullptr;
GstStructure *s;
const gchar *format;
std::string str;
GstMapInfo info;
if (!meta || !meta->subtitle || !meta->stream)
return;
caps = gst_stream_get_caps (meta->stream);
if (!caps)
return;
if (gst_buffer_get_size (meta->subtitle) == 0)
goto out;
if (!gst_buffer_map (meta->subtitle, &info, GST_MAP_READ))
goto out;
s = gst_caps_get_structure (caps, 0);
format = gst_structure_get_string (s, "format");
/* TODO: parse pango attributs and make layout based on that */
if (g_strcmp0 (format, "pango-markup") == 0) {
gchar *stripped = gst_dwrite_text_overlay_strip_markup (self,
(gchar *) info.data);
gst_buffer_unmap (meta->subtitle, &info);
if (!stripped)
goto out;
if (priv->text.empty ()) {
priv->text = stripped;
} else {
priv->text += "\n";
priv->text += stripped;
}
} else {
std::string ret;
ret.resize (info.size);
memcpy (&ret[0], info.data, info.size);
gst_buffer_unmap (meta->subtitle, &info);
auto len = strlen (ret.c_str ());
ret.resize (len);
if (priv->text.empty ())
priv->text = ret;
else
priv->text += " " + ret;
}
out:
gst_clear_caps (&caps);
}
static gboolean
gst_dwrite_text_overlay_foreach_meta (GstBuffer * buffer, GstMeta ** meta,
GstDWriteTextOverlay * self)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
GstVideoCaptionMeta *cc_meta;
if (priv->enable_cc && (*meta)->info->api == GST_VIDEO_CAPTION_META_API_TYPE) {
cc_meta = (GstVideoCaptionMeta *) (*meta);
switch (cc_meta->caption_type) {
case GST_VIDEO_CAPTION_TYPE_CEA608_RAW:
gst_dwrite_text_overlay_decode_raw (self, cc_meta->data, cc_meta->size,
priv->running_time);
break;
case GST_VIDEO_CAPTION_TYPE_CEA608_S334_1A:
gst_dwrite_text_overlay_decode_s334_1a (self, cc_meta->data,
cc_meta->size, priv->running_time);
break;
case GST_VIDEO_CAPTION_TYPE_CEA708_RAW:
gst_dwrite_text_overlay_decode_cc_data (self, cc_meta->data,
cc_meta->size, priv->running_time);
break;
case GST_VIDEO_CAPTION_TYPE_CEA708_CDP:
{
guint len, pos = 0;
len = gst_dwrite_text_overlay_extract_cdp (self, cc_meta->data,
cc_meta->size, &pos);
if (len > 0) {
gst_dwrite_text_overlay_decode_cc_data (self, cc_meta->data + pos,
len, priv->running_time);
}
break;
}
default:
break;
}
} else if ((*meta)->info->api == GST_DWRITE_SUBTITLE_META_API_TYPE) {
GstDWriteSubtitleMeta *smeta = (GstDWriteSubtitleMeta *) (*meta);
gst_dwrite_text_overlay_extract_meta (self, smeta);
}
return TRUE;
}
static WString
gst_dwrite_text_overlay_get_text (GstDWriteBaseOverlay * overlay,
const WString & default_text, GstBuffer * buffer)
{
GstBaseTransform *trans = GST_BASE_TRANSFORM (overlay);
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (overlay);
GstDWriteTextOverlayPrivate *priv = self->priv;
std::lock_guard < std::mutex > lk (priv->lock);
WString text_wide;
priv->text.clear ();
priv->running_time = gst_segment_to_running_time (&trans->segment,
GST_FORMAT_TIME, GST_BUFFER_PTS (buffer));
gst_buffer_foreach_meta (buffer,
(GstBufferForeachMetaFunc) gst_dwrite_text_overlay_foreach_meta, self);
if (priv->enable_cc) {
if (GST_CLOCK_TIME_IS_VALID (priv->timeout) &&
GST_CLOCK_TIME_IS_VALID (priv->running_time) &&
GST_CLOCK_TIME_IS_VALID (priv->caption_running_time) &&
priv->running_time >= priv->caption_running_time) {
GstClockTime diff = priv->running_time - priv->caption_running_time;
if (diff > priv->timeout) {
GST_INFO_OBJECT (self, "Reached timeout, clearing text");
priv->closed_caption.clear ();
}
}
} else {
priv->closed_caption.clear ();
}
if (priv->closed_caption.empty () && priv->text.empty ())
return default_text;
if (!priv->text.empty ())
text_wide = gst_dwrite_string_to_wstring (priv->text);
if (!priv->closed_caption.empty ()) {
if (!text_wide.empty ())
text_wide += L"\n";
text_wide += gst_dwrite_string_to_wstring (priv->closed_caption);
}
if (default_text.empty ())
return text_wide;
return default_text + WString (L" ") + text_wide;
}
static gboolean
gst_dwrite_text_overlay_remove_meta (GstBuffer * buffer, GstMeta ** meta,
GstDWriteTextOverlay * self)
{
GstDWriteTextOverlayPrivate *priv = self->priv;
if ((*meta)->info->api == GST_VIDEO_CAPTION_META_API_TYPE &&
priv->enable_cc && priv->remove_caption_meta) {
GST_TRACE_OBJECT (self, "Removing caption meta");
*meta = nullptr;
} else if ((*meta)->info->api == GST_DWRITE_SUBTITLE_META_API_TYPE) {
*meta = nullptr;
}
return TRUE;
}
static void
gst_dwrite_text_overlay_after_transform (GstDWriteBaseOverlay * overlay,
GstBuffer * buffer)
{
GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (overlay);
GstDWriteTextOverlayPrivate *priv = self->priv;
std::lock_guard < std::mutex > lk (priv->lock);
gst_buffer_foreach_meta (buffer,
(GstBufferForeachMetaFunc) gst_dwrite_text_overlay_remove_meta, self);
}
void
gst_dwrite_text_overlay_build_param_specs (std::vector < GParamSpec * >&pspec)
{
pspec.push_back (g_param_spec_boolean ("enable-cc", "Enable CC",
"Enable closed caption rendering",
DEFAULT_ENABLE_CC,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
pspec.push_back (g_param_spec_int ("cc-field", "CC Field",
"The closed caption field to render when available, (-1 = automatic)",
-1, 1, DEFAULT_CC_FIELD,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
pspec.push_back (g_param_spec_uint64 ("cc-timeout", "CC Timeout",
"Duration after which to erase overlay when no cc data has arrived "
"for the selected field, in nanoseconds unit", 16 * GST_SECOND,
GST_CLOCK_TIME_NONE, DEFAULT_CC_TIMEOUT,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
pspec.push_back (g_param_spec_boolean ("remove-cc-meta", "Remove CC Meta",
"Remove caption meta from output buffers "
"when closed caption rendering is enabled",
DEFAULT_REMOVE_CC_META,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
}