Commit be0e73ee authored by Wim Taymans's avatar Wim Taymans
Browse files

gst/rtp/gstrtph264depay.*: Add experimental support for outputting...

gst/rtp/gstrtph264depay.*: Add experimental support for outputting quicktime-like AVC output in addition to the exist...

Original commit message from CVS:
* gst/rtp/gstrtph264depay.c: (gst_rtp_h264_depay_class_init),
(gst_rtp_h264_depay_init), (gst_rtp_h264_depay_set_property),
(gst_rtp_h264_depay_get_property), (gst_rtp_h264_depay_setcaps),
(gst_rtp_h264_depay_process):
* gst/rtp/gstrtph264depay.h:
Add experimental support for outputting quicktime-like AVC output in
addition to the existing bytestream output.
* gst/rtp/gstrtph264pay.c: (gst_h264_scan_mode_get_type),
(gst_rtp_h264_pay_class_init), (gst_rtp_h264_pay_init),
(gst_rtp_h264_pay_setcaps), (gst_rtp_h264_pay_payload_nal),
(gst_rtp_h264_pay_handle_buffer), (gst_rtp_h264_pay_set_property),
(gst_rtp_h264_pay_get_property):
* gst/rtp/gstrtph264pay.h:
Make the parsing mode configurable, for some inputs we don't need to
scan every byte for start codes.
Only set the marker bit on ACCESS units.
parent 3d3f7cd6
2008-05-20 Wim Taymans <wim.taymans@collabora.co.uk>
* gst/rtp/gstrtph264depay.c: (gst_rtp_h264_depay_class_init),
(gst_rtp_h264_depay_init), (gst_rtp_h264_depay_set_property),
(gst_rtp_h264_depay_get_property), (gst_rtp_h264_depay_setcaps),
(gst_rtp_h264_depay_process):
* gst/rtp/gstrtph264depay.h:
Add experimental support for outputting quicktime-like AVC output in
addition to the existing bytestream output.
* gst/rtp/gstrtph264pay.c: (gst_h264_scan_mode_get_type),
(gst_rtp_h264_pay_class_init), (gst_rtp_h264_pay_init),
(gst_rtp_h264_pay_setcaps), (gst_rtp_h264_pay_payload_nal),
(gst_rtp_h264_pay_handle_buffer), (gst_rtp_h264_pay_set_property),
(gst_rtp_h264_pay_get_property):
* gst/rtp/gstrtph264pay.h:
Make the parsing mode configurable, for some inputs we don't need to
scan every byte for start codes.
Only set the marker bit on ACCESS units.
2008-05-20 Sebastian Dröge <slomo@circular-chaos.org>
 
