Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
O
opencv
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
submodule
opencv
Commits
1367a58b
Commit
1367a58b
authored
Apr 08, 2018
by
Alexander Nesterov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added encode-pipeline to sample
parent
9615f8c9
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
380 additions
and
94 deletions
+380
-94
gstreamer_pipeline.cpp
samples/cpp/gstreamer_pipeline.cpp
+380
-94
No files found.
samples/cpp/gstreamer_pipeline.cpp
View file @
1367a58b
...
@@ -8,144 +8,430 @@
...
@@ -8,144 +8,430 @@
using
namespace
std
;
using
namespace
std
;
using
namespace
cv
;
using
namespace
cv
;
string
getGstDemuxPlugin
(
string
container
);
class
GStreamerPipeline
string
getGstAvDecodePlugin
(
string
codec
);
int
main
(
int
argc
,
char
*
argv
[])
{
{
const
string
keys
=
public
:
"{h help usage ? | | print help messages }"
// Preprocessing arguments command line
"{p pipeline |gst-default| pipeline name (supported: 'gst-default', 'gst-vaapi', 'gst-libav', 'ffmpeg') }"
GStreamerPipeline
(
int
argc
,
char
*
argv
[])
"{ct container |mp4 | container name (supported: 'mp4', 'mov', 'avi', 'mkv') }"
{
"{cd codec |h264 | codec name (supported: 'h264', 'h265', 'mpeg2', 'mpeg4', 'mjpeg', 'vp8') }"
const
string
keys
=
"{f file path | | path to file }"
"{h help usage ? | | print help messages }"
"{fm fast | | fast measure fps }"
;
"{m mode | | coding mode (supported: encode, decode) }"
"{p pipeline |default | pipeline name (supported: 'default', 'gst-basic', 'gst-vaapi', 'gst-libav', 'ffmpeg') }"
"{ct container |mp4 | container name (supported: 'mp4', 'mov', 'avi', 'mkv') }"
"{cd codec |h264 | codec name (supported: 'h264', 'h265', 'mpeg2', 'mpeg4', 'mjpeg', 'vp8') }"
"{f file path | | path to file }"
"{vr resolution |720p | video resolution for encoding (supported: '720p', '1080p', '4k') }"
"{fps |30 | fix frame per second for encoding (supported: fps > 0) }"
"{fm fast | | fast measure fps }"
;
cmd_parser
=
new
CommandLineParser
(
argc
,
argv
,
keys
);
cmd_parser
->
about
(
"This program shows how to read a video file with GStreamer pipeline with OpenCV."
);
if
(
cmd_parser
->
has
(
"help"
))
{
cmd_parser
->
printMessage
();
exit_code
=
-
1
;
}
CommandLineParser
parser
(
argc
,
argv
,
keys
);
fast_measure
=
cmd_parser
->
has
(
"fast"
);
// fast measure fps
fix_fps
=
cmd_parser
->
get
<
int
>
(
"fps"
);
// fixed frame per second
pipeline
=
cmd_parser
->
get
<
string
>
(
"pipeline"
),
// gstreamer pipeline type
container
=
cmd_parser
->
get
<
string
>
(
"container"
),
// container type
mode
=
cmd_parser
->
get
<
string
>
(
"mode"
),
// coding mode
codec
=
cmd_parser
->
get
<
string
>
(
"codec"
),
// codec type
file_name
=
cmd_parser
->
get
<
string
>
(
"file"
),
// path to videofile
resolution
=
cmd_parser
->
get
<
string
>
(
"resolution"
);
// video resolution
if
(
!
cmd_parser
->
check
())
{
cmd_parser
->
printErrors
();
exit_code
=
-
1
;
}
exit_code
=
0
;
}
parser
.
about
(
"This program shows how to read a video file with GStreamer pipeline with OpenCV."
);
~
GStreamerPipeline
()
{
delete
cmd_parser
;
}
if
(
parser
.
has
(
"help"
))
// Start pipeline
int
run
()
{
{
parser
.
printMessage
();
if
(
exit_code
<
0
)
{
return
exit_code
;
}
if
(
mode
==
"decode"
)
{
if
(
createDecodePipeline
()
<
0
)
return
-
1
;
}
else
if
(
mode
==
"encode"
)
{
if
(
createEncodePipeline
()
<
0
)
return
-
1
;
}
else
{
cout
<<
"Unsupported mode: "
<<
mode
<<
endl
;
cmd_parser
->
printErrors
();
return
-
1
;
}
cout
<<
"_____________________________________"
<<
endl
;
cout
<<
"Pipeline "
<<
mode
<<
":"
<<
endl
;
cout
<<
stream_pipeline
.
str
()
<<
endl
;
// Choose a show video or only measure fps
cout
<<
"_____________________________________"
<<
endl
;
cout
<<
"Start measure frame per seconds (fps)"
<<
endl
;
cout
<<
"Loading ..."
<<
endl
;
vector
<
double
>
tick_counts
;
cout
<<
"Start "
<<
mode
<<
": "
<<
file_name
;
cout
<<
" ("
<<
pipeline
<<
")"
<<
endl
;
while
(
true
)
{
int64
temp_count_tick
=
0
;
if
(
mode
==
"decode"
)
{
Mat
frame
;
temp_count_tick
=
getTickCount
();
cap
>>
frame
;
temp_count_tick
=
getTickCount
()
-
temp_count_tick
;
if
(
frame
.
empty
())
{
break
;
}
}
else
if
(
mode
==
"encode"
)
{
Mat
element
;
while
(
!
cap
.
grab
());
cap
.
retrieve
(
element
);
temp_count_tick
=
getTickCount
();
wrt
<<
element
;
temp_count_tick
=
getTickCount
()
-
temp_count_tick
;
}
tick_counts
.
push_back
(
static_cast
<
double
>
(
temp_count_tick
));
if
(((
mode
==
"decode"
)
&&
fast_measure
&&
(
tick_counts
.
size
()
>
1e3
))
||
((
mode
==
"encode"
)
&&
(
tick_counts
.
size
()
>
3e3
))
||
((
mode
==
"encode"
)
&&
fast_measure
&&
(
tick_counts
.
size
()
>
1e2
)))
{
break
;
}
}
double
time_fps
=
sum
(
tick_counts
)[
0
]
/
getTickFrequency
();
if
(
tick_counts
.
size
()
!=
0
)
{
cout
<<
"Finished: "
<<
tick_counts
.
size
()
<<
" in "
<<
time_fps
<<
" sec ~ "
;
cout
<<
tick_counts
.
size
()
/
time_fps
<<
" fps "
<<
endl
;
}
else
{
cout
<<
"Failed "
<<
mode
<<
": "
<<
file_name
;
cout
<<
" ("
<<
pipeline
<<
")"
<<
endl
;
return
-
1
;
}
return
0
;
return
0
;
}
}
bool
arg_fast_measure
=
parser
.
has
(
"fast"
);
// fast measure fps
// Free video resource
string
arg_pipeline
=
parser
.
get
<
string
>
(
"pipeline"
),
// GStreamer pipeline type
void
close
()
arg_container
=
parser
.
get
<
string
>
(
"container"
),
// container type
{
arg_codec
=
parser
.
get
<
string
>
(
"codec"
),
// codec type
cap
.
release
();
arg_file_name
=
parser
.
get
<
string
>
(
"file"
);
// path to videofile
wrt
.
release
();
VideoCapture
cap
;
}
if
(
!
parser
.
check
())
private
:
// Choose the constructed GStreamer pipeline for decode
int
createDecodePipeline
()
{
{
parser
.
printErrors
();
if
(
pipeline
==
"default"
)
{
cap
=
VideoCapture
(
file_name
,
CAP_GSTREAMER
);
}
else
if
(
pipeline
.
find
(
"gst"
)
==
0
)
{
stream_pipeline
<<
"filesrc location=
\"
"
<<
file_name
<<
"
\"
"
;
stream_pipeline
<<
" ! "
<<
getGstMuxPlugin
();
if
(
pipeline
.
find
(
"basic"
)
==
4
)
{
stream_pipeline
<<
getGstDefaultCodePlugin
();
}
else
if
(
pipeline
.
find
(
"vaapi1710"
)
==
4
)
{
stream_pipeline
<<
getGstVaapiCodePlugin
();
}
else
if
(
pipeline
.
find
(
"libav"
)
==
4
)
{
stream_pipeline
<<
getGstAvCodePlugin
();
}
else
{
cout
<<
"Unsupported pipeline: "
<<
pipeline
<<
endl
;
cmd_parser
->
printErrors
();
return
-
1
;
}
stream_pipeline
<<
" ! videoconvert n-threads="
<<
getNumThreads
();
stream_pipeline
<<
" ! appsink sync=false"
;
cap
=
VideoCapture
(
stream_pipeline
.
str
(),
CAP_GSTREAMER
);
}
else
if
(
pipeline
==
"ffmpeg"
)
{
cap
=
VideoCapture
(
file_name
,
CAP_FFMPEG
);
stream_pipeline
<<
"default pipeline for ffmpeg"
<<
endl
;
}
else
{
cout
<<
"Unsupported pipeline: "
<<
pipeline
<<
endl
;
cmd_parser
->
printErrors
();
return
-
1
;
}
return
0
;
return
0
;
}
}
// Choose the constructed GStreamer pipeline
// Choose the constructed GStreamer pipeline
for encode
i
f
(
arg_pipeline
.
find
(
"gst"
)
==
0
)
i
nt
createEncodePipeline
(
)
{
{
ostringstream
pipeline
;
if
(
checkConfiguration
()
<
0
)
return
-
1
;
pipeline
<<
"filesrc location=
\"
"
<<
arg_file_name
<<
"
\"
"
;
ostringstream
test_pipeline
;
pipeline
<<
" ! "
<<
getGstDemuxPlugin
(
arg_container
);
test_pipeline
<<
"videotestsrc pattern=smpte"
;
test_pipeline
<<
" ! video/x-raw, "
<<
getVideoSettings
();
test_pipeline
<<
" ! appsink sync=false"
;
cap
=
VideoCapture
(
test_pipeline
.
str
(),
CAP_GSTREAMER
);
if
(
arg_pipeline
.
find
(
"default"
)
==
4
)
{
if
(
pipeline
==
"default"
)
{
pipeline
<<
" ! decodebin"
;
wrt
=
VideoWriter
(
file_name
,
CAP_GSTREAMER
,
getFourccCode
(),
fix_fps
,
fix_size
,
true
)
;
}
}
else
if
(
arg_pipeline
.
find
(
"vaapi1710"
)
==
4
)
else
if
(
pipeline
.
find
(
"gst"
)
==
0
)
{
{
pipeline
<<
" ! vaapidecodebin"
;
stream_pipeline
<<
"appsrc ! videoconvert n-threads="
<<
getNumThreads
()
<<
" ! "
;
if
(
arg_container
==
"mkv"
)
if
(
pipeline
.
find
(
"basic"
)
==
4
)
{
stream_pipeline
<<
getGstDefaultCodePlugin
();
}
else
if
(
pipeline
.
find
(
"vaapi1710"
)
==
4
)
{
{
pipeline
<<
" ! autovideoconvert"
;
stream_pipeline
<<
getGstVaapiCodePlugin
();
}
else
if
(
pipeline
.
find
(
"libav"
)
==
4
)
{
stream_pipeline
<<
getGstAvCodePlugin
();
}
}
else
else
{
{
pipeline
<<
" ! video/x-raw, format=YV12"
;
cout
<<
"Unsupported pipeline: "
<<
pipeline
<<
endl
;
cmd_parser
->
printErrors
();
return
-
1
;
}
}
stream_pipeline
<<
" ! "
<<
getGstMuxPlugin
();
stream_pipeline
<<
" ! filesink location=
\"
"
<<
file_name
<<
"
\"
"
;
wrt
=
VideoWriter
(
stream_pipeline
.
str
(),
CAP_GSTREAMER
,
0
,
fix_fps
,
fix_size
,
true
);
}
}
else
if
(
arg_pipeline
.
find
(
"libav"
)
==
4
)
else
if
(
pipeline
==
"ffmpeg"
)
{
{
pipeline
<<
" ! "
<<
getGstAvDecodePlugin
(
arg_codec
);
wrt
=
VideoWriter
(
file_name
,
CAP_FFMPEG
,
getFourccCode
(),
fix_fps
,
fix_size
,
true
);
stream_pipeline
<<
"default pipeline for ffmpeg"
<<
endl
;
}
}
else
else
{
{
parser
.
printMessage
()
;
cout
<<
"Unsupported pipeline: "
<<
pipeline
<<
endl
;
c
out
<<
"Unsupported pipeline: "
<<
arg_pipeline
<<
endl
;
c
md_parser
->
printErrors
()
;
return
-
4
;
return
-
1
;
}
}
return
0
;
pipeline
<<
" ! videoconvert"
;
pipeline
<<
" n-threads="
<<
getNumThreads
();
pipeline
<<
" ! appsink sync=false"
;
cap
=
VideoCapture
(
pipeline
.
str
(),
CAP_GSTREAMER
);
}
}
else
if
(
arg_pipeline
==
"ffmpeg"
)
// Choose video resolution for encoding
string
getVideoSettings
()
{
{
cap
=
VideoCapture
(
arg_file_name
,
CAP_FFMPEG
);
ostringstream
video_size
;
if
(
fix_fps
>
0
)
{
video_size
<<
"framerate="
<<
fix_fps
<<
"/1, "
;
}
else
{
cout
<<
"Unsupported fps (< 0): "
<<
fix_fps
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
if
(
resolution
==
"720p"
)
{
fix_size
=
Size
(
1280
,
720
);
}
else
if
(
resolution
==
"1080p"
)
{
fix_size
=
Size
(
1920
,
1080
);
}
else
if
(
resolution
==
"4k"
)
{
fix_size
=
Size
(
3840
,
2160
);
}
else
{
cout
<<
"Unsupported video resolution: "
<<
resolution
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
video_size
<<
"width="
<<
fix_size
.
width
<<
", height="
<<
fix_size
.
height
;
return
video_size
.
str
();
}
}
else
// Choose a video container
string
getGstMuxPlugin
()
{
{
parser
.
printMessage
();
ostringstream
plugin
;
cout
<<
"Unsupported pipeline: "
<<
arg_pipeline
<<
endl
;
if
(
container
==
"avi"
)
{
plugin
<<
"avi"
;
}
return
-
4
;
else
if
(
container
==
"mp4"
)
{
plugin
<<
"qt"
;
}
else
if
(
container
==
"mov"
)
{
plugin
<<
"qt"
;
}
else
if
(
container
==
"mkv"
)
{
plugin
<<
"matroska"
;
}
else
{
cout
<<
"Unsupported container: "
<<
container
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
if
(
mode
==
"decode"
)
{
plugin
<<
"demux"
;
}
else
if
(
mode
==
"encode"
)
{
plugin
<<
"mux"
;
}
else
{
cout
<<
"Unsupported mode: "
<<
mode
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
return
plugin
.
str
();
}
}
// Choose a show video or only measure fps
// Choose a libav codec
cout
<<
"_____________________________________"
<<
'\n'
;
string
getGstAvCodePlugin
()
cout
<<
"Start measure frame per seconds (fps)"
<<
'\n'
;
{
cout
<<
"Loading ..."
<<
'\n'
;
ostringstream
plugin
;
if
(
mode
==
"decode"
)
{
if
(
codec
==
"h264"
)
{
plugin
<<
"h264parse ! "
;
}
else
if
(
codec
==
"h265"
)
{
plugin
<<
"h265parse ! "
;
}
plugin
<<
"avdec_"
;
}
else
if
(
mode
==
"encode"
)
{
plugin
<<
"avenc_"
;
}
else
{
cout
<<
"Unsupported mode: "
<<
mode
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
Mat
frame
;
if
(
codec
==
"h264"
)
{
plugin
<<
"h264"
;
}
vector
<
double
>
tick_counts
;
else
if
(
codec
==
"h265"
)
{
plugin
<<
"h265"
;
}
else
if
(
codec
==
"mpeg2"
)
{
plugin
<<
"mpeg2video"
;
}
else
if
(
codec
==
"mpeg4"
)
{
plugin
<<
"mpeg4"
;
}
else
if
(
codec
==
"mjpeg"
)
{
plugin
<<
"mjpeg"
;
}
else
if
(
codec
==
"vp8"
)
{
plugin
<<
"vp8"
;
}
else
{
cout
<<
"Unsupported libav codec: "
<<
codec
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
cout
<<
"Start decoding: "
<<
arg_file_name
;
return
plugin
.
str
()
;
cout
<<
" ("
<<
arg_pipeline
<<
")"
<<
endl
;
}
while
(
true
)
// Choose a vaapi codec
string
getGstVaapiCodePlugin
()
{
{
int64
temp_count_tick
=
getTickCount
();
ostringstream
plugin
;
cap
>>
frame
;
if
(
mode
==
"decode"
)
temp_count_tick
=
getTickCount
()
-
temp_count_tick
;
{
if
(
frame
.
empty
())
{
break
;
}
plugin
<<
"vaapidecodebin"
;
tick_counts
.
push_back
(
static_cast
<
double
>
(
temp_count_tick
));
if
(
container
==
"mkv"
)
{
plugin
<<
" ! autovideoconvert"
;
}
if
(
arg_fast_measure
&&
(
tick_counts
.
size
()
>
1000
))
{
break
;
}
else
{
plugin
<<
" ! video/x-raw, format=YV12"
;
}
}
else
if
(
mode
==
"encode"
)
{
if
(
codec
==
"h264"
)
{
plugin
<<
"vaapih264enc"
;
}
else
if
(
codec
==
"h265"
)
{
plugin
<<
"vaapih265enc"
;
}
else
if
(
codec
==
"mpeg2"
)
{
plugin
<<
"vaapimpeg2enc"
;
}
else
if
(
codec
==
"mjpeg"
)
{
plugin
<<
"vaapijpegenc"
;
}
else
if
(
codec
==
"vp8"
)
{
plugin
<<
"vaapivp8enc"
;
}
else
{
cout
<<
"Unsupported vaapi codec: "
<<
codec
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
}
else
{
cout
<<
"Unsupported mode: "
<<
resolution
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
return
plugin
.
str
();
}
}
double
time_fps
=
sum
(
tick_counts
)[
0
]
/
getTickFrequency
();
if
(
tick_counts
.
size
()
!=
0
)
// Choose a default codec
string
getGstDefaultCodePlugin
()
{
ostringstream
plugin
;
if
(
mode
==
"decode"
)
{
plugin
<<
" ! decodebin"
;
}
else
if
(
mode
==
"encode"
)
{
if
(
codec
==
"h264"
)
{
plugin
<<
"x264enc"
;
}
else
if
(
codec
==
"h265"
)
{
plugin
<<
"x265enc"
;
}
else
if
(
codec
==
"mpeg2"
)
{
plugin
<<
"mpeg2enc"
;
}
else
if
(
codec
==
"mjpeg"
)
{
plugin
<<
"jpegenc"
;
}
else
if
(
codec
==
"vp8"
)
{
plugin
<<
"vp8enc"
;
}
else
{
cout
<<
"Unsupported default codec: "
<<
codec
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
}
else
{
cout
<<
"Unsupported mode: "
<<
resolution
<<
endl
;
cmd_parser
->
printErrors
();
return
string
();
}
return
plugin
.
str
();
}
// Get fourcc for codec
int
getFourccCode
()
{
{
cout
<<
"Finished: "
<<
tick_counts
.
size
()
<<
" in "
<<
time_fps
<<
" sec ~ "
;
if
(
codec
==
"h264"
)
{
return
VideoWriter
::
fourcc
(
'H'
,
'2'
,
'6'
,
'4'
);
}
cout
<<
tick_counts
.
size
()
/
time_fps
<<
" fps "
<<
endl
;
else
if
(
codec
==
"h265"
)
{
return
VideoWriter
::
fourcc
(
'H'
,
'E'
,
'V'
,
'C'
);
}
else
if
(
codec
==
"mpeg2"
)
{
return
VideoWriter
::
fourcc
(
'M'
,
'P'
,
'E'
,
'G'
);
}
else
if
(
codec
==
"mpeg4"
)
{
return
VideoWriter
::
fourcc
(
'M'
,
'P'
,
'4'
,
'2'
);
}
else
if
(
codec
==
"mjpeg"
)
{
return
VideoWriter
::
fourcc
(
'M'
,
'J'
,
'P'
,
'G'
);
}
else
if
(
codec
==
"vp8"
)
{
return
VideoWriter
::
fourcc
(
'V'
,
'P'
,
'8'
,
'0'
);
}
else
{
cout
<<
"Unsupported ffmpeg codec: "
<<
codec
<<
endl
;
cmd_parser
->
printErrors
();
return
0
;
}
}
}
else
// Check bad configuration
int
checkConfiguration
()
{
{
cout
<<
"Failed decoding: "
<<
arg_file_name
;
if
((
codec
==
"mpeg2"
&&
getGstMuxPlugin
()
==
"qtmux"
)
||
cout
<<
" ("
<<
arg_pipeline
<<
")"
<<
endl
;
(
codec
==
"h265"
&&
getGstMuxPlugin
()
==
"avimux"
)
||
return
-
5
;
(
pipeline
==
"gst-libav"
&&
(
codec
==
"h264"
||
codec
==
"h265"
))
||
(
pipeline
==
"gst-vaapi1710"
&&
codec
==
"mpeg2"
&&
resolution
==
"4k"
)
||
(
pipeline
==
"gst-vaapi1710"
&&
codec
==
"mpeg2"
&&
resolution
==
"1080p"
&&
fix_fps
>
30
))
{
cout
<<
"Unsupported configuration"
<<
endl
;
cmd_parser
->
printErrors
();
return
-
1
;
}
return
0
;
}
}
return
0
;
}
// Choose a video container
bool
fast_measure
;
// fast measure fps
string
getGstDemuxPlugin
(
string
container
)
{
string
pipeline
,
// gstreamer pipeline type
if
(
container
==
"avi"
)
{
return
"avidemux"
;
}
container
,
// container type
else
if
(
container
==
"mp4"
)
{
return
"qtdemux"
;
}
mode
,
// coding mode
else
if
(
container
==
"mov"
)
{
return
"qtdemux"
;
}
codec
,
// codec type
else
if
(
container
==
"mkv"
)
{
return
"matroskademux"
;
}
file_name
,
// path to videofile
return
string
();
resolution
;
// video resolution
}
int
fix_fps
;
// fixed frame per second
Size
fix_size
;
// fixed frame size
int
exit_code
;
VideoWriter
wrt
;
VideoCapture
cap
;
ostringstream
stream_pipeline
;
CommandLineParser
*
cmd_parser
;
};
// Choose a codec
int
main
(
int
argc
,
char
*
argv
[])
string
getGstAvDecodePlugin
(
string
codec
)
{
{
if
(
codec
==
"h264"
)
{
return
"h264parse ! avdec_h264"
;
}
GStreamerPipeline
pipe
(
argc
,
argv
);
else
if
(
codec
==
"h265"
)
{
return
"h265parse ! avdec_h265"
;
}
return
pipe
.
run
();
else
if
(
codec
==
"mpeg2"
)
{
return
"avdec_mpeg2video"
;
}
else
if
(
codec
==
"mpeg4"
)
{
return
"avdec_mpeg4"
;
}
else
if
(
codec
==
"mjpeg"
)
{
return
"avdec_mjpeg"
;
}
else
if
(
codec
==
"vp8"
)
{
return
"avdec_vp8"
;
}
return
string
();
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment