mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-01-16 20:36:06 +00:00
meta: expose API to register and create custom meta
Custom meta is backed by a GstStructure, and does not require that users of the API expose their GstMeta implementation as public API for other components to make use of it. In addition, it provides a simpler interface by ignoring the impl vs. api distinction that the regular API exposes. This new API is meant to be the meta counterpart to custom events and messages, and to be more convenient than the lower-level API when the absolute best performance isn't a requirement. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/609>
This commit is contained in:
parent
e9c99c05ae
commit
bbca6b1ddf
7 changed files with 443 additions and 3 deletions
|
@ -1129,6 +1129,7 @@ gst_deinit (void)
|
|||
|
||||
_priv_gst_caps_features_cleanup ();
|
||||
_priv_gst_caps_cleanup ();
|
||||
_priv_gst_meta_cleanup ();
|
||||
|
||||
g_type_class_unref (g_type_class_peek (gst_object_get_type ()));
|
||||
g_type_class_unref (g_type_class_peek (gst_pad_get_type ()));
|
||||
|
|
|
@ -148,6 +148,7 @@ G_GNUC_INTERNAL void _priv_gst_allocator_cleanup (void);
|
|||
G_GNUC_INTERNAL void _priv_gst_caps_features_cleanup (void);
|
||||
G_GNUC_INTERNAL void _priv_gst_caps_cleanup (void);
|
||||
G_GNUC_INTERNAL void _priv_gst_debug_cleanup (void);
|
||||
G_GNUC_INTERNAL void _priv_gst_meta_cleanup (void);
|
||||
|
||||
/* called from gst_task_cleanup_all(). */
|
||||
G_GNUC_INTERNAL void _priv_gst_element_cleanup (void);
|
||||
|
|
|
@ -2871,3 +2871,65 @@ gst_reference_timestamp_meta_get_info (void)
|
|||
|
||||
return meta_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_buffer_add_custom_meta:
|
||||
* @buffer: (transfer none): a #GstBuffer
|
||||
* @name: the registered name of the desired custom meta
|
||||
*
|
||||
* Creates and adds a #GstCustomMeta for the desired @name. @name must have
|
||||
* been successfully registered with gst_meta_register_custom().
|
||||
*
|
||||
* Returns: (transfer none) (nullable): The #GstCustomMeta that was added to the buffer
|
||||
*
|
||||
* Since: 1.20
|
||||
*/
|
||||
GstCustomMeta *
|
||||
gst_buffer_add_custom_meta (GstBuffer * buffer, const gchar * name)
|
||||
{
|
||||
GstCustomMeta *meta;
|
||||
const GstMetaInfo *info;
|
||||
|
||||
g_return_val_if_fail (name != NULL, NULL);
|
||||
g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);
|
||||
|
||||
info = gst_meta_get_info (name);
|
||||
|
||||
if (info == NULL || !gst_meta_info_is_custom (info))
|
||||
return NULL;
|
||||
|
||||
meta = (GstCustomMeta *) gst_buffer_add_meta (buffer, info, NULL);
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_buffer_get_custom_meta:
|
||||
* @buffer: a #GstBuffer
|
||||
* @name: the registered name of the custom meta to retrieve.
|
||||
*
|
||||
* Find the first #GstCustomMeta on @buffer for the desired @name.
|
||||
*
|
||||
* Returns: (transfer none) (nullable): the #GstCustomMeta or %NULL when there
|
||||
* is no such metadata on @buffer.
|
||||
*
|
||||
* Since: 1.20
|
||||
*/
|
||||
GstCustomMeta *
|
||||
gst_buffer_get_custom_meta (GstBuffer * buffer, const gchar * name)
|
||||
{
|
||||
const GstMetaInfo *info;
|
||||
|
||||
g_return_val_if_fail (buffer != NULL, NULL);
|
||||
g_return_val_if_fail (name != NULL, NULL);
|
||||
|
||||
info = gst_meta_get_info (name);
|
||||
|
||||
if (!info)
|
||||
return NULL;
|
||||
|
||||
if (!gst_meta_info_is_custom (info))
|
||||
return NULL;
|
||||
|
||||
return (GstCustomMeta *) gst_buffer_get_meta (buffer, info->api);
|
||||
}
|
||||
|
|
|
@ -664,6 +664,14 @@ gboolean gst_buffer_foreach_meta (GstBuffer *buffer,
|
|||
GstBufferForeachMetaFunc func,
|
||||
gpointer user_data);
|
||||
|
||||
GST_API
|
||||
GstCustomMeta * gst_buffer_add_custom_meta (GstBuffer *buffer,
|
||||
const gchar *name);
|
||||
|
||||
GST_API
|
||||
GstCustomMeta * gst_buffer_get_custom_meta (GstBuffer *buffer,
|
||||
const gchar *name);
|
||||
|
||||
/**
|
||||
* gst_value_set_buffer:
|
||||
* @v: a #GValue to receive the data
|
||||
|
|
213
gst/gstmeta.c
213
gst/gstmeta.c
|
@ -58,16 +58,60 @@ static GRWLock lock;
|
|||
GQuark _gst_meta_transform_copy;
|
||||
GQuark _gst_meta_tag_memory;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GstCustomMeta meta;
|
||||
|
||||
GstStructure *structure;
|
||||
} GstCustomMetaImpl;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GstMetaInfo info;
|
||||
GstCustomMetaTransformFunction custom_transform_func;
|
||||
gpointer custom_transform_user_data;
|
||||
GDestroyNotify custom_transform_destroy_notify;
|
||||
gboolean is_custom;
|
||||
} GstMetaInfoImpl;
|
||||
|
||||
static void
|
||||
free_info (gpointer data)
|
||||
{
|
||||
g_slice_free (GstMetaInfoImpl, data);
|
||||
}
|
||||
|
||||
void
|
||||
_priv_gst_meta_initialize (void)
|
||||
{
|
||||
g_rw_lock_init (&lock);
|
||||
metainfo = g_hash_table_new (g_str_hash, g_str_equal);
|
||||
metainfo = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, free_info);
|
||||
|
||||
_gst_meta_transform_copy = g_quark_from_static_string ("gst-copy");
|
||||
_gst_meta_tag_memory = g_quark_from_static_string ("memory");
|
||||
}
|
||||
|
||||
static gboolean
|
||||
notify_custom (gchar * key, GstMetaInfo * info, gpointer unused)
|
||||
{
|
||||
GstMetaInfoImpl *impl = (GstMetaInfoImpl *) info;
|
||||
|
||||
if (impl->is_custom) {
|
||||
if (impl->custom_transform_destroy_notify)
|
||||
impl->custom_transform_destroy_notify (impl->custom_transform_user_data);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
_priv_gst_meta_cleanup (void)
|
||||
{
|
||||
if (metainfo != NULL) {
|
||||
g_hash_table_foreach_remove (metainfo, (GHRFunc) notify_custom, NULL);
|
||||
g_hash_table_unref (metainfo);
|
||||
metainfo = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_meta_api_type_register:
|
||||
* @api: an API to register
|
||||
|
@ -104,6 +148,168 @@ gst_meta_api_type_register (const gchar * api, const gchar ** tags)
|
|||
return type;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
custom_init_func (GstMeta * meta, gpointer params, GstBuffer * buffer)
|
||||
{
|
||||
GstCustomMetaImpl *cmeta = (GstCustomMetaImpl *) meta;
|
||||
|
||||
cmeta->structure = gst_structure_new_empty (g_type_name (meta->info->type));
|
||||
|
||||
gst_structure_set_parent_refcount (cmeta->structure,
|
||||
&GST_MINI_OBJECT_REFCOUNT (buffer));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
custom_free_func (GstMeta * meta, GstBuffer * buffer)
|
||||
{
|
||||
GstCustomMetaImpl *cmeta = (GstCustomMetaImpl *) meta;
|
||||
|
||||
gst_structure_set_parent_refcount (cmeta->structure, NULL);
|
||||
gst_structure_free (cmeta->structure);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
custom_transform_func (GstBuffer * transbuf, GstMeta * meta,
|
||||
GstBuffer * buffer, GQuark type, gpointer data)
|
||||
{
|
||||
GstCustomMetaImpl *custom, *cmeta = (GstCustomMetaImpl *) meta;
|
||||
GstMetaInfoImpl *info = (GstMetaInfoImpl *) meta->info;
|
||||
|
||||
if (info->custom_transform_func)
|
||||
return info->custom_transform_func (transbuf, (GstCustomMeta *) meta,
|
||||
buffer, type, data, info->custom_transform_user_data);
|
||||
|
||||
if (GST_META_TRANSFORM_IS_COPY (type)) {
|
||||
custom =
|
||||
(GstCustomMetaImpl *) gst_buffer_add_meta (transbuf, meta->info, NULL);
|
||||
gst_structure_set_parent_refcount (custom->structure, NULL);
|
||||
gst_structure_take (&custom->structure,
|
||||
gst_structure_copy (cmeta->structure));
|
||||
gst_structure_set_parent_refcount (custom->structure,
|
||||
&GST_MINI_OBJECT_REFCOUNT (buffer));
|
||||
} else {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_custom_meta_get_structure:
|
||||
*
|
||||
* Retrieve the #GstStructure backing a custom meta, the structure's mutability
|
||||
* is conditioned to the writability of the #GstBuffer @meta is attached to.
|
||||
*
|
||||
* Returns: (transfer none): the #GstStructure backing @meta
|
||||
* Since: 1.20
|
||||
*/
|
||||
GstStructure *
|
||||
gst_custom_meta_get_structure (GstCustomMeta * meta)
|
||||
{
|
||||
g_return_val_if_fail (meta != NULL, NULL);
|
||||
g_return_val_if_fail (gst_meta_info_is_custom (((GstMeta *) meta)->info),
|
||||
NULL);
|
||||
|
||||
return ((GstCustomMetaImpl *) meta)->structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_custom_meta_has_name:
|
||||
*
|
||||
* Checks whether the name of the custom meta is @name
|
||||
*
|
||||
* Returns: Whether @name is the name of the custom meta
|
||||
* Since: 1.20
|
||||
*/
|
||||
gboolean
|
||||
gst_custom_meta_has_name (GstCustomMeta * meta, const gchar * name)
|
||||
{
|
||||
g_return_val_if_fail (meta != NULL, FALSE);
|
||||
g_return_val_if_fail (gst_meta_info_is_custom (((GstMeta *) meta)->info),
|
||||
FALSE);
|
||||
|
||||
return gst_structure_has_name (((GstCustomMetaImpl *) meta)->structure, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_meta_register_custom:
|
||||
* @name: the name of the #GstMeta implementation
|
||||
* @tags: (array zero-terminated=1): tags for @api
|
||||
* @transform_func: (scope notified) (nullable): a #GstMetaTransformFunction
|
||||
* @user_data: (closure): user data passed to @transform_func
|
||||
* @destroy_data: #GDestroyNotify for user_data
|
||||
*
|
||||
* Register a new custom #GstMeta implementation, backed by an opaque
|
||||
* structure holding a #GstStructure.
|
||||
*
|
||||
* The registered info can be retrieved later with gst_meta_get_info() by using
|
||||
* @name as the key.
|
||||
*
|
||||
* The backing #GstStructure can be retrieved with
|
||||
* gst_custom_meta_get_structure(), its mutability is conditioned by the
|
||||
* writability of the buffer the meta is attached to.
|
||||
*
|
||||
* When @transform_func is %NULL, the meta and its backing #GstStructure
|
||||
* will always be copied when the transform operation is copy, other operations
|
||||
* are discarded, copy regions are ignored.
|
||||
*
|
||||
* Returns: (transfer none): a #GstMetaInfo that can be used to
|
||||
* access metadata.
|
||||
* Since: 1.20
|
||||
*/
|
||||
const GstMetaInfo *
|
||||
gst_meta_register_custom (const gchar * name, const gchar ** tags,
|
||||
GstCustomMetaTransformFunction transform_func,
|
||||
gpointer user_data, GDestroyNotify destroy_data)
|
||||
{
|
||||
gchar *api_name = g_strdup_printf ("%s-api", name);
|
||||
GType api;
|
||||
GstMetaInfoImpl *info;
|
||||
GstMetaInfo *ret = NULL;
|
||||
|
||||
g_return_val_if_fail (tags != NULL, NULL);
|
||||
g_return_val_if_fail (name != NULL, NULL);
|
||||
|
||||
api = gst_meta_api_type_register (api_name, tags);
|
||||
g_free (api_name);
|
||||
if (api == G_TYPE_INVALID)
|
||||
goto done;
|
||||
|
||||
info = (GstMetaInfoImpl *) gst_meta_register (api, name,
|
||||
sizeof (GstCustomMetaImpl),
|
||||
custom_init_func, custom_free_func, custom_transform_func);
|
||||
|
||||
if (!info)
|
||||
goto done;
|
||||
|
||||
info->is_custom = TRUE;
|
||||
info->custom_transform_func = transform_func;
|
||||
info->custom_transform_user_data = user_data;
|
||||
info->custom_transform_destroy_notify = destroy_data;
|
||||
|
||||
ret = (GstMetaInfo *) info;
|
||||
|
||||
done:
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_meta_info_is_custom:
|
||||
*
|
||||
* Returns: whether @info was registered as a #GstCustomMeta with
|
||||
* gst_meta_register_custom()
|
||||
* Since:1.20
|
||||
*/
|
||||
gboolean
|
||||
gst_meta_info_is_custom (const GstMetaInfo * info)
|
||||
{
|
||||
g_return_val_if_fail (info != NULL, FALSE);
|
||||
|
||||
return ((GstMetaInfoImpl *) info)->is_custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_meta_api_type_has_tag:
|
||||
* @api: an API
|
||||
|
@ -158,7 +364,7 @@ gst_meta_api_type_get_tags (GType api)
|
|||
* The same @info can be retrieved later with gst_meta_get_info() by using
|
||||
* @impl as the key.
|
||||
*
|
||||
* Returns: (transfer none) (nullable): a #GstMetaInfo that can be used to
|
||||
* Returns: (transfer none): a #GstMetaInfo that can be used to
|
||||
* access metadata.
|
||||
*/
|
||||
|
||||
|
@ -185,13 +391,14 @@ gst_meta_register (GType api, const gchar * impl, gsize size,
|
|||
if (type == 0)
|
||||
return NULL;
|
||||
|
||||
info = g_slice_new (GstMetaInfo);
|
||||
info = (GstMetaInfo *) g_slice_new (GstMetaInfoImpl);
|
||||
info->api = api;
|
||||
info->type = type;
|
||||
info->size = size;
|
||||
info->init_func = init_func;
|
||||
info->free_func = free_func;
|
||||
info->transform_func = transform_func;
|
||||
((GstMetaInfoImpl *) info)->is_custom = FALSE;
|
||||
|
||||
GST_CAT_DEBUG (GST_CAT_META,
|
||||
"register \"%s\" implementing \"%s\" of size %" G_GSIZE_FORMAT, impl,
|
||||
|
|
|
@ -105,6 +105,17 @@ struct _GstMeta {
|
|||
const GstMetaInfo *info;
|
||||
};
|
||||
|
||||
/**
|
||||
* GstCustomMeta:
|
||||
*
|
||||
* Simple typing wrapper around #GstMeta
|
||||
*
|
||||
* Since: 1.20
|
||||
*/
|
||||
typedef struct {
|
||||
GstMeta meta;
|
||||
} GstCustomMeta;
|
||||
|
||||
#include <gst/gstbuffer.h>
|
||||
|
||||
/**
|
||||
|
@ -178,6 +189,30 @@ typedef gboolean (*GstMetaTransformFunction) (GstBuffer *transbuf,
|
|||
GstMeta *meta, GstBuffer *buffer,
|
||||
GQuark type, gpointer data);
|
||||
|
||||
/**
|
||||
* GstCustomMetaTransformFunction:
|
||||
* @transbuf: a #GstBuffer
|
||||
* @meta: a #GstCustomMeta
|
||||
* @buffer: a #GstBuffer
|
||||
* @type: the transform type
|
||||
* @data: transform specific data.
|
||||
* @user_data: user data passed when registering the meta
|
||||
*
|
||||
* Function called for each @meta in @buffer as a result of performing a
|
||||
* transformation on @transbuf. Additional @type specific transform data
|
||||
* is passed to the function as @data.
|
||||
*
|
||||
* Implementations should check the @type of the transform and parse
|
||||
* additional type specific fields in @data that should be used to update
|
||||
* the metadata on @transbuf.
|
||||
*
|
||||
* Returns: %TRUE if the transform could be performed
|
||||
* Since: 1.20
|
||||
*/
|
||||
typedef gboolean (*GstCustomMetaTransformFunction) (GstBuffer *transbuf,
|
||||
GstCustomMeta *meta, GstBuffer *buffer,
|
||||
GQuark type, gpointer data, gpointer user_data);
|
||||
|
||||
/**
|
||||
* GstMetaInfo:
|
||||
* @api: tag identifying the metadata structure and api
|
||||
|
@ -216,6 +251,21 @@ const GstMetaInfo * gst_meta_register (GType api, const gchar *impl,
|
|||
GstMetaInitFunction init_func,
|
||||
GstMetaFreeFunction free_func,
|
||||
GstMetaTransformFunction transform_func);
|
||||
|
||||
GST_API
|
||||
const GstMetaInfo * gst_meta_register_custom (const gchar *name, const gchar **tags,
|
||||
GstCustomMetaTransformFunction transform_func,
|
||||
gpointer user_data, GDestroyNotify destroy_data);
|
||||
|
||||
GST_API
|
||||
gboolean gst_meta_info_is_custom (const GstMetaInfo *info);
|
||||
|
||||
GST_API
|
||||
GstStructure * gst_custom_meta_get_structure (GstCustomMeta *meta);
|
||||
|
||||
GST_API
|
||||
gboolean gst_custom_meta_has_name (GstCustomMeta *meta, const gchar * name);
|
||||
|
||||
GST_API
|
||||
const GstMetaInfo * gst_meta_get_info (const gchar * impl);
|
||||
|
||||
|
|
|
@ -688,6 +688,115 @@ GST_START_TEST (test_meta_seqnum)
|
|||
|
||||
GST_END_TEST;
|
||||
|
||||
GST_START_TEST (test_meta_custom)
|
||||
{
|
||||
GstBuffer *buffer;
|
||||
const GstMetaInfo *info;
|
||||
GstCustomMeta *meta;
|
||||
GstMeta *it;
|
||||
GstStructure *s, *expected;
|
||||
gpointer state = NULL;
|
||||
const gchar *tags[] = { "test-tag", NULL };
|
||||
|
||||
info = gst_meta_register_custom ("test-custom", tags, NULL, NULL, NULL);
|
||||
|
||||
fail_unless (info != NULL);
|
||||
|
||||
buffer = gst_buffer_new_and_alloc (4);
|
||||
fail_if (buffer == NULL);
|
||||
|
||||
/* add some metadata */
|
||||
meta = gst_buffer_add_custom_meta (buffer, "test-custom");
|
||||
fail_if (meta == NULL);
|
||||
|
||||
fail_unless (gst_custom_meta_has_name ((GstCustomMeta *) meta,
|
||||
"test-custom"));
|
||||
|
||||
expected = gst_structure_new_empty ("test-custom");
|
||||
s = gst_custom_meta_get_structure (meta);
|
||||
fail_unless (gst_structure_is_equal (s, expected));
|
||||
gst_structure_free (expected);
|
||||
|
||||
gst_structure_set (s, "test-field", G_TYPE_INT, 42, NULL);
|
||||
gst_buffer_ref (buffer);
|
||||
ASSERT_CRITICAL (gst_structure_set (s, "test-field", G_TYPE_INT, 43, NULL));
|
||||
gst_buffer_unref (buffer);
|
||||
expected = gst_structure_new ("test-custom",
|
||||
"test-field", G_TYPE_INT, 42, NULL);
|
||||
fail_unless (gst_structure_is_equal (s, expected));
|
||||
gst_structure_free (expected);
|
||||
|
||||
it = gst_buffer_iterate_meta (buffer, &state);
|
||||
|
||||
fail_unless ((GstCustomMeta *) it == meta);
|
||||
|
||||
fail_unless (it->info == info);
|
||||
|
||||
/* clean up */
|
||||
gst_buffer_unref (buffer);
|
||||
}
|
||||
|
||||
GST_END_TEST;
|
||||
|
||||
static gboolean
|
||||
transform_custom (GstBuffer * transbuf, GstMeta * meta, GstBuffer * buffer,
|
||||
GQuark type, gpointer data, gint * user_data)
|
||||
{
|
||||
if (GST_META_TRANSFORM_IS_COPY (type)) {
|
||||
GstStructure *s;
|
||||
GstCustomMeta *custom;
|
||||
|
||||
custom = (GstCustomMeta *) gst_buffer_add_meta (transbuf, meta->info, NULL);
|
||||
s = gst_custom_meta_get_structure (custom);
|
||||
gst_structure_set (s, "test-field", G_TYPE_INT, *user_data, NULL);
|
||||
} else {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GST_START_TEST (test_meta_custom_transform)
|
||||
{
|
||||
GstBuffer *buffer, *buffer_copy;
|
||||
const GstMetaInfo *info;
|
||||
GstCustomMeta *meta;
|
||||
GstStructure *s, *expected;
|
||||
const gchar *tags[] = { "test-tag", NULL };
|
||||
gint *user_data;
|
||||
|
||||
/* That memory should be deallocated at gst_deinit time */
|
||||
user_data = g_malloc (sizeof (gint));
|
||||
*user_data = 42;
|
||||
info =
|
||||
gst_meta_register_custom ("test-custom", tags,
|
||||
(GstCustomMetaTransformFunction) transform_custom, user_data, g_free);
|
||||
|
||||
fail_unless (info != NULL);
|
||||
|
||||
buffer = gst_buffer_new_and_alloc (4);
|
||||
fail_if (buffer == NULL);
|
||||
|
||||
/* add some metadata */
|
||||
meta = gst_buffer_add_custom_meta (buffer, "test-custom");
|
||||
fail_if (meta == NULL);
|
||||
|
||||
buffer_copy = gst_buffer_copy (buffer);
|
||||
meta = gst_buffer_get_custom_meta (buffer_copy, "test-custom");
|
||||
fail_unless (meta != NULL);
|
||||
expected =
|
||||
gst_structure_new ("test-custom", "test-field", G_TYPE_INT, 42, NULL);
|
||||
s = gst_custom_meta_get_structure (meta);
|
||||
fail_unless (gst_structure_is_equal (s, expected));
|
||||
gst_structure_free (expected);
|
||||
|
||||
/* clean up */
|
||||
gst_buffer_unref (buffer_copy);
|
||||
gst_buffer_unref (buffer);
|
||||
}
|
||||
|
||||
GST_END_TEST;
|
||||
|
||||
static Suite *
|
||||
gst_buffermeta_suite (void)
|
||||
{
|
||||
|
@ -705,6 +814,8 @@ gst_buffermeta_suite (void)
|
|||
tcase_add_test (tc_chain, test_meta_foreach_remove_several);
|
||||
tcase_add_test (tc_chain, test_meta_iterate);
|
||||
tcase_add_test (tc_chain, test_meta_seqnum);
|
||||
tcase_add_test (tc_chain, test_meta_custom);
|
||||
tcase_add_test (tc_chain, test_meta_custom_transform);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue