diff --git a/Jamulus.pro b/Jamulus.pro index 75fcdbf841..0fcffb7ea7 100755 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -414,7 +414,8 @@ HEADERS += src/buffer.h \ src/recorder/jamrecorder.h \ src/recorder/creaperproject.h \ src/recorder/cwavestream.h \ - src/signalhandler.h + src/signalhandler.h \ + src/streamer/jamstreamer.h HEADERS_GUI = src/audiomixerboard.h \ src/chatdlg.h \ @@ -512,7 +513,8 @@ SOURCES += src/buffer.cpp \ src/util.cpp \ src/recorder/jamrecorder.cpp \ src/recorder/creaperproject.cpp \ - src/recorder/cwavestream.cpp + src/recorder/cwavestream.cpp \ + src/streamer/jamstreamer.cpp SOURCES_GUI = src/audiomixerboard.cpp \ src/chatdlg.cpp \ diff --git a/src/main.cpp b/src/main.cpp index f47b790150..5b603321e1 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -81,6 +81,7 @@ int main ( int argc, char** argv ) QString strHTMLStatusFileName = ""; QString strLoggingFileName = ""; QString strRecordingDirName = ""; + QString strStreamDest = ""; QString strCentralServer = ""; QString strServerInfo = ""; QString strServerPublicIP = ""; @@ -369,6 +370,22 @@ int main ( int argc, char** argv ) } + // Stream destination --------------------------------------------------------- + if ( GetStringArgument ( argc, + argv, + i, + "--streamto", // no short form + "--streamto", + strArgument ) ) + { + strStreamDest = strArgument; + qInfo() << qUtf8Printable( QString( "- stream destination: %1" ) + .arg( strStreamDest ) ); + CommandLineOptions << "--streamto"; + continue; + } + + // Disable recording on startup ---------------------------------------- if ( GetFlagArgument ( argv, i, @@ -715,6 +732,7 @@ int main ( int argc, char** argv ) strServerListFilter, strWelcomeMessage, strRecordingDirName, + strStreamDest, bDisconnectAllClientsOnQuit, bUseDoubleSystemFrameSize, bUseMultithreading, @@ -833,6 +851,8 @@ QString UsageArguments ( char **argv ) " --serverpublicip specify your public IP address when\n" " running a slave and your own central server\n" " behind the same NAT\n" + " --streamto pass ffmpeg output arguments to stream a stereo mix\n" + " from the server (see \"ffmpeg -h\" for reference)\n" "\nClient only:\n" " -M, --mutestream starts the application in muted state\n" " --mutemyown mute me in my personal mix (headless only)\n" diff --git a/src/server.cpp b/src/server.cpp index 9ef505efb5..f190f37211 100755 --- a/src/server.cpp +++ b/src/server.cpp @@ -229,6 +229,7 @@ CServer::CServer ( const int iNewMaxNumChan, const QString& strServerPublicIP, const QString& strNewWelcomeMessage, const QString& strRecordingDirName, + const QString& strStreamDest, const bool bNDisconnectAllClientsOnQuit, const bool bNUseDoubleSystemFrameSize, const bool bNUseMultithreading, @@ -405,7 +406,21 @@ CServer::CServer ( const int iNewMaxNumChan, // that jam recorder needs the frame size which is given to the jam // recorder in the SetRecordingDir() function) SetRecordingDir ( strRecordingDirName ); - +#ifndef _WIN32 + // enable jam streaming + if ( !strStreamDest.isEmpty() ) + { + bStream = true; + QThread* pthJamStreamer = new QThread; + streamer::CJamStreamer* pJamStreamer = new streamer::CJamStreamer(); + pJamStreamer->Init( strStreamDest ); + pJamStreamer->moveToThread(pthJamStreamer); + QObject::connect( this, &CServer::Started, pJamStreamer, &streamer::CJamStreamer::OnStarted ); + QObject::connect( this, &CServer::Stopped, pJamStreamer, &streamer::CJamStreamer::OnStopped ); + QObject::connect( this, &CServer::StreamFrame, pJamStreamer, &streamer::CJamStreamer::process ); + pthJamStreamer->start(); + } +#endif // enable all channels (for the server all channel must be enabled the // entire life time of the software) for ( i = 0; i < iMaxNumChannels; i++ ) @@ -863,6 +878,11 @@ static CTimingMeas JitterMeas ( 1000, "test2.dat" ); JitterMeas.Measure(); // TE vecNumAudioChannels, vecvecsData, vecChannelLevels ); +#ifndef _WIN32 + if ( bStream == true ) { + MixStream ( iNumClients ); + } +#endif for ( int iChanCnt = 0; iChanCnt < iNumClients; iChanCnt++ ) { @@ -1331,6 +1351,44 @@ opus_custom_encoder_ctl ( pCurOpusEncoder, OPUS_SET_BITRATE ( CalcBitRateBitsPer Q_UNUSED ( iUnused ) } +/// @brief Mix the audio data from all clients and send the mix to the jamstreamer +void CServer::MixStream ( const int iNumClients ) +{ + int i, j, k; + CVector& vecsSendData = vecvecsSendData[0]; // use reference for faster access + + // init intermediate processing vector with zeros since we mix all channels on that vector + vecsSendData.Reset ( 0 ); + + // Stereo target channel ----------------------------------------------- + for ( j = 0; j < iNumClients; j++ ) + { + // get a reference to the audio data of the current client + const CVector& vecsData = vecvecsData[j]; + + if ( vecNumAudioChannels[j] == 1 ) + { + // mono: copy same mono data in both out stereo audio channels + for ( i = 0, k = 0; i < iServerFrameSizeSamples; i++, k += 2 ) + { + // left/right channel + vecsSendData[k] += vecsData[i]; + vecsSendData[k + 1] += vecsData[i]; + } + } + else + { + // stereo + for ( i = 0; i < ( 2 * iServerFrameSizeSamples ); i++ ) + { + vecsSendData[i] += vecsData[i]; + } + } + } + + emit StreamFrame ( iServerFrameSizeSamples, vecsSendData ); +} + CVector CServer::CreateChannelList() { CVector vecChanInfo ( 0 ); diff --git a/src/server.h b/src/server.h index b4e78b4462..08dc80b869 100755 --- a/src/server.h +++ b/src/server.h @@ -46,6 +46,7 @@ #include "serverlogging.h" #include "serverlist.h" #include "recorder/jamcontroller.h" +#include "streamer/jamstreamer.h" /* Definitions ****************************************************************/ // no valid channel number @@ -179,6 +180,7 @@ class CServer : const QString& strServerPublicIP, const QString& strNewWelcomeMessage, const QString& strRecordingDirName, + const QString& strStreamDest, const bool bNDisconnectAllClientsOnQuit, const bool bNUseDoubleSystemFrameSize, const bool bNUseMultithreading, @@ -314,6 +316,8 @@ class CServer : void MixEncodeTransmitData ( const int iChanCnt, const int iNumClients ); + void MixStream ( const int iNumClients ); + virtual void customEvent ( QEvent* pEvent ); // if server mode is normal or double system frame size @@ -391,6 +395,9 @@ class CServer : recorder::CJamController JamController; bool bDisableRecording; + // jam streamer + bool bStream = false; + // GUI settings bool bAutoRunMinimized; @@ -412,6 +419,8 @@ class CServer : const int iNumAudChan, const CVector vecsData ); + void StreamFrame ( const int iServerFrameSizeSamples, const CVector& data ); + void CLVersionAndOSReceived ( CHostAddress InetAddr, COSUtil::EOpSystemType eOSType, QString strVersion ); diff --git a/src/streamer/jamstreamer.cpp b/src/streamer/jamstreamer.cpp new file mode 100644 index 0000000000..1254b4b6d6 --- /dev/null +++ b/src/streamer/jamstreamer.cpp @@ -0,0 +1,72 @@ +#ifndef _WIN32 +#include "jamstreamer.h" + +using namespace streamer; + +CJamStreamer::CJamStreamer() : qproc ( NULL ) {} + +void CJamStreamer::process( int iServerFrameSizeSamples, const CVector& data ) { + qproc->write ( reinterpret_cast (&data[0]), sizeof (int16_t) * ( 2 * iServerFrameSizeSamples ) ); +} + +void CJamStreamer::Init( const QString strStreamDest ) { + this->strStreamDest = strStreamDest; +} + +void CJamStreamer::OnStarted() { + if ( !qproc ) + { + qproc = new QProcess; + QObject::connect ( qproc, &QProcess::errorOccurred, this, &CJamStreamer::onError ); + QObject::connect ( qproc, QOverload::of( &QProcess::finished ), this, &CJamStreamer::onFinished ); + qproc->setStandardOutputFile ( QProcess::nullDevice() ); + } + QStringList argumentList = { "-loglevel", "error", + "-y", "-f", "s16le", + "-ar", "48000", "-ac", "2", + "-i", "-" }; + argumentList += strStreamDest.split( QRegExp("\\s+") ); + // Note that program name is also repeated as first argument + qproc->start ( "ffmpeg", argumentList ); +} + +void CJamStreamer::onError(QProcess::ProcessError error) +{ + QString errDesc; + switch (error) { + case QProcess::FailedToStart: + errDesc = "failed to start"; + break; + case QProcess::Crashed: + errDesc = "crashed"; + break; + case QProcess::Timedout: + errDesc = "timed out"; + break; + case QProcess::WriteError: + errDesc = "write error"; + break; + case QProcess::ReadError: + errDesc = "read error"; + break; + case QProcess::UnknownError: + errDesc = "unknown error"; + break; + default: + errDesc = "UNKNOWN unknown error"; + break; + } + qWarning() << "QProcess Error: " << errDesc << "\n"; +} + +void CJamStreamer::onFinished( int exitCode, QProcess::ExitStatus exitStatus ) +{ + Q_UNUSED ( exitStatus ); + QByteArray stderr = qproc->readAllStandardError (); + qInfo() << "ffmpeg exited with exitCode" << exitCode << ", stderr:" << stderr; +} + +void CJamStreamer::OnStopped() { + qproc->closeWriteChannel (); +} +#endif diff --git a/src/streamer/jamstreamer.h b/src/streamer/jamstreamer.h new file mode 100644 index 0000000000..94b16744b5 --- /dev/null +++ b/src/streamer/jamstreamer.h @@ -0,0 +1,28 @@ +#ifndef _WIN32 +#include +#include +#include "../util.h" + +namespace streamer { + +class CJamStreamer : public QObject { + Q_OBJECT + +public: + CJamStreamer(); + void Init( const QString strStreamDest ); + +public slots: + void process( int iServerFrameSizeSamples, const CVector& data ); + void OnStarted(); + void OnStopped(); +private slots: + void onError(QProcess::ProcessError error); + void onFinished( int exitCode, QProcess::ExitStatus exitStatus ); + +private: + QString strStreamDest; // stream destination to pass to ffmpeg as output part of arguments + QProcess* qproc; // ffmpeg subprocess +}; +} +#endif