Reverting to branch with origin

parent a3321401
......@@ -48,13 +48,16 @@ class VistaConnectionIP;
class ITA_DATA_SOURCES_API CITANetAudioMessage
{
public:
CITANetAudioMessage( VistaConnectionIP* );
CITANetAudioMessage( VistaSerializingToolset::ByteOrderSwapBehavior bSwapBuffers );
void SetConnection( VistaConnectionIP* );
VistaConnectionIP* GetConnection() const;
void ClearConnection();
void WriteMessage();
bool TryReadMessage();
void ReadMessage();
void WriteAnswer();
void ReadAnswer();
void ResetMessage();
......@@ -63,7 +66,10 @@ public:
bool GetOutgoingMessageHasData() const;
void SetMessageType( int nType );
void SetAnswerType( int nType );
int GetMessageType() const;
int GetAnswerType() const;
void WriteInt( const int );
void WriteBool( const bool );
......@@ -94,11 +100,11 @@ public:
private:
int m_nMessageType;
int m_nMessageId;
int m_nAnswerType;
VistaByteBufferSerializer m_oOutgoing; //!< Serialization buffer for messages
VistaByteBufferDeSerializer m_oIncoming; //!< Deserialization buffer for messages
std::vector< VistaType::byte > m_vecIncomingBuffer; // Net IO buffer
int m_nTimeoutMilliseconds; //!< Timeout for try-read message
VistaConnectionIP* m_pConnection;
};
......
......@@ -60,6 +60,7 @@ public:
static const int NP_SERVER_OPEN = 201;
static const int NP_SERVER_GET_RINGBUFFER_SIZE = 210;
static const int NP_SERVER_GET_RINGBUFFER_FREE = 211;
static const int NP_SERVER_WAITING_FOR_TRIGGER = 221;
static const int NP_SERVER_SEND_SAMPLES = 222;
inline CITANetAudioProtocol() {};
......
......@@ -67,9 +67,7 @@ private:
CITANetAudioStream* m_pStream;
CITANetAudioProtocol* m_pProtocol;
CITANetAudioMessage* m_pIncomingMessage;
CITANetAudioMessage* m_pOutgoingMessage;
CITANetAudioMessage* m_pMessage;
VistaConnectionIP* m_pConnection;
VistaThreadEvent m_oBlockIncrementEvent;
......
......@@ -86,8 +86,7 @@ private:
VistaConnectionIP* m_pConnection;
CITANetAudioProtocol::StreamingParameters m_oServerParams;
CITANetAudioMessage* m_pIncomingMessage;
CITANetAudioMessage* m_pOutgoingMessage;
CITANetAudioMessage* m_pMessage;
int m_iUpdateStrategy;
int m_iClientRingBufferFreeSamples;
......
......@@ -13,14 +13,13 @@
static int S_nMessageIds = 0;
CITANetAudioMessage::CITANetAudioMessage( VistaConnectionIP* pConnection )
CITANetAudioMessage::CITANetAudioMessage( VistaSerializingToolset::ByteOrderSwapBehavior bSwapBuffers )
: m_vecIncomingBuffer( 2048 )
, m_oOutgoing( 2048 )
, m_pConnection( pConnection )
, m_nTimeoutMilliseconds( 1 )
, m_pConnection( NULL )
{
m_oOutgoing.SetByteorderSwapFlag( pConnection->GetByteorderSwapFlag() );
m_oIncoming.SetByteorderSwapFlag( pConnection->GetByteorderSwapFlag() );
m_oOutgoing.SetByteorderSwapFlag( bSwapBuffers );
m_oIncoming.SetByteorderSwapFlag( bSwapBuffers );
ResetMessage();
}
......@@ -45,12 +44,20 @@ void CITANetAudioMessage::ResetMessage()
m_oIncoming.SetBuffer( NULL, 0 );
m_nMessageType = CITANetAudioProtocol::NP_INVALID;
m_nAnswerType = CITANetAudioProtocol::NP_INVALID;
m_pConnection = NULL;
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [Preparing] (id=" << std::setw( 4 ) << m_nMessageId << ")" << std::endl;
#endif
}
void CITANetAudioMessage::SetConnection( VistaConnectionIP* pConn )
{
m_pConnection = pConn;
}
void CITANetAudioMessage::WriteMessage()
{
VistaType::byte* pBuffer = ( VistaType::byte* ) m_oOutgoing.GetBuffer();
......@@ -86,7 +93,6 @@ void CITANetAudioMessage::WriteMessage()
{
// It appears safe to send even very big data payload, so we will send at once
int iRawBufferSize = m_oOutgoing.GetBufferSize();
assert( iRawBufferSize > 4 );
int nRet = m_pConnection->Send( m_oOutgoing.GetBuffer(), iRawBufferSize );
#if NET_AUDIO_SHOW_TRAFFIC
......@@ -97,26 +103,19 @@ void CITANetAudioMessage::WriteMessage()
if( nRet != m_oOutgoing.GetBufferSize() )
VISTA_THROW( "ITANetAudioMessage: could not send all data from output buffer via network connection", 255 );
}
catch( VistaExceptionBase& ex )
catch (VistaExceptionBase& ex)
{
ITA_EXCEPT1( NETWORK_ERROR, ex.GetExceptionText() );
}
}
bool CITANetAudioMessage::TryReadMessage()
void CITANetAudioMessage::ReadMessage()
{
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ TryRead ] Waiting for incoming data for " << m_nTimeoutMilliseconds << std::endl;
vstr::out() << "CITANetAudioMessage [ Reading ] Waiting for incoming data" << std::endl;
#endif
long nIncomingBytes = m_pConnection->WaitForIncomingData( m_nTimeoutMilliseconds );
if( nIncomingBytes <= 0 )
{
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ TryRead ] nothing incoming" << std::endl;
#endif
return false;
}
long nIncomingBytes = m_pConnection->WaitForIncomingData( 0 );
assert( nIncomingBytes >= 4 ); // we need at least the size of message
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ Reading ] " << nIncomingBytes << " bytes incoming" << std::endl;
......@@ -129,14 +128,14 @@ bool CITANetAudioMessage::TryReadMessage()
vstr::out() << "CITANetAudioMessage [ Reading ] Expecting " << nMessagePayloadSize << " bytes message payload" << std::endl;
#endif
// we need at least the two protocol ints
//assert( nMessagePayloadSize >= 2 * sizeof( VistaType::sint32 ) );
assert( nMessagePayloadSize >= 2 * sizeof( VistaType::sint32 ) );
if( nMessagePayloadSize > ( int ) m_vecIncomingBuffer.size() )
m_vecIncomingBuffer.resize( nMessagePayloadSize );
// Receive all incoming data (potentially splitted)
int iBytesReceivedTotal = 0;
while( nMessagePayloadSize < iBytesReceivedTotal )
while( nMessagePayloadSize != iBytesReceivedTotal )
{
int iIncommingBytes = m_pConnection->WaitForIncomingData( 0 );
int iBytesReceived = m_pConnection->Receive( &m_vecIncomingBuffer[ iBytesReceivedTotal ], iIncommingBytes );
......@@ -154,8 +153,93 @@ bool CITANetAudioMessage::TryReadMessage()
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ Reading ] Finished receiving " << m_nMessageType << " (id=" << std::setw( 4 ) << m_nMessageId << ")" << std::endl;
#endif
}
void CITANetAudioMessage::WriteAnswer()
{
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ Answering] to " << m_nMessageType << " with " << m_nAnswerType << " (id=" << std::setw( 4 ) << m_nMessageId << ")" << std::endl;
#endif
assert( m_nAnswerType != CITANetAudioProtocol::NP_INVALID );
VistaType::byte* pBuffer = ( VistaType::byte* )m_oOutgoing.GetBuffer();
VistaType::sint32 iSwapDummy;
// rewrite size dummy
iSwapDummy = m_oOutgoing.GetBufferSize() - sizeof( VistaType::sint32 );
if( m_oOutgoing.GetByteorderSwapFlag() )
VistaSerializingToolset::Swap4( &iSwapDummy );
memcpy( pBuffer, &iSwapDummy, sizeof( VistaType::sint32 ) );
pBuffer += sizeof( VistaType::sint32 );
// rewrite type dummy
iSwapDummy = m_nAnswerType;
if( m_oOutgoing.GetByteorderSwapFlag() )
VistaSerializingToolset::Swap4( &iSwapDummy );
memcpy( pBuffer, &iSwapDummy, sizeof( VistaType::sint32 ) );
pBuffer += sizeof( VistaType::sint32 );
// rewrite message dummy
iSwapDummy = m_nMessageId;
if( m_oOutgoing.GetByteorderSwapFlag() )
VistaSerializingToolset::Swap4( &iSwapDummy );
memcpy( pBuffer, &iSwapDummy, sizeof( VistaType::sint32 ) );
return true;
int nRet = m_pConnection->Send( m_oOutgoing.GetBuffer(), m_oOutgoing.GetBufferSize() );
m_pConnection->WaitForSendFinish();
if( nRet != m_oOutgoing.GetBufferSize() )
ITA_EXCEPT1( UNKNOWN, "Could not write the expected number of bytes" );
}
void CITANetAudioMessage::ReadAnswer()
{
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ Reading] yet unkown answer from initial message type " << m_nMessageType << " (id=" << std::setw( 4 ) << m_nMessageId << ") OK" << std::endl;
#endif
VistaType::sint32 nMessagePayloadSize;
int nReturn;
nReturn = m_pConnection->ReadInt32( nMessagePayloadSize );
assert( nReturn == sizeof( VistaType::sint32 ) );
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "CITANetAudioMessage [ Reading] Answer type " << nReturn << " (id=" << std::setw( 4 ) << m_nMessageId << ") OK" << std::endl;
#endif
// We need at least the message type and message id in payload
assert( nMessagePayloadSize >= 2 * sizeof( VistaType::sint32 ) );
if( nMessagePayloadSize > ( int ) m_vecIncomingBuffer.size() )
m_vecIncomingBuffer.resize( nMessagePayloadSize );
// @todo: read over while( received < total ) loop!!!
int iBytesReceivedTotal = 0;
while( nMessagePayloadSize != iBytesReceivedTotal )
{
int iIncommingBytes = m_pConnection->WaitForIncomingData( 0 );
int iBytesReceived = m_pConnection->Receive( &m_vecIncomingBuffer[ iBytesReceivedTotal ], iIncommingBytes );
iBytesReceivedTotal += iBytesReceived;
#if NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "[ CITANetAudioMessage ] " << std::setw( 3 ) << std::floor( iBytesReceivedTotal / float( nMessagePayloadSize ) * 100.0f ) << "% of answer transmitted" << std::endl;
#endif
}
if( iBytesReceivedTotal != nMessagePayloadSize )
ITA_EXCEPT1( UNKNOWN, "Protokoll error, Received less bytes than expected when trying to receive answer" );
// Swap data to deserialization buffer
m_oIncoming.SetBuffer( &m_vecIncomingBuffer[ 0 ], nMessagePayloadSize, false );
// Take out the two protocol variables type and message id from deserialization buffer
m_nAnswerType = ReadInt();
int nMessageID = ReadInt();
assert( nMessageID == m_nMessageId );
}
int CITANetAudioMessage::GetMessageType() const
......@@ -169,6 +253,17 @@ void CITANetAudioMessage::SetMessageType( int nType )
m_nMessageType = nType;
}
void CITANetAudioMessage::SetAnswerType( int nType )
{
assert( m_nAnswerType == CITANetAudioProtocol::NP_INVALID ); // should only be set once
m_nAnswerType = nType;
}
int CITANetAudioMessage::GetAnswerType() const
{
return m_nAnswerType;
}
int CITANetAudioMessage::GetIncomingMessageSize() const
{
return m_oIncoming.GetTailSize();
......@@ -368,3 +463,4 @@ void CITANetAudioMessage::WriteSampleFrame( ITASampleFrame* pSamples )
}
}
}
......@@ -61,10 +61,25 @@ CITANetAudioStreamingClient::CITANetAudioStreamingClient( CITANetAudioStream* pP
m_pClientLogger = new ITABufferedDataLoggerImplClient( );
m_pClientLogger->setOutputFile(paras);
iStreamingBlockId = 0;
m_pMessage = new CITANetAudioMessage( VistaSerializingToolset::SWAPS_MULTIBYTE_VALUES );
}
CITANetAudioStreamingClient::~CITANetAudioStreamingClient()
{
//try{
if (m_pConnection->GetIsOpen())
{
m_pMessage->ResetMessage();
m_pMessage->SetConnection(m_pConnection);
m_pMessage->SetMessageType(CITANetAudioProtocol::NP_CLIENT_CLOSE);
m_pMessage->WriteMessage();
m_pClient->Disconnect();
//Disconnect();
}
//}
//catch (ITAException e){
// std::cout << e << std::endl;
//}
delete m_pClientLogger;
}
......@@ -78,13 +93,20 @@ bool CITANetAudioStreamingClient::Connect( const std::string& sAddress, int iPor
m_pConnection = m_pClient->GetConnection();
m_pIncomingMessage = new CITANetAudioMessage( m_pConnection );
m_pOutgoingMessage = new CITANetAudioMessage( m_pConnection );
m_pMessage->ResetMessage();
m_pMessage->SetConnection( m_pConnection );
// Validate streaming parameters of server and client
m_pOutgoingMessage->SetMessageType( CITANetAudioProtocol::NP_CLIENT_OPEN );
m_pOutgoingMessage->WriteStreamingParameters( m_oParams );
m_pOutgoingMessage->WriteMessage();
m_pMessage->SetMessageType( CITANetAudioProtocol::NP_CLIENT_OPEN );
m_pMessage->WriteStreamingParameters( m_oParams );
m_pMessage->WriteMessage();
m_pMessage->ReadAnswer();
assert( m_pMessage->GetAnswerType() == CITANetAudioProtocol::NP_SERVER_OPEN );
bool bOK = m_pMessage->ReadBool();
if( !bOK )
ITA_EXCEPT1( INVALID_PARAMETER, "Streaming server declined connection, detected streaming parameter mismatch." );
Run();
......@@ -99,25 +121,21 @@ bool CITANetAudioStreamingClient::LoopBody()
if( m_bStopIndicated )
return true;
// Send message to server that (and how many) samples can be received
m_pIncomingMessage->ResetMessage();
if( !m_pIncomingMessage->TryReadMessage() )
{
int iFreeSamplesUntilAllowedReached = m_pStream->GetAllowedLatencySamples() - m_pStream->GetRingBufferAvailableSamples();
oLog.iFreeSamples = iFreeSamplesUntilAllowedReached;
if( iFreeSamplesUntilAllowedReached < 0 )
iFreeSamplesUntilAllowedReached = 0;
m_pOutgoingMessage->ResetMessage();
m_pIncomingMessage->SetMessageType( CITANetAudioProtocol::NP_CLIENT_WAITING_FOR_SAMPLES );
m_pOutgoingMessage->WriteInt( iFreeSamplesUntilAllowedReached );
m_pOutgoingMessage->WriteMessage();
return false;
}
int iIncomingMessageType = m_pIncomingMessage->GetMessageType();
switch( iIncomingMessageType )
// Send message to server that samples can be received
m_pMessage->ResetMessage();
m_pMessage->SetConnection( m_pConnection );
m_pMessage->SetMessageType( CITANetAudioProtocol::NP_CLIENT_WAITING_FOR_SAMPLES );
int iFreeSamplesUntilAllowedReached = m_pStream->GetAllowedLatencySamples() - m_pStream->GetRingBufferAvailableSamples();
oLog.iFreeSamples = iFreeSamplesUntilAllowedReached;
if( iFreeSamplesUntilAllowedReached < 0 )
iFreeSamplesUntilAllowedReached = 0;
m_pMessage->WriteInt( iFreeSamplesUntilAllowedReached );
m_pMessage->WriteMessage();
// Wait for answer of server
m_pMessage->ReadAnswer();
int iAnswerType = m_pMessage->GetAnswerType();
switch( iAnswerType )
{
case CITANetAudioProtocol::NP_INVALID:
......@@ -129,10 +147,15 @@ bool CITANetAudioStreamingClient::LoopBody()
Disconnect();
break;
case CITANetAudioProtocol::NP_SERVER_WAITING_FOR_TRIGGER:
// Wait until block increment is triggered by audio context (more free samples in ring buffer)
m_oBlockIncrementEvent.WaitForEvent( true );
break;
case CITANetAudioProtocol::NP_SERVER_SEND_SAMPLES:
// Receive samples from net message and forward them to the stream ring buffer
m_pIncomingMessage->ReadSampleFrame( &m_sfReceivingBuffer );
m_pMessage->ReadSampleFrame( &m_sfReceivingBuffer );
if ( m_pStream->GetRingBufferFreeSamples( ) >= m_sfReceivingBuffer.GetLength( ) )
m_pStream->Transmit( m_sfReceivingBuffer, m_sfReceivingBuffer.GetLength( ) );
//else
......@@ -143,7 +166,7 @@ bool CITANetAudioStreamingClient::LoopBody()
break;
}
oLog.iChannel = m_pStream->GetNumberOfChannels();
oLog.iProtocolStatus = iIncomingMessageType;
oLog.iProtocolStatus = iAnswerType;
oLog.dWorldTimeStamp = ITAClock::getDefaultClock( )->getTime( );
m_pClientLogger->log( oLog );
return true;
......@@ -162,10 +185,9 @@ bool CITANetAudioStreamingClient::GetIsConnected() const
void CITANetAudioStreamingClient::Disconnect()
{
m_bStopIndicated = true;
StopGently( true );
delete m_pIncomingMessage;
delete m_pOutgoingMessage;
//delete m_pConnection;
m_pConnection = NULL;
m_bStopIndicated = false;
......
......@@ -38,86 +38,105 @@ bool CITANetAudioStreamingServer::Start( const std::string& sAddress, int iPort
m_pConnection = m_pNetAudioServer->GetConnection();
m_pIncomingMessage = new CITANetAudioMessage( m_pConnection );
m_pOutgoingMessage = new CITANetAudioMessage( m_pConnection );
m_pMessage = new CITANetAudioMessage( m_pConnection->GetByteorderSwapFlag() );
m_pMessage->ResetMessage();
m_pMessage->SetConnection( m_pConnection );
m_pMessage->ReadMessage(); // blocking
Run();
assert( m_pMessage->GetMessageType() == CITANetAudioProtocol::NP_CLIENT_OPEN );
CITANetAudioProtocol::StreamingParameters oClientParams = m_pMessage->ReadStreamingParameters();
return true;
bool bOK = false;
if( m_oServerParams == oClientParams )
{
bOK = true;
#ifdef NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "[ITANetAudioStreamingServer] Server and client parameters matched. Will resume with streaming" << std::endl;
}
else
{
vstr::out() << "[ITANetAudioStreamingServer] Server and client parameters mismatch detected. Will notify client and stop." << std::endl;
#endif
}
m_pMessage->SetAnswerType( CITANetAudioProtocol::NP_SERVER_OPEN );
m_pMessage->WriteBool( bOK );
m_pMessage->WriteAnswer();
if( bOK )
Run();
return bOK;
}
bool CITANetAudioStreamingServer::LoopBody()
{
m_pIncomingMessage->ResetMessage();
m_pMessage->ResetMessage();
m_pMessage->SetConnection( m_pConnection );
m_pMessage->ReadMessage(); // blocking
if( m_pIncomingMessage->TryReadMessage() )
int iMsgType = m_pMessage->GetMessageType();
switch( iMsgType )
{
int iMsgType = m_pIncomingMessage->GetMessageType();
switch( iMsgType )
{
case CITANetAudioProtocol::NP_CLIENT_OPEN:
vstr::out() << m_pIncomingMessage->ReadStreamingParameters().dSampleRate << std::endl;
break;
case CITANetAudioProtocol::NP_CLIENT_WAITING_FOR_SAMPLES:
{
int iFreeSamples = m_pMessage->ReadInt();
case CITANetAudioProtocol::NP_CLIENT_WAITING_FOR_SAMPLES:
if( iFreeSamples >= int( m_pInputStream->GetBlocklength() ) )
{
int iFreeSamples = m_pIncomingMessage->ReadInt();
if( iFreeSamples >= int( m_pInputStream->GetBlocklength() ) )
// Send Samples
for( int i = 0; i < int( m_pInputStream->GetNumberOfChannels() ); i++ )
{
// Send Samples
for( int i = 0; i < int( m_pInputStream->GetNumberOfChannels() ); i++ )
{
ITAStreamInfo oStreamInfo;
oStreamInfo.nSamples = m_sfTempTransmitBuffer.GetLength();
const float* pfData = m_pInputStream->GetBlockPointer( i, &oStreamInfo );
if( pfData != 0 )
m_sfTempTransmitBuffer[ i ].write( pfData, m_sfTempTransmitBuffer.GetLength() );
}
m_pInputStream->IncrementBlockPointer();
m_pOutgoingMessage->ResetMessage();
m_pOutgoingMessage->SetMessageType( CITANetAudioProtocol::NP_SERVER_SEND_SAMPLES );
m_pIncomingMessage->WriteSampleFrame( &m_sfTempTransmitBuffer );
m_pIncomingMessage->WriteMessage();
ITAStreamInfo oStreamInfo;
oStreamInfo.nSamples = m_sfTempTransmitBuffer.GetLength();
const float* pfData = m_pInputStream->GetBlockPointer( i, &oStreamInfo );
if( pfData != 0 )
m_sfTempTransmitBuffer[ i ].write( pfData, m_sfTempTransmitBuffer.GetLength() );
}
m_pInputStream->IncrementBlockPointer();
m_pMessage->SetAnswerType( CITANetAudioProtocol::NP_SERVER_SEND_SAMPLES );
m_pMessage->WriteSampleFrame( &m_sfTempTransmitBuffer );
m_pMessage->WriteAnswer();
#ifdef NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "[ITANetAudioStreamingServer] Transmitted "<< m_sfTempTransmitBuffer.GetLength() << " samples for "
<< m_pInputStream->GetNumberOfChannels() << " channels" << std::endl;
vstr::out() << "[ITANetAudioStreamingServer] Transmitted "<< m_sfTempTransmitBuffer.GetLength() << " samples for "
<< m_pInputStream->GetNumberOfChannels() << " channels" << std::endl;
#endif
}
else
{
}
else
{
// Waiting for Trigger
m_pMessage->SetAnswerType( CITANetAudioProtocol::NP_SERVER_WAITING_FOR_TRIGGER );
m_pMessage->WriteAnswer();
#ifdef NET_AUDIO_SHOW_TRAFFIC
vstr::out() << "[ITANetAudioStreamingServer] Not enough free samples in client buffer, continuing without sending samples" << std::endl;
vstr::out() << "[ITANetAudioStreamingServer] Not enough free samples in client buffer, requesting a trigger when more free samples available" << std::endl;
#endif
break;
}
break;
}
case CITANetAudioProtocol::NP_CLIENT_CLOSE:
{
Stop();
return false;
}
default:
{
vstr::out() << "[ITANetAudioStreamingServer] Unkown protocol type : " << iMsgType << std::endl;
break;
}
}
break;
}
else
case CITANetAudioProtocol::NP_CLIENT_CLOSE:
{
//m_pMessage->SetAnswerType( CITANetAudioProtocol::NP_SERVER_CLOSE );
//m_pMessage->WriteAnswer();
StopGently(false);
//m_pConnection = NULL;
Stop();
return false;
}
default:
{
// Request ringbuffer free sample size
m_pOutgoingMessage->ResetMessage();
m_pOutgoingMessage->SetMessageType( CITANetAudioProtocol::NP_SERVER_GET_RINGBUFFER_FREE );
m_pOutgoingMessage->WriteMessage();
vstr::out() << "[ITANetAudioStreamingServer] Unkown protocol type : " << iMsgType << std::endl;
break;
}
}
return false;
return true;
}
void CITANetAudioStreamingServer::SetInputStream( ITADatasource* pInStream )
......
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