Browse Source

Merge remote-tracking branch 'mrscotty/develop'

Patrik Dahlström 4 years ago
parent
commit
6fb224e23f

+ 12 - 0
Configuration.cpp

@@ -3,6 +3,7 @@
 #include <boost/property_tree/ptree.hpp>
 #include <boost/property_tree/ptree.hpp>
 #include <boost/property_tree/ini_parser.hpp>
 #include <boost/property_tree/ini_parser.hpp>
 #include <boost/format.hpp>
 #include <boost/format.hpp>
+#include <boost/foreach.hpp>
 
 
 using namespace config;
 using namespace config;
 
 
@@ -50,4 +51,15 @@ std::string config::Configuration::getString(const std::string &property) {
     return get<std::string>(impl->ptree, property);
     return get<std::string>(impl->ptree, property);
 }
 }
 
 
+// TODO: return set
+std::unordered_map<std::string, std::string> config::Configuration::getChildren(const std::string &property) {
+    std::unordered_map<std::string, std::string> pins;
+    BOOST_FOREACH(boost::property_tree::ptree::value_type &v,
+            impl->ptree.get_child(property)) {
+        //pins[v.first.data()] = get<std::string>(impl->ptree, property + "." + v.second.data());
+        pins[v.first.data()] = v.second.data();
+    }
+    return pins;
+}
+
 
 

+ 3 - 0
Configuration.hpp

@@ -5,6 +5,7 @@
 #include <vector>
 #include <vector>
 #include <string>
 #include <string>
 #include <stdexcept>
 #include <stdexcept>
