Commit 30e90d0f authored by johan's avatar johan

Merge remote-tracking branch 'origin/master' into dev_dtls

parents d5389594 ae0c6d4b
......@@ -18,6 +18,11 @@ fi
INTLTOOLIZE=$(which intltoolize)
#workaround for mingw bug in intltoolize script.
if test "$INTLTOOLIZE" = "/bin/intltoolize" ; then
INTLTOOLIZE=/usr/bin/intltoolize
fi
libtoolize="libtoolize"
for lt in glibtoolize libtoolize15 libtoolize14 libtoolize13 ; do
if test -x /usr/bin/$lt ; then
......
......@@ -255,7 +255,7 @@ LOCAL_STATIC_LIBRARIES += \
ifeq ($(BUILD_ZRTP), 1)
LOCAL_STATIC_LIBRARIES += libbzrtp
LOCAL_CFLAGS += -DHAVE_zrtp
LOCAL_CFLAGS += -DHAVE_ZRTP
LOCAL_C_INCLUDES += $(ZRTP_C_INCLUDE)
endif #ZRTP
......
......@@ -226,7 +226,7 @@ else
if test "$USE_NLS" = "yes" ; then
AC_DEFINE(ENABLE_NLS,1,[Tells whether localisation is possible])
AC_DEFINE(HAVE_GETTEXT,1,[Tells wheter localisation is possible])
LIBS="$LIBS -lintl"
LIBS="$LIBS -L/usr/lib -lintl"
fi
fi
......
......@@ -143,6 +143,7 @@ MS2_PUBLIC int ms_list_index(const MSList *list, void *data);
MS2_PUBLIC MSList *ms_list_insert_sorted(MSList *list, void *data, MSCompareFunc compare_func);
MS2_PUBLIC MSList *ms_list_insert(MSList *list, MSList *before, void *data);
MS2_PUBLIC MSList *ms_list_copy(const MSList *list);
MS2_PUBLIC MSList *ms_list_copy_with_data(const MSList *list, void *(*copyfunc)(void *));
#undef MIN
#define MIN(a,b) ((a)>(b) ? (b) : (a))
......
......@@ -266,7 +266,7 @@ MS2_PUBLIC void ms_yuv_buf_allocator_free(MSYuvBufAllocator *obj);
MS2_PUBLIC void ms_rgb_to_yuv(const uint8_t rgb[3], uint8_t yuv[3]);
#ifdef __arm__
#if defined(__arm__) || defined(__arm64__)
MS2_PUBLIC void rotate_plane_neon_clockwise(int wDest, int hDest, int full_width, uint8_t* src, uint8_t* dst);
MS2_PUBLIC void rotate_plane_neon_anticlockwise(int wDest, int hDest, int full_width, uint8_t* src, uint8_t* dst);
MS2_PUBLIC void deinterlace_and_rotate_180_neon(uint8_t* ysrc, uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* udst, uint8_t* vdst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row);
......
......@@ -483,3 +483,7 @@ yuv2rgb.fs.h: yuv2rgb.fs
yuv2rgb.vs.h: yuv2rgb.vs
cd $(abs_srcdir) && \
xxd -i yuv2rgb.vs | sed s/}\;/,0x00}\;/ > $(abs_builddir)/yuv2rgb.vs.h
#because make bundle serahc in this dir
install-data-local:
$(MKDIR_P) $(DESTDIR)$(libdir)/mediastreamer/plugins
......@@ -38,7 +38,7 @@ static SoundDeviceDescription devices[]={
{ "HTC", "HTC Desire", "", 0, 250 },
{ "HTC", "HTC Sensation Z710e", "", 0, 200 },
{ "HTC", "HTC Wildfire", "", 0, 270 },
{ "HTC", "HTC One mini 2","", DEVICE_HAS_BUILTIN_AEC|DEVICE_HAS_BUILTIN_OPENSLES_AEC, 0, 0},
{ "LGE", "LS670", "", 0, 170 },
{ "LGE", "Nexus 5", "msm8974", 0, 0 , 16000 },
......
......@@ -688,20 +688,23 @@ static void ms_opus_dec_process(MSFilter *f) {
// try fec : info are stored in the next packet, do we have it?
if (d->rtp_picker_context.picker) {
im = d->rtp_picker_context.picker(&d->rtp_picker_context,d->sequence_number+1);
if (im) {
imLength=rtp_get_payload(im,&payload);
d->statsfec++;
} else {
d->statsplc++;
/* FEC information is in the next packet, last valid packet was d->sequence_number, the missing one shall then be d->sequence_number+1, so check jitter buffer for d->sequence_number+2 */
/* but we may have the n+1 packet in the buffer and adaptative jitter control keeping it for later, in that case, just go for PLC */
if (d->rtp_picker_context.picker(&d->rtp_picker_context,d->sequence_number+1) == NULL) { /* missing packet is really missing */
im = d->rtp_picker_context.picker(&d->rtp_picker_context,d->sequence_number+2); /* try to get n+2 */
if (im) {
imLength=rtp_get_payload(im,&payload);
}
}
}
om = allocb(5760 * d->channels * SIGNAL_SAMPLE_SIZE, 0); /* 5760 is the maximum number of sample in a packet (120ms at 48KHz) */
/* call to the decoder, we'll have either FEC or PLC, do it on the same length that last received packet */
if (payload) { // found frame to try FEC
d->statsfec++;
frames = opus_decode(d->state, payload, imLength, (opus_int16 *)om->b_wptr, d->lastPacketLength, 1);
} else { // do PLC: PLC doesn't seem to be able to generate more than 960 samples (20 ms at 48000 Hz), get PLC until we have the correct number of sample
//frames = opus_decode(d->state, NULL, 0, (opus_int16 *)om->b_wptr, d->lastPacketLength, 0); // this should have work if opus_decode returns the requested number of samples
d->statsplc++;
frames = 0;
while (frames < d->lastPacketLength) {
frames += opus_decode(d->state, NULL, 0, (opus_int16 *)(om->b_wptr + (frames*d->channels*SIGNAL_SAMPLE_SIZE)), d->lastPacketLength-frames, 0);
......
......@@ -275,6 +275,15 @@ MSList *ms_list_copy(const MSList *list){
return copy;
}
MSList *ms_list_copy_with_data(const MSList *list, void *(*copyfunc)(void *)){
MSList *copy=NULL;
const MSList *iter;
for(iter=list;iter!=NULL;iter=ms_list_next(iter)){
copy=ms_list_append(copy,copyfunc(iter->data));
}
return copy;
}
int ms_load_plugins(const char *dir){
return ms_factory_load_plugins(ms_factory_get_fallback(),dir);
}
......
......@@ -297,11 +297,19 @@ static void capture_queue_cleanup(void* p) {
}
input = [AVCaptureDeviceInput deviceInputWithDevice:device
error:&error];
[input retain]; // keep reference on an externally allocated object
AVCaptureSession *session = [(AVCaptureVideoPreviewLayer *)self.layer session];
[session addInput:input];
[session addOutput:output];
if ( input && [session canAddInput:input] ){
[input retain]; // keep reference on an externally allocated object
[session addInput:input];
} else {
ms_error("Error: input nil or cannot be added: %p", input);
}
if( output && [session canAddOutput:output] ){
[session addOutput:output];
} else {
ms_error("Error: output nil or cannot be added: %p", output);
}
}
- (void)dealloc {
......
......@@ -27,7 +27,12 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include <malloc.h>
#endif
#ifdef __arm__
#if defined(__arm__) || defined(__arm64__)
#define MS_HAS_ARM 1
#endif
#if MS_HAS_ARM
#include "msvideo_neon.h"
#endif
......@@ -453,7 +458,7 @@ static MSScalerContext *ff_create_swscale_context(int src_w, int src_h, MSPixFmt
int ff_flags=0;
MSFFScalerContext *ctx=ms_new0(MSFFScalerContext,1);
ctx->src_h=src_h;
#if __arm__
#if MS_HAS_ARM
ff_flags|=SWS_FAST_BILINEAR;
#else
if (flags & MS_SCALER_METHOD_BILINEAR)
......@@ -659,7 +664,7 @@ static void rotate_plane(int wDest, int hDest, int full_width, uint8_t* src, uin
static int hasNeon = -1;
#elif defined (__ARM_NEON__)
static int hasNeon = 1;
#elif defined(__arm__)
#elif MS_HAS_ARM
static int hasNeon = 0;
#endif
......@@ -677,12 +682,18 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(uint8_t
uint8_t* dstv;
mblk_t *yuv_block = ms_yuv_buf_alloc(&pict, w, h);
#ifdef ANDROID
if (hasNeon == -1) {
hasNeon = (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM && (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0);
#ifdef __arm64__
ms_warning("Warning: ARM64 NEON routines for video rotation are not yes implemented for Android: using SOFT version!");
#endif
}
#endif
#ifdef __arm__
#if MS_HAS_ARM
if (down_scale && !hasNeon) {
ms_error("down scaling by two requires NEON, returning empty block");
return yuv_block;
......@@ -703,7 +714,7 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(uint8_t
uint8_t* u_dest=pict.planes[1], *v_dest=pict.planes[2];
if (rotation == 0) {
#ifdef __arm__
#if MS_HAS_ARM
if (hasNeon) {
deinterlace_down_scale_neon(y, cbcr, pict.planes[0], u_dest, v_dest, w, h, y_byte_per_row, cbcr_byte_per_row,down_scale);
} else
......@@ -722,7 +733,7 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(uint8_t
}
}
} else {
#ifdef __arm__
#if defined(__arm__)
if (hasNeon) {
deinterlace_down_scale_and_rotate_180_neon(y, cbcr, pict.planes[0], u_dest, v_dest, w, h, y_byte_per_row, cbcr_byte_per_row,down_scale);
} else
......@@ -745,7 +756,7 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(uint8_t
} else {
bool_t clockwise = rotation == 90 ? TRUE : FALSE;
// Rotate Y
#ifdef __arm__
#if defined(__arm__)
if (hasNeon) {
if (clockwise) {
rotate_down_scale_plane_neon_clockwise(w,h,y_byte_per_row,(uint8_t*)y,pict.planes[0],down_scale);
......@@ -760,7 +771,7 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(uint8_t
rotate_plane(w,h,y_byte_per_row,srcy,dsty,1, clockwise);
}
#ifdef __arm__
#if defined(__arm__)
if (hasNeon) {
rotate_down_scale_cbcr_to_cr_cb(uv_w,uv_h, cbcr_byte_per_row/2, (uint8_t*)cbcr, pict.planes[2], pict.planes[1],clockwise,down_scale);
} else
......
......@@ -21,11 +21,18 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include "mediastreamer2/msvideo.h"
#ifdef __arm__
#if defined(__arm__) || defined(__arm64__)
#define MS_HAS_ARM 1
#endif
#if MS_HAS_ARM
#ifdef __ARM_NEON__
#include <arm_neon.h>
#endif
#ifdef __arm__
#define MATRIX_LOAD_8X8 \
/*load 8x8 pixel \
[ 0, 1, 2, 3, 4, 5, 6, 7] \
......@@ -456,72 +463,6 @@ static void deinterlace_down_scale_and_reverse_2x16bytes_neon(unsigned char* src
#endif
}
void deinterlace_down_scale_neon(uint8_t* ysrc, uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* u_dst, uint8_t* v_dst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row,bool_t down_scale) {
#ifdef __ARM_NEON__
char y_inc=down_scale?2:1;
char x_inc=down_scale?32:16;
int src_h=down_scale?2*h:h;
int src_w=down_scale?2*w:w;
int x,y;
// plain copy
uint8_t* ysrc_ptr = ysrc;
uint8_t* ydest_ptr = ydst;
uint8_t* cbcrsrc_ptr = cbcrsrc;
uint8_t* udest_ptr = u_dst;
uint8_t* vdest_ptr = v_dst;
int crcb_dest_offset=0;
for(y=0; y<src_h; y+=y_inc) {
if (down_scale) {
for(x=0;x<src_w;x+=x_inc) {
__asm volatile ("vld2.8 {q0,q1},[%0]! \n\t"
/* store in dest */
"vst1.8 {d0,d1},[%1]! \n\t"
:"+r"(ysrc_ptr),"+r"(ydest_ptr) /*out*/
: "r"(ysrc_ptr),"r"(ydest_ptr)/*in*/
: "q0","q1" /*modified*/
);
}
} else {
memcpy(ydest_ptr,ysrc_ptr,w);
ydest_ptr+=w;
}
ysrc_ptr= ysrc + y* y_byte_per_row;
}
// de-interlace u/v
for(y=0; y<src_h>>1; y+=y_inc) {
for(x=0;x<src_w;x+=x_inc) {
if (down_scale) {
__asm volatile ("vld4.8 {d0,d1,d2,d3},[%0]! \n\t"
/* store in dest */
"vst1.8 {d0},[%1]! \n\t"
"vst1.8 {d1},[%2]! \n\t"
:"=r"(cbcrsrc_ptr),"=r"(udest_ptr),"=r"(vdest_ptr) /*out*/
: "0"(cbcrsrc_ptr),"1"(udest_ptr),"2"(vdest_ptr) /*in*/
: "q0","q1" /*modified*/
);
} else {
__asm volatile ("vld2.8 {d0,d1},[%0]! \n\t"
/* store in dest */
"vst1.8 {d0},[%1]! \n\t"
"vst1.8 {d1},[%2]! \n\t"
:"=r"(cbcrsrc_ptr),"=r"(udest_ptr),"=r"(vdest_ptr) /*out*/
: "0"(cbcrsrc_ptr),"1"(udest_ptr),"2"(vdest_ptr) /*in*/
: "q0" /*modified*/
);
}
}
cbcrsrc_ptr= cbcrsrc + y * cbcr_byte_per_row;
crcb_dest_offset+=down_scale?(src_w>>2):(src_w>>1);
udest_ptr=u_dst + crcb_dest_offset;
vdest_ptr=v_dst + crcb_dest_offset;
}
#endif
}
void deinterlace_down_scale_and_rotate_180_neon(uint8_t* ysrc, uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* udst, uint8_t* vdst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row,bool_t down_scale) {
#ifdef __ARM_NEON__
......@@ -534,12 +475,12 @@ void deinterlace_down_scale_and_rotate_180_neon(uint8_t* ysrc, uint8_t* cbcrsrc,
char x_dest_inc=16;
char y_inc=down_scale?2:1;
// 180° y rotation
uint8_t* src_ptr=ysrc;
uint8_t* dest_ptr=ydst + h*w; /*start at the end of dest*/
uint8_t* dest_u_ptr;
uint8_t* dest_v_ptr;
for(y=0; y<src_h; y+=y_inc) {
for(x=0; x<src_w; x+=x_src_inc) {
dest_ptr-=x_dest_inc;
......@@ -560,17 +501,17 @@ void deinterlace_down_scale_and_rotate_180_neon(uint8_t* ysrc, uint8_t* cbcrsrc,
for(y=0; y<src_uv_h; y+=y_inc) {
for(x=0; x<src_uv_w; x+=x_src_inc) {
dest_u_ptr-=x_dest_inc>>1;
dest_v_ptr-=x_dest_inc>>1;
dest_v_ptr-=x_dest_inc>>1;
if (down_scale) {
deinterlace_down_scale_and_reverse_2x16bytes_neon(src_ptr, dest_u_ptr, dest_v_ptr);
} else {
deinterlace_and_reverse_2x8bytes_neon(src_ptr, dest_u_ptr, dest_v_ptr);
}
src_ptr+=x_src_inc;
}
src_ptr=cbcrsrc+ y*cbcr_byte_per_row;
}
}
#else
ms_error("Neon function '%s' used without hw neon support", __FUNCTION__);
......@@ -580,5 +521,66 @@ void deinterlace_and_rotate_180_neon(uint8_t* ysrc, uint8_t* cbcrsrc, uint8_t* y
return deinterlace_down_scale_and_rotate_180_neon(ysrc, cbcrsrc, ydst, udst, vdst, w, h, y_byte_per_row,cbcr_byte_per_row,FALSE);
}
#endif /* defined(__arm__), the above functions are not used in iOS 64bits, so only the function below is implemented for __arm64__ */
void deinterlace_down_scale_neon(uint8_t* ysrc, uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* u_dst, uint8_t* v_dst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row,bool_t down_scale) {
#ifdef __ARM_NEON__
char y_inc = down_scale?2:1;
char x_inc = down_scale?32:16;
int src_h = down_scale?2*h:h;
int src_w = down_scale?2*w:w;
int x,y;
// plain copy
uint8_t* ysrc_ptr = ysrc;
uint8_t* ydest_ptr = ydst;
uint8_t* cbcrsrc_ptr = cbcrsrc;
uint8_t* udest_ptr = u_dst;
uint8_t* vdest_ptr = v_dst;
int crcb_dest_offset=0;
for(y=0; y<src_h; y+=y_inc) {
if (down_scale) {
for(x=0;x<src_w;x+=x_inc) {
uint8x16x2_t src = vld2q_u8(ysrc_ptr);
vst1q_u8(ydest_ptr, src.val[0]);
ysrc_ptr += 32;
ydest_ptr += 16;
}
} else {
memcpy(ydest_ptr,ysrc_ptr,w);
ydest_ptr+=w;
}
ysrc_ptr= ysrc + y* y_byte_per_row;
}
// de-interlace u/v
for(y=0; y < (src_h>>1); y+=y_inc) {
for(x=0;x<src_w;x+=x_inc) {
if (down_scale) {
uint8x8x4_t cbr = vld4_u8(cbcrsrc_ptr);
vst1_u8(udest_ptr, cbr.val[0]);
vst1_u8(vdest_ptr, cbr.val[1]);
cbcrsrc_ptr+=32;
vdest_ptr+=8;
udest_ptr+=8;
} else {
uint8x8x2_t cbr = vld2_u8(cbcrsrc_ptr);
vst1_u8(udest_ptr, cbr.val[0]);
vst1_u8(vdest_ptr, cbr.val[1]);
cbcrsrc_ptr+=16;
vdest_ptr+=8;
udest_ptr+=8;
}
}
cbcrsrc_ptr= cbcrsrc + y * cbcr_byte_per_row;
crcb_dest_offset+=down_scale?(src_w>>2):(src_w>>1);
udest_ptr=u_dst + crcb_dest_offset;
vdest_ptr=v_dst + crcb_dest_offset;
}
#endif
}
#endif
......@@ -690,7 +690,7 @@ static void generate_frames_list(Vp8RtpFmtUnpackerCtx *ctx, MSList *packets_list
static void output_frame(MSQueue *out, Vp8RtpFmtFrame *frame) {
Vp8RtpFmtPartition *partition;
mblk_t *om = NULL;
mblk_t *curm;
mblk_t *curm = NULL;
int i;
for (i = 0; i <= frame->partitions_info.nb_partitions; i++) {
......
......@@ -27,6 +27,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include <stdio.h>
#include "CUnit/Basic.h"
#include "CUnit/Automated.h"
#if HAVE_CU_CURSES
#include "CUnit/CUCurses.h"
#endif
......@@ -40,7 +41,8 @@ static int nb_test_suites = 0;
static const char* tester_fileroot = SOUND_FILE_PATH;
static const char* tester_writable_dir= WRITE_FILE_PATH;
static unsigned char xml = 0;
static const char *xml_file = NULL;
#if HAVE_CU_CURSES
static unsigned char curses = 0;
......@@ -171,44 +173,50 @@ void mediastreamer2_tester_uninit(void) {
int mediastreamer2_tester_run_tests(const char *suite_name, const char *test_name) {
int ret;
if( xml_file != NULL ){
CU_set_output_filename(xml_file);
}
if (xml) {
CU_automated_run_tests();
} else {
#if HAVE_CU_GET_SUITE
if (suite_name){
CU_pSuite suite;
CU_basic_set_mode(CU_BRM_VERBOSE);
suite=CU_get_suite(suite_name);
if (!suite) {
fprintf(stderr, "Could not find suite '%s'. Available suites are:\n", suite_name);
list_suites();
} else if (test_name) {
CU_pTest test=CU_get_test_by_name(test_name, suite);
if (!test) {
fprintf(stderr, "Could not find test '%s' in suite '%s'. Available tests are:\n", test_name, suite_name);
// do not use suite_name here, since this method is case sentisitive
list_suite_tests(suite->pName);
if (suite_name){
CU_pSuite suite;
CU_basic_set_mode(CU_BRM_VERBOSE);
suite=CU_get_suite(suite_name);
if (!suite) {
fprintf(stderr, "Could not find suite '%s'. Available suites are:\n", suite_name);
list_suites();
} else if (test_name) {
CU_pTest test=CU_get_test_by_name(test_name, suite);
if (!test) {
fprintf(stderr, "Could not find test '%s' in suite '%s'. Available tests are:\n", test_name, suite_name);
// do not use suite_name here, since this method is case sentisitive
list_suite_tests(suite->pName);
} else {
CU_ErrorCode err= CU_basic_run_test(suite, test);
if (err != CUE_SUCCESS) fprintf(stderr, "CU_basic_run_test error=%d\n", err);
}
} else {
CU_ErrorCode err= CU_basic_run_test(suite, test);
if (err != CUE_SUCCESS) fprintf(stderr, "CU_basic_run_test error=%d\n", err);
CU_basic_run_suite(suite);
}
} else {
CU_basic_run_suite(suite);
}
} else
} else
#endif
{
{
#if HAVE_CU_CURSES
if (curses) {
/* Run tests using the CUnit curses interface */
CU_curses_run_tests();
}
else
if (curses) {
/* Run tests using the CUnit curses interface */
CU_curses_run_tests();
}
else
#endif
{
/* Run all tests using the CUnit Basic interface */
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
{
/* Run all tests using the CUnit Basic interface */
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
}
}
}
/* Redisplay list of failed tests on end */
if (CU_get_number_of_failure_records()){
CU_basic_show_failures(CU_get_failure_list());
......@@ -234,6 +242,8 @@ void helper(const char *name) {
#if HAVE_CU_CURSES
"\t\t\t--curses\n"
#endif
"\t\t\t--xml\n"
"\t\t\t--xml-file <xml file prefix (will be suffixed by '-Results.xml')>\n"
, name);
}
......@@ -269,6 +279,11 @@ int main (int argc, char *argv[]) {
verbose = TRUE;
} else if (strcmp(argv[i], "--silent") == 0) {
verbose = FALSE;
} else if (strcmp(argv[i], "--xml-file") == 0){
CHECK_ARG("--xml-file", ++i, argc);
xml_file = argv[i];
} else if (strcmp(argv[i], "--xml") == 0){
xml = 1;
}
#if HAVE_CU_GET_SUITE
else if (strcmp(argv[i], "--test")==0) {
......@@ -313,6 +328,18 @@ int main (int argc, char *argv[]) {
putenv("MEDIASTREAMER_DEBUG=0");
}
#ifdef HAVE_CU_CURSES
if( xml && curses ){
printf("Cannot use both xml and curses\n");
return -1;
}
#endif
if( xml && (suite_name || test_name) ){
printf("Cannot use both xml and specific test suite\n");
return -1;
}
ret = mediastreamer2_tester_run_tests(suite_name, test_name);
CU_cleanup_registry();
mediastreamer2_tester_uninit();
......
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