mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-27 04:01:08 +00:00
avtp: Introduce the CRF Check element
This commit introduces the AVTP Clock Reference Format (CRF) Checker element. This element re-uses the GstAvtpCrfBase class introduced along with the CRF Synchronizer element. This element will typically be used along with the avtpsrc element to ensure that the AVTP timestamp (and H264 timestamp in case of CVF-H264 packets) is "aligned" with the incoming CRF stream. Here, "aligned" means that the timestamp value should be within 25% of the period of the media clock recovered from the CRF stream. The user can also set an option (drop-invalid) in order to drop any packet whose timestamp is not within the thresholds of the incoming CRF stream.
This commit is contained in:
parent
12ad2a4bcd
commit
e47fa2006f
4 changed files with 333 additions and 5 deletions
|
@ -92,8 +92,10 @@
|
||||||
* systems.
|
* systems.
|
||||||
*
|
*
|
||||||
* This can be achieved by using the avtpcrfsync element which implements CRF
|
* This can be achieved by using the avtpcrfsync element which implements CRF
|
||||||
* as described in Chapter 10 of IEEE 1722-2016. For further details, look at
|
* as described in Chapter 10 of IEEE 1722-2016. avtpcrfcheck can also be used
|
||||||
* the documentation for avtpcrfsync.
|
* to validate that the adjustment conforms to the criteria specified in the
|
||||||
|
* spec. For further details, look at the documentation for the respective
|
||||||
|
* elements.
|
||||||
*
|
*
|
||||||
* ### Traffic Control Setup
|
* ### Traffic Control Setup
|
||||||
*
|
*
|
||||||
|
@ -147,9 +149,9 @@
|
||||||
* to several elements. Basic properties are:
|
* to several elements. Basic properties are:
|
||||||
*
|
*
|
||||||
* * streamid (avtpaafpay, avtpcvfpay, avtpaafdepay, avtpcvfdepay,
|
* * streamid (avtpaafpay, avtpcvfpay, avtpaafdepay, avtpcvfdepay,
|
||||||
* avtpcrfsync): Stream ID associated with the stream.
|
* avtpcrfsync, avtpcrfcheck): Stream ID associated with the stream.
|
||||||
*
|
*
|
||||||
* * ifname (avtpsink, avtpsrc, avtpcrfsync): Network interface
|
* * ifname (avtpsink, avtpsrc, avtpcrfsync, avtpcrfcheck): Network interface
|
||||||
* used to send/receive AVTP packets.
|
* used to send/receive AVTP packets.
|
||||||
*
|
*
|
||||||
* * dst-macaddr (avtpsink, avtpsrc): Destination MAC address for the stream.
|
* * dst-macaddr (avtpsink, avtpsrc): Destination MAC address for the stream.
|
||||||
|
@ -195,7 +197,8 @@
|
||||||
* On the AVTP listener host, the following pipeline can be used to get the
|
* On the AVTP listener host, the following pipeline can be used to get the
|
||||||
* AVTP stream, depacketize it and show it on the screen:
|
* AVTP stream, depacketize it and show it on the screen:
|
||||||
*
|
*
|
||||||
* $ gst-launch-1.0 -k ptp avtpsrc ifname=$IFNAME ! avtpcvfdepay ! \
|
* $ gst-launch-1.0 -k ptp avtpsrc ifname=$IFNAME ! \
|
||||||
|
* avtpcrfcheck ifname=$IFNAME ! avtpcvfdepay ! \
|
||||||
* vaapih264dec ! videoconvert ! clockoverlay halignment=right ! \
|
* vaapih264dec ! videoconvert ! clockoverlay halignment=right ! \
|
||||||
* queue ! autovideosink
|
* queue ! autovideosink
|
||||||
*
|
*
|
||||||
|
@ -237,6 +240,7 @@
|
||||||
#include "gstavtpsink.h"
|
#include "gstavtpsink.h"
|
||||||
#include "gstavtpsrc.h"
|
#include "gstavtpsrc.h"
|
||||||
#include "gstavtpcrfsync.h"
|
#include "gstavtpcrfsync.h"
|
||||||
|
#include "gstavtpcrfcheck.h"
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
plugin_init (GstPlugin * plugin)
|
plugin_init (GstPlugin * plugin)
|
||||||
|
@ -255,6 +259,8 @@ plugin_init (GstPlugin * plugin)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
if (!gst_avtp_crf_sync_plugin_init (plugin))
|
if (!gst_avtp_crf_sync_plugin_init (plugin))
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
if (!gst_avtp_crf_check_plugin_init (plugin))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
263
ext/avtp/gstavtpcrfcheck.c
Normal file
263
ext/avtp/gstavtpcrfcheck.c
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
/*
|
||||||
|
* GStreamer AVTP Plugin
|
||||||
|
* Copyright (C) 2019 Intel Corporation
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Library General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Library General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Library General Public
|
||||||
|
* License along with this library; if not, write to the
|
||||||
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SECTION:element-avtpcrfcheck
|
||||||
|
* @see_also: avtpcrfsync
|
||||||
|
*
|
||||||
|
* Validate whether the presentation time for the AVTPDU aligns with the CRF
|
||||||
|
* stream. For detailed information see Chapter 10 of
|
||||||
|
* https://standards.ieee.org/standard/1722-2016.html.
|
||||||
|
*
|
||||||
|
* <refsect2>
|
||||||
|
* <title>Example pipeline</title>
|
||||||
|
* |[
|
||||||
|
* gst-launch-1.0 avtpsrc ! avtpcrfcheck ! avtpaafdepay ! autoaudiosink
|
||||||
|
* ]| This example pipeline will validate AVTP timestamps for AVTPDUs. Refer to
|
||||||
|
* the avtpcrfsync to adjust the AVTP timestamps for the packet.
|
||||||
|
* </refsect2>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <avtp.h>
|
||||||
|
#include <avtp_crf.h>
|
||||||
|
#include <avtp_cvf.h>
|
||||||
|
#include <glib.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
#include "gstavtpcrfbase.h"
|
||||||
|
#include "gstavtpcrfcheck.h"
|
||||||
|
#include "gstavtpcrfutil.h"
|
||||||
|
|
||||||
|
GST_DEBUG_CATEGORY_STATIC (avtpcrfcheck_debug);
|
||||||
|
#define GST_CAT_DEFAULT (avtpcrfcheck_debug)
|
||||||
|
|
||||||
|
#define DEFAULT_DROP_INVALID FALSE
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
PROP_0,
|
||||||
|
PROP_DROP_INVALID,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define gst_avtp_crf_check_parent_class parent_class
|
||||||
|
G_DEFINE_TYPE (GstAvtpCrfCheck, gst_avtp_crf_check, GST_TYPE_AVTP_CRF_BASE);
|
||||||
|
|
||||||
|
static void gst_avtp_crf_check_set_property (GObject * object, guint prop_id,
|
||||||
|
const GValue * value, GParamSpec * pspec);
|
||||||
|
static void gst_avtp_crf_check_get_property (GObject * object, guint prop_id,
|
||||||
|
GValue * value, GParamSpec * pspec);
|
||||||
|
static GstFlowReturn gst_avtp_crf_check_transform_ip (GstBaseTransform * parent,
|
||||||
|
GstBuffer * buffer);
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_avtp_crf_check_class_init (GstAvtpCrfCheckClass * klass)
|
||||||
|
{
|
||||||
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
||||||
|
GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
|
||||||
|
|
||||||
|
gst_element_class_set_static_metadata (element_class,
|
||||||
|
"Clock Reference Format (CRF) Checker",
|
||||||
|
"Filter/Network/AVTP",
|
||||||
|
"Check if the AVTP presentation time is synchronized with clock provided by a CRF stream",
|
||||||
|
"Vedang Patel <vedang.patel@intel.com>");
|
||||||
|
|
||||||
|
object_class->get_property =
|
||||||
|
GST_DEBUG_FUNCPTR (gst_avtp_crf_check_get_property);
|
||||||
|
object_class->set_property =
|
||||||
|
GST_DEBUG_FUNCPTR (gst_avtp_crf_check_set_property);
|
||||||
|
GST_BASE_TRANSFORM_CLASS (klass)->transform_ip =
|
||||||
|
GST_DEBUG_FUNCPTR (gst_avtp_crf_check_transform_ip);
|
||||||
|
|
||||||
|
g_object_class_install_property (object_class, PROP_DROP_INVALID,
|
||||||
|
g_param_spec_boolean ("drop-invalid", "Drop invalid packets",
|
||||||
|
"Drop the packets which are not within 25%% of the sample period of the CRF timestamps",
|
||||||
|
DEFAULT_DROP_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||||
|
|
||||||
|
GST_DEBUG_CATEGORY_INIT (avtpcrfcheck_debug, "avtpcrfcheck", 0,
|
||||||
|
"CRF Checker");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_avtp_crf_check_init (GstAvtpCrfCheck * avtpcrfcheck)
|
||||||
|
{
|
||||||
|
avtpcrfcheck->drop_invalid = DEFAULT_DROP_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_avtp_crf_check_set_property (GObject * object, guint prop_id,
|
||||||
|
const GValue * value, GParamSpec * pspec)
|
||||||
|
{
|
||||||
|
GstAvtpCrfCheck *avtpcrfcheck = GST_AVTP_CRF_CHECK (object);
|
||||||
|
|
||||||
|
GST_DEBUG_OBJECT (avtpcrfcheck, "prop_id %u", prop_id);
|
||||||
|
|
||||||
|
switch (prop_id) {
|
||||||
|
case PROP_DROP_INVALID:
|
||||||
|
avtpcrfcheck->drop_invalid = g_value_get_boolean (value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_avtp_crf_check_get_property (GObject * object, guint prop_id,
|
||||||
|
GValue * value, GParamSpec * pspec)
|
||||||
|
{
|
||||||
|
GstAvtpCrfCheck *avtpcrfcheck = GST_AVTP_CRF_CHECK (object);
|
||||||
|
|
||||||
|
GST_DEBUG_OBJECT (avtpcrfcheck, "prop_id %u", prop_id);
|
||||||
|
|
||||||
|
switch (prop_id) {
|
||||||
|
case PROP_DROP_INVALID:
|
||||||
|
g_value_set_boolean (value, avtpcrfcheck->drop_invalid);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
post_qos_message (GstBaseTransform * parent, GstBuffer * buffer)
|
||||||
|
{
|
||||||
|
GstAvtpCrfBase *avtpcrfbase = GST_AVTP_CRF_BASE (parent);
|
||||||
|
guint64 running_time =
|
||||||
|
gst_segment_to_running_time (&avtpcrfbase->element.segment,
|
||||||
|
GST_FORMAT_TIME, GST_BUFFER_DTS_OR_PTS (buffer));
|
||||||
|
guint64 stream_time =
|
||||||
|
gst_segment_to_running_time (&avtpcrfbase->element.segment,
|
||||||
|
GST_FORMAT_TIME, GST_BUFFER_DTS_OR_PTS (buffer));
|
||||||
|
guint64 timestamp = GST_BUFFER_DTS_OR_PTS (buffer);
|
||||||
|
guint64 duration = GST_BUFFER_DURATION (buffer);
|
||||||
|
|
||||||
|
GstMessage *qos_msg =
|
||||||
|
gst_message_new_qos (GST_OBJECT (parent), FALSE, running_time,
|
||||||
|
stream_time, timestamp, duration);
|
||||||
|
gst_element_post_message (GST_ELEMENT (parent), qos_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GstFlowReturn
|
||||||
|
gst_avtp_crf_check_transform_ip (GstBaseTransform * parent, GstBuffer * buffer)
|
||||||
|
{
|
||||||
|
GstAvtpCrfBase *avtpcrfbase = GST_AVTP_CRF_BASE (parent);
|
||||||
|
GstAvtpCrfCheck *avtpcrfcheck = GST_AVTP_CRF_CHECK (avtpcrfbase);
|
||||||
|
GstAvtpCrfThreadData *thread_data = &avtpcrfbase->thread_data;
|
||||||
|
GstClockTime current_ts = thread_data->current_ts;
|
||||||
|
gdouble avg_period = thread_data->average_period;
|
||||||
|
GstClockTime tstamp, adjusted_tstamp;
|
||||||
|
struct avtp_stream_pdu *pdu;
|
||||||
|
GstClockTime h264_time;
|
||||||
|
GstMapInfo info;
|
||||||
|
gboolean res;
|
||||||
|
|
||||||
|
if (!avg_period || !current_ts)
|
||||||
|
return GST_FLOW_OK;
|
||||||
|
|
||||||
|
res = gst_buffer_map (buffer, &info, GST_MAP_READ);
|
||||||
|
if (!res) {
|
||||||
|
GST_ELEMENT_ERROR (avtpcrfcheck, RESOURCE, OPEN_WRITE,
|
||||||
|
("cannot access buffer"), (NULL));
|
||||||
|
return GST_FLOW_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buffer_size_valid (&info)) {
|
||||||
|
GST_DEBUG_OBJECT (avtpcrfcheck, "Malformed AVTPDU, discarding it");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdu = (struct avtp_stream_pdu *) info.data;
|
||||||
|
|
||||||
|
if (h264_tstamp_valid (pdu)) {
|
||||||
|
GstClockTime adjusted_h264_time;
|
||||||
|
|
||||||
|
res = avtp_cvf_pdu_get (pdu, AVTP_CVF_FIELD_H264_TIMESTAMP, &h264_time);
|
||||||
|
g_assert (res == 0);
|
||||||
|
/* Extrapolate tstamp to 64 bit and assume it's greater than CRF timestamp. */
|
||||||
|
h264_time |= current_ts & 0xFFFFFFFF00000000;
|
||||||
|
if (h264_time < current_ts)
|
||||||
|
h264_time += (1ULL << 32);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* float typecasted to guint64 truncates the decimal part. So, round() it
|
||||||
|
* before casting.
|
||||||
|
*/
|
||||||
|
adjusted_h264_time =
|
||||||
|
(GstClockTime) roundl (current_ts + roundl ((h264_time -
|
||||||
|
current_ts) / avg_period) * avg_period);
|
||||||
|
|
||||||
|
if (llabs ((gint64) adjusted_h264_time - (gint64) h264_time) >
|
||||||
|
0.25 * thread_data->average_period) {
|
||||||
|
GST_LOG_OBJECT (avtpcrfcheck,
|
||||||
|
"H264 timestamp not synchronized. Expected: %" G_GUINT64_FORMAT
|
||||||
|
" Actual: %" G_GUINT64_FORMAT,
|
||||||
|
adjusted_h264_time & 0xFFFFFFFF, h264_time & 0xFFFFFFFF);
|
||||||
|
if (avtpcrfcheck->drop_invalid) {
|
||||||
|
post_qos_message (parent, buffer);
|
||||||
|
gst_buffer_unmap (buffer, &info);
|
||||||
|
return GST_BASE_TRANSFORM_FLOW_DROPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tstamp = get_avtp_tstamp (avtpcrfbase, pdu);
|
||||||
|
if (tstamp == GST_CLOCK_TIME_NONE)
|
||||||
|
goto exit;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extrapolate the 32-bit AVTP Timestamp to 64-bit and assume it's greater
|
||||||
|
* than the 64-bit CRF timestamp.
|
||||||
|
*/
|
||||||
|
tstamp |= current_ts & 0xFFFFFFFF00000000;
|
||||||
|
if (tstamp < current_ts)
|
||||||
|
tstamp += (1ULL << 32);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* float typecasted to guint64 truncates the decimal part. So, round() it
|
||||||
|
* before casting.
|
||||||
|
*/
|
||||||
|
adjusted_tstamp =
|
||||||
|
(GstClockTime) roundl (current_ts + roundl ((tstamp -
|
||||||
|
current_ts) / avg_period) * avg_period);
|
||||||
|
if (llabs ((gint64) adjusted_tstamp - (gint64) tstamp) >
|
||||||
|
0.25 * thread_data->average_period) {
|
||||||
|
GST_LOG_OBJECT (avtpcrfcheck,
|
||||||
|
"AVTP Timestamp not synchronized. Expected: %" G_GUINT64_FORMAT
|
||||||
|
" Actual: %" G_GUINT64_FORMAT,
|
||||||
|
adjusted_tstamp & 0xFFFFFFFF, tstamp & 0xFFFFFFFF);
|
||||||
|
if (avtpcrfcheck->drop_invalid) {
|
||||||
|
post_qos_message (parent, buffer);
|
||||||
|
gst_buffer_unmap (buffer, &info);
|
||||||
|
return GST_BASE_TRANSFORM_FLOW_DROPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit:
|
||||||
|
gst_buffer_unmap (buffer, &info);
|
||||||
|
return GST_FLOW_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
gst_avtp_crf_check_plugin_init (GstPlugin * plugin)
|
||||||
|
{
|
||||||
|
return gst_element_register (plugin, "avtpcrfcheck", GST_RANK_NONE,
|
||||||
|
GST_TYPE_AVTP_CRF_CHECK);
|
||||||
|
}
|
58
ext/avtp/gstavtpcrfcheck.h
Normal file
58
ext/avtp/gstavtpcrfcheck.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* GStreamer AVTP Plugin
|
||||||
|
* Copyright (C) 2019 Intel Corporation
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __GST_AVTP_CRF_CHECK_H__
|
||||||
|
#define __GST_AVTP_CRF_CHECK_H__
|
||||||
|
|
||||||
|
#include <gst/gst.h>
|
||||||
|
|
||||||
|
#include "gstavtpcrfbase.h"
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
#define GST_TYPE_AVTP_CRF_CHECK (gst_avtp_crf_check_get_type())
|
||||||
|
#define GST_AVTP_CRF_CHECK(obj) \
|
||||||
|
(G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AVTP_CRF_CHECK,GstAvtpCrfCheck))
|
||||||
|
#define GST_AVTP_CRF_CHECK_CLASS(klass) \
|
||||||
|
(G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AVTP_CRF_CHECK,GstAvtpCrfCheckClass))
|
||||||
|
#define GST_IS_AVTP_CRF_CHECK(obj) \
|
||||||
|
(G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AVTP_CRF_CHECK))
|
||||||
|
#define GST_IS_AVTP_CRF_CHECK_CLASS(klass) \
|
||||||
|
(G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AVTP_CRF_CHECK))
|
||||||
|
typedef struct _GstAvtpCrfCheck GstAvtpCrfCheck;
|
||||||
|
typedef struct _GstAvtpCrfCheckClass GstAvtpCrfCheckClass;
|
||||||
|
|
||||||
|
struct _GstAvtpCrfCheck
|
||||||
|
{
|
||||||
|
GstAvtpCrfBase avtpcrfbase;
|
||||||
|
|
||||||
|
gboolean drop_invalid;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct _GstAvtpCrfCheckClass
|
||||||
|
{
|
||||||
|
GstAvtpCrfBaseClass parent_class;
|
||||||
|
};
|
||||||
|
|
||||||
|
GType gst_avtp_crf_check_get_type (void);
|
||||||
|
|
||||||
|
gboolean gst_avtp_crf_check_plugin_init (GstPlugin * plugin);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
#endif /* __GST_AVTP_CRF_CHECK_H__ */
|
|
@ -11,6 +11,7 @@ avtp_sources = [
|
||||||
'gstavtpcrfutil.c',
|
'gstavtpcrfutil.c',
|
||||||
'gstavtpcrfbase.c',
|
'gstavtpcrfbase.c',
|
||||||
'gstavtpcrfsync.c',
|
'gstavtpcrfsync.c',
|
||||||
|
'gstavtpcrfcheck.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
avtp_dep = dependency('avtp', required: get_option('avtp'))
|
avtp_dep = dependency('avtp', required: get_option('avtp'))
|
||||||
|
|
Loading…
Reference in a new issue