diff --git a/Configuration.cpp b/Configuration.cpp index 0c94a8a..c93b9e0 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace config; @@ -50,4 +51,15 @@ std::string config::Configuration::getString(const std::string &property) { return get(impl->ptree, property); } +// TODO: return set +std::unordered_map config::Configuration::getChildren(const std::string &property) { + std::unordered_map pins; + BOOST_FOREACH(boost::property_tree::ptree::value_type &v, + impl->ptree.get_child(property)) { + //pins[v.first.data()] = get(impl->ptree, property + "." + v.second.data()); + pins[v.first.data()] = v.second.data(); + } + return pins; +} + diff --git a/Configuration.hpp b/Configuration.hpp index 8c38acb..c36a87c 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace config { @@ -31,6 +32,8 @@ namespace config { std::string getString(const std::string &property); + std::unordered_map getChildren(const std::string &property); + private: ConfigurationImpl *impl; }; diff --git a/MumbleChannelJoiner.cpp b/MumbleChannelJoiner.cpp index 30ad1b1..d3d33b4 100644 --- a/MumbleChannelJoiner.cpp +++ b/MumbleChannelJoiner.cpp @@ -5,12 +5,20 @@ using namespace std; mumble::MumbleChannelJoiner::MumbleChannelJoiner(std::string channelNameRegex) : channelNameRegex(boost::regex(channelNameRegex)), logger(log4cpp::Category::getInstance("MumbleChannelJoiner")){ + //std::vector *channels = new std::vector(); } +std::vector mumble::MumbleChannelJoiner::channels; + void mumble::MumbleChannelJoiner::checkChannel(std::string channel_name, int channel_id) { boost::smatch s; + ChannelEntry ent; logger.debug("Channel %s available (%d)", channel_name.c_str(), channel_id); + ent.name = channel_name; + ent.id = channel_id; + + channels.push_back(ent); if(boost::regex_match(channel_name, s, channelNameRegex)) { this->channel_id = channel_id; @@ -23,3 +31,28 @@ void mumble::MumbleChannelJoiner::maybeJoinChannel(mumble::MumbleCommunicator *m } } +/* This is a secondary channel-switching object that relys on updates to the + * class variable 'channels' for the channel list from the server. + */ +void mumble::MumbleChannelJoiner::findJoinChannel(mumble::MumbleCommunicator *mc) { + boost::smatch s; + + int found = -1; + + for(std::vector::iterator it = channels.begin(); it != channels.end(); ++it) { + if(boost::regex_match(it->name, s, channelNameRegex)) { + found = it->id; + } + } + + if(found > -1) { + mc->joinChannel(found); + } +} + +void mumble::MumbleChannelJoiner::joinOtherChannel(mumble::MumbleCommunicator *mc, std::string channelNameRegex) { + this->channelNameRegex = boost::regex(channelNameRegex); + findJoinChannel(mc); +} + + diff --git a/MumbleChannelJoiner.hpp b/MumbleChannelJoiner.hpp index 356d022..156665e 100644 --- a/MumbleChannelJoiner.hpp +++ b/MumbleChannelJoiner.hpp @@ -3,21 +3,31 @@ #include #include +#include #include #include #include "MumbleCommunicator.hpp" namespace mumble { class MumbleChannelJoiner : boost::noncopyable { + + struct ChannelEntry { + int id; + std::string name; + }; + public: MumbleChannelJoiner(std::string channelNameRegex); void checkChannel(std::string channel_name, int channel_id); void maybeJoinChannel(mumble::MumbleCommunicator *mc); + void findJoinChannel(mumble::MumbleCommunicator *mc); + void joinOtherChannel(mumble::MumbleCommunicator *mc, std::string channelNameRegex); private: log4cpp::Category &logger; boost::regex channelNameRegex; int channel_id; + static std::vector channels; }; } diff --git a/MumbleCommunicator.cpp b/MumbleCommunicator.cpp index 9a5c719..948c769 100644 --- a/MumbleCommunicator.cpp +++ b/MumbleCommunicator.cpp @@ -9,13 +9,14 @@ namespace mumble { std::shared_ptr mum; MumbleCommunicator *communicator; + // called by Mumlib when receiving audio from mumble server virtual void audio( int target, int sessionId, int sequenceNumber, int16_t *pcm_data, uint32_t pcm_data_size) override { - communicator->onIncomingPcmSamples(sessionId, sequenceNumber, pcm_data, pcm_data_size); + communicator->onIncomingPcmSamples(communicator->callId, sessionId, sequenceNumber, pcm_data, pcm_data_size); } virtual void channelState( @@ -39,6 +40,26 @@ namespace mumble { communicator->onServerSync(); }; + /* + virtual void onUserState( + int32_t session, + int32_t actor, + std::string name, + int32_t user_id, + int32_t channel_id, + int32_t mute, + int32_t deaf, + int32_t suppress, + int32_t self_mute, + int32_t self_deaf, + std::string comment, + int32_t priority_speaker, + int32_t recording + ) override { + communicator->onUserState(); + }; + */ + }; } @@ -51,14 +72,48 @@ void mumble::MumbleCommunicator::connect(MumbleCommunicatorConfig &config) { callback.reset(new MumlibCallback()); + mumbleConf = config; + mumConfig = mumlib::MumlibConfiguration(); mumConfig.opusEncoderBitrate = config.opusEncoderBitrate; + mumConfig.cert_file = config.cert_file; + mumConfig.privkey_file = config.privkey_file; mum.reset(new mumlib::Mumlib(*callback, ioService, mumConfig)); callback->communicator = this; callback->mum = mum; - mum->connect(config.host, config.port, config.user, config.password); + // IMPORTANT: comment these out when experimenting with onConnect + if ( ! MUM_DELAYED_CONNECT ) { + mum->connect(config.host, config.port, config.user, config.password); + if ( mumbleConf.autodeaf ) { + mum->sendUserState(mumlib::UserState::SELF_DEAF, true); + } + } +} + +void mumble::MumbleCommunicator::onConnect() { + if ( MUM_DELAYED_CONNECT ) { + mum->connect(mumbleConf.host, mumbleConf.port, mumbleConf.user, mumbleConf.password); + } + + if ( mumbleConf.comment.size() > 0 ) { + mum->sendUserState(mumlib::UserState::COMMENT, mumbleConf.comment); + } + if ( mumbleConf.autodeaf ) { + mum->sendUserState(mumlib::UserState::SELF_DEAF, true); + } +} + +void mumble::MumbleCommunicator::onDisconnect() { + if ( MUM_DELAYED_CONNECT ) { + mum->disconnect(); + } else { + } +} + +void mumble::MumbleCommunicator::onCallerAuth() { + //onServerSync(); } void mumble::MumbleCommunicator::sendPcmSamples(int16_t *samples, unsigned int length) { @@ -73,6 +128,49 @@ void mumble::MumbleCommunicator::sendTextMessage(std::string message) { mum->sendTextMessage(message); } +/* +void mumble::MumbleCommunicator::onUserState( + int32_t session, + int32_t actor, + std::string name, + int32_t user_id, + int32_t channel_id, + int32_t mute, + int32_t deaf, + int32_t suppress, + int32_t self_mute, + int32_t self_deaf, + std::string comment, + int32_t priority_speaker, + int32_t recording) { + + logger::notice("Entered onUserState(...)"); + + userState.mute = mute; + userState.deaf = deaf; + userState.suppress = suppress; + userState.self_mute = self_mute; + userState.self_deaf = self_deaf; + userState.priority_speaker = priority_speaker; + userState.recording = recording; +} +*/ + + void mumble::MumbleCommunicator::joinChannel(int channel_id) { mum->joinChannel(channel_id); + + if ( mumbleConf.autodeaf ) { + mum->sendUserState(mumlib::UserState::SELF_DEAF, true); + } } + + +void mumble::MumbleCommunicator::sendUserState(mumlib::UserState field, bool val) { + mum->sendUserState(field, val); +} + +void mumble::MumbleCommunicator::sendUserState(mumlib::UserState field, std::string val) { + mum->sendUserState(field, val); +} + diff --git a/MumbleCommunicator.hpp b/MumbleCommunicator.hpp index be04a4b..39b04b9 100644 --- a/MumbleCommunicator.hpp +++ b/MumbleCommunicator.hpp @@ -8,8 +8,13 @@ #include #include +// 0 = mumble users connected at start; 1 = connect at dial-in +// TODO: fix mumlib::TransportException when this option is enabled +#define MUM_DELAYED_CONNECT 0 + namespace mumble { + class Exception : public std::runtime_error { public: Exception(const char *message) : std::runtime_error(message) { } @@ -21,8 +26,25 @@ namespace mumble { std::string user; std::string password; std::string host; + std::string cert_file; + std::string privkey_file; int opusEncoderBitrate; int port = 0; + bool autodeaf; + std::string comment; + int max_calls = 1; + std::string authchan; // config.ini: channelAuthExpression + }; + + // This is the subset that is of interest to us + struct MumbleUserState { + int32_t mute; + int32_t deaf; + int32_t suppress; + int32_t self_mute; + int32_t self_deaf; + int32_t priority_speaker; + int32_t recording; }; class MumbleCommunicator : boost::noncopyable { @@ -31,6 +53,10 @@ namespace mumble { boost::asio::io_service &ioService); void connect(MumbleCommunicatorConfig &config); + void onConnect(); + void onDisconnect(); + void onCallerAuth(); + //void onCallerUnauth(); virtual ~MumbleCommunicator(); @@ -38,9 +64,9 @@ namespace mumble { /** * This callback is called when communicator has received samples. - * Arguments: session ID, sequence number, PCM samples, length of samples + * Arguments: call ID, session ID, sequence number, PCM samples, length of samples */ - std::function onIncomingPcmSamples; + std::function onIncomingPcmSamples; /** * This callback is called when a channel state message (e.g. Channel @@ -50,15 +76,27 @@ namespace mumble { std::function onServerSync; + std::function onUserState; + void sendTextMessage(std::string message); void joinChannel(int channel_id); + void sendUserState(mumlib::UserState field, bool val); + + void sendUserState(mumlib::UserState field, std::string val); + + MumbleUserState userState; + + int callId; + private: boost::asio::io_service &ioService; log4cpp::Category &logger; + MumbleCommunicatorConfig mumbleConf; + mumlib::MumlibConfiguration mumConfig; std::shared_ptr mum; @@ -66,5 +104,6 @@ namespace mumble { std::unique_ptr callback; friend class MumlibCallback; + }; } diff --git a/PjsuaCommunicator.cpp b/PjsuaCommunicator.cpp index 7a1e11e..63c660b 100644 --- a/PjsuaCommunicator.cpp +++ b/PjsuaCommunicator.cpp @@ -6,6 +6,8 @@ #include #include +#include "main.hpp" + using namespace std; namespace sip { @@ -38,9 +40,9 @@ namespace sip { class _MumlibAudioMedia : public pj::AudioMedia { public: - _MumlibAudioMedia(sip::PjsuaCommunicator &comm, int frameTimeLength) + _MumlibAudioMedia(int call_id, sip::PjsuaCommunicator &comm, int frameTimeLength) : communicator(comm) { - createMediaPort(frameTimeLength); + createMediaPort(call_id, frameTimeLength); registerMediaPort(&mediaPort); } @@ -62,7 +64,7 @@ namespace sip { return communicator->mediaPortPutFrame(port, frame); } - void createMediaPort(int frameTimeLength) { + void createMediaPort(int call_id, int frameTimeLength) { auto name = pj_str((char *) "MumsiMediaPort"); @@ -88,6 +90,8 @@ namespace sip { } mediaPort.port_data.pdata = &communicator; + // track call id in port_data + mediaPort.port_data.ldata = (long) call_id; mediaPort.get_frame = &callback_getFrame; mediaPort.put_frame = &callback_putFrame; @@ -107,6 +111,9 @@ namespace sip { virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override; + virtual void playAudioFile(std::string file); + virtual void playAudioFile(std::string file, bool in_chan); + private: sip::PjsuaCommunicator &communicator; pj::Account &account; @@ -114,8 +121,8 @@ namespace sip { class _Account : public pj::Account { public: - _Account(sip::PjsuaCommunicator &comm) - : communicator(comm) { } + _Account(sip::PjsuaCommunicator &comm, int max_calls) + : communicator(comm) { this->max_calls = max_calls; } virtual void onRegState(pj::OnRegStateParam &prm) override; @@ -124,7 +131,8 @@ namespace sip { private: sip::PjsuaCommunicator &communicator; - bool available = true; + int active_calls = 0; + int max_calls; friend class _Call; }; @@ -142,23 +150,67 @@ namespace sip { if (ci.state == PJSIP_INV_STATE_CONFIRMED) { auto msgText = "Incoming call from " + address + "."; + // first, login to Mumble (only matters if MUM_DELAYED_CONNECT) + communicator.calls[ci.id].onConnect(); + pj_thread_sleep(500); // sleep a moment to allow connection to stabilize + communicator.logger.notice(msgText); - communicator.onStateChange(msgText); + communicator.calls[ci.id].sendUserStateStr(mumlib::UserState::COMMENT, msgText); + communicator.calls[ci.id].onStateChange(msgText); + + pj_thread_sleep(500); // sleep a moment to allow connection to stabilize + this->playAudioFile(communicator.file_welcome); + + communicator.got_dtmf = ""; + + /* + * if no pin is set, go ahead and turn off mute/deaf + * otherwise, wait for pin to be entered + */ + if ( communicator.pins.size() == 0 ) { + // No PIN set... enter DTMF root menu and turn off mute/deaf + communicator.dtmf_mode = DTMF_MODE_ROOT; + // turning off mute automatically turns off deaf + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false); + pj_thread_sleep(500); // sleep a moment to allow connection to stabilize + this->playAudioFile(communicator.file_announce_new_caller, true); + } else { + // PIN set... enter DTMF unauth menu and play PIN prompt message + communicator.dtmf_mode = DTMF_MODE_UNAUTH; + communicator.calls[ci.id].joinDefaultChannel(); + pj_thread_sleep(500); // pause briefly after announcement + + this->playAudioFile(communicator.file_prompt_pin); + } + } else if (ci.state == PJSIP_INV_STATE_DISCONNECTED) { auto &acc = dynamic_cast<_Account &>(account); - if (not acc.available) { + /* + * Not sure why we check acc.available, but with multi-call + * functionality, this check doesn't work. + */ + //if (not acc.available) { auto msgText = "Call from " + address + " finished."; - communicator.mixer->clear(); + communicator.calls[ci.id].mixer->clear(); communicator.logger.notice(msgText); - communicator.onStateChange(msgText); + communicator.calls[ci.id].sendUserStateStr(mumlib::UserState::COMMENT, msgText); + communicator.calls[ci.id].onStateChange(msgText); + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_DEAF, true); + communicator.calls[ci.id].joinDefaultChannel(); - acc.available = true; - } + communicator.calls[ci.id].onDisconnect(); + + //acc.available = true; + acc.active_calls--; + //} delete this; + } else { + communicator.logger.notice("MYDEBUG: unexpected state in onCallState() call:%d state:%d", + ci.id, ci.state); } } @@ -172,15 +224,207 @@ namespace sip { if (ci.media[0].status == PJSUA_CALL_MEDIA_ACTIVE) { auto *aud_med = static_cast(getMedia(0)); - communicator.media->startTransmit(*aud_med); - aud_med->startTransmit(*communicator.media); + communicator.calls[ci.id].media->startTransmit(*aud_med); + aud_med->startTransmit(*communicator.calls[ci.id].media); } else if (ci.media[0].status == PJSUA_CALL_MEDIA_NONE) { - dynamic_cast<_Account &>(account).available = true; + dynamic_cast<_Account &>(account).active_calls++; + } + } + + void _Call::playAudioFile(std::string file) { + this->playAudioFile(file, false); // default is NOT to echo to mumble + } + + /* TODO: + * - local deafen before playing and undeafen after? + */ + void _Call::playAudioFile(std::string file, bool in_chan) { + communicator.logger.info("Entered playAudioFile(%s)", file.c_str()); + pj::AudioMediaPlayer player; + pj::MediaFormatAudio mfa; + pj::AudioMediaPlayerInfo pinfo; + int wavsize; + int sleeptime; + + if ( ! pj_file_exists(file.c_str()) ) { + communicator.logger.warn("File not found (%s)", file.c_str()); + return; + } + + /* TODO: use some library to get the actual length in millisec + * + * This just gets the file size and divides by a constant to + * estimate the length of the WAVE file in milliseconds. + * This depends on the encoding bitrate, etc. + */ + + auto ci = getInfo(); + if (ci.media.size() != 1) { + throw sip::Exception("ci.media.size is not 1"); + } + + if (ci.media[0].status == PJSUA_CALL_MEDIA_ACTIVE) { + auto *aud_med = static_cast(getMedia(0)); + + try { + player.createPlayer(file, PJMEDIA_FILE_NO_LOOP); + pinfo = player.getInfo(); + sleeptime = (pinfo.sizeBytes / (pinfo.payloadBitsPerSample * 2.75)); + + if ( in_chan ) { // choose the target sound output + player.startTransmit(*communicator.calls[ci.id].media); + } else { + player.startTransmit(*aud_med); + } + + pj_thread_sleep(sleeptime); + + if ( in_chan ) { // choose the target sound output + player.stopTransmit(*communicator.calls[ci.id].media); + } else { + player.stopTransmit(*aud_med); + } + + } catch (...) { + communicator.logger.notice("Error playing file %s", file.c_str()); + } + } else { + communicator.logger.notice("Call not active - can't play file %s", file.c_str()); } } void _Call::onDtmfDigit(pj::OnDtmfDigitParam &prm) { - communicator.logger.notice("DTMF digit '%s' (call %d).", prm.digit.c_str(), getId()); + //communicator.logger.notice("DTMF digit '%s' (call %d).", + // prm.digit.c_str(), getId()); + pj::CallOpParam param; + + auto ci = getInfo(); + std::string chanName; + + /* + * DTMF CALLER MENU + */ + + switch ( communicator.dtmf_mode ) { + case DTMF_MODE_UNAUTH: + /* + * IF UNAUTH, the only thing we allow is to authorize. + */ + switch ( prm.digit[0] ) { + case '#': + /* + * When user presses '#', test PIN entry + */ + if ( communicator.pins.size() > 0 ) { + if ( communicator.pins.count(communicator.got_dtmf) > 0 ) { + communicator.logger.info("Caller entered correct PIN"); + communicator.dtmf_mode = DTMF_MODE_ROOT; + communicator.logger.notice("MYDEBUG: %s:%s", + communicator.got_dtmf.c_str(), + communicator.pins[communicator.got_dtmf].c_str()); + communicator.calls[ci.id].joinOtherChannel( + communicator.pins[communicator.got_dtmf]); + + this->playAudioFile(communicator.file_entering_channel); + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false); + this->playAudioFile(communicator.file_announce_new_caller, true); + } else { + communicator.logger.info("Caller entered wrong PIN"); + this->playAudioFile(communicator.file_invalid_pin); + if ( communicator.pin_fails++ >= MAX_PIN_FAILS ) { + param.statusCode = PJSIP_SC_SERVICE_UNAVAILABLE; + pj_thread_sleep(500); // pause before next announcement + this->playAudioFile(communicator.file_goodbye); + pj_thread_sleep(500); // pause before next announcement + this->hangup(param); + } + this->playAudioFile(communicator.file_prompt_pin); + } + communicator.got_dtmf = ""; + } + break; + case '*': + /* + * Allow user to reset PIN entry by pressing '*' + */ + communicator.got_dtmf = ""; + this->playAudioFile(communicator.file_prompt_pin); + break; + default: + /* + * In all other cases, add input digit to stack + */ + communicator.got_dtmf = communicator.got_dtmf + prm.digit; + if ( communicator.got_dtmf.size() > MAX_CALLER_PIN_LEN ) { + // just drop 'em if too long + param.statusCode = PJSIP_SC_SERVICE_UNAVAILABLE; + this->playAudioFile(communicator.file_goodbye); + pj_thread_sleep(500); // pause before next announcement + this->hangup(param); + } + } + break; + case DTMF_MODE_ROOT: + /* + * User already authenticated; no data entry pending + */ + switch ( prm.digit[0] ) { + case '*': + /* + * Switch user to 'star' menu + */ + communicator.dtmf_mode = DTMF_MODE_STAR; + break; + default: + /* + * Default is to ignore all digits in root + */ + communicator.logger.info("Ignore DTMF digit '%s' in ROOT state", prm.digit.c_str()); + } + break; + case DTMF_MODE_STAR: + /* + * User already entered '*'; time to perform action + */ + switch ( prm.digit[0] ) { + case '5': + // Mute line + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, true); + this->playAudioFile(communicator.file_mute_on); + break; + case '6': + // Un-mute line + this->playAudioFile(communicator.file_mute_off); + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_MUTE, false); + break; + case '9': + if ( communicator.pins.size() > 0 ) { + communicator.dtmf_mode = DTMF_MODE_UNAUTH; + communicator.calls[ci.id].sendUserState(mumlib::UserState::SELF_DEAF, true); + communicator.calls[ci.id].joinDefaultChannel(); + this->playAudioFile(communicator.file_prompt_pin); + } else { + // we should have a 'not supported' message + } + break; + case '0': // block these for the menu itself + case '*': + default: + // play menu + communicator.logger.info("Unsupported DTMF digit '%s' in state STAR", prm.digit.c_str()); + this->playAudioFile(communicator.file_menu); + break; + } + /* + * In any case, switch back to root after one digit + */ + communicator.dtmf_mode = DTMF_MODE_ROOT; + break; + default: + communicator.logger.info("Unexpected DTMF '%s' in unknown state '%d'", prm.digit.c_str(), + communicator.dtmf_mode); + } + } void _Account::onRegState(pj::OnRegStateParam &prm) { @@ -200,10 +444,12 @@ namespace sip { if (communicator.uriValidator.validateUri(uri)) { - if (available) { + if (active_calls < max_calls) { param.statusCode = PJSIP_SC_OK; - available = false; + active_calls++; } else { + communicator.logger.notice("BUSY - reject incoming call from %s.", uri.c_str()); + param.statusCode = PJSIP_SC_OK; param.statusCode = PJSIP_SC_BUSY_EVERYWHERE; } @@ -216,18 +462,20 @@ namespace sip { } } -sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength) +sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength, int maxCalls) : logger(log4cpp::Category::getInstance("SipCommunicator")), pjsuaLogger(log4cpp::Category::getInstance("Pjsua")), uriValidator(validator) { logWriter.reset(new sip::_LogWriter(pjsuaLogger)); + max_calls = maxCalls; + endpoint.libCreate(); pj::EpConfig endpointConfig; endpointConfig.uaConfig.userAgent = "Mumsi Mumble-SIP gateway"; - endpointConfig.uaConfig.maxCalls = 1; + endpointConfig.uaConfig.maxCalls = maxCalls; endpointConfig.logConfig.writer = logWriter.get(); endpointConfig.logConfig.level = 5; @@ -236,11 +484,12 @@ sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator endpoint.libInit(endpointConfig); - pj_caching_pool_init(&cachingPool, &pj_pool_factory_default_policy, 0); - - mixer.reset(new mixer::AudioFramesMixer(cachingPool.factory)); - - media.reset(new _MumlibAudioMedia(*this, frameTimeLength)); + for(int i=0; iaddFrameToBuffer(sessionId, sequenceNumber, samples, length); +void sip::PjsuaCommunicator::sendPcmSamples(int callId, int sessionId, int sequenceNumber, int16_t *samples, unsigned int length) { + calls[callId].mixer->addFrameToBuffer(sessionId, sequenceNumber, samples, length); } pj_status_t sip::PjsuaCommunicator::mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame) { @@ -280,7 +529,8 @@ pj_status_t sip::PjsuaCommunicator::mediaPortGetFrame(pjmedia_port *port, pjmedi pj_int16_t *samples = static_cast(frame->buf); pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&(port->info)); - const int readSamples = mixer->getMixedSamples(samples, count); + int call_id = (int) port->port_data.ldata; + const int readSamples = calls[call_id].mixer->getMixedSamples(samples, count); if (readSamples < count) { pjsuaLogger.debug("Requested %d samples, available %d, filling remaining with zeros.", @@ -299,9 +549,11 @@ pj_status_t sip::PjsuaCommunicator::mediaPortPutFrame(pjmedia_port *port, pjmedi pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&port->info); frame->type = PJMEDIA_FRAME_TYPE_AUDIO; + int call_id = (int) port->port_data.ldata; + if (count > 0) { - pjsuaLogger.debug("Calling onIncomingPcmSamples with %d samples.", count); - onIncomingPcmSamples(samples, count); + pjsuaLogger.debug("Calling onIncomingPcmSamples with %d samples (call_id=%d).", count, call_id); + this->calls[call_id].onIncomingPcmSamples(samples, count); } return PJ_SUCCESS; @@ -318,6 +570,7 @@ void sip::PjsuaCommunicator::registerAccount(string host, string user, string pa accountConfig.sipConfig.authCreds.push_back(cred); logger.info("Registering account for URI: %s.", uri.c_str()); - account.reset(new _Account(*this)); + account.reset(new _Account(*this, max_calls)); account->create(accountConfig); -} \ No newline at end of file +} + diff --git a/PjsuaCommunicator.hpp b/PjsuaCommunicator.hpp index 395fe5b..72ce42b 100644 --- a/PjsuaCommunicator.hpp +++ b/PjsuaCommunicator.hpp @@ -18,10 +18,19 @@ #include #include +// for userState enum +#include + +#include "main.hpp" + +enum dtmf_modes_t {DTMF_MODE_UNAUTH, DTMF_MODE_ROOT, DTMF_MODE_STAR}; + namespace sip { constexpr int DEFAULT_PORT = 5060; constexpr int SAMPLING_RATE = 48000; + constexpr int MAX_CALLER_PIN_LEN = 64; + constexpr int MAX_PIN_FAILS = 2; class Exception : public std::runtime_error { public: @@ -58,9 +67,27 @@ namespace sip { class _MumlibAudioMedia; + struct call { + unsigned index; + std::unique_ptr mixer; + std::unique_ptr media; + pj_caching_pool cachingPool; + std::function onStateChange; + std::function onIncomingPcmSamples; + std::function onMuteDeafChange; + std::function sendUserState; + std::function sendUserStateStr; + std::function onConnect; + std::function onDisconnect; + std::function onCallerAuth; + std::function joinAuthChannel; // DEPRECATE ? + std::function joinOtherChannel; + std::function joinDefaultChannel; + }; + class PjsuaCommunicator : boost::noncopyable { public: - PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength); + PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength, int maxCalls); void connect( std::string host, @@ -71,30 +98,44 @@ namespace sip { virtual ~PjsuaCommunicator(); void sendPcmSamples( + int callId, int sessionId, int sequenceNumber, int16_t *samples, unsigned int length); - std::function onIncomingPcmSamples; + // config params we get from config.ini + std::string caller_pin; + std::string file_welcome; + std::string file_prompt_pin; + std::string file_entering_channel; + std::string file_announce_new_caller; + std::string file_invalid_pin; + std::string file_goodbye; + std::string file_mute_on; + std::string file_mute_off; + std::string file_menu; + int max_calls; - std::function onStateChange; + // TODO: move these to private? + std::string got_dtmf; + dtmf_modes_t dtmf_mode = DTMF_MODE_ROOT; + int pin_fails = 0; pj_status_t mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame); pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame); + call calls[MY_MAX_CALLS]; + + std::unordered_map pins; + private: log4cpp::Category &logger; log4cpp::Category &pjsuaLogger; - std::unique_ptr mixer; - std::unique_ptr<_LogWriter> logWriter; std::unique_ptr<_Account> account; - std::unique_ptr<_MumlibAudioMedia> media; - - pj_caching_pool cachingPool; pj::Endpoint endpoint; diff --git a/README.md b/README.md index 9a3dd99..d1b11e8 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,91 @@ Remember to add URIs which you want to make calls from. Calls from other URIs wo ./mumsi config.ini ``` +## Configuring + +### Multi-Line Support + +If your SIP provider allows multiple simultaneous calls, mumsi can be configured to accept +calls and map them to separate Mumble users. The max\_calls is configure in *config.ini*: + +``` +[sip] +... +max_calls = 32 +... +``` + +Currently, the Mumble connections are established at server start. For usability, the following +options are recommended: + +* caller\_pin +* autodeaf +* channelAuthExpression + +The maximum number of calls is set in *main.hpp* and should not exceed the +*PJSUA_MAX_CALLS* in *pjsua.h*, which by default is 32. This can also be recompiled to +more, if desired. + +When mumsi logs into Mumble, it uses the user name from *config.ini* and appends +the character '-', followed by the connection number (counter). + +*LIMITATIONS:* The code is _alpha_ and needs testing/debugging, especialy in +the code that uses mumlib::Transport. Also, there is initial work on connecting +the Mumble user only when the SIP call is active, so the UI for other users is +better, but this code is still very buggy and therefore disabled. + +### Caller PIN + +When the caller\_pin is set, the incoming SIP connection is mute/deaf until the +caller enters the correct PIN, followed by the '#' symbol. On three failed +attempts, the SIP connection is hung up. On success, the Mumble user is moved +into the channel matching channelAuthExpression, if specified, and then mute/deaf +is turned off. As a courtesy to the other users, a brief announcement audio +file is played in the Mumble channel. + +The caller\_pin is configured in *config.ini* in the *app* section: + +``` +[app] +caller_pin = 12345 +``` + +In addition to the caller\_pin, a channelAuthExpression can be set. After +the caller authenticates with the PIN, the mumsi Mumble user will switch +to the Mumble channel that matches this expression. When the call is +completed, the mumsi Mumble user will return to the default channel that +matches channelNameExpression. + +This helps keep the unused SIP connections from cluttering your channel. + +### Autodeaf + +By default (i.e. autodeaf=0), other Mumble users can only see whether the mumsi +connection has an active caller if they are in the same channel. This is becaue +the 'talking mouth' icon is not visible to users in other channels. The mute/deaf +icons, on the other hand, can be seen by Mumble users when they are in different +channels, making it easier to spot when a new caller has connected. + +Setting `autodeaf=1' causes the mumsi Mumble user to be mute/deaf when there +is no active SIP call. + +### Audio Files + +When certain events occur, it is user-friendly to provide some sort of prompting +confirmation to the user. An example set of WAV files is provided, but they +can easily be customized or replaced with local versions, if needed. If the +files are not found, no sound is played. The following events are supported: + +- welcome: Played to caller when first connecting to mumsi +- prompt\_pin: Prompt the caller to enter the PIN +- entering\_channel: Caller entered PIN and is now entering the Mumble channel +- announce\_new\_caller: Played to the Mumble channel when adding a new caller +- invalid\_pin: Let the caller know they entered the wrong PIN +- goodbye: Hanging up on the caller +- mute\_on: Self-mute has been turned on (not implemented) +- mute\_off: Self-mute has been turned off (not implemented) +- menu: Tell caller the menu options (not implemented) + ## Start at boot *mumsi* provides no *init.d* scripts, but you can use great daemon mangaer, [Supervisor](http://supervisord.org/). @@ -65,7 +150,6 @@ stdout_capture_maxbytes=1MB redirect_stderr=true ``` - ## Issues #### Port and NAT @@ -82,6 +166,12 @@ pjsua_conf_add_port(mediaPool, (pjmedia_port *)port, &id) error: Invalid operati Some older versions of PJSIP are affected (confirmed for 2.3). In this case you have to update PJSIP to most recent version (2.4.5). +#### mumlib::TrasportException + +The multi-caller code is _alpha_ and needs testing/debugging, especialy in +the code that uses mumlib::Transport. Also, there is initial work on connecting +the Mumble user only when the SIP call is active, so the UI for other users is +better, but this code is still very buggy and therefore disabled. ## TODO: diff --git a/config.ini.example b/config.ini.example index 5044909..d661fb7 100644 --- a/config.ini.example +++ b/config.ini.example @@ -16,6 +16,10 @@ password = foobar # Adjust it if you need to meet the specific bandwidth requirements of Murmur server frameLength = 40 +# Set the maximum number of SIP calls to allow simultaneously. This should be <= 32. +# If you need more, recompile PJSUA LIB and also modify the define in main.hpp. +max_calls = 1 + [mumble] host = example.org port = 64738 @@ -23,6 +27,37 @@ user = mumsi password = foobar channelNameExpression = +# When here is no SIP connection, the mumble state is set to self_mute/self_deaf +# so the other users can easily see whether the SIP is connected even when not +# in the same group +autodeaf = 1 + # Bitrate of Opus encoder in B/s # Adjust it if you need to meet the specific bandwidth requirements of Murmur server -opusEncoderBitrate = 16000 \ No newline at end of file +opusEncoderBitrate = 16000 + +# Set to 1 to use client certificates. The certs must be named -cert.pem and +# the private keys -key.pem. +use_certs = 0 + +[app] + +# Caller PIN needed to authenticate the phone call itself. The caller presses +# the PIN, followed by the hash '#' key. On success, the caller is +# unmuted/undeafened. On failure, the SIP call is hung up. +pin = 4321 + +[files] +# These files are used for the caller and mumble channel audio clips. +# The paths below assume that you are running ./mumsi in the build/ dir. +welcome = ../media/welcome.wav +prompt_pin = ../media/prompt-pin.wav +entering_channel = ../media/entering-channel.wav +announce_new_caller = ../media/announce-new-caller.wav +invalid_pin = ../media/invalid-pin.wav +goodbye = ../media/goodbye.wav +mute_on = ../media/mute-on.wav +mute_off = ../media/mute-off.wav +menu = ../media/menu.wav + + diff --git a/main.cpp b/main.cpp index bfd872a..f345cf2 100644 --- a/main.cpp +++ b/main.cpp @@ -10,6 +10,8 @@ #include +#include "main.hpp" + /* * Code from http://stackoverflow.com/a/77336/5419223 */ @@ -26,13 +28,15 @@ static void sigsegv_handler(int sig) { int main(int argc, char *argv[]) { signal(SIGSEGV, sigsegv_handler); + int max_calls; log4cpp::OstreamAppender appender("console", &std::cout); log4cpp::PatternLayout layout; layout.setConversionPattern("%d [%p] %c: %m%n"); appender.setLayout(&layout); log4cpp::Category &logger = log4cpp::Category::getRoot(); - logger.setPriority(log4cpp::Priority::NOTICE); + logger.setPriority(log4cpp::Priority::DEBUG); + //logger.setPriority(log4cpp::Priority::NOTICE); logger.addAppender(appender); if (argc == 1) { @@ -48,36 +52,18 @@ int main(int argc, char *argv[]) { boost::asio::io_service ioService; - sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength")); + try { + max_calls = conf.getInt("sip.max_calls"); + } catch (...) { + max_calls = 1; + } - mumble::MumbleCommunicator mumbleCommunicator(ioService); + sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength"), max_calls); - mumble::MumbleChannelJoiner mumbleChannelJoiner(conf.getString("mumble.channelNameExpression")); - - using namespace std::placeholders; - pjsuaCommunicator.onIncomingPcmSamples = std::bind( - &mumble::MumbleCommunicator::sendPcmSamples, - &mumbleCommunicator, - _1, _2); - - pjsuaCommunicator.onStateChange = std::bind( - &mumble::MumbleCommunicator::sendTextMessage, - &mumbleCommunicator, _1); - - mumbleCommunicator.onIncomingPcmSamples = std::bind( - &sip::PjsuaCommunicator::sendPcmSamples, - &pjsuaCommunicator, - _1, _2, _3, _4); - - mumbleCommunicator.onIncomingChannelState = std::bind( - &mumble::MumbleChannelJoiner::checkChannel, - &mumbleChannelJoiner, - _1, _2); - - mumbleCommunicator.onServerSync = std::bind( - &mumble::MumbleChannelJoiner::maybeJoinChannel, - &mumbleChannelJoiner, - &mumbleCommunicator); + try { + pjsuaCommunicator.pins = conf.getChildren("pins"); + } catch (...) { + } mumble::MumbleCommunicatorConfig mumbleConf; mumbleConf.host = conf.getString("mumble.host"); @@ -85,8 +71,173 @@ int main(int argc, char *argv[]) { mumbleConf.user = conf.getString("mumble.user"); mumbleConf.password = conf.getString("mumble.password"); mumbleConf.opusEncoderBitrate = conf.getInt("mumble.opusEncoderBitrate"); + /* default to 'false' if not found */ + try { + mumbleConf.autodeaf = conf.getBool("mumble.autodeaf"); + } catch (...) { + mumbleConf.autodeaf = false; + } - mumbleCommunicator.connect(mumbleConf); + try { + mumbleConf.comment = conf.getString("app.comment"); + } catch (...) { + mumbleConf.comment = ""; + } + + try { pjsuaCommunicator.file_welcome = conf.getString("files.welcome"); + } catch (...) { + pjsuaCommunicator.file_welcome = "welcome.wav"; + } + + try { pjsuaCommunicator.file_prompt_pin = conf.getString("files.prompt_pin"); + } catch (...) { + pjsuaCommunicator.file_prompt_pin = "prompt-pin.wav"; + } + + try { pjsuaCommunicator.file_entering_channel = conf.getString("files.entering_channel"); + } catch (...) { + pjsuaCommunicator.file_entering_channel = "entering-channel.wav"; + } + + try { pjsuaCommunicator.file_announce_new_caller = conf.getString("files.announce_new_caller"); + } catch (...) { + pjsuaCommunicator.file_announce_new_caller = "announce-new-caller.wav"; + } + + try { pjsuaCommunicator.file_invalid_pin = conf.getString("files.invalid_pin"); + } catch (...) { + pjsuaCommunicator.file_invalid_pin = "invalid-pin.wav"; + } + + try { pjsuaCommunicator.file_goodbye = conf.getString("files.goodbye"); + } catch (...) { + pjsuaCommunicator.file_goodbye = "goodbye.wav"; + } + + try { pjsuaCommunicator.file_mute_on = conf.getString("files.mute_on"); + } catch (...) { + pjsuaCommunicator.file_mute_on = "mute-on.wav"; + } + + try { pjsuaCommunicator.file_mute_off = conf.getString("files.mute_off"); + } catch (...) { + pjsuaCommunicator.file_mute_off = "mute-off.wav"; + } + + try { pjsuaCommunicator.file_menu = conf.getString("files.menu"); + } catch (...) { + pjsuaCommunicator.file_menu = "menu.wav"; + } + + std::string defaultChan = conf.getString("mumble.channelNameExpression"); + + mumble::MumbleChannelJoiner mumbleChannelJoiner(defaultChan); + mumble::MumbleChannelJoiner mumbleOtherChannelJoiner(defaultChan); + + for (int i = 0; icallId = i; + + using namespace std::placeholders; + // Passing audio input from SIP to Mumble + pjsuaCommunicator.calls[i].onIncomingPcmSamples = std::bind( + &mumble::MumbleCommunicator::sendPcmSamples, + mumcom, + _1, _2); + + // PJ sends text message to Mumble + pjsuaCommunicator.calls[i].onStateChange = std::bind( + &mumble::MumbleCommunicator::sendTextMessage, + mumcom, + _1); + + /* + // Send mute/deaf to Mumble + pjsuaCommunicator.calls[i].onMuteDeafChange = std::bind( + &mumble::MumbleCommunicator::mutedeaf, + mumcom, + _1); + */ + + // Send UserState to Mumble + pjsuaCommunicator.calls[i].sendUserState = std::bind( + static_cast + (&mumble::MumbleCommunicator::sendUserState), + mumcom, + _1, _2); + + // Send UserState to Mumble + pjsuaCommunicator.calls[i].sendUserStateStr = std::bind( + static_cast + (&mumble::MumbleCommunicator::sendUserState), + mumcom, + _1, _2); + + // PJ triggers Mumble connect + pjsuaCommunicator.calls[i].onConnect = std::bind( + &mumble::MumbleCommunicator::onConnect, + mumcom); + + // PJ triggers Mumble disconnect + pjsuaCommunicator.calls[i].onDisconnect = std::bind( + &mumble::MumbleCommunicator::onDisconnect, + mumcom); + + // PJ notifies Mumble that Caller Auth is done + pjsuaCommunicator.calls[i].onCallerAuth = std::bind( + &mumble::MumbleCommunicator::onCallerAuth, + mumcom); + + /* + // PJ notifies Mumble that Caller Auth is done + pjsuaCommunicator.calls[i].onCallerUnauth = std::bind( + &mumble::MumbleCommunicator::onCallerUnauth, + mumcom); + */ + + // PJ notifies Mumble that Caller Auth is done + pjsuaCommunicator.calls[i].joinDefaultChannel = std::bind( + &mumble::MumbleChannelJoiner::findJoinChannel, + &mumbleChannelJoiner, + mumcom); + + // PJ notifies Mumble to join other channel + pjsuaCommunicator.calls[i].joinOtherChannel = std::bind( + &mumble::MumbleChannelJoiner::joinOtherChannel, + &mumbleOtherChannelJoiner, + mumcom, + _1); + + // Passing audio from Mumble to SIP + mumcom->onIncomingPcmSamples = std::bind( + &sip::PjsuaCommunicator::sendPcmSamples, + &pjsuaCommunicator, + _1, _2, _3, _4, _5); + + // Handle Channel State messages from Mumble + mumcom->onIncomingChannelState = std::bind( + &mumble::MumbleChannelJoiner::checkChannel, + &mumbleChannelJoiner, + _1, _2); + + // Handle Server Sync message from Mumble + mumcom->onServerSync = std::bind( + &mumble::MumbleChannelJoiner::maybeJoinChannel, + &mumbleChannelJoiner, + mumcom); + + mumbleConf.user = conf.getString("mumble.user") + '-' + std::to_string(i); + try { + if ( conf.getBool("mumble.use_certs") ) { + mumbleConf.cert_file = mumbleConf.user + "-cert.pem"; + mumbleConf.privkey_file = mumbleConf.user + "-key.pem"; + } + } catch (...) { + logger.info("Client certs not enabled in config"); + } + mumcom->connect(mumbleConf); + } pjsuaCommunicator.connect( conf.getString("sip.host"), diff --git a/main.hpp b/main.hpp new file mode 100644 index 0000000..501350c --- /dev/null +++ b/main.hpp @@ -0,0 +1,6 @@ +#pragma once + +// IMPORTANT: The default PJSUA_MAX_CALLS in pjsua.h is 32. +// If you need more, you'll need to re-compile pjsua, too. +// +#define MY_MAX_CALLS 32 diff --git a/make-client-certs.sh b/make-client-certs.sh new file mode 100755 index 0000000..2b57794 --- /dev/null +++ b/make-client-certs.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# make-client-certs.sh - creates the client certs for registering with Mumble +# +# Usage: +# +# make-client-certs.sh +# +# make-client-certs.sh +# +# Notes: +# +# * The certs are self-signed and are not passphrase protected. Depending on +# the target environment and usage, this may or may not be OK. If you need +# a passphrase, you'll need to hack Mumlib. +# +# * The names are hard-coded in mumsi to match -key.pem and +# -cert.pem. This is done to make it easier to configure multi-line +# functionality. +# +# * When generating files for a series of users, the counter is appended to the +# user name, from '0' to one less than the COUNT. + +function usage { + cat <