Merge remote-tracking branch 'mrscotty/develop'

This commit is contained in:
Patrik Dahlström 2020-03-26 22:17:25 +01:00
commit 6fb224e23f
33 changed files with 948 additions and 76 deletions

View File

@ -3,6 +3,7 @@
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/ini_parser.hpp>
#include <boost/format.hpp>
#include <boost/foreach.hpp>
using namespace config;
@ -50,4 +51,15 @@ std::string config::Configuration::getString(const std::string &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;
}

View File

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

View File

@ -5,12 +5,20 @@ using namespace std;
mumble::MumbleChannelJoiner::MumbleChannelJoiner(std::string channelNameRegex) : channelNameRegex(boost::regex(channelNameRegex)),
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) {
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<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);
}

View File

@ -3,21 +3,31 @@
#include <boost/noncopyable.hpp>
#include <log4cpp/Category.hh>
#include <vector>
#include <string>
#include <boost/regex.hpp>
#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<ChannelEntry> channels;
};
}

View File

@ -9,13 +9,14 @@ namespace mumble {
std::shared_ptr<mumlib::Mumlib> 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);
}

View File

@ -8,8 +8,13 @@
#include <string>
#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 {
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<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
@ -50,15 +76,27 @@ namespace mumble {
std::function<void()> onServerSync;
std::function<void()> 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<mumlib::Mumlib> mum;
@ -66,5 +104,6 @@ namespace mumble {
std::unique_ptr<MumlibCallback> callback;
friend class MumlibCallback;
};
}

View File

@ -6,6 +6,8 @@
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#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<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) {
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) {
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; 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);
}
@ -271,8 +520,8 @@ sip::PjsuaCommunicator::~PjsuaCommunicator() {
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) {
@ -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_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);
}
}

View File

@ -18,10 +18,19 @@
#include <climits>
#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 {
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::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 {
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<void(int16_t *, int)> 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<void(std::string)> 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<std::string, std::string> pins;
private:
log4cpp::Category &logger;
log4cpp::Category &pjsuaLogger;
std::unique_ptr<mixer::AudioFramesMixer> mixer;
std::unique_ptr<_LogWriter> logWriter;
std::unique_ptr<_Account> account;
std::unique_ptr<_MumlibAudioMedia> media;
pj_caching_pool cachingPool;
pj::Endpoint endpoint;

View File

@ -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:

View File

@ -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
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

211
main.cpp
View File

@ -10,6 +10,8 @@
#include <execinfo.h>
#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; 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(
conf.getString("sip.host"),

6
main.hpp Normal file
View File

@ -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
make-client-certs.sh Executable file
View File

@ -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
media/Makefile Normal file
View File

@ -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 "$@"

View File

@ -0,0 +1 @@
Caller joined

Binary file not shown.

BIN
media/blow.wav Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
entering channel

BIN
media/entering-channel.wav Normal file

Binary file not shown.

1
media/goodbye.msg Normal file
View File

@ -0,0 +1 @@
Goodbye

BIN
media/goodbye.wav Normal file

Binary file not shown.

1
media/invalid-pin.msg Normal file
View File

@ -0,0 +1 @@
Invalid pin

BIN
media/invalid-pin.wav Normal file

Binary file not shown.

3
media/menu.msg Normal file
View File

@ -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 Normal file

Binary file not shown.

1
media/mute-off.msg Normal file
View File

@ -0,0 +1 @@
mute off

BIN
media/mute-off.wav Normal file

Binary file not shown.

1
media/mute-on.msg Normal file
View File

@ -0,0 +1 @@
mute on

BIN
media/mute-on.wav Normal file

Binary file not shown.

1
media/prompt-pin.msg Normal file
View File

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

BIN
media/prompt-pin.wav Normal file

Binary file not shown.

1
media/welcome.msg Normal file
View File

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

BIN
media/welcome.wav Normal file

Binary file not shown.