Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
vlc-1.1
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Redmine
Redmine
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
videolan
vlc-1.1
Commits
1aac6d65
Commit
1aac6d65
authored
Feb 19, 2008
by
Christophe Mutricy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
PulseAudio output module. Thanks to Martin Hamrle. Closes #1215
parent
48fb0a1d
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
487 additions
and
0 deletions
+487
-0
THANKS
THANKS
+1
-0
configure.ac
configure.ac
+14
-0
modules/audio_output/Modules.am
modules/audio_output/Modules.am
+1
-0
modules/audio_output/pulse.c
modules/audio_output/pulse.c
+471
-0
No files found.
THANKS
View file @
1aac6d65
...
@@ -144,6 +144,7 @@ Marián Hikaník <podnety _at_ mojepreklady _dot_ net> - Slovak localisation
...
@@ -144,6 +144,7 @@ Marián Hikaník <podnety _at_ mojepreklady _dot_ net> - Slovak localisation
Mark Gritter <mgritter at kealia.com> - fix for netlist packet leak in demuxPSI
Mark Gritter <mgritter at kealia.com> - fix for netlist packet leak in demuxPSI
Markus Kern <markus-kern at gmx dot net> - video output window fixes (win32)
Markus Kern <markus-kern at gmx dot net> - video output window fixes (win32)
Markus Kuespert <ltlBeBoy at beosmail.com> - BeOS CSS support
Markus Kuespert <ltlBeBoy at beosmail.com> - BeOS CSS support
Martin Hamrle <hamrle 47 post d0t cz> - PulseAudio output module
Martin Kahr <martin --at-- martinkahr dot com> - Apple Remote support
Martin Kahr <martin --at-- martinkahr dot com> - Apple Remote support
Matej Urbančič <matej.urban at gmail.com> - Slovenian translation
Matej Urbančič <matej.urban at gmail.com> - Slovenian translation
Mateus Krepsky Ludwich <mateus @t csp dot com d.t br> - rc interface mosaic-order callback
Mateus Krepsky Ludwich <mateus @t csp dot com d.t br> - rc interface mosaic-order callback
...
...
configure.ac
View file @
1aac6d65
...
@@ -4649,6 +4649,20 @@ AC_ARG_ENABLE(esd,
...
@@ -4649,6 +4649,20 @@ AC_ARG_ENABLE(esd,
fi
fi
fi])
fi])
dnl
dnl Pulseaudio module
dnl
AC_ARG_ENABLE(pulse,
[ --enable-pulse Pulseaudio support (default enabled)])
if test "${enable_pulse}" != "no"
then
PKG_CHECK_MODULES(PULSE, libpulse,
[ VLC_ADD_PLUGINS([pulse])
VLC_ADD_CFLAGS([pulse],[${PULSE_CFLAGS}])
VLC_ADD_LDFLAGS([pulse],[${PULSE_LIBS}])]:,
[AC_MSG_WARN(pulsaudio library not found)])
fi
dnl
dnl
dnl Portaudio module
dnl Portaudio module
dnl
dnl
...
...
modules/audio_output/Modules.am
View file @
1aac6d65
...
@@ -10,3 +10,4 @@ SOURCES_hd1000a = hd1000a.cpp
...
@@ -10,3 +10,4 @@ SOURCES_hd1000a = hd1000a.cpp
SOURCES_portaudio = portaudio.c
SOURCES_portaudio = portaudio.c
SOURCES_auhal = auhal.c
SOURCES_auhal = auhal.c
SOURCES_jack = jack.c
SOURCES_jack = jack.c
SOURCES_pulse = pulse.c
modules/audio_output/pulse.c
0 → 100644
View file @
1aac6d65
/*****************************************************************************
* pulse.c : Pulseaudio output plugin for vlc
*****************************************************************************
* Copyright (C) 2008 the VideoLAN team
*
* Authors: Martin Hamrle <hamrle @ post . cz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
/*****************************************************************************
* Preamble
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <vlc/vlc.h>
#include <vlc_aout.h>
#include <pulse/pulseaudio.h>
/*****************************************************************************
* aout_sys_t: Pulseaudio output method descriptor
*****************************************************************************
* This structure is part of the audio output thread descriptor.
* It describes the specific properties of an audio device.
*****************************************************************************/
struct
aout_sys_t
{
/** PulseAudio playback stream object */
struct
pa_stream
*
stream
;
/** PulseAudio connection context */
struct
pa_context
*
context
;
/** Main event loop object */
struct
pa_threaded_mainloop
*
mainloop
;
int
started
;
size_t
buffer_size
;
mtime_t
start_date
;
};
#define PULSE_CLIENT_NAME N_("VLC media player")
#if 0
#define PULSE_DEBUG( ...) \
msg_Dbg( p_aout, __VA_ARGS__ )
#else
#define PULSE_DEBUG( ...) \
(void) 0
#endif
#define CHECK_DEAD_GOTO(label) do { \
if (!p_sys->context || pa_context_get_state(p_sys->context) != PA_CONTEXT_READY || \
!p_sys->stream || pa_stream_get_state(p_sys->stream) != PA_STREAM_READY) { \
msg_Err(p_aout, "Connection died: %s", p_sys->context ? pa_strerror(pa_context_errno(p_sys->context)) : "NULL"); \
goto label; \
} \
} while(0);
/*****************************************************************************
* Local prototypes
*****************************************************************************/
static
int
Open
(
vlc_object_t
*
);
static
void
Close
(
vlc_object_t
*
);
static
void
Play
(
aout_instance_t
*
);
static
void
context_state_cb
(
pa_context
*
c
,
void
*
userdata
);
static
void
stream_state_cb
(
pa_stream
*
s
,
void
*
userdata
);
static
void
stream_request_cb
(
pa_stream
*
s
,
size_t
length
,
void
*
userdata
);
static
void
stream_latency_update_cb
(
pa_stream
*
s
,
void
*
userdata
);
static
void
success_cb
(
pa_stream
*
s
,
int
sucess
,
void
*
userdata
);
static
void
uninit
(
aout_instance_t
*
p_aout
);
/*****************************************************************************
* Module descriptor
*****************************************************************************/
vlc_module_begin
();
set_shortname
(
"Pulse Audio"
);
set_description
(
_
(
"Pulseaudio audio output"
)
);
set_capability
(
"audio output"
,
40
);
set_category
(
CAT_AUDIO
);
set_subcategory
(
SUBCAT_AUDIO_AOUT
);
add_shortcut
(
"pulseaudio"
);
add_shortcut
(
"pa"
);
set_callbacks
(
Open
,
Close
);
vlc_module_end
();
/*****************************************************************************
* Open: open the audio device
*****************************************************************************/
static
int
Open
(
vlc_object_t
*
p_this
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
p_this
;
struct
aout_sys_t
*
p_sys
;
struct
pa_sample_spec
ss
;
const
struct
pa_buffer_attr
*
buffer_attr
;
struct
pa_buffer_attr
a
;
struct
pa_channel_map
map
;
/* Allocate structures */
p_aout
->
output
.
p_sys
=
p_sys
=
malloc
(
sizeof
(
aout_sys_t
)
);
if
(
p_sys
==
NULL
)
{
msg_Err
(
p_aout
,
"out of memory"
);
return
VLC_ENOMEM
;
}
p_sys
->
started
=
0
;
p_sys
->
stream
=
NULL
;
p_sys
->
mainloop
=
NULL
;
p_sys
->
context
=
NULL
;
PULSE_DEBUG
(
"Pulse start initialization"
);
ss
.
rate
=
p_aout
->
output
.
output
.
i_rate
;
ss
.
channels
=
2
;
ss
.
format
=
PA_SAMPLE_S16LE
;
p_aout
->
output
.
output
.
i_physical_channels
=
AOUT_CHAN_LEFT
|
AOUT_CHAN_RIGHT
;
p_aout
->
output
.
output
.
i_format
=
AOUT_FMT_S16_NE
;
if
(
!
pa_sample_spec_valid
(
&
ss
))
{
msg_Err
(
p_aout
,
"Invalid sample spec"
);
goto
fail
;
}
a
.
maxlength
=
pa_bytes_per_second
(
&
ss
)
/
4
/
pa_frame_size
(
&
ss
);
a
.
tlength
=
a
.
maxlength
*
9
/
10
;
a
.
prebuf
=
a
.
tlength
/
2
;
a
.
minreq
=
a
.
tlength
/
10
;
a
.
maxlength
*=
pa_frame_size
(
&
ss
);
a
.
tlength
*=
pa_frame_size
(
&
ss
);
a
.
prebuf
*=
pa_frame_size
(
&
ss
);
a
.
minreq
*=
pa_frame_size
(
&
ss
);
p_sys
->
buffer_size
=
a
.
minreq
;
pa_channel_map_init_stereo
(
&
map
);
if
(
!
(
p_sys
->
mainloop
=
pa_threaded_mainloop_new
()))
{
msg_Err
(
p_aout
,
"Failed to allocate main loop"
);
goto
fail
;
}
if
(
!
(
p_sys
->
context
=
pa_context_new
(
pa_threaded_mainloop_get_api
(
p_sys
->
mainloop
),
_
(
PULSE_CLIENT_NAME
))))
{
msg_Err
(
p_aout
,
"Failed to allocate context"
);
goto
fail
;
}
pa_context_set_state_callback
(
p_sys
->
context
,
context_state_cb
,
p_aout
);
PULSE_DEBUG
(
"Pulse before context connect"
);
if
(
pa_context_connect
(
p_sys
->
context
,
NULL
,
0
,
NULL
)
<
0
)
{
msg_Err
(
p_aout
,
"Failed to connect to server: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
goto
fail
;
}
PULSE_DEBUG
(
"Pulse after context connect"
);
pa_threaded_mainloop_lock
(
p_sys
->
mainloop
);
if
(
pa_threaded_mainloop_start
(
p_sys
->
mainloop
)
<
0
)
{
msg_Err
(
p_aout
,
"Failed to start main loop"
);
goto
unlock_and_fail
;
}
msg_Dbg
(
p_aout
,
"Pulse mainloop started"
);
/* Wait until the context is ready */
pa_threaded_mainloop_wait
(
p_sys
->
mainloop
);
if
(
pa_context_get_state
(
p_sys
->
context
)
!=
PA_CONTEXT_READY
)
{
msg_Err
(
p_aout
,
"Failed to connect to server: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
goto
unlock_and_fail
;
}
if
(
!
(
p_sys
->
stream
=
pa_stream_new
(
p_sys
->
context
,
"audio stream"
,
&
ss
,
&
map
)))
{
msg_Err
(
p_aout
,
"Failed to create stream: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
goto
unlock_and_fail
;
}
PULSE_DEBUG
(
"Pulse after new stream"
);
pa_stream_set_state_callback
(
p_sys
->
stream
,
stream_state_cb
,
p_aout
);
pa_stream_set_write_callback
(
p_sys
->
stream
,
stream_request_cb
,
p_aout
);
pa_stream_set_latency_update_callback
(
p_sys
->
stream
,
stream_latency_update_cb
,
p_aout
);
if
(
pa_stream_connect_playback
(
p_sys
->
stream
,
NULL
,
&
a
,
PA_STREAM_INTERPOLATE_TIMING
|
PA_STREAM_AUTO_TIMING_UPDATE
,
NULL
,
NULL
)
<
0
)
{
msg_Err
(
p_aout
,
"Failed to connect stream: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
goto
unlock_and_fail
;
}
PULSE_DEBUG
(
"Pulse stream connect"
);
/* Wait until the stream is ready */
pa_threaded_mainloop_wait
(
p_sys
->
mainloop
);
msg_Dbg
(
p_aout
,
"Pulse stream connected"
);
if
(
pa_stream_get_state
(
p_sys
->
stream
)
!=
PA_STREAM_READY
)
{
msg_Err
(
p_aout
,
"Failed to connect to server: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
goto
unlock_and_fail
;
}
PULSE_DEBUG
(
"Pulse after stream get status"
);
pa_threaded_mainloop_unlock
(
p_sys
->
mainloop
);
buffer_attr
=
pa_stream_get_buffer_attr
(
p_sys
->
stream
);
p_aout
->
output
.
i_nb_samples
=
buffer_attr
->
minreq
/
pa_frame_size
(
&
ss
);
p_aout
->
output
.
pf_play
=
Play
;
aout_VolumeSoftInit
(
p_aout
);
msg_Dbg
(
p_aout
,
"Pulse initialized successfully"
);
{
char
cmt
[
PA_CHANNEL_MAP_SNPRINT_MAX
],
sst
[
PA_SAMPLE_SPEC_SNPRINT_MAX
];
msg_Dbg
(
p_aout
,
"Buffer metrics: maxlength=%u, tlength=%u, prebuf=%u, minreq=%u"
,
buffer_attr
->
maxlength
,
buffer_attr
->
tlength
,
buffer_attr
->
prebuf
,
buffer_attr
->
minreq
);
msg_Dbg
(
p_aout
,
"Using sample spec '%s', channel map '%s'."
,
pa_sample_spec_snprint
(
sst
,
sizeof
(
sst
),
pa_stream_get_sample_spec
(
p_sys
->
stream
)),
pa_channel_map_snprint
(
cmt
,
sizeof
(
cmt
),
pa_stream_get_channel_map
(
p_sys
->
stream
)));
msg_Dbg
(
p_aout
,
"Connected to device %s (%u, %ssuspended)."
,
pa_stream_get_device_name
(
p_sys
->
stream
),
pa_stream_get_device_index
(
p_sys
->
stream
),
pa_stream_is_suspended
(
p_sys
->
stream
)
?
""
:
"not "
);
}
return
VLC_SUCCESS
;
unlock_and_fail:
msg_Dbg
(
p_aout
,
"Pulse initialization unlock and fail"
);
if
(
p_sys
->
mainloop
)
pa_threaded_mainloop_unlock
(
p_sys
->
mainloop
);
fail:
msg_Err
(
p_aout
,
"Pulse initialization failed"
);
uninit
(
p_aout
);
return
VLC_EGENERIC
;
}
/*****************************************************************************
* Play: play a sound samples buffer
*****************************************************************************/
static
void
Play
(
aout_instance_t
*
p_aout
)
{
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
pa_operation
*
o
;
if
(
!
p_sys
->
started
){
msg_Dbg
(
p_aout
,
"Pulse stream started"
);
p_sys
->
start_date
=
aout_FifoFirstDate
(
p_aout
,
&
p_aout
->
output
.
fifo
);
p_sys
->
started
=
1
;
pa_threaded_mainloop_lock
(
p_sys
->
mainloop
);
if
((
o
=
pa_stream_flush
(
p_sys
->
stream
,
success_cb
,
p_aout
))){
pa_operation_unref
(
o
);
}
pa_threaded_mainloop_unlock
(
p_sys
->
mainloop
);
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
}
}
/*****************************************************************************
* Close: close the audio device
*****************************************************************************/
static
void
Close
(
vlc_object_t
*
p_this
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
p_this
;
struct
aout_sys_t
*
p_sys
=
p_aout
->
output
.
p_sys
;
msg_Dbg
(
p_aout
,
"Pulse Close"
);
if
(
p_sys
->
stream
){
pa_operation
*
o
;
pa_threaded_mainloop_lock
(
p_sys
->
mainloop
);
pa_stream_set_write_callback
(
p_sys
->
stream
,
NULL
,
NULL
);
if
((
o
=
pa_stream_drain
(
p_sys
->
stream
,
success_cb
,
p_aout
))){
while
(
pa_operation_get_state
(
o
)
!=
PA_OPERATION_DONE
)
{
CHECK_DEAD_GOTO
(
fail
);
pa_threaded_mainloop_wait
(
p_sys
->
mainloop
);
}
fail:
pa_operation_unref
(
o
);
}
pa_threaded_mainloop_unlock
(
p_sys
->
mainloop
);
}
uninit
(
p_aout
);
}
static
void
uninit
(
aout_instance_t
*
p_aout
){
struct
aout_sys_t
*
p_sys
=
p_aout
->
output
.
p_sys
;
if
(
p_sys
->
mainloop
)
pa_threaded_mainloop_stop
(
p_sys
->
mainloop
);
if
(
p_sys
->
stream
)
{
pa_stream_disconnect
(
p_sys
->
stream
);
pa_stream_unref
(
p_sys
->
stream
);
p_sys
->
stream
=
NULL
;
}
if
(
p_sys
->
context
)
{
pa_context_disconnect
(
p_sys
->
context
);
pa_context_unref
(
p_sys
->
context
);
p_sys
->
context
=
NULL
;
}
if
(
p_sys
->
mainloop
)
{
pa_threaded_mainloop_free
(
p_sys
->
mainloop
);
p_sys
->
mainloop
=
NULL
;
}
free
(
p_sys
);
p_aout
->
output
.
p_sys
=
NULL
;
}
static
void
context_state_cb
(
pa_context
*
c
,
void
*
userdata
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
userdata
;
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
assert
(
c
);
PULSE_DEBUG
(
"Pulse context state changed"
);
switch
(
pa_context_get_state
(
c
))
{
case
PA_CONTEXT_READY
:
case
PA_CONTEXT_TERMINATED
:
case
PA_CONTEXT_FAILED
:
PULSE_DEBUG
(
"Pulse context state changed signal"
);
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
break
;
case
PA_CONTEXT_UNCONNECTED
:
case
PA_CONTEXT_CONNECTING
:
case
PA_CONTEXT_AUTHORIZING
:
case
PA_CONTEXT_SETTING_NAME
:
PULSE_DEBUG
(
"Pulse context state changed no signal"
);
break
;
}
}
static
void
stream_state_cb
(
pa_stream
*
s
,
void
*
userdata
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
userdata
;
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
assert
(
s
);
PULSE_DEBUG
(
"Pulse stream state changed"
);
switch
(
pa_stream_get_state
(
s
))
{
case
PA_STREAM_READY
:
case
PA_STREAM_FAILED
:
case
PA_STREAM_TERMINATED
:
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
break
;
case
PA_STREAM_UNCONNECTED
:
case
PA_STREAM_CREATING
:
break
;
}
}
static
void
stream_request_cb
(
pa_stream
*
s
,
size_t
length
,
void
*
userdata
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
userdata
;
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
mtime_t
next_date
;
assert
(
s
);
assert
(
p_sys
);
size_t
buffer_size
=
p_sys
->
buffer_size
;
PULSE_DEBUG
(
"Pulse stream request %d"
,
length
);
do
{
aout_buffer_t
*
p_buffer
=
NULL
;
if
(
p_sys
->
started
){
pa_usec_t
latency
;
int
negative
;
if
(
pa_stream_get_latency
(
p_sys
->
stream
,
&
latency
,
&
negative
)
<
0
){
if
(
pa_context_errno
(
p_sys
->
context
)
!=
PA_ERR_NODATA
)
{
msg_Err
(
p_aout
,
"pa_stream_get_latency() failed: %s"
,
pa_strerror
(
pa_context_errno
(
p_sys
->
context
)));
}
latency
=
0
;
}
PULSE_DEBUG
(
"Pulse stream request latency="
I64Fd
""
,
latency
);
next_date
=
mdate
()
+
latency
;
if
(
p_sys
->
start_date
<
next_date
+
AOUT_PTS_TOLERANCE
){
/*
vlc_mutex_lock( &p_aout->output_fifo_lock );
p_buffer = aout_FifoPop( p_aout, &p_aout->output.fifo );
vlc_mutex_unlock( &p_aout->output_fifo_lock );
*/
p_buffer
=
aout_OutputNextBuffer
(
p_aout
,
next_date
,
0
);
}
}
if
(
p_buffer
!=
NULL
)
{
PULSE_DEBUG
(
"Pulse stream request write buffer %d"
,
p_buffer
->
i_nb_bytes
);
pa_stream_write
(
p_sys
->
stream
,
p_buffer
->
p_buffer
,
p_buffer
->
i_nb_bytes
,
NULL
,
0
,
PA_SEEK_RELATIVE
);
length
-=
p_buffer
->
i_nb_bytes
;
aout_BufferFree
(
p_buffer
);
}
else
{
PULSE_DEBUG
(
"Pulse stream request write zeroes"
);
void
*
data
=
pa_xmalloc
(
buffer_size
);
bzero
(
data
,
buffer_size
);
pa_stream_write
(
p_sys
->
stream
,
data
,
buffer_size
,
pa_xfree
,
0
,
PA_SEEK_RELATIVE
);
length
-=
buffer_size
;
}
}
while
(
length
>
buffer_size
);
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
}
static
void
stream_latency_update_cb
(
pa_stream
*
s
,
void
*
userdata
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
userdata
;
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
assert
(
s
);
PULSE_DEBUG
(
"Pulse stream latency update"
);
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
}
static
void
success_cb
(
pa_stream
*
s
,
int
sucess
,
void
*
userdata
)
{
aout_instance_t
*
p_aout
=
(
aout_instance_t
*
)
userdata
;
struct
aout_sys_t
*
p_sys
=
(
struct
aout_sys_t
*
)
p_aout
->
output
.
p_sys
;
VLC_UNUSED
(
sucess
);
assert
(
s
);
pa_threaded_mainloop_signal
(
p_sys
->
mainloop
,
0
);
}
#undef PULSE_DEBUG
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