BlinkenArea - GitList
Repositories
Blog
Wiki
Blinker
Code
Commits
Branches
Tags
Search
Tree:
9eeb24a
Branches
Tags
master
Blinker
src
common
SipPhone.cpp
implement common sound dir, add some sounds
Stefan Schuermans
commited
9eeb24a
at 2019-09-01 13:45:33
SipPhone.cpp
Blame
History
Raw
/* Blinker Copyright 2011-2019 Stefan Schuermans <stefan@blinkenarea.org> Copyleft GNU public license - http://www.gnu.org/copyleft/gpl.html a blinkenarea.org project */ #include <chrono> #include <condition_variable> #include <iostream> #include <mutex> #include <sstream> #include <stdarg.h> #include <stdio.h> #include <string> #include <string.h> #include <thread> #include <vector> #ifdef BLINKER_CFG_LINPHONE #include <linphone/linphonecore.h> #endif #include "Directory.h" #include "File.h" #include "Module.h" #include "NameFile.h" #include "OpConn.h" #include "OpConnIf.h" #include "SipPhone.h" #include "Time.h" #include "TimeCallee.h" namespace Blinker { #ifdef BLINKER_CFG_LINPHONE /** * everything using the linphone library happens in a separate thread, * because some calls to linphone take long (up to two seconds) */ namespace linphone { /// interlock for liblinphone, higher in mutex hierarchy than shared data mutex static std::mutex g_mtx; static char const *loglevel2str(OrtpLogLevel lev) { switch(lev) { case ORTP_DEBUG: return "DEBUG"; case ORTP_MESSAGE: return "MESSAGE"; case ORTP_WARNING: return "WARNING"; case ORTP_ERROR: return "ERROR"; case ORTP_FATAL: return "FATAL"; case ORTP_TRACE: return "TRACE"; case ORTP_LOGLEV_END: return "LOGLEV_END"; default: return "?"; } } static char const *gstate2str(LinphoneGlobalState gstate) { switch (gstate) { case LinphoneGlobalOff: return "Off"; case LinphoneGlobalStartup: return "Startup"; case LinphoneGlobalOn: return "On"; case LinphoneGlobalShutdown: return "Shutdown"; default: return "?"; } } static char const *rstate2str(LinphoneRegistrationState rstate) { switch (rstate) { case LinphoneRegistrationNone: return "None"; case LinphoneRegistrationProgress: return "Progress"; case LinphoneRegistrationOk: return "Ok"; case LinphoneRegistrationCleared: return "Cleared"; case LinphoneRegistrationFailed: return "Failed"; default: return "?"; } } static char const *cstate2str(LinphoneCallState cstate) { switch (cstate) { case LinphoneCallIdle: return "Idle"; case LinphoneCallIncomingReceived: return "IncomingReceived"; case LinphoneCallOutgoingInit: return "OutgoingInit"; case LinphoneCallOutgoingProgress: return "OutgoingProgress"; case LinphoneCallOutgoingRinging: return "OutgoingRinging"; case LinphoneCallOutgoingEarlyMedia: return "OutgoingEarlyMedia"; case LinphoneCallConnected: return "Connected"; case LinphoneCallStreamsRunning: return "StreamsRunning"; case LinphoneCallPausing: return "Pausing"; case LinphoneCallPaused: return "Paused"; case LinphoneCallResuming: return "Resuming"; case LinphoneCallRefered: return "Refered"; case LinphoneCallError: return "Error"; case LinphoneCallEnd: return "End"; case LinphoneCallPausedByRemote: return "PausedByRemote"; case LinphoneCallUpdatedByRemote: return "UpdatedByRemote"; case LinphoneCallIncomingEarlyMedia: return "IncomingEarlyMedia"; case LinphoneCallUpdating: return "Updating"; case LinphoneCallReleased: return "Released"; default: return "?"; } } std::string glLogFileName; ///< name of global log file std::ofstream glLogStream; ///< stream to open global log file unsigned int glLogLineCnt; ///< number of lines already logged /** * @brief log information from linphone library * @param[in] domain log domain - ignored * @param[in] lev log level * @param[in] fmt format string * @param[in] args arguments to insert into format string */ void log_handler(const char *domain, OrtpLogLevel lev, const char *fmt, va_list args) { // (re-)open logfile on first line if (glLogLineCnt == 0) { // close old file and clean up (just to be on the safe side) glLogStream.close(); glLogStream.clear(); // keep previous log file rename(glLogFileName.c_str(), (glLogFileName + ".prev").c_str()); // open new file glLogStream.open(glLogFileName.c_str(), std::ios::out); } // write log line char buffer[4096]; vsnprintf(buffer, sizeof(buffer) - 1, fmt, args); buffer[sizeof(buffer) - 1] = 0; glLogStream << Time::now().toStr() << " " << loglevel2str(lev) << " " << buffer << std::endl; // count lines ++glLogLineCnt; // close file if maximum line count reached if (glLogLineCnt >= 10000) { // close old file and clean up glLogStream.close(); glLogStream.clear(); // reset line count, so new file is opened on next line glLogLineCnt = 0; } (void)domain; } /// global init void init(std::string const &logFileName) { glLogFileName = logFileName; glLogLineCnt = 0; linphone_core_set_log_handler(log_handler); } /// data of SIP client worker thread struct Data { SipPhone::ConfigData const *configData; ///< config data for worker thread SipPhone::SharedData *sharedData; ///< data shared with main Blinker thread LinphoneCoreCbs *callbacks; ///< callbacks object, if active LinphoneCore *lc; ///< linphone core object, if active LinphoneCall *call; ///< current call, if available std::string playback; ///< name of playback file std::ofstream logStream; ///< stream to open log file unsigned int logLineCnt; ///< number of lines already logged to file }; /** * @brief set playback file * @param[in,out] data data of SIP client * @param[in] name name of sound file (without extension) */ void set_playback(Data &data, std::string const &name) { // search sound directories for file with mathcing name for (Directory const & soundDir : data.configData->soundDirs) { File soundFile(soundDir.getFile(name + ".wav")); if (soundFile.exists()) { data.playback = soundFile.getPath(); return; // first existing file found is set for playback } } data.playback.clear(); // no file found -> no playback } /** * @brief start playback of sound * @param[in,out] data data of SIP client */ void start_playback(Data &data) { if (data.lc && ! data.playback.empty()) { linphone_core_set_play_file(data.lc, data.playback.c_str()); } } /** * @brief write log message * @param[in,out] data data of SIP client * @param[in] line lie to log */ void log(Data &data, std::string const &line) { // (re-)open logfile on first line if (data.logLineCnt == 0) { // close old file and clean up (just to be on the safe side) data.logStream.close(); data.logStream.clear(); // keep previous log file rename(data.configData->logFileName.c_str(), (data.configData->logFileName + ".prev").c_str()); // open new file data.logStream.open(data.configData->logFileName.c_str(), std::ios::out); } // write log line data.logStream << Time::now().toStr() << " " << line << std::endl; // count lines ++data.logLineCnt; // close file if maximum line count reached if (data.logLineCnt >= 10000) { // close old file and clean up data.logStream.close(); data.logStream.clear(); // reset line count, so new file is opened on next line data.logLineCnt = 0; } } /** * @brief global SIP state changed * @param[in] lc linphone core object * @param[in] gstate new global SIP state * @param[in] message informational message */ void global_state_changed(struct _LinphoneCore *lc, LinphoneGlobalState gstate, const char *message) { Data *data = (Data*)linphone_core_get_user_data(lc); if (data == nullptr) { return; // still initializing -> ignore } std::stringstream line; line << "global state changed to " << gstate2str(gstate) << ": " << message; log(*data, line.str()); } /** * @brief registration state changed * @param[in] lc linphone core object * @param[in] cfg proxy configuration * @param[in] rstate new registration state * @param[in] message informational message */ void registration_state_changed(struct _LinphoneCore *lc, LinphoneProxyConfig *cfg, LinphoneRegistrationState rstate, const char *message) { Data *data = (Data*)linphone_core_get_user_data(lc); if (data == nullptr) { return; // still initializing -> ignore } (void)cfg; std::stringstream line; line << "registration state changed to " << rstate2str(rstate) << ": " << message; log(*data, line.str()); } /** * @brief call state changed * @param[in] lc linphone core object * @param[in] call call object * @param[in] cstate new call state * @param[in] message informational message */ void call_state_changed(struct _LinphoneCore *lc, LinphoneCall *call, LinphoneCallState cstate, const char *message) { Data *data = (Data*)linphone_core_get_user_data(lc); if (data == nullptr) { return; // still initializing -> ignore } std::stringstream line; line << "call state changed to " << cstate2str(cstate) << ": " << message; log(*data, line.str()); // current call -> handle state changes if (data->call == call) { switch (cstate) { case LinphoneCallStreamsRunning: start_playback(*data); break; case LinphoneCallRefered: case LinphoneCallError: case LinphoneCallEnd: case LinphoneCallReleased: data->call = nullptr; { std::lock_guard<std::mutex> lock(data->sharedData->mtx); data->sharedData->terminated = true; } break; default: break; } } // other call, but current call active -> reject call else if (data->call) { switch (cstate) { case LinphoneCallIncomingReceived: log(*data, "rejecting call (busy)"); linphone_call_decline(call, LinphoneReasonBusy); break; default: break; } } // no call active -> accept call else { switch (cstate) { case LinphoneCallIncomingReceived: data->call = call; { std::lock_guard<std::mutex> lock(data->sharedData->mtx); data->sharedData->accepted = true; } data->playback.clear(); log(*data, "accepting call"); linphone_call_accept(call); break; default: break; } } } /** * @brief DTMF received * @param[in] lc linphone core object * @param[in] call call object * @param[in] dtmf DTMF as ASCII character */ void dtmf_received(struct _LinphoneCore* lc, LinphoneCall *call, int dtmf) { Data *data = (Data*)linphone_core_get_user_data(lc); if (data == nullptr) { return; // still initializing -> ignore } // check if current call bool current = data->call == call; // check DTMF char c; if (dtmf >= '0' && dtmf <= '9') { c = dtmf; } else if (dtmf == '*') { c = '*'; } else if (dtmf == '#') { c = '#'; } else { c = '?'; } std::stringstream line; line << "dtmf received " << (current ? "(current call)" : "(other call)") << ": " << c; log(*data, line.str()); // ignore DTMF from other calls if (! current) { return; } // report DTMF keys { std::lock_guard<std::mutex> lock(data->sharedData->mtx); data->sharedData->dtmf += c; } } /** * @brief register if not active and login data available * @param[in,out] data data of SIP client * @param[in] server SIP server * @param[in] username SIP username * @param[in] password SIP password */ void do_register(Data &data, std::string const &server, std::string const &username, std::string &password) { if (data.lc) { return; } if (server.empty()) { return; } LinphoneFactory *factory = linphone_factory_get(); // create linphone callback object data.callbacks = linphone_factory_create_core_cbs(factory); if (! data.callbacks) { log(data, "failed to create linphone callback object"); return; } // set callbacks linphone_core_cbs_set_global_state_changed(data.callbacks, global_state_changed); linphone_core_cbs_set_registration_state_changed(data.callbacks, registration_state_changed); linphone_core_cbs_set_call_state_changed(data.callbacks, call_state_changed); linphone_core_cbs_set_dtmf_received(data.callbacks, dtmf_received); // create linphone core object data.lc = linphone_factory_create_core(factory, data.callbacks, NULL, NULL); if (! data.lc) { log(data, "failed to create linphone core"); linphone_core_cbs_unref(data.callbacks); data.callbacks = nullptr; return; } linphone_core_set_user_data(data.lc, &data); log(data, "linphone core created"); // set ports to random (to allow multiple SIP endpoints) LinphoneTransports *transports = linphone_factory_create_transports(factory); if (transports != nullptr) { linphone_transports_set_udp_port(transports, LC_SIP_TRANSPORT_RANDOM); linphone_transports_set_tcp_port(transports, LC_SIP_TRANSPORT_RANDOM); linphone_transports_set_tls_port(transports, LC_SIP_TRANSPORT_RANDOM); linphone_transports_set_dtls_port(transports, LC_SIP_TRANSPORT_RANDOM); linphone_core_set_transports(data.lc, transports); linphone_transports_unref(transports); log(data, "linphone core: transports set to random"); } else { log(data, "linphone core: cannot set transports"); } // set auth data if (! username.empty() && ! password.empty()) { LinphoneAuthInfo *auth_info = linphone_auth_info_new(username.c_str(), username.c_str(), password.c_str(), nullptr, "", ""); if (! auth_info) { log(data, "failed to create auth info"); linphone_core_unref(data.lc); data.lc = nullptr; linphone_core_cbs_unref(data.callbacks); data.callbacks = nullptr; return; } linphone_core_add_auth_info(data.lc, auth_info); log(data, "auth info set"); } else { log(data, "no auth data available"); } // configure proxy std::string sipserver = "sip:" + server; std::string identity = "sip:" + username + "@" + server; LinphoneProxyConfig *proxy = linphone_core_create_proxy_config(data.lc); if (! proxy) { log(data, "failed to create proxy config"); linphone_core_unref(data.lc); data.lc = nullptr; linphone_core_cbs_unref(data.callbacks); data.callbacks = nullptr; return; } linphone_proxy_config_set_server_addr(proxy, sipserver.c_str()); LinphoneAddress *addr = linphone_proxy_config_normalize_sip_uri(proxy, identity.c_str()); if (addr != NULL) { linphone_proxy_config_set_identity_address(proxy, addr); } linphone_proxy_config_enable_register(proxy, TRUE); linphone_core_add_proxy_config(data.lc, proxy); log(data, "proxy config set"); // tell linphone not to rind and to use files instead of sound card linphone_core_set_ring(data.lc, nullptr); linphone_core_use_files(data.lc, TRUE); linphone_core_set_play_file(data.lc, nullptr); } /** * @brief deregister if active * @param[in,out] data data of SIP client */ void do_deregister(Data &data) { if (! data.lc) { return; } log(data, "unreferencing..."); linphone_core_unref(data.lc); data.lc = nullptr; linphone_core_cbs_unref(data.callbacks); data.callbacks = nullptr; data.call = nullptr; log(data, "unreferenced"); } /** * @brief play sound on call if active * @param[in,out] data data of SIP client * @param[in] name name of sound */ void do_play(Data &data, std::string const &name) { if (! data.lc) { return; } if (! data.call) { return; } std::stringstream line; line << "playing sound \"" << name << "\""; log(data, line.str()); set_playback(data, name); start_playback(data); } /** * @brief hangup call if active * @param[in,out] data data of SIP client */ void do_hangup(Data &data) { if (! data.lc) { return; } if (! data.call) { return; } log(data, "terminating call..."); linphone_call_terminate(data.call); data.call = nullptr; log(data, "call terminated"); } /** * @brief linphone worker thread * @param[in] configData configuration data * @param[in,out] data shared data for communication with main Blinker thread */ void worker(SipPhone::ConfigData const &configData, SipPhone::SharedData &sharedData) { // set up data of SIP client Data data; data.configData = &configData; data.sharedData = &sharedData; data.callbacks = nullptr; data.lc = nullptr; data.call = nullptr; data.logLineCnt = 0; // main loop of worker - with shared data locked std::chrono::milliseconds timeout(10); std::unique_lock<std::mutex> g_lock(g_mtx); std::unique_lock<std::mutex> lock(sharedData.mtx); while (sharedData.run) { // hangup if (sharedData.hangup) { sharedData.hangup = false; sharedData.reqPlay = false; sharedData.dtmf.clear(); sharedData.terminated = true; // execute hangup with unlocked shared data (parallelism!) lock.unlock(); do_hangup(data); lock.lock(); continue; // re-check everything after re-locking mutex } // re-register (including initial registration and derigstration) if (sharedData.reregister) { sharedData.reregister = false; sharedData.hangup = false; sharedData.reqPlay = false; sharedData.dtmf.clear(); sharedData.terminated = true; std::string server = sharedData.server; std::string username = sharedData.username; std::string password = sharedData.password; // execute re-registration with unlocked shared data (parallelism!) lock.unlock(); do_deregister(data); do_register(data, server, username, password); lock.lock(); continue; // re-check everything after re-locking mutex } // play sound if (sharedData.reqPlay) { sharedData.reqPlay = false; std::string name = sharedData.reqPlayName; // execute hangup with unlocked shared data (parallelism!) lock.unlock(); do_play(data, name); lock.lock(); continue; // re-check everything after re-locking mutex } // background processing by linphone if active if (data.lc) { lock.unlock(); linphone_core_iterate(data.lc); lock.lock(); } // nothing to do, wait for signal (with both locks released) lock.unlock(); sharedData.cond.wait_for(g_lock, timeout); lock.lock(); } // while (sharedData.run) } } // namespace linphone #endif // #ifdef BLINKER_CFG_LINPHONE /** * @brief global initialization (call once before using this class) * @param[in] globalLogDir directory for global SIP log files */ void SipPhone::init(const Directory &globalLogDir) { #ifdef BLINKER_CFG_LINPHONE linphone::init(globalLogDir.getFile("sip.log").getPath()); #endif } /** * @brief constructor * @param[in] name module name * @param[in] mgrs managers * @param[in] dirBase base directory */ SipPhone::SipPhone(const std::string &name, Mgrs &mgrs, const Directory &dirBase): Module(name, mgrs, dirBase), m_fileServer(dirBase.getFile("server")), m_fileUsername(dirBase.getFile("username")), m_filePassword(dirBase.getFile("password")), m_fileTarget(dirBase.getFile("target")), m_configData(), m_worker(), m_sharedData(), m_curConn(nullptr) { m_configData.soundDirs.clear(); m_configData.soundDirs.push_back(dirBase.getSubdir("sounds")); m_configData.soundDirs.push_back(dirBase.getParent().getParent() .getSubdir("sounds")); m_configData.logFileName = dirBase.getFile("sip.log").getPath(); m_sharedData.run = true; m_sharedData.reregister = false; m_sharedData.reqPlay = false; m_sharedData.hangup = false; m_sharedData.accepted = false; m_sharedData.terminated = false; m_mgrs.m_callMgr.requestTimeCall(this, Time::now()); #ifdef BLINKER_CFG_LINPHONE m_worker = std::thread(linphone::worker, std::ref(m_configData), std::ref(m_sharedData)); #endif sipRegister(); } /// virtual destructor SipPhone::~SipPhone() { if (m_curConn) { m_curConn->close(); m_curConn = nullptr; } sipDeregister(); { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.run = false; } m_sharedData.cond.notify_one(); #ifdef BLINKER_CFG_LINPHONE m_worker.join(); #endif m_mgrs.m_callMgr.cancelTimeCall(this); } /// check for update of configuration void SipPhone::updateConfig() { // server, username or password file modified -> re-connect if (m_fileServer.checkModified() || m_fileUsername.checkModified() || m_filePassword.checkModified()) { // re-register sipDeregister(); sipRegister(); } // target file was modified -> re-get target operator interface to connect to if (m_fileTarget.checkModified()) { m_fileTarget.update(); } } /// callback when requested time reached void SipPhone::timeCall() { Time now = Time::now(); // get information from SIP phone worker bool accepted; std::string dtmf; bool terminated; { std::lock_guard<std::mutex> lock(m_sharedData.mtx); accepted = m_sharedData.accepted; m_sharedData.accepted = false; dtmf = m_sharedData.dtmf; m_sharedData.dtmf.clear(); terminated = m_sharedData.terminated; m_sharedData.terminated = false; } // phone call has terminated or new one has been accepted if (terminated || accepted) { // close connection if any if (m_curConn) { // request hang up of phone { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.hangup = true; } // close and forget connection m_curConn->close(); m_curConn = nullptr; } } // phone call has been accepted if (accepted) { // try to open new connection to target operator interface if (m_fileTarget.m_valid) { m_curConn = m_mgrs.m_opMgr.connect(m_fileTarget.m_obj.m_str, this); } // operator connection failed -> request hang up of phone if (! m_curConn) { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.hangup = true; } } // DTMF received -> send to operator connection if (! dtmf.empty()) { if (m_curConn) { for (char key: dtmf) { m_curConn->sendKey(key); } } } // request next call Time interval; interval.fromMs(20); m_mgrs.m_callMgr.requestTimeCall(this, now + interval); } /** * @brief key command received on operator connection * @param[in] pConn operator connection object * @param[in] key key that was pressed */ void SipPhone::opConnRecvKey(OpConn *pConn, char key) { // ignore keys received (void)pConn; (void)key; } /** * @brief play command received on operator connection * @param[in] pConn operator connection object * @param[in] sound name of sound to play */ void SipPhone::opConnRecvPlay(OpConn *pConn, const std::string &sound) { // only process current connection if (pConn != m_curConn) { return; } // request playing sound { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.reqPlay = true; m_sharedData.reqPlayName = sound; } } /** * @brief operator connection is closed * @param[in] pConn operator connection object * * The connection may not be used for sending any more in this callback. */ void SipPhone::opConnClose(OpConn *pConn) { // only process current connection if (pConn != m_curConn) { return; } // request hang up of phone { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.hangup = true; } // forget connection m_curConn = nullptr; } /// (re-)register with SIP server void SipPhone::sipRegister() { // read settings m_fileServer.update(); m_fileUsername.update(); m_filePassword.update(); // put server, username and password into shared data, trigger registration { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.server = m_fileServer.m_valid ? m_fileServer.m_obj.toStr() : std::string(); m_sharedData.username = m_fileUsername.m_valid ? m_fileUsername.m_obj.toStr() : std::string(); m_sharedData.password = m_filePassword.m_valid ? m_filePassword.m_obj.toStr() : std::string(); m_sharedData.reregister = true; } m_sharedData.cond.notify_one(); } /// deregister with SIP server void SipPhone::sipDeregister() { // remove login infor from shared data, trigger re-registration { std::lock_guard<std::mutex> lock(m_sharedData.mtx); m_sharedData.server.clear(); m_sharedData.username.clear(); m_sharedData.password.clear(); m_sharedData.reregister = true; } m_sharedData.cond.notify_one(); } } // namespace Blinker