+#include <unordered_map>
 
 
 namespace config {
 namespace config {
 
 
@@ -31,6 +32,8 @@ namespace config {
 
 
         std::string getString(const std::string &property);
         std::string getString(const std::string &property);
 
 
+        std::unordered_map<std::string, std::string> getChildren(const std::string &property);
+
     private:
     private:
         ConfigurationImpl *impl;
         ConfigurationImpl *impl;
     };
     };

+ 33 - 0
MumbleChannelJoiner.cpp

@@ -5,12 +5,20 @@ using namespace std;
 
 
 mumble::MumbleChannelJoiner::MumbleChannelJoiner(std::string channelNameRegex) : channelNameRegex(boost::regex(channelNameRegex)),
 mumble::MumbleChannelJoiner::MumbleChannelJoiner(std::string channelNameRegex) : channelNameRegex(boost::regex(channelNameRegex)),
 logger(log4cpp::Category::getInstance("MumbleChannelJoiner")){
 logger(log4cpp::Category::getInstance("MumbleChannelJoiner")){
+    //std::vector<ChannelEntry> *channels = new std::vector<ChannelEntry>();
 }
 }
 
 
+std::vector<mumble::MumbleChannelJoiner::ChannelEntry> mumble::MumbleChannelJoiner::channels;
+
 void mumble::MumbleChannelJoiner::checkChannel(std::string channel_name, int channel_id) {
 void mumble::MumbleChannelJoiner::checkChannel(std::string channel_name, int channel_id) {
     boost::smatch s;
     boost::smatch s;
+    ChannelEntry ent;
     logger.debug("Channel %s available (%d)", channel_name.c_str(), channel_id);
     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)) {
     if(boost::regex_match(channel_name, s, channelNameRegex)) {
       this->channel_id = channel_id;
       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<ChannelEntry>::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);
+}
+
+

+ 10 - 0
MumbleChannelJoiner.hpp

@@ -3,21 +3,31 @@
 #include <boost/noncopyable.hpp>
 #include <boost/noncopyable.hpp>
 #include <log4cpp/Category.hh>
 #include <log4cpp/Category.hh>
 
 
+#include <vector>
 #include <string>
 #include <string>
 #include <boost/regex.hpp>
 #include <boost/regex.hpp>
 #include "MumbleCommunicator.hpp"
 #include "MumbleCommunicator.hpp"
 
 
 namespace mumble {
 namespace mumble {
     class MumbleChannelJoiner : boost::noncopyable {
     class MumbleChannelJoiner : boost::noncopyable {
+
+        struct ChannelEntry {
+            int id;
+            std::string name;
+        };
+
     public:
     public:
         MumbleChannelJoiner(std::string channelNameRegex);
         MumbleChannelJoiner(std::string channelNameRegex);
 
 
         void checkChannel(std::string channel_name, int channel_id);
         void checkChannel(std::string channel_name, int channel_id);
         void maybeJoinChannel(mumble::MumbleCommunicator *mc);
         void maybeJoinChannel(mumble::MumbleCommunicator *mc);
+        void findJoinChannel(mumble::MumbleCommunicator *mc);
+        void joinOtherChannel(mumble::MumbleCommunicator *mc, std::string channelNameRegex);
 
 
     private:
     private:
         log4cpp::Category &logger;
         log4cpp::Category &logger;
         boost::regex channelNameRegex;
         boost::regex channelNameRegex;
         int channel_id;
         int channel_id;
+        static std::vector<ChannelEntry> channels;
     };
     };
 }
 }

+ 100 - 2
MumbleCommunicator.cpp

@@ -9,13 +9,14 @@ namespace mumble {
         std::shared_ptr<mumlib::Mumlib> mum;
         std::shared_ptr<mumlib::Mumlib> mum;
         MumbleCommunicator *communicator;
         MumbleCommunicator *communicator;
 
 
+        // called by Mumlib when receiving audio from mumble server 
         virtual void audio(
         virtual void audio(
                 int target,
                 int target,
                 int sessionId,
                 int sessionId,
                 int sequenceNumber,
                 int sequenceNumber,
                 int16_t *pcm_data,
                 int16_t *pcm_data,
                 uint32_t pcm_data_size) override {
                 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(
         virtual void channelState(
@@ -39,6 +40,26 @@ namespace mumble {
             communicator->onServerSync();
             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());
     callback.reset(new MumlibCallback());
 
 
+    mumbleConf = config;
+
     mumConfig = mumlib::MumlibConfiguration();
     mumConfig = mumlib::MumlibConfiguration();
     mumConfig.opusEncoderBitrate = config.opusEncoderBitrate;
     mumConfig.opusEncoderBitrate = config.opusEncoderBitrate;
+    mumConfig.cert_file = config.cert_file;
+    mumConfig.privkey_file = config.privkey_file;
 
 
     mum.reset(new mumlib::Mumlib(*callback, ioService, mumConfig));
     mum.reset(new mumlib::Mumlib(*callback, ioService, mumConfig));
     callback->communicator = this;
     callback->communicator = this;
     callback->mum = mum;
     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) {
 void mumble::MumbleCommunicator::sendPcmSamples(int16_t *samples, unsigned int length) {
@@ -73,6 +128,49 @@ void mumble::MumbleCommunicator::sendTextMessage(std::string message) {
     mum->sendTextMessage(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) {
 void mumble::MumbleCommunicator::joinChannel(int channel_id) {
     mum->joinChannel(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);
+}
+

+ 41 - 2
MumbleCommunicator.hpp

@@ -8,8 +8,13 @@
 #include <string>
 #include <string>
 #include <stdexcept>
 #include <stdexcept>
 
 
+// 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 {
 namespace mumble {
 
 
+
     class Exception : public std::runtime_error {
     class Exception : public std::runtime_error {
     public:
     public:
         Exception(const char *message) : std::runtime_error(message) { }
         Exception(const char *message) : std::runtime_error(message) { }
@@ -21,8 +26,25 @@ namespace mumble {
         std::string user;
         std::string user;
         std::string password;
         std::string password;
         std::string host;
         std::string host;
+        std::string cert_file;
+        std::string privkey_file;
         int opusEncoderBitrate;
         int opusEncoderBitrate;
         int port = 0;
         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 {
     class MumbleCommunicator : boost::noncopyable {
@@ -31,6 +53,10 @@ namespace mumble {
                 boost::asio::io_service &ioService);
                 boost::asio::io_service &ioService);
 
 
         void connect(MumbleCommunicatorConfig &config);
         void connect(MumbleCommunicatorConfig &config);
+        void onConnect();
+        void onDisconnect();
+        void onCallerAuth();
+        //void onCallerUnauth();
 
 
         virtual ~MumbleCommunicator();
         virtual ~MumbleCommunicator();
 
 
@@ -38,9 +64,9 @@ namespace mumble {
 
 
         /**
         /**
          * This callback is called when communicator has received samples.
          * 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<void(int, int, int16_t *, int)> onIncomingPcmSamples;
+        std::function<void(int, int, int, int16_t *, int)> onIncomingPcmSamples;
 
 
         /**
         /**
          * This callback is called when a channel state message (e.g. Channel
          * This callback is called when a channel state message (e.g. Channel
@@ -50,15 +76,27 @@ namespace mumble {
 
 
         std::function<void()> onServerSync;
         std::function<void()> onServerSync;
 
 
+        std::function<void()> onUserState;
+
         void sendTextMessage(std::string message);
         void sendTextMessage(std::string message);
 
 
         void joinChannel(int channel_id);
         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:
     private:
         boost::asio::io_service &ioService;
         boost::asio::io_service &ioService;
 
 
         log4cpp::Category &logger;
         log4cpp::Category &logger;
 
 
+        MumbleCommunicatorConfig mumbleConf;
+
         mumlib::MumlibConfiguration mumConfig;
         mumlib::MumlibConfiguration mumConfig;
 
 
         std::shared_ptr<mumlib::Mumlib> mum;
         std::shared_ptr<mumlib::Mumlib> mum;
@@ -66,5 +104,6 @@ namespace mumble {
         std::unique_ptr<MumlibCallback> callback;
         std::unique_ptr<MumlibCallback> callback;
 
 
         friend class MumlibCallback;
         friend class MumlibCallback;
+
     };
     };
 }
 }

+ 285 - 32
PjsuaCommunicator.cpp

@@ -6,6 +6,8 @@
 #include <boost/algorithm/string.hpp>
 #include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 #include <boost/format.hpp>
 
 
+#include "main.hpp"
+
 using namespace std;
 using namespace std;
 
 
 namespace sip {
 namespace sip {
@@ -38,9 +40,9 @@ namespace sip {
 
 
     class _MumlibAudioMedia : public pj::AudioMedia {
     class _MumlibAudioMedia : public pj::AudioMedia {
     public:
     public:
-        _MumlibAudioMedia(sip::PjsuaCommunicator &comm, int frameTimeLength)
+        _MumlibAudioMedia(int call_id, sip::PjsuaCommunicator &comm, int frameTimeLength)
                 : communicator(comm) {
                 : communicator(comm) {
-            createMediaPort(frameTimeLength);
+            createMediaPort(call_id, frameTimeLength);
             registerMediaPort(&mediaPort);
             registerMediaPort(&mediaPort);
         }
         }
 
 
@@ -62,7 +64,7 @@ namespace sip {
             return communicator->mediaPortPutFrame(port, frame);
             return communicator->mediaPortPutFrame(port, frame);
         }
         }
 
 
-        void createMediaPort(int frameTimeLength) {
+        void createMediaPort(int call_id, int frameTimeLength) {
 
 
             auto name = pj_str((char *) "MumsiMediaPort");
             auto name = pj_str((char *) "MumsiMediaPort");
 
 
@@ -88,6 +90,8 @@ namespace sip {
             }
             }
 
 
             mediaPort.port_data.pdata = &communicator;
             mediaPort.port_data.pdata = &communicator;
+            // track call id in port_data
+            mediaPort.port_data.ldata = (long) call_id;
 
 
             mediaPort.get_frame = &callback_getFrame;
             mediaPort.get_frame = &callback_getFrame;
             mediaPort.put_frame = &callback_putFrame;
             mediaPort.put_frame = &callback_putFrame;
@@ -107,6 +111,9 @@ namespace sip {
 
 
         virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override;
         virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override;
 
 
+        virtual void playAudioFile(std::string file);
+        virtual void playAudioFile(std::string file, bool in_chan);
+
     private:
     private:
         sip::PjsuaCommunicator &communicator;
         sip::PjsuaCommunicator &communicator;
         pj::Account &account;
         pj::Account &account;
@@ -114,8 +121,8 @@ namespace sip {
 
 
     class _Account : public pj::Account {
     class _Account : public pj::Account {
     public:
     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;
         virtual void onRegState(pj::OnRegStateParam &prm) override;
 
 
@@ -124,7 +131,8 @@ namespace sip {
     private:
     private:
         sip::PjsuaCommunicator &communicator;
         sip::PjsuaCommunicator &communicator;
 
 
-        bool available = true;
+        int active_calls = 0;
+        int max_calls;
 
 
         friend class _Call;
         friend class _Call;
     };
     };
@@ -142,23 +150,67 @@ namespace sip {
         if (ci.state == PJSIP_INV_STATE_CONFIRMED) {
         if (ci.state == PJSIP_INV_STATE_CONFIRMED) {
             auto msgText = "Incoming call from " + address + ".";
             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.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) {
         } else if (ci.state == PJSIP_INV_STATE_DISCONNECTED) {
             auto &acc = dynamic_cast<_Account &>(account);
             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.";
                 auto msgText = "Call from " + address + " finished.";
 
 
-                communicator.mixer->clear();
+                communicator.calls[ci.id].mixer->clear();
 
 
                 communicator.logger.notice(msgText);
                 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;
             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) {
         if (ci.media[0].status == PJSUA_CALL_MEDIA_ACTIVE) {
             auto *aud_med = static_cast<pj::AudioMedia *>(getMedia(0));
             auto *aud_med = static_cast<pj::AudioMedia *>(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) {
         } 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<pj::AudioMedia *>(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) {
     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) {
     void _Account::onRegState(pj::OnRegStateParam &prm) {
@@ -200,10 +444,12 @@ namespace sip {
 
 
         if (communicator.uriValidator.validateUri(uri)) {
         if (communicator.uriValidator.validateUri(uri)) {
 
 
-            if (available) {
+            if (active_calls < max_calls) {
                 param.statusCode = PJSIP_SC_OK;
                 param.statusCode = PJSIP_SC_OK;
-                available = false;
+                active_calls++;
             } else {
             } else {
+                communicator.logger.notice("BUSY - reject incoming call from %s.", uri.c_str());
+                param.statusCode = PJSIP_SC_OK;
                 param.statusCode = PJSIP_SC_BUSY_EVERYWHERE;
                 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")),
         : logger(log4cpp::Category::getInstance("SipCommunicator")),
           pjsuaLogger(log4cpp::Category::getInstance("Pjsua")),
           pjsuaLogger(log4cpp::Category::getInstance("Pjsua")),
           uriValidator(validator) {
           uriValidator(validator) {
 
 
     logWriter.reset(new sip::_LogWriter(pjsuaLogger));
     logWriter.reset(new sip::_LogWriter(pjsuaLogger));
+    max_calls = maxCalls;
+
 
 
     endpoint.libCreate();
     endpoint.libCreate();
 
 
     pj::EpConfig endpointConfig;
     pj::EpConfig endpointConfig;
     endpointConfig.uaConfig.userAgent = "Mumsi Mumble-SIP gateway";
     endpointConfig.uaConfig.userAgent = "Mumsi Mumble-SIP gateway";
-    endpointConfig.uaConfig.maxCalls = 1;
+    endpointConfig.uaConfig.maxCalls = maxCalls;
 
 
     endpointConfig.logConfig.writer = logWriter.get();
     endpointConfig.logConfig.writer = logWriter.get();
     endpointConfig.logConfig.level = 5;
     endpointConfig.logConfig.level = 5;
@@ -236,11 +484,12 @@ sip::PjsuaCommunicator::PjsuaCommunicator(IncomingConnectionValidator &validator
 
 
     endpoint.libInit(endpointConfig);
     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; i<maxCalls; ++i) {
+        calls[i].index = i;
+        pj_caching_pool_init(&(calls[i].cachingPool), &pj_pool_factory_default_policy, 0);
+        calls[i].mixer.reset(new mixer::AudioFramesMixer(calls[i].cachingPool.factory));
+        calls[i].media.reset(new _MumlibAudioMedia(i, *this, frameTimeLength));
+    }
 
 
     logger.info("Created Pjsua communicator with frame length %d ms.", frameTimeLength);
     logger.info("Created Pjsua communicator with frame length %d ms.", frameTimeLength);
 }
 }
@@ -271,8 +520,8 @@ sip::PjsuaCommunicator::~PjsuaCommunicator() {
     endpoint.libDestroy();
     endpoint.libDestroy();
 }
 }
 
 
-void sip::PjsuaCommunicator::sendPcmSamples(int sessionId, int sequenceNumber, int16_t *samples, unsigned int length) {
-    mixer->addFrameToBuffer(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) {
 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<pj_int16_t *>(frame->buf);
     pj_int16_t *samples = static_cast<pj_int16_t *>(frame->buf);
     pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&(port->info));
     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) {
     if (readSamples < count) {
         pjsuaLogger.debug("Requested %d samples, available %d, filling remaining with zeros.",
         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);
     pj_size_t count = frame->size / 2 / PJMEDIA_PIA_CCNT(&port->info);
     frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
     frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
 
 
+    int call_id = (int) port->port_data.ldata;
+
     if (count > 0) {
     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;
     return PJ_SUCCESS;
@@ -318,6 +570,7 @@ void sip::PjsuaCommunicator::registerAccount(string host, string user, string pa
     accountConfig.sipConfig.authCreds.push_back(cred);
     accountConfig.sipConfig.authCreds.push_back(cred);
 
 
     logger.info("Registering account for URI: %s.", uri.c_str());
     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);
     account->create(accountConfig);
-}
+}
+

+ 50 - 9
PjsuaCommunicator.hpp

@@ -18,10 +18,19 @@
 #include <climits>
 #include <climits>
 #include <bits/unique_ptr.h>
 #include <bits/unique_ptr.h>
 
 
+// for userState enum
+#include <mumlib.hpp>
+
+#include "main.hpp"
+
+enum dtmf_modes_t {DTMF_MODE_UNAUTH, DTMF_MODE_ROOT, DTMF_MODE_STAR};
+
 namespace sip {
 namespace sip {
 
 
     constexpr int DEFAULT_PORT = 5060;
     constexpr int DEFAULT_PORT = 5060;
     constexpr int SAMPLING_RATE = 48000;
     constexpr int SAMPLING_RATE = 48000;
+    constexpr int MAX_CALLER_PIN_LEN = 64;
+    constexpr int MAX_PIN_FAILS = 2;
 
 
     class Exception : public std::runtime_error {
     class Exception : public std::runtime_error {
     public:
     public:
@@ -58,9 +67,27 @@ namespace sip {
 
 
     class _MumlibAudioMedia;
     class _MumlibAudioMedia;
 
 
+    struct call {
+        unsigned index;
+        std::unique_ptr<mixer::AudioFramesMixer> mixer;
+        std::unique_ptr<sip::_MumlibAudioMedia> media;
+        pj_caching_pool cachingPool;
+        std::function<void(std::string)> onStateChange;
+        std::function<void(int16_t *, int)> onIncomingPcmSamples;
+        std::function<void(int)> onMuteDeafChange;
+        std::function<void(mumlib::UserState field, bool val)> sendUserState;
+        std::function<void(mumlib::UserState field, std::string val)> sendUserStateStr;
+        std::function<void()> onConnect;
+        std::function<void()> onDisconnect;
+        std::function<void()> onCallerAuth;
+        std::function<void()> joinAuthChannel; // DEPRECATE ?
+        std::function<void(std::string channelNameRegex)> joinOtherChannel;
+        std::function<void()> joinDefaultChannel;
+    };
+
     class PjsuaCommunicator : boost::noncopyable {
     class PjsuaCommunicator : boost::noncopyable {
     public:
     public:
-        PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength);
+        PjsuaCommunicator(IncomingConnectionValidator &validator, int frameTimeLength, int maxCalls);
 
 
         void connect(
         void connect(
                 std::string host,
                 std::string host,
@@ -71,30 +98,44 @@ namespace sip {
         virtual ~PjsuaCommunicator();
         virtual ~PjsuaCommunicator();
 
 
         void sendPcmSamples(
         void sendPcmSamples(
+                int callId,
                 int sessionId,
                 int sessionId,
                 int sequenceNumber,
                 int sequenceNumber,
                 int16_t *samples,
                 int16_t *samples,
                 unsigned int length);
                 unsigned int length);
 
 
-        std::function<void(int16_t *, int)> onIncomingPcmSamples;
-
-        std::function<void(std::string)> onStateChange;
+        // 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;
+
+        // 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 mediaPortGetFrame(pjmedia_port *port, pjmedia_frame *frame);
 
 
         pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);
         pj_status_t mediaPortPutFrame(pjmedia_port *port, pjmedia_frame *frame);
 
 
+        call calls[MY_MAX_CALLS];
+
+        std::unordered_map<std::string, std::string> pins;
+
     private:
     private:
         log4cpp::Category &logger;
         log4cpp::Category &logger;
         log4cpp::Category &pjsuaLogger;
         log4cpp::Category &pjsuaLogger;
 
 
-        std::unique_ptr<mixer::AudioFramesMixer> mixer;
-
         std::unique_ptr<_LogWriter> logWriter;
         std::unique_ptr<_LogWriter> logWriter;
         std::unique_ptr<_Account> account;
         std::unique_ptr<_Account> account;
-        std::unique_ptr<_MumlibAudioMedia> media;
-
-        pj_caching_pool cachingPool;
 
 
         pj::Endpoint endpoint;
         pj::Endpoint endpoint;
 
 

+ 91 - 1
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
 ./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
 ## Start at boot
 
 
 *mumsi* provides no *init.d* scripts, but you can use great daemon mangaer, [Supervisor](http://supervisord.org/).
 *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
 redirect_stderr=true
 ```
 ```
 
 
-
 ## Issues
 ## Issues
 
 
 #### Port and NAT
 #### 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).
 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:
 ## TODO:
 
 

+ 36 - 1
config.ini.example

@@ -16,6 +16,10 @@ password = foobar
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
 frameLength = 40
 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]
 [mumble]
 host = example.org
 host = example.org
 port = 64738
 port = 64738
@@ -23,6 +27,37 @@ user = mumsi
 password = foobar
 password = foobar
 channelNameExpression =
 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
 # Bitrate of Opus encoder in B/s
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
 # Adjust it if you need to meet the specific bandwidth requirements of Murmur server
-opusEncoderBitrate = 16000
+opusEncoderBitrate = 16000
+
+# Set to 1 to use client certificates. The certs must be named <user>-cert.pem and
+# the private keys <user>-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
+
+

+ 181 - 30
main.cpp

@@ -10,6 +10,8 @@
 
 
 #include <execinfo.h>
 #include <execinfo.h>
 
 
+#include "main.hpp"
+
 /*
 /*
  * Code from http://stackoverflow.com/a/77336/5419223
  * Code from http://stackoverflow.com/a/77336/5419223
  */
  */
@@ -26,13 +28,15 @@ static void sigsegv_handler(int sig) {
 
 
 int main(int argc, char *argv[]) {
 int main(int argc, char *argv[]) {
     signal(SIGSEGV, sigsegv_handler);
     signal(SIGSEGV, sigsegv_handler);
+    int max_calls;
 
 
     log4cpp::OstreamAppender appender("console", &std::cout);
     log4cpp::OstreamAppender appender("console", &std::cout);
     log4cpp::PatternLayout layout;
     log4cpp::PatternLayout layout;
     layout.setConversionPattern("%d [%p] %c: %m%n");
     layout.setConversionPattern("%d [%p] %c: %m%n");
     appender.setLayout(&layout);
     appender.setLayout(&layout);
     log4cpp::Category &logger = log4cpp::Category::getRoot();
     log4cpp::Category &logger = log4cpp::Category::getRoot();
-    logger.setPriority(log4cpp::Priority::NOTICE);
+    logger.setPriority(log4cpp::Priority::DEBUG);
+    //logger.setPriority(log4cpp::Priority::NOTICE);
     logger.addAppender(appender);
     logger.addAppender(appender);
 
 
     if (argc == 1) {
     if (argc == 1) {
@@ -48,36 +52,18 @@ int main(int argc, char *argv[]) {
 
 
     boost::asio::io_service ioService;
     boost::asio::io_service ioService;
 
 
-    sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength"));
-
-    mumble::MumbleCommunicator mumbleCommunicator(ioService);
-
-    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);
+    try {
+        max_calls = conf.getInt("sip.max_calls");
+    } catch (...) {
+        max_calls = 1;
+    }
 
 
-    mumbleCommunicator.onIncomingChannelState = std::bind(
-            &mumble::MumbleChannelJoiner::checkChannel,
-            &mumbleChannelJoiner,
-            _1, _2);
+    sip::PjsuaCommunicator pjsuaCommunicator(connectionValidator, conf.getInt("sip.frameLength"), max_calls);
 
 
-    mumbleCommunicator.onServerSync = std::bind(
-            &mumble::MumbleChannelJoiner::maybeJoinChannel,
-            &mumbleChannelJoiner,
-            &mumbleCommunicator);
+    try {
+        pjsuaCommunicator.pins = conf.getChildren("pins");
+    } catch (...) {
+    }
 
 
     mumble::MumbleCommunicatorConfig mumbleConf;
     mumble::MumbleCommunicatorConfig mumbleConf;
     mumbleConf.host = conf.getString("mumble.host");
     mumbleConf.host = conf.getString("mumble.host");
@@ -85,8 +71,173 @@ int main(int argc, char *argv[]) {
     mumbleConf.user = conf.getString("mumble.user");
     mumbleConf.user = conf.getString("mumble.user");
     mumbleConf.password = conf.getString("mumble.password");
     mumbleConf.password = conf.getString("mumble.password");
     mumbleConf.opusEncoderBitrate = conf.getInt("mumble.opusEncoderBitrate");
     mumbleConf.opusEncoderBitrate = conf.getInt("mumble.opusEncoderBitrate");
+    /* default to 'false' if not found */
+    try {
+        mumbleConf.autodeaf = conf.getBool("mumble.autodeaf");
+    } catch (...) {
+        mumbleConf.autodeaf = false;
+    }
+
+    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";
+    }
 
 
-    mumbleCommunicator.connect(mumbleConf);
+    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; i<max_calls; i++) {
+
+        auto *mumcom = new mumble::MumbleCommunicator(ioService);
+        mumcom->callId = 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<void(mumble::MumbleCommunicator::*)(mumlib::UserState, bool)>
+                (&mumble::MumbleCommunicator::sendUserState),
+                mumcom,
+                _1, _2);
+
+        // Send UserState to Mumble
+        pjsuaCommunicator.calls[i].sendUserStateStr = std::bind(
+                static_cast<void(mumble::MumbleCommunicator::*)(mumlib::UserState, std::string)>
+                (&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(
     pjsuaCommunicator.connect(
             conf.getString("sip.host"),
             conf.getString("sip.host"),

+ 6 - 0
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

+ 66 - 0
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 <username>
+#
+#   make-client-certs.sh <userprefix> <count>
+#
+# 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 <username>-key.pem and 
+#   <username>-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 <<EOF
+Usage:
+
+    $0 username
+    $0 user-prefix count
+EOF
+    exit 1
+}
+
+USER="$1"
+COUNT="$2"
+
+# In this 'format', the %s is replaced with the user name generated in
+# the for loop.
+SUBJFMT="/C=DE/ST=HE/L=Ffm/O=Mumble Ext./CN=%s"
+
+if [ -z "$USER" ]; then
+    usage
+fi
+
+if [ -n "$3" ]; then
+    usage
+fi
+
+if [ -z "$COUNT" ]; then
+    COUNT=1
+fi
+
+for ((i=0; i<$COUNT; i++)) {
+    prefix="${USER}${i}"
+    subj=$(printf "$SUBJFMT" $prefix)
+
+    openssl req \
+        -nodes \
+        -new \
+        -x509 \
+        -keyout ${prefix}-key.pem \
+        -out ${prefix}-cert.pem \
+        -subj "$subj"
+}
+
+

+ 24 - 0
media/Makefile

@@ -0,0 +1,24 @@
+# Makefile
+#
+# This file generates the WAVE files from text files on macOS
+#
+
+WAVES := $(subst .msg,.wav,$(wildcard *.msg)) blow.wav
+
+VOICE := --voice=Samantha
+RATE := --rate=15
+QUALITY := --quality=127
+
+
+all: $(WAVES)
+
+%.wav: %.aiff
+	afconvert "$<" -d LEI16 "$@"
+
+%.aiff: %.msg
+	say $(VOICE) $(RATE) -o $@ < $<
+
+#announce-new-caller.wav: /System/Library/Sounds/Blow.aiff
+#	afconvert "$<" -d LEI16 "$@"
+
+

+ 1 - 0
media/announce-new-caller.msg

@@ -0,0 +1 @@
+Caller joined

BIN
media/announce-new-caller.wav


BIN
media/blow.wav


+ 1 - 0
media/entering-channel.msg

@@ -0,0 +1 @@
+entering channel

BIN
media/entering-channel.wav


+ 1 - 0
media/goodbye.msg

@@ -0,0 +1 @@
+Goodbye

BIN
media/goodbye.wav


+ 1 - 0
media/invalid-pin.msg

@@ -0,0 +1 @@
+Invalid pin

BIN
media/invalid-pin.wav


+ 3 - 0
media/menu.msg

@@ -0,0 +1,3 @@
+Press star five to mute.
+Press star six to un-mute.
+Press star nine to change channel.

BIN
media/menu.wav


+ 1 - 0
media/mute-off.msg

@@ -0,0 +1 @@
+mute off

BIN
media/mute-off.wav


+ 1 - 0
media/mute-on.msg

@@ -0,0 +1 @@
+mute on

BIN
media/mute-on.wav


+ 1 - 0
media/prompt-pin.msg

@@ -0,0 +1 @@
+Please enter pin, followed by the hash symbol

BIN
media/prompt-pin.wav


+ 1 - 0
media/welcome.msg

@@ -0,0 +1 @@
+Welcome to the conference bridge

BIN
media/welcome.wav