* gst/equalizer/gstiirequalizer.c:
......
......@@ -29,6 +29,16 @@
GST_DEBUG_CATEGORY_STATIC (rtph264depay_debug);
#define GST_CAT_DEFAULT (rtph264depay_debug)
#define DEFAULT_BYTE_STREAM TRUE
enum
{
PROP_0,
PROP_BYTE_STREAM,
PROP_LAST
};
/* 3 zero bytes syncword */
static const guint8 sync_bytes[] = { 0, 0, 0, 1 };
......@@ -77,6 +87,10 @@ GST_BOILERPLATE (GstRtpH264Depay, gst_rtp_h264_depay, GstBaseRTPDepayload,
GST_TYPE_BASE_RTP_DEPAYLOAD);
static void gst_rtp_h264_depay_finalize (GObject * object);
static void gst_rtp_h264_depay_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_rtp_h264_depay_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static GstStateChangeReturn gst_rtp_h264_depay_change_state (GstElement *
element, GstStateChange transition);
......@@ -112,6 +126,14 @@ gst_rtp_h264_depay_class_init (GstRtpH264DepayClass * klass)
gobject_class->finalize = gst_rtp_h264_depay_finalize;
gobject_class->set_property = gst_rtp_h264_depay_set_property;
gobject_class->get_property = gst_rtp_h264_depay_get_property;
g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_BYTE_STREAM,
g_param_spec_boolean ("byte-stream", "Byte Stream",
"Generate byte stream format of NALU", DEFAULT_BYTE_STREAM,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gstelement_class->change_state = gst_rtp_h264_depay_change_state;
gstbasertpdepayload_class->process = gst_rtp_h264_depay_process;
......@@ -126,6 +148,7 @@ gst_rtp_h264_depay_init (GstRtpH264Depay * rtph264depay,
GstRtpH264DepayClass * klass)
{
rtph264depay->adapter = gst_adapter_new ();
rtph264depay->byte_stream = DEFAULT_BYTE_STREAM;
}
static void
......@@ -143,6 +166,42 @@ gst_rtp_h264_depay_finalize (GObject * object)
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_rtp_h264_depay_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstRtpH264Depay *rtph264depay;
rtph264depay = GST_RTP_H264_DEPAY (object);
switch (prop_id) {
case PROP_BYTE_STREAM:
rtph264depay->byte_stream = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_rtp_h264_depay_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstRtpH264Depay *rtph264depay;
rtph264depay = GST_RTP_H264_DEPAY (object);
switch (prop_id) {
case PROP_BYTE_STREAM:
g_value_set_boolean (value, rtph264depay->byte_stream);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static const guint8 a2bin[256] = {
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
......@@ -198,6 +257,9 @@ gst_rtp_h264_depay_setcaps (GstBaseRTPDepayload * depayload, GstCaps * caps)
gint clock_rate = 90000;
GstStructure *structure = gst_caps_get_structure (caps, 0);
GstRtpH264Depay *rtph264depay;
const gchar *ps, *profile;
GstBuffer *codec_data;
guint8 *b64;
rtph264depay = GST_RTP_H264_DEPAY (depayload);
......@@ -206,16 +268,18 @@ gst_rtp_h264_depay_setcaps (GstBaseRTPDepayload * depayload, GstCaps * caps)
srccaps = gst_caps_new_simple ("video/x-h264", NULL);
if (gst_structure_has_field (structure, "sprop-parameter-sets")) {
const gchar *ps;
/* Base64 encoded, comma separated config NALs */
ps = gst_structure_get_string (structure, "sprop-parameter-sets");
/* hex: AVCProfileIndication:8 | profile_compat:8 | AVCLevelIndication:8 */
profile = gst_structure_get_string (structure, "profile-level-id");
if (rtph264depay->byte_stream && ps != NULL) {
/* for bytestream we only need the parameter sets but we don't error out
* when they are not there, we assume they are in the stream. */
gchar **params;
guint len, total;
gint i;
GstBuffer *codec_data;
guint8 *b64;
/* Base64 encoded, comma separated config NALs */
ps = gst_structure_get_string (structure, "sprop-parameter-sets");
params = g_strsplit (ps, ",", 0);
/* count total number of bytes in base64. Also include the sync bytes in
......@@ -246,14 +310,106 @@ gst_rtp_h264_depay_setcaps (GstBaseRTPDepayload * depayload, GstCaps * caps)
if (rtph264depay->codec_data)
gst_buffer_unref (rtph264depay->codec_data);
rtph264depay->codec_data = codec_data;
} else if (!rtph264depay->byte_stream) {
gchar **params;
guint8 **sps, **pps;
guint len, num_sps, num_pps;
gint i;
guint8 *data;
guint32 profile_id;
if (ps == NULL || profile == NULL)
goto incomplete_caps;
params = g_strsplit (ps, ",", 0);
len = g_strv_length (params);
GST_DEBUG_OBJECT (depayload, "we have %d params", len);
sps = g_new0 (guint8 *, len + 1);
pps = g_new0 (guint8 *, len + 1);
num_sps = num_pps = 0;
/* start with 7 bytes header */
len = 7;
for (i = 0; params[i]; i++) {
gint nal_len;
guint8 *nalp;
nal_len = strlen (params[i]);
nalp = g_malloc (nal_len + 2);
nal_len = decode_base64 (params[i], nalp + 2);
nalp[0] = (nal_len >> 8) & 0xff;
nalp[1] = nal_len & 0xff;
len += nal_len + 2;
/* copy to the right list */
if ((nalp[2] & 0x1f) == 7) {
GST_DEBUG_OBJECT (depayload, "adding param %d as SPS %d", i, num_sps);
sps[num_sps++] = nalp;
} else {
GST_DEBUG_OBJECT (depayload, "adding param %d as PPS %d", i, num_pps);
pps[num_pps++] = nalp;
}
}
g_strfreev (params);
codec_data = gst_buffer_new_and_alloc (len);
data = GST_BUFFER_DATA (codec_data);
/* 8 bits version == 1 */
*data++ = 1;
/* hex: AVCProfileIndication:8 | profile_compat:8 | AVCLevelIndication:8 */
sscanf (profile, "%6x", &profile_id);
*data++ = (profile_id >> 16) & 0xff;
*data++ = (profile_id >> 8) & 0xff;
*data++ = profile_id & 0xff;
/* 6 bits reserved | 2 bits lengthSizeMinusOn */
*data++ = 0xff;
/* 3 bits reserved | 5 bits numOfSequenceParameterSets */
*data++ = 0xe0 | (num_sps & 0x1f);
/* copy all SPS */
for (i = 0; sps[i]; i++) {
len = ((sps[i][0] << 8) | sps[i][1]) + 2;
GST_DEBUG_OBJECT (depayload, "copy SPS %d of length %d", i, len);
memcpy (data, sps[i], len);
g_free (sps[i]);
data += len;
}
g_free (sps);
/* 8 bits numOfPictureParameterSets */
*data++ = num_pps;
/* copy all PPS */
for (i = 0; pps[i]; i++) {
len = ((pps[i][0] << 8) | pps[i][1]) + 2;
GST_DEBUG_OBJECT (depayload, "copy PPS %d of length %d", i, len);
memcpy (data, pps[i], len);
g_free (pps[i]);
data += len;
}
g_free (pps);
GST_BUFFER_SIZE (codec_data) = data - GST_BUFFER_DATA (codec_data);
gst_caps_set_simple (srccaps,
"codec_data", GST_TYPE_BUFFER, codec_data, NULL);
}
gst_pad_set_caps (depayload->srcpad, srccaps);
gst_caps_unref (srccaps);
return TRUE;
/* ERRORS */
incomplete_caps:
{
GST_DEBUG_OBJECT (depayload, "we have incomplete caps");
return FALSE;
}
}
/* FIXME, non-bytestream handling is freaking out ffmpeg. Apparently we need to
* group all NAL units belonging to one frame together */
static GstBuffer *
gst_rtp_h264_depay_process (GstBaseRTPDepayload * depayload, GstBuffer * buf)
{
......@@ -337,17 +493,24 @@ gst_rtp_h264_depay_process (GstBaseRTPDepayload * depayload, GstBuffer * buf)
*/
nalu_size = (payload[0] << 8) | payload[1];
/* strip NALU size */
payload += 2;
payload_len -= 2;
if (nalu_size > payload_len)
nalu_size = payload_len;
outsize = nalu_size + sizeof (sync_bytes);
outbuf = gst_buffer_new_and_alloc (outsize);
outdata = GST_BUFFER_DATA (outbuf);
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
if (rtph264depay->byte_stream) {
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
} else {
outdata[0] = outdata[1] = 0;
outdata[2] = payload[0];
outdata[3] = payload[1];
}
/* strip NALU size */
payload += 2;
payload_len -= 2;
outdata += sizeof (sync_bytes);
memcpy (outdata, payload, nalu_size);
......@@ -414,7 +577,6 @@ gst_rtp_h264_depay_process (GstBaseRTPDepayload * depayload, GstBuffer * buf)
outsize = nalu_size + sizeof (sync_bytes);
outbuf = gst_buffer_new_and_alloc (outsize);
outdata = GST_BUFFER_DATA (outbuf);
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
outdata += sizeof (sync_bytes);
memcpy (outdata, payload, nalu_size);
outdata[0] = nal_header;
......@@ -445,7 +607,17 @@ gst_rtp_h264_depay_process (GstBaseRTPDepayload * depayload, GstBuffer * buf)
outsize = gst_adapter_available (rtph264depay->adapter);
outbuf = gst_adapter_take_buffer (rtph264depay->adapter, outsize);
outdata = GST_BUFFER_DATA (outbuf);
if (rtph264depay->byte_stream) {
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
} else {
outsize -= 4;
outdata[0] = (outsize >> 24);
outdata[1] = (outsize >> 16);
outdata[2] = (outsize >> 8);
outdata[3] = (outsize);
}
gst_buffer_set_caps (outbuf, GST_PAD_CAPS (depayload->srcpad));
/* push codec_data first */
......@@ -469,7 +641,13 @@ gst_rtp_h264_depay_process (GstBaseRTPDepayload * depayload, GstBuffer * buf)
outsize = nalu_size + sizeof (sync_bytes);
outbuf = gst_buffer_new_and_alloc (outsize);
outdata = GST_BUFFER_DATA (outbuf);
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
if (rtph264depay->byte_stream) {
memcpy (outdata, sync_bytes, sizeof (sync_bytes));
} else {
outdata[0] = outdata[1] = 0;
outdata[2] = nalu_size >> 8;
outdata[3] = nalu_size & 0xff;
}
outdata += sizeof (sync_bytes);
memcpy (outdata, payload, nalu_size);
......
......@@ -44,9 +44,12 @@ struct _GstRtpH264Depay
{
GstBaseRTPDepayload depayload;
gboolean byte_stream;
GstBuffer *codec_data;
GstAdapter *adapter;
gboolean wait_start;
};
struct _GstRtpH264DepayClass
......
......@@ -64,16 +64,46 @@ GST_STATIC_PAD_TEMPLATE ("src",
"clock-rate = (int) 90000, " "encoding-name = (string) \"H264\"")
);
#define GST_TYPE_H264_SCAN_MODE (gst_h264_scan_mode_get_type())
static GType
gst_h264_scan_mode_get_type (void)
{
static GType h264_scan_mode_type = 0;
static const GEnumValue h264_scan_modes[] = {
{GST_H264_SCAN_MODE_BYTESTREAM,
"Scan complete bytestream for NALUs (not implemented)",
"bytestream"},
{GST_H264_SCAN_MODE_MULTI_NAL, "Buffers contain multiple complete NALUs",
"multiple"},
{GST_H264_SCAN_MODE_SINLE_NAL, "Buffers contain a single complete NALU",
"single"},
{0, NULL, NULL},
};
if (!h264_scan_mode_type) {
h264_scan_mode_type =
g_enum_register_static ("GstH264PayScanMode", h264_scan_modes);
}
return h264_scan_mode_type;
}
#define DEFAULT_PROFILE_LEVEL_ID NULL
#define DEFAULT_SPROP_PARAMETER_SETS NULL
#define DEFAULT_SPROP_PARAMETER_SETS NULL
#define DEFAULT_SCAN_MODE GST_H264_SCAN_MODE_MULTI_NAL
enum
{
ARG_0,
ARG_PROFILE_LEVEL_ID,
ARG_SPROP_PARAMETER_SETS
PROP_0,
PROP_PROFILE_LEVEL_ID,
PROP_SPROP_PARAMETER_SETS,
PROP_SCAN_MODE,
PROP_LAST
};
#define IS_ACCESS_UNIT(x) (((x) > 0x00) && ((x) < 0x06))
static void gst_rtp_h264_pay_finalize (GObject * object);
static GstStateChangeReturn gst_rtp_h264_pay_change_state (GstElement * element,
......@@ -119,19 +149,29 @@ gst_rtp_h264_pay_class_init (GstRtpH264PayClass * klass)
gobject_class->set_property = gst_rtp_h264_pay_set_property;
gobject_class->get_property = gst_rtp_h264_pay_get_property;
g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_PROFILE_LEVEL_ID,
g_param_spec_string ("profile-level-id", "profile-level-id",
"The base64 profile-level-id to set in out caps (set to NULL to extract from stream)",
g_object_class_install_property (G_OBJECT_CLASS (klass),
PROP_PROFILE_LEVEL_ID, g_param_spec_string ("profile-level-id",
"profile-level-id",
"The base64 profile-level-id to set in out caps (set to NULL to "
"extract from stream)",
DEFAULT_PROFILE_LEVEL_ID,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (G_OBJECT_CLASS (klass),
ARG_SPROP_PARAMETER_SETS, g_param_spec_string ("sprop-parameter-sets",
PROP_SPROP_PARAMETER_SETS, g_param_spec_string ("sprop-parameter-sets",
"sprop-parameter-sets",
"The base64 sprop-parameter-sets to set in out caps (set to NULL to extract from stream)",
"The base64 sprop-parameter-sets to set in out caps (set to NULL to "
"extract from stream)",
DEFAULT_SPROP_PARAMETER_SETS,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SCAN_MODE,
g_param_spec_enum ("scan-mode", "Scan Mode",
"How to scan the input buffers for NAL units. Performance can be "
"increased when certain assumptions are made about the input buffers",
GST_TYPE_H264_SCAN_MODE, DEFAULT_SCAN_MODE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gobject_class->finalize = gst_rtp_h264_pay_finalize;
gstelement_class->change_state = gst_rtp_h264_pay_change_state;
......@@ -149,6 +189,7 @@ gst_rtp_h264_pay_init (GstRtpH264Pay * rtph264pay, GstRtpH264PayClass * klass)
rtph264pay->profile = 0;
rtph264pay->sps = NULL;
rtph264pay->pps = NULL;
rtph264pay->scan_mode = GST_H264_SCAN_MODE_MULTI_NAL;
}
static void
......@@ -239,6 +280,8 @@ gst_rtp_h264_pay_setcaps (GstBaseRTPPayload * basepayload, GstCaps * caps)
GST_DEBUG_OBJECT (rtph264pay, "profile %06x", profile);
/* 6 bits reserved | 2 bits lengthSizeMinusOne */
/* this is the number of bytes in front of the NAL units to mark their
* length */
rtph264pay->nal_length_size = (data[4] & 0x03) + 1;
GST_DEBUG_OBJECT (rtph264pay, "nal length %u", rtph264pay->nal_length_size);
/* 3 bits reserved | 5 bits numOfSequenceParameterSets */
......@@ -277,6 +320,7 @@ gst_rtp_h264_pay_setcaps (GstBaseRTPPayload * basepayload, GstCaps * caps)
if (size < 1)
goto avcc_error;
/* 8 bits numOfPictureParameterSets */
num_pps = data[0];
data += 1;
size -= 1;
......@@ -579,7 +623,10 @@ gst_rtp_h264_pay_payload_nal (GstBaseRTPPayload * basepayload, guint8 * data,
outbuf = gst_rtp_buffer_new_allocate (size, 0, 0);
GST_BUFFER_TIMESTAMP (outbuf) = timestamp;
gst_rtp_buffer_set_marker (outbuf, 1);
/* only set the marker bit on packets containing access units */
if (IS_ACCESS_UNIT (nalType)) {
gst_rtp_buffer_set_marker (outbuf, 1);
}
payload = gst_rtp_buffer_get_payload (outbuf);
GST_DEBUG_OBJECT (basepayload, "Copying %d bytes to outbuf", size);
......@@ -621,7 +668,9 @@ gst_rtp_h264_pay_payload_nal (GstBaseRTPPayload * basepayload, guint8 * data,
GST_DEBUG_OBJECT (basepayload, "end size=%d iteration=%d", size, ii);
end = 1;
}
gst_rtp_buffer_set_marker (outbuf, end);
if (IS_ACCESS_UNIT (nalType)) {
gst_rtp_buffer_set_marker (outbuf, end);
}
/* FU indicator */
payload[0] = (nalHeader & 0x60) | 28;
......@@ -723,14 +772,20 @@ gst_rtp_h264_pay_handle_buffer (GstBaseRTPPayload * basepayload,
data += 4;
size -= 4;
/* use next_start_code() to scan buffer.
* next_start_code() returns the offset in data,
* starting from zero to the first byte of 0.0.0.1
* If no start code is found, it returns the value of the
* 'size' parameter.
* data is unchanged by the call to next_start_code()
*/
next = next_start_code (data, size);
if (rtph264pay->scan_mode == GST_H264_SCAN_MODE_SINLE_NAL) {
/* we are told that there is only a single NAL in this packet so that we
* can avoid scanning for the next NAL. */
next = size;
} else {
/* use next_start_code() to scan buffer.
* next_start_code() returns the offset in data,
* starting from zero to the first byte of 0.0.0.1
* If no start code is found, it returns the value of the
* 'size' parameter.
* data is unchanged by the call to next_start_code()
*/
next = next_start_code (data, size);
}
/* nal length is distance to next start code */
nal_len = next;
......@@ -804,15 +859,21 @@ gst_rtp_h264_pay_set_property (GObject * object, guint prop_id,
rtph264pay = GST_RTP_H264_PAY (object);
switch (prop_id) {
case ARG_PROFILE_LEVEL_ID:
case PROP_PROFILE_LEVEL_ID:
g_free (rtph264pay->profile_level_id);
rtph264pay->profile_level_id = g_value_dup_string (value);
rtph264pay->update_caps = TRUE;
break;
case ARG_SPROP_PARAMETER_SETS:
case PROP_SPROP_PARAMETER_SETS:
g_free (rtph264pay->sprop_parameter_sets);
rtph264pay->sprop_parameter_sets = g_value_dup_string (value);
rtph264pay->update_caps = TRUE;
break;
case PROP_SCAN_MODE:
rtph264pay->scan_mode = g_value_get_enum (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
......@@ -826,13 +887,17 @@ gst_rtp_h264_pay_get_property (GObject * object, guint prop_id,
rtph264pay = GST_RTP_H264_PAY (object);
switch (prop_id) {
case ARG_PROFILE_LEVEL_ID:
case PROP_PROFILE_LEVEL_ID:
g_value_set_string (value, rtph264pay->profile_level_id);
break;
case ARG_SPROP_PARAMETER_SETS:
case PROP_SPROP_PARAMETER_SETS:
g_value_set_string (value, rtph264pay->sprop_parameter_sets);
break;
case PROP_SCAN_MODE:
g_value_set_enum (value, rtph264pay->scan_mode);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
......
......@@ -36,6 +36,13 @@ G_BEGIN_DECLS
#define GST_IS_RTP_H264_PAY_CLASS(klass) \
(G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_RTP_H264_PAY))
typedef enum
{
GST_H264_SCAN_MODE_BYTESTREAM,
GST_H264_SCAN_MODE_MULTI_NAL,
GST_H264_SCAN_MODE_SINLE_NAL
} GstH264ScanMode;
typedef struct _GstRtpH264Pay GstRtpH264Pay;
typedef struct _GstRtpH264PayClass GstRtpH264PayClass;
......@@ -53,6 +60,7 @@ struct _GstRtpH264Pay
gchar *profile_level_id;
gchar *sprop_parameter_sets;
gboolean update_caps;
GstH264ScanMode scan_mode;
};
struct _GstRtpH264PayClass
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment