mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-23 00:36:51 +00:00
element: add API to get property change notifications via messages
Be notified in the application thread via bus messages about notify::* and deep-notify::* property changes, instead of having to deal with it in a non-application thread. API: gst_element_add_property_notify_watch() API: gst_element_add_property_deep_notify_watch() API: gst_element_remove_property_notify_watch() API: gst_message_new_property_notify() API: gst_message_parse_property_notify() API: GST_MESSAGE_PROPERTY_NOTIFY https://bugzilla.gnome.org/show_bug.cgi?id=763142
This commit is contained in:
parent
c78ff47a87
commit
6e3fb7af52
9 changed files with 364 additions and 2 deletions
|
@ -912,6 +912,11 @@ gst_element_send_event
|
|||
gst_element_seek_simple
|
||||
gst_element_seek
|
||||
|
||||
<SUBSECTION element-property-notifications>
|
||||
gst_element_add_property_notify_watch
|
||||
gst_element_add_property_deep_notify_watch
|
||||
gst_element_remove_property_notify_watch
|
||||
|
||||
<SUBSECTION Standard>
|
||||
GST_ELEMENT
|
||||
GST_IS_ELEMENT
|
||||
|
@ -1637,6 +1642,9 @@ gst_message_new_device_added
|
|||
gst_message_new_device_removed
|
||||
gst_message_parse_device_added
|
||||
gst_message_parse_device_removed
|
||||
|
||||
gst_message_new_property_notify
|
||||
gst_message_parse_property_notify
|
||||
<SUBSECTION Standard>
|
||||
GstMessageClass
|
||||
GST_MESSAGE
|
||||
|
|
119
gst/gstelement.c
119
gst/gstelement.c
|
@ -3245,3 +3245,122 @@ gst_element_get_context (GstElement * element, const gchar * context_type)
|
|||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_element_property_post_notify_msg (GstElement * element, GObject * obj,
|
||||
GParamSpec * pspec, gboolean include_value)
|
||||
{
|
||||
GValue val = G_VALUE_INIT;
|
||||
GValue *v;
|
||||
|
||||
GST_LOG_OBJECT (element, "property '%s' of object %" GST_PTR_FORMAT " has "
|
||||
"changed, posting message with%s value", pspec->name, obj,
|
||||
include_value ? "" : "out");
|
||||
|
||||
if (include_value && (pspec->flags & G_PARAM_READABLE) != 0) {
|
||||
g_value_init (&val, pspec->value_type);
|
||||
g_object_get_property (obj, pspec->name, &val);
|
||||
v = &val;
|
||||
} else {
|
||||
v = NULL;
|
||||
}
|
||||
gst_element_post_message (element,
|
||||
gst_message_new_property_notify (GST_OBJECT_CAST (obj), pspec->name, v));
|
||||
}
|
||||
|
||||
static void
|
||||
gst_element_property_deep_notify_cb (GstElement * element, GObject * prop_obj,
|
||||
GParamSpec * pspec, gpointer user_data)
|
||||
{
|
||||
gboolean include_value = GPOINTER_TO_INT (user_data);
|
||||
|
||||
gst_element_property_post_notify_msg (element, prop_obj, pspec,
|
||||
include_value);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_element_property_notify_cb (GObject * obj, GParamSpec * pspec,
|
||||
gpointer user_data)
|
||||
{
|
||||
gboolean include_value = GPOINTER_TO_INT (user_data);
|
||||
|
||||
gst_element_property_post_notify_msg (GST_ELEMENT_CAST (obj), obj, pspec,
|
||||
include_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_element_add_property_notify_watch:
|
||||
* @element: a #GstElement to watch for property changes
|
||||
* @property_name: (allow-none): name of property to watch for changes, or
|
||||
* NULL to watch all properties
|
||||
* @include_value: whether to include the new property value in the message
|
||||
*
|
||||
* Returns: a watch id, which can be used in connection with
|
||||
* gst_element_remove_property_notify_watch() to remove the watch again.
|
||||
*
|
||||
* Since: 1.10
|
||||
*/
|
||||
gulong
|
||||
gst_element_add_property_notify_watch (GstElement * element,
|
||||
const gchar * property_name, gboolean include_value)
|
||||
{
|
||||
const gchar *sep;
|
||||
gchar *signal_name;
|
||||
gulong id;
|
||||
|
||||
g_return_val_if_fail (GST_IS_ELEMENT (element), 0);
|
||||
|
||||
sep = (property_name != NULL) ? "::" : NULL;
|
||||
signal_name = g_strconcat ("notify", sep, property_name, NULL);
|
||||
id = g_signal_connect (element, signal_name,
|
||||
G_CALLBACK (gst_element_property_notify_cb),
|
||||
GINT_TO_POINTER (include_value));
|
||||
g_free (signal_name);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_element_add_property_deep_notify_watch:
|
||||
* @element: a #GstElement to watch (recursively) for property changes
|
||||
* @property_name: (allow-none): name of property to watch for changes, or
|
||||
* NULL to watch all properties
|
||||
* @include_value: whether to include the new property value in the message
|
||||
*
|
||||
* Returns: a watch id, which can be used in connection with
|
||||
* gst_element_remove_property_notify_watch() to remove the watch again.
|
||||
*
|
||||
* Since: 1.10
|
||||
*/
|
||||
gulong
|
||||
gst_element_add_property_deep_notify_watch (GstElement * element,
|
||||
const gchar * property_name, gboolean include_value)
|
||||
{
|
||||
const gchar *sep;
|
||||
gchar *signal_name;
|
||||
gulong id;
|
||||
|
||||
g_return_val_if_fail (GST_IS_ELEMENT (element), 0);
|
||||
|
||||
sep = (property_name != NULL) ? "::" : NULL;
|
||||
signal_name = g_strconcat ("deep-notify", sep, property_name, NULL);
|
||||
id = g_signal_connect (element, signal_name,
|
||||
G_CALLBACK (gst_element_property_deep_notify_cb),
|
||||
GINT_TO_POINTER (include_value));
|
||||
g_free (signal_name);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_element_remove_property_notify_watch:
|
||||
* @element: a #GstElement being watched for property changes
|
||||
* @watch_id: watch id to remove
|
||||
*
|
||||
* Since: 1.10
|
||||
*/
|
||||
void
|
||||
gst_element_remove_property_notify_watch (GstElement * element, gulong watch_id)
|
||||
{
|
||||
g_signal_handler_disconnect (element, watch_id);
|
||||
}
|
||||
|
|
|
@ -819,6 +819,18 @@ void gst_element_lost_state (GstElement * element);
|
|||
/* factory management */
|
||||
GstElementFactory* gst_element_get_factory (GstElement *element);
|
||||
|
||||
/* utility functions */
|
||||
gulong gst_element_add_property_notify_watch (GstElement * element,
|
||||
const gchar * property_name,
|
||||
gboolean include_value);
|
||||
|
||||
gulong gst_element_add_property_deep_notify_watch (GstElement * element,
|
||||
const gchar * property_name,
|
||||
gboolean include_value);
|
||||
|
||||
void gst_element_remove_property_notify_watch (GstElement * element,
|
||||
gulong watch_id);
|
||||
|
||||
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstElement, gst_object_unref)
|
||||
#endif
|
||||
|
|
|
@ -105,6 +105,7 @@ static GstMessageQuarks message_quarks[] = {
|
|||
{GST_MESSAGE_HAVE_CONTEXT, "have-context", 0},
|
||||
{GST_MESSAGE_DEVICE_ADDED, "device-added", 0},
|
||||
{GST_MESSAGE_DEVICE_REMOVED, "device-removed", 0},
|
||||
{GST_MESSAGE_PROPERTY_NOTIFY, "property-notify", 0},
|
||||
{0, NULL, 0}
|
||||
};
|
||||
|
||||
|
@ -2450,3 +2451,74 @@ gst_message_parse_device_removed (GstMessage * message, GstDevice ** device)
|
|||
gst_structure_id_get (GST_MESSAGE_STRUCTURE (message),
|
||||
GST_QUARK (DEVICE), GST_TYPE_DEVICE, device, NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_message_new_property_notify:
|
||||
* @src: The #GstObject whose property changed (may or may not be a #GstElement)
|
||||
* @property_name: name of the property that changed
|
||||
* @val: (allow-none) (transfer full): new property value, or %NULL
|
||||
*
|
||||
* Returns: a newly allocated #GstMessage
|
||||
*
|
||||
* Since: 1.10
|
||||
*/
|
||||
GstMessage *
|
||||
gst_message_new_property_notify (GstObject * src, const gchar * property_name,
|
||||
GValue * val)
|
||||
{
|
||||
GstStructure *structure;
|
||||
GValue name_val = G_VALUE_INIT;
|
||||
|
||||
g_return_val_if_fail (property_name != NULL, NULL);
|
||||
|
||||
structure = gst_structure_new_id_empty (GST_QUARK (MESSAGE_PROPERTY_NOTIFY));
|
||||
g_value_init (&name_val, G_TYPE_STRING);
|
||||
/* should already be interned, but let's make sure */
|
||||
g_value_set_static_string (&name_val, g_intern_string (property_name));
|
||||
gst_structure_id_take_value (structure, GST_QUARK (PROPERTY_NAME), &name_val);
|
||||
if (val != NULL)
|
||||
gst_structure_id_take_value (structure, GST_QUARK (PROPERTY_VALUE), val);
|
||||
|
||||
return gst_message_new_custom (GST_MESSAGE_PROPERTY_NOTIFY, src, structure);
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_message_parse_property_notify:
|
||||
* @message: a #GstMessage of type %GST_MESSAGE_PROPERTY_NOTIFY
|
||||
* @object: (out) (allow-none) (transfer none): location where to store a
|
||||
* pointer to the object whose property got changed, or %NULL
|
||||
* @property_name: (out) (allow-none): return location for the name of the
|
||||
* property that got changed, or %NULL
|
||||
* @property_value: (out) (allow-none): return location for the new value of
|
||||
* the property that got changed, or %NULL. This will only be set if the
|
||||
* property notify watch was told to include the value when it was set up
|
||||
*
|
||||
* Parses a property-notify message. These will be posted on the bus only
|
||||
* when set up with gst_element_add_property_notify_watch() or
|
||||
* gst_element_add_property_deep_notify_watch().
|
||||
*
|
||||
* Since: 1.10
|
||||
*/
|
||||
void
|
||||
gst_message_parse_property_notify (GstMessage * message, GstObject ** object,
|
||||
const gchar ** property_name, const GValue ** property_value)
|
||||
{
|
||||
const GstStructure *s = GST_MESSAGE_STRUCTURE (message);
|
||||
|
||||
g_return_if_fail (GST_IS_MESSAGE (message));
|
||||
g_return_if_fail (GST_MESSAGE_TYPE (message) == GST_MESSAGE_PROPERTY_NOTIFY);
|
||||
|
||||
if (object)
|
||||
*object = GST_MESSAGE_SRC (message);
|
||||
|
||||
if (property_name) {
|
||||
const GValue *name_value;
|
||||
|
||||
name_value = gst_structure_id_get_value (s, GST_QUARK (PROPERTY_NAME));
|
||||
*property_name = g_value_get_string (name_value);
|
||||
}
|
||||
|
||||
if (property_value)
|
||||
*property_value =
|
||||
gst_structure_id_get_value (s, GST_QUARK (PROPERTY_VALUE));
|
||||
}
|
||||
|
|
|
@ -108,6 +108,8 @@ typedef struct _GstMessage GstMessage;
|
|||
* a #GstDeviceProvider (Since 1.4)
|
||||
* @GST_MESSAGE_DEVICE_REMOVED: Message indicating a #GstDevice was removed
|
||||
* from a #GstDeviceProvider (Since 1.4)
|
||||
* @GST_MESSAGE_PROPERTY_NOTIFY: Message indicating a #GObject property has
|
||||
* changed (Since 1.10)
|
||||
* @GST_MESSAGE_ANY: mask for all of the above messages.
|
||||
*
|
||||
* The different message types that are available.
|
||||
|
@ -156,6 +158,7 @@ typedef enum
|
|||
GST_MESSAGE_EXTENDED = (1 << 31),
|
||||
GST_MESSAGE_DEVICE_ADDED = GST_MESSAGE_EXTENDED + 1,
|
||||
GST_MESSAGE_DEVICE_REMOVED = GST_MESSAGE_EXTENDED + 2,
|
||||
GST_MESSAGE_PROPERTY_NOTIFY = GST_MESSAGE_EXTENDED + 3,
|
||||
GST_MESSAGE_ANY = (gint) (0xffffffff)
|
||||
} GstMessageType;
|
||||
|
||||
|
@ -592,6 +595,9 @@ void gst_message_parse_device_added (GstMessage * message, GstDevice
|
|||
GstMessage * gst_message_new_device_removed (GstObject * src, GstDevice * device) G_GNUC_MALLOC;
|
||||
void gst_message_parse_device_removed (GstMessage * message, GstDevice ** device);
|
||||
|
||||
/* PROPERTY_NOTIFY */
|
||||
GstMessage * gst_message_new_property_notify (GstObject * src, const gchar * property_name, GValue * val) G_GNUC_MALLOC;
|
||||
void gst_message_parse_property_notify (GstMessage * message, GstObject ** object, const gchar ** property_name, const GValue ** property_value);
|
||||
|
||||
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstMessage, gst_message_unref)
|
||||
|
|
|
@ -70,7 +70,8 @@ static const gchar *_quark_strings[] = {
|
|||
"GstMessageNeedContext", "GstMessageHaveContext", "context", "context-type",
|
||||
"GstMessageStreamStart", "group-id", "uri-redirection",
|
||||
"GstMessageDeviceAdded", "GstMessageDeviceRemoved", "device",
|
||||
"uri-redirection-permanent"
|
||||
"uri-redirection-permanent", "GstMessagePropertyNotify", "property-name",
|
||||
"property-value"
|
||||
};
|
||||
|
||||
GQuark _priv_gst_quark_table[GST_QUARK_MAX];
|
||||
|
|
|
@ -202,7 +202,10 @@ typedef enum _GstQuarkId
|
|||
GST_QUARK_MESSAGE_DEVICE_REMOVED = 171,
|
||||
GST_QUARK_DEVICE = 172,
|
||||
GST_QUARK_URI_REDIRECTION_PERMANENT = 173,
|
||||
GST_QUARK_MAX = 174
|
||||
GST_QUARK_MESSAGE_PROPERTY_NOTIFY = 174,
|
||||
GST_QUARK_PROPERTY_NAME = 175,
|
||||
GST_QUARK_PROPERTY_VALUE = 176,
|
||||
GST_QUARK_MAX = 177
|
||||
} GstQuarkId;
|
||||
|
||||
extern GQuark _priv_gst_quark_table[GST_QUARK_MAX];
|
||||
|
|
|
@ -347,6 +347,141 @@ GST_START_TEST (test_pad_templates)
|
|||
|
||||
GST_END_TEST;
|
||||
|
||||
/* need to return the message here because object, property name and value
|
||||
* are only valid as long as we keep the message alive */
|
||||
static GstMessage *
|
||||
bus_wait_for_notify_message (GstBus * bus, GstElement ** obj,
|
||||
const gchar ** prop_name, const GValue ** val)
|
||||
{
|
||||
GstMessage *msg;
|
||||
|
||||
do {
|
||||
msg = gst_bus_timed_pop_filtered (bus, -1, GST_MESSAGE_ANY);
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_PROPERTY_NOTIFY)
|
||||
break;
|
||||
gst_message_unref (msg);
|
||||
} while (TRUE);
|
||||
|
||||
gst_message_parse_property_notify (msg, (GstObject **) obj, prop_name, val);
|
||||
return msg;
|
||||
}
|
||||
|
||||
GST_START_TEST (test_property_notify_message)
|
||||
{
|
||||
GstElement *pipeline, *identity;
|
||||
gulong watch_id0, watch_id1, watch_id2, deep_watch_id1, deep_watch_id2;
|
||||
GstBus *bus;
|
||||
|
||||
pipeline = gst_pipeline_new (NULL);
|
||||
identity = gst_element_factory_make ("identity", NULL);
|
||||
gst_bin_add (GST_BIN (pipeline), identity);
|
||||
|
||||
bus = GST_ELEMENT_BUS (pipeline);
|
||||
|
||||
/* need to set state to READY, otherwise bus will be flushing and discard
|
||||
* our messages */
|
||||
gst_element_set_state (pipeline, GST_STATE_READY);
|
||||
|
||||
watch_id0 = gst_element_add_property_notify_watch (identity, NULL, FALSE);
|
||||
|
||||
watch_id1 = gst_element_add_property_notify_watch (identity, "sync", FALSE);
|
||||
|
||||
watch_id2 = gst_element_add_property_notify_watch (identity, "silent", TRUE);
|
||||
|
||||
deep_watch_id1 =
|
||||
gst_element_add_property_deep_notify_watch (pipeline, NULL, TRUE);
|
||||
|
||||
deep_watch_id2 =
|
||||
gst_element_add_property_deep_notify_watch (pipeline, "silent", FALSE);
|
||||
|
||||
/* Now test property changes and if we get the messages we expect. We rely
|
||||
* on the signals being fired in the order that they were set up here. */
|
||||
{
|
||||
const GValue *val;
|
||||
const gchar *name;
|
||||
GstMessage *msg;
|
||||
GstElement *obj;
|
||||
|
||||
/* A - This should be picked up by... */
|
||||
g_object_set (identity, "dump", TRUE, NULL);
|
||||
/* 1) the catch-all notify on the element (no value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless (obj == identity);
|
||||
fail_unless_equals_string (name, "dump");
|
||||
fail_unless (val == NULL);
|
||||
gst_message_unref (msg);
|
||||
/* 2) the catch-all deep-notify on the pipeline (with value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless_equals_string (name, "dump");
|
||||
fail_unless (obj == identity);
|
||||
fail_unless (G_VALUE_HOLDS_BOOLEAN (val));
|
||||
fail_unless_equals_int (g_value_get_boolean (val), TRUE);
|
||||
gst_message_unref (msg);
|
||||
|
||||
/* B - This should be picked up by... */
|
||||
g_object_set (identity, "sync", TRUE, NULL);
|
||||
/* 1) the catch-all notify on the element (no value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless (obj == identity);
|
||||
fail_unless_equals_string (name, "sync");
|
||||
fail_unless (val == NULL);
|
||||
gst_message_unref (msg);
|
||||
/* 2) the "sync" notify on the element (no value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless (obj == identity);
|
||||
fail_unless_equals_string (name, "sync");
|
||||
fail_unless (val == NULL);
|
||||
gst_message_unref (msg);
|
||||
/* 3) the catch-all deep-notify on the pipeline (with value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless_equals_string (name, "sync");
|
||||
fail_unless (obj == identity);
|
||||
fail_unless (G_VALUE_HOLDS_BOOLEAN (val));
|
||||
fail_unless_equals_int (g_value_get_boolean (val), TRUE);
|
||||
gst_message_unref (msg);
|
||||
|
||||
/* C - This should be picked up by... */
|
||||
g_object_set (identity, "silent", FALSE, NULL);
|
||||
/* 1) the catch-all notify on the element (no value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless (obj == identity);
|
||||
fail_unless_equals_string (name, "silent");
|
||||
fail_unless (val == NULL);
|
||||
gst_message_unref (msg);
|
||||
/* 2) the "silent" notify on the element (with value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless (obj == identity);
|
||||
fail_unless_equals_string (name, "silent");
|
||||
fail_unless (val != NULL);
|
||||
fail_unless (G_VALUE_HOLDS_BOOLEAN (val));
|
||||
fail_unless_equals_int (g_value_get_boolean (val), FALSE);
|
||||
gst_message_unref (msg);
|
||||
/* 3) the catch-all deep-notify on the pipeline (with value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless_equals_string (name, "silent");
|
||||
fail_unless (obj == identity);
|
||||
fail_unless (G_VALUE_HOLDS_BOOLEAN (val));
|
||||
fail_unless_equals_int (g_value_get_boolean (val), FALSE);
|
||||
gst_message_unref (msg);
|
||||
/* 4) the "silent" deep-notify on the pipeline (without value) */
|
||||
msg = bus_wait_for_notify_message (bus, &obj, &name, &val);
|
||||
fail_unless_equals_string (name, "silent");
|
||||
fail_unless (obj == identity);
|
||||
fail_unless (val == NULL);
|
||||
gst_message_unref (msg);
|
||||
}
|
||||
|
||||
gst_element_remove_property_notify_watch (identity, watch_id0);
|
||||
gst_element_remove_property_notify_watch (identity, watch_id1);
|
||||
gst_element_remove_property_notify_watch (identity, watch_id2);
|
||||
gst_element_remove_property_notify_watch (pipeline, deep_watch_id1);
|
||||
gst_element_remove_property_notify_watch (pipeline, deep_watch_id2);
|
||||
gst_element_set_state (pipeline, GST_STATE_NULL);
|
||||
gst_object_unref (pipeline);
|
||||
}
|
||||
|
||||
GST_END_TEST;
|
||||
|
||||
static Suite *
|
||||
gst_element_suite (void)
|
||||
{
|
||||
|
@ -360,6 +495,7 @@ gst_element_suite (void)
|
|||
tcase_add_test (tc_chain, test_link);
|
||||
tcase_add_test (tc_chain, test_link_no_pads);
|
||||
tcase_add_test (tc_chain, test_pad_templates);
|
||||
tcase_add_test (tc_chain, test_property_notify_message);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
|
|
@ -476,6 +476,8 @@ EXPORTS
|
|||
gst_double_range_get_type
|
||||
gst_element_abort_state
|
||||
gst_element_add_pad
|
||||
gst_element_add_property_deep_notify_watch
|
||||
gst_element_add_property_notify_watch
|
||||
gst_element_change_state
|
||||
gst_element_class_add_metadata
|
||||
gst_element_class_add_pad_template
|
||||
|
@ -545,6 +547,7 @@ EXPORTS
|
|||
gst_element_register
|
||||
gst_element_release_request_pad
|
||||
gst_element_remove_pad
|
||||
gst_element_remove_property_notify_watch
|
||||
gst_element_request_pad
|
||||
gst_element_seek
|
||||
gst_element_seek_simple
|
||||
|
@ -712,6 +715,7 @@ EXPORTS
|
|||
gst_message_new_need_context
|
||||
gst_message_new_new_clock
|
||||
gst_message_new_progress
|
||||
gst_message_new_property_notify
|
||||
gst_message_new_qos
|
||||
gst_message_new_request_state
|
||||
gst_message_new_reset_time
|
||||
|
@ -741,6 +745,7 @@ EXPORTS
|
|||
gst_message_parse_info
|
||||
gst_message_parse_new_clock
|
||||
gst_message_parse_progress
|
||||
gst_message_parse_property_notify
|
||||
gst_message_parse_qos
|
||||
gst_message_parse_qos_stats
|
||||
gst_message_parse_qos_values
|
||||
|
|
Loading…
Reference in a new issue