Project 'savoirfairelinux/ring-daemon' was moved to 'savoirfairelinux/jami-daemon'. Please update any links and bookmarks that may still have the old path.
Select Git revision
fileutils.h
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
managerimpl.cpp 74.84 KiB
/*
* Copyright (C) 2004-2015 Savoir-Faire Linux Inc.
* Author: Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
* Author: Yan Morin <yan.morin@savoirfairelinux.com>
* Author: Laurielle Lea <laurielle.lea@savoirfairelinux.com>
* Author: Emmanuel Milou <emmanuel.milou@savoirfairelinux.com>
* Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
* Author: Guillaume Carmel-Archambault <guillaume.carmel-archambault@savoirfairelinux.com>
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this program, or any covered work, by linking or
* combining it with the OpenSSL project's OpenSSL library (or a
* modified version of that library), containing parts covered by the
* terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
* grants you additional permission to convey the resulting work.
* Corresponding Source for a non-source form of such a combination
* shall include the source code for the parts of OpenSSL used as well
* as that of the covered work.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "logger.h"
#include "managerimpl.h"
#include "account_schema.h"
#include "plugin_manager.h"
#include "fileutils.h"
#include "map_utils.h"
#include "account.h"
#include "string_utils.h"
#if HAVE_DHT
#include "ringdht/ringaccount.h"
#endif
#include "call_factory.h"
#include "sip/sip_utils.h"
#include "im/instant_messaging.h"
#include "numbercleaner.h"
#include "config/yamlparser.h"
#if HAVE_ALSA
#include "audio/alsa/alsalayer.h"
#endif
#include "audio/sound/tonelist.h"
#include "audio/sound/audiofile.h"
#include "audio/sound/dtmf.h"
#include "audio/ringbufferpool.h"
#include "history/history.h"
#include "manager.h"
#ifdef RING_VIDEO
#include "client/videomanager.h"
#endif
#include "conference.h"
#include "ice_transport.h"
#include "client/signal.h"
#include <cerrno>
#include <algorithm>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <sstream>
#include <sys/types.h> // mkdir(2)
#include <sys/stat.h> // mkdir(2)
#include <memory>
namespace ring {
std::atomic_bool ManagerImpl::initialized = {false};
static void
copy_over(const std::string &srcPath, const std::string &destPath)
{
std::ifstream src(srcPath.c_str());
std::ofstream dest(destPath.c_str());
dest << src.rdbuf();
src.close();
dest.close();
}
// Creates a backup of the file at "path" with a .bak suffix appended
static void
make_backup(const std::string &path)
{
const std::string backup_path(path + ".bak");
copy_over(path, backup_path);
}
// Restore last backup of the configuration file
static void
restore_backup(const std::string &path)
{
const std::string backup_path(path + ".bak");
copy_over(backup_path, path);
}
/**
* Set pjsip's log level based on the SIPLOGLEVEL environment variable.
* SIPLOGLEVEL = 0 minimum logging
* SIPLOGLEVEL = 6 maximum logging
*/
/** Environment variable used to set pjsip's logging level */
static constexpr const char* SIPLOGLEVEL = "SIPLOGLEVEL";
static void
setSipLogLevel()
{
char* envvar = getenv(SIPLOGLEVEL);
int level = 0;
if (envvar != nullptr) {
if (not (std::istringstream(envvar) >> level))
level = 0;
// From 0 (min) to 6 (max)
level = std::max(0, std::min(level, 6));
}
pj_log_set_level(level);
}
void
ManagerImpl::loadDefaultAccountMap()
{
accountFactory_.initIP2IPAccount();
}
ManagerImpl::ManagerImpl() :
pluginManager_(new PluginManager)
, preferences(), voipPreferences(),
hookPreference(), audioPreference(), shortcutPreferences(),
hasTriedToRegister_(false),
currentCallMutex_(), dtmfKey_(), dtmfBuf_(0, AudioFormat::MONO()),
toneMutex_(), telephoneTone_(), audiofile_(), audioLayerMutex_(),
waitingCalls_(), waitingCallsMutex_(), path_()
, ringbufferpool_(new RingBufferPool)
, callFactory(), conferenceMap_(), history_()
, accountFactory_(), ice_tf_()
{
// initialize random generator
// mt19937_64 should be seeded with 2 x 32 bits
std::random_device rdev;
std::seed_seq seed {rdev(), rdev()};
rand_.seed(seed);
}
ManagerImpl::~ManagerImpl()
{}
bool
ManagerImpl::parseConfiguration()
{
bool result = true;
try {
YAML::Node parsedFile = YAML::LoadFile(path_);
const int error_count = loadAccountMap(parsedFile);
if (error_count > 0) {
RING_WARN("Errors while parsing %s", path_.c_str());
result = false;
}
} catch (const YAML::BadFile &e) {
RING_WARN("Could not open config file: creating default account map");
loadDefaultAccountMap();
}
return result;
}
void
ManagerImpl::init(const std::string &config_file)
{
// FIXME: this is no good
initialized = true;
#define TRY(ret) do { \
if (ret != PJ_SUCCESS) \
throw std::runtime_error(#ret " failed"); \
} while (0)
srand(time(NULL)); // to get random number for RANDOM_PORT
// Initialize PJSIP (SIP and ICE implementation)
TRY(pj_init());
setSipLogLevel();
TRY(pjlib_util_init());
TRY(pjnath_init());
#undef TRY
RING_DBG("pjsip version %s for %s initialized",
pj_get_version(), PJ_OS_NAME);
ice_tf_.reset(new IceTransportFactory());
path_ = config_file.empty() ? retrieveConfigPath() : config_file;
RING_DBG("Configuration file path: %s", path_.c_str());
bool no_errors = true;
// manager can restart without being recreated (android)
finished_ = false;
try {
no_errors = parseConfiguration();
} catch (const YAML::Exception &e) {
RING_ERR("%s", e.what());
no_errors = false;
}
// always back up last error-free configuration
if (no_errors) {
make_backup(path_);
} else {
// restore previous configuration
RING_WARN("Restoring last working configuration");
try {
// remove accounts from broken configuration
removeAccounts();
restore_backup(path_);
parseConfiguration();
} catch (const YAML::Exception &e) {
RING_ERR("%s", e.what());
RING_WARN("Restoring backup failed, creating default account map");
loadDefaultAccountMap();
}
}
initAudioDriver();
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (audiodriver_) {
{
std::lock_guard<std::mutex> toneLock(toneMutex_);
telephoneTone_.reset(new TelephoneTone(preferences.getZoneToneChoice(), audiodriver_->getSampleRate()));
}
dtmfKey_.reset(new DTMF(getRingBufferPool().getInternalSamplingRate()));
}
}
history_.load(preferences.getHistoryLimit());
registerAccounts();
}
void
ManagerImpl::setPath(const std::string &path)
{
history_.setPath(path);
}
void
ManagerImpl::finish() noexcept
{
bool expected = false;
if (not finished_.compare_exchange_strong(expected, true))
return;
try {
// Forbid call creation
callFactory.forbid();
// Hangup all remaining active calls
RING_DBG("Hangup %zu remaining call(s)", callFactory.callCount());
for (const auto call : callFactory.getAllCalls())
hangupCall(call->getCallId());
callFactory.clear();
// Save accounts config and call's history
saveConfig();
saveHistory();
// Disconnect accounts, close link stacks and free allocated ressources
unregisterAccounts();
accountFactory_.clear();
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audiodriver_.reset();
}
ice_tf_.reset();
pj_shutdown();
} catch (const VoipLinkException &err) {
RING_ERR("%s", err.what());
}
}
bool
ManagerImpl::isCurrentCall(const Call& call) const
{
return currentCall_.get() == &call;
}
bool
ManagerImpl::hasCurrentCall() const
{
return static_cast<bool>(currentCall_);
}
std::shared_ptr<Call>
ManagerImpl::getCurrentCall() const
{
return currentCall_;
}
const std::string
ManagerImpl::getCurrentCallId() const
{
return currentCall_ ? currentCall_->getCallId() : "";
}
/**
* Set current call ID to empty string
*/
void
ManagerImpl::unsetCurrentCall()
{
currentCall_.reset();
}
/**
* Switch of current call id
* @param id The new callid
*/
void
ManagerImpl::switchCall(std::shared_ptr<Call> call)
{
std::lock_guard<std::mutex> m(currentCallMutex_);
RING_DBG("----- Switch current call id to '%s' -----",
call ? call->getCallId().c_str() : "<nullptr>");
currentCall_ = call;
}
///////////////////////////////////////////////////////////////////////////////
// Management of events' IP-phone user
///////////////////////////////////////////////////////////////////////////////
/* Main Thread */
bool
ManagerImpl::outgoingCall(const std::string& preferred_account_id,
const std::string& call_id,
const std::string& to,
const std::string& conf_id)
{
if (call_id.empty()) {
RING_DBG("New outgoing call abort, missing callid");
return false;
}
// Call ID must be unique
if (isValidCall(call_id)) {
RING_ERR("Call id already exists in outgoing call");
return false;
}
RING_DBG("New outgoing call %s to %s", call_id.c_str(), to.c_str());
stopTone();
std::string current_call_id(getCurrentCallId());
std::string prefix(hookPreference.getNumberAddPrefix());
std::string to_cleaned(NumberCleaner::clean(to, prefix));
// in any cases we have to detach from current communication
if (hasCurrentCall()) {
RING_DBG("Has current call (%s) put it onhold", current_call_id.c_str());
// if this is not a conference and this and is not a conference participant
if (not isConference(current_call_id) and not isConferenceParticipant(current_call_id))
onHoldCall(current_call_id);
else if (isConference(current_call_id) and not isConferenceParticipant(call_id))
detachParticipant(RingBufferPool::DEFAULT_ID);
}
std::shared_ptr<Call> call;
try {
/* RING_WARN: after this call the account_id is obsolete
* as the factory may decide to use another account (like IP2IP).
*/
RING_DBG("New outgoing call to %s", to_cleaned.c_str());
call = newOutgoingCall(call_id, to_cleaned, preferred_account_id);
} catch (const std::exception &e) {
RING_ERR("%s", e.what());
return false;
}
if (not call)
return false;
// try to reverse match the peer name using the cache
if (call->getDisplayName().empty()) {
const auto& name = history_.getNameFromHistory(call->getPeerNumber(),
call->getAccountId());
const std::string pseudo_contact_name(name);
if (not pseudo_contact_name.empty())
call->setDisplayName(pseudo_contact_name);
}
switchCall(call);
call->setConfId(conf_id);
return true;
}
//THREAD=Main : for outgoing Call
bool
ManagerImpl::answerCall(const std::string& call_id)
{
bool result = true;
auto call = getCallFromCallID(call_id);
if (!call) {
RING_ERR("Call %s is NULL", call_id.c_str());
return false;
}
// If ring is ringing
stopTone();
// store the current call id
std::string current_call_id(getCurrentCallId());
// in any cases we have to detach from current communication
if (hasCurrentCall()) {
RING_DBG("Currently conversing with %s", current_call_id.c_str());
if (not isConference(current_call_id) and not isConferenceParticipant(current_call_id)) {
RING_DBG("Answer call: Put the current call (%s) on hold", current_call_id.c_str());
onHoldCall(current_call_id);
} else if (isConference(current_call_id) and not isConferenceParticipant(call_id)) {
// if we are talking to a conference and we are answering an incoming call
RING_DBG("Detach main participant from conference");
detachParticipant(RingBufferPool::DEFAULT_ID);
}
}
try {
call->answer();
} catch (const std::runtime_error &e) {
RING_ERR("%s", e.what());
result = false;
}
// if it was waiting, it's waiting no more
removeWaitingCall(call_id);
// if we dragged this call into a conference already
if (isConferenceParticipant(call_id))
switchCall(callFactory.getCall(call->getConfId()));
else
switchCall(call);
// Connect streams
addStream(*call);
// Start recording if set in preference
if (audioPreference.getIsAlwaysRecording())
toggleRecordingCall(call_id);
//callStateChanged(call_id, "CURRENT");
emitSignal<DRing::CallSignal::StateChange>(call_id, "CURRENT");
return result;
}
void
ManagerImpl::checkAudio()
{
if (getCallList().empty()) {
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (audiodriver_)
audiodriver_->stopStream();
}
}
//THREAD=Main
bool
ManagerImpl::hangupCall(const std::string& callId)
{
// store the current call id
std::string currentCallId(getCurrentCallId());
stopTone();
RING_DBG("Send call state change (HUNGUP) for id %s", callId.c_str());
emitSignal<DRing::CallSignal::StateChange>(callId, "HUNGUP");
/* We often get here when the call was hungup before being created */
auto call = getCallFromCallID(callId);
if (not call) {
RING_WARN("Could not hang up non-existant call %s", callId.c_str());
checkAudio();
return false;
}
// Disconnect streams
removeStream(*call);
if (isConferenceParticipant(callId)) {
removeParticipant(callId);
} else {
// we are not participating in a conference, current call switched to ""
if (not isConference(currentCallId))
unsetCurrentCall();
}
try {
history_.addCall(call.get(), preferences.getHistoryLimit());
call->hangup(0);
checkAudio();
saveHistory();
} catch (const VoipLinkException &e) {
RING_ERR("%s", e.what());
return false;
}
return true;
}
bool
ManagerImpl::hangupConference(const std::string& id)
{
RING_DBG("Hangup conference %s", id.c_str());
ConferenceMap::iterator iter_conf = conferenceMap_.find(id);
if (iter_conf != conferenceMap_.end()) {
auto conf = iter_conf->second;
if (conf) {
ParticipantSet participants(conf->getParticipantList());
for (const auto &item : participants)
hangupCall(item);
} else {
RING_ERR("No such conference %s", id.c_str());
return false;
}
}
unsetCurrentCall();
return true;
}
//THREAD=Main
bool
ManagerImpl::onHoldCall(const std::string& callId)
{
bool result = true;
stopTone();
std::string current_call_id(getCurrentCallId());
if (auto call = getCallFromCallID(callId)) {
try {
call->onhold();
removeStream(*call); // Unbind calls in main buffer
} catch (const VoipLinkException &e) {
RING_ERR("%s", e.what());
result = false;
}
} else {
RING_DBG("CallID %s doesn't exist in call onHold", callId.c_str());
return false;
}
// Remove call from teh queue if it was still there
removeWaitingCall(callId);
// keeps current call id if the action is not holding this call or a new outgoing call
// this could happen in case of a conference
if (current_call_id == callId)
unsetCurrentCall();
emitSignal<DRing::CallSignal::StateChange>(callId, "HOLD");
return result;
}
//THREAD=Main
bool
ManagerImpl::offHoldCall(const std::string& callId)
{
bool result = true;
stopTone();
const std::string currentCallId(getCurrentCallId());
// Place current call on hold if it isn't
if (hasCurrentCall()) {
if (not isConference(currentCallId) and not isConferenceParticipant(currentCallId)) {
RING_DBG("Has current call (%s), put on hold", currentCallId.c_str());
onHoldCall(currentCallId);
} else if (isConference(currentCallId) && callId != currentCallId) {
holdConference(currentCallId);
} else if (isConference(currentCallId) and not isConferenceParticipant(callId))
detachParticipant(RingBufferPool::DEFAULT_ID);
}
std::shared_ptr<Call> call;
try {
call = getCallFromCallID(callId);
if (call)
call->offhold();
else
result = false;
} catch (const VoipLinkException &e) {
RING_ERR("%s", e.what());
return false;
}
emitSignal<DRing::CallSignal::StateChange>(callId, "UNHOLD");
if (isConferenceParticipant(callId))
switchCall(getCallFromCallID(call->getConfId()));
else
switchCall(call);
addStream(*call);
return result;
}
//THREAD=Main
bool
ManagerImpl::transferCall(const std::string& callId, const std::string& to)
{
if (isConferenceParticipant(callId)) {
removeParticipant(callId);
} else if (not isConference(getCurrentCallId()))
unsetCurrentCall();
if (auto call = getCallFromCallID(callId))
call->transfer(to);
else
return false;
// remove waiting call in case we make transfer without even answer
removeWaitingCall(callId);
return true;
}
void
ManagerImpl::transferFailed()
{
emitSignal<DRing::CallSignal::TransferFailed>();
}
void
ManagerImpl::transferSucceeded()
{
transferSucceeded();
}
bool
ManagerImpl::attendedTransfer(const std::string& transferID,
const std::string& targetID)
{
if (auto call = getCallFromCallID(transferID))
return call->attendedTransfer(targetID);
return false;
}
//THREAD=Main : Call:Incoming
bool
ManagerImpl::refuseCall(const std::string& id)
{
auto call = getCallFromCallID(id);
if (!call)
return false;
stopTone();
if (getCallList().size() <= 1) {
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audiodriver_->stopStream();
}
call->refuse();
checkAudio();
removeWaitingCall(id);
emitSignal<DRing::CallSignal::StateChange>(id, "HUNGUP");
// Disconnect streams
removeStream(*call);
return true;
}
std::shared_ptr<Conference>
ManagerImpl::createConference(const std::string& id1, const std::string& id2)
{
RING_DBG("Create conference with call %s and %s", id1.c_str(), id2.c_str());
auto conf = std::make_shared<Conference>();
conf->add(id1);
conf->add(id2);
// Add conference to map
conferenceMap_.insert(std::make_pair(conf->getConfID(), conf));
emitSignal<DRing::CallSignal::ConferenceCreated>(conf->getConfID());
return conf;
}
void
ManagerImpl::removeConference(const std::string& conference_id)
{
RING_DBG("Remove conference %s", conference_id.c_str());
RING_DBG("number of participants: %u", conferenceMap_.size());
ConferenceMap::iterator iter = conferenceMap_.find(conference_id);
std::shared_ptr<Conference> conf;
if (iter != conferenceMap_.end())
conf = iter->second;
if (not conf) {
RING_ERR("Conference not found");
return;
}
emitSignal<DRing::CallSignal::ConferenceRemoved>(conference_id);
// We now need to bind the audio to the remain participant
// Unbind main participant audio from conference
getRingBufferPool().unBindAll(RingBufferPool::DEFAULT_ID);
ParticipantSet participants(conf->getParticipantList());
// bind main participant audio to remaining conference call
ParticipantSet::iterator iter_p = participants.begin();
if (iter_p != participants.end())
getRingBufferPool().bindCallID(*iter_p, RingBufferPool::DEFAULT_ID);
// Then remove the conference from the conference map
if (conferenceMap_.erase(conference_id))
RING_DBG("Conference %s removed successfully", conference_id.c_str());
else
RING_ERR("Cannot remove conference: %s", conference_id.c_str());
}
std::shared_ptr<Conference>
ManagerImpl::getConferenceFromCallID(const std::string& call_id)
{
auto call = getCallFromCallID(call_id);
if (!call)
return nullptr;
ConferenceMap::const_iterator iter(conferenceMap_.find(call->getConfId()));
if (iter != conferenceMap_.end())
return iter->second;
else
return nullptr;
}
bool
ManagerImpl::holdConference(const std::string& id)
{
ConferenceMap::iterator iter_conf = conferenceMap_.find(id);
if (iter_conf == conferenceMap_.end())
return false;
auto conf = iter_conf->second;
bool isRec = conf->getState() == Conference::ACTIVE_ATTACHED_REC or
conf->getState() == Conference::ACTIVE_DETACHED_REC or
conf->getState() == Conference::HOLD_REC;
ParticipantSet participants(conf->getParticipantList());
for (const auto &item : participants) {
switchCall(getCallFromCallID(item));
onHoldCall(item);
}
conf->setState(isRec ? Conference::HOLD_REC : Conference::HOLD);
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getConfID(), conf->getStateStr());
return true;
}
bool
ManagerImpl::unHoldConference(const std::string& id)
{
ConferenceMap::iterator iter_conf = conferenceMap_.find(id);
if (iter_conf == conferenceMap_.end() or iter_conf->second == 0)
return false;
auto conf = iter_conf->second;
bool isRec = conf->getState() == Conference::ACTIVE_ATTACHED_REC or
conf->getState() == Conference::ACTIVE_DETACHED_REC or
conf->getState() == Conference::HOLD_REC;
ParticipantSet participants(conf->getParticipantList());
for (const auto &item : participants) {
if (auto call = getCallFromCallID(item)) {
// if one call is currently recording, the conference is in state recording
isRec |= call->isRecording();
switchCall(call);
offHoldCall(item);
}
}
conf->setState(isRec ? Conference::ACTIVE_ATTACHED_REC : Conference::ACTIVE_ATTACHED);
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getConfID(), conf->getStateStr());
return true;
}
bool
ManagerImpl::isConference(const std::string& id) const
{
return conferenceMap_.find(id) != conferenceMap_.end();
}
bool
ManagerImpl::isConferenceParticipant(const std::string& call_id)
{
auto call = getCallFromCallID(call_id);
return call and not call->getConfId().empty();
}
bool
ManagerImpl::addParticipant(const std::string& callId,
const std::string& conferenceId)
{
RING_DBG("Add participant %s to %s", callId.c_str(), conferenceId.c_str());
ConferenceMap::iterator iter = conferenceMap_.find(conferenceId);
if (iter == conferenceMap_.end()) {
RING_ERR("Conference id is not valid");
return false;
}
auto call = getCallFromCallID(callId);
if (!call) {
RING_ERR("Call id %s is not valid", callId.c_str());
return false;
}
// ensure that calls are only in one conference at a time
if (isConferenceParticipant(callId))
detachParticipant(callId);
// store the current call id (it will change in offHoldCall or in answerCall)
std::string current_call_id(getCurrentCallId());
// detach from prior communication and switch to this conference
if (current_call_id != callId) {
if (isConference(current_call_id))
detachParticipant(RingBufferPool::DEFAULT_ID);
else
onHoldCall(current_call_id);
}
// TODO: remove this ugly hack => There should be different calls when double clicking
// a conference to add main participant to it, or (in this case) adding a participant
// toconference
unsetCurrentCall();
// Add main participant
addMainParticipant(conferenceId);
auto conf = iter->second;
switchCall(getCallFromCallID(conf->getConfID()));
// Add coresponding IDs in conf and call
call->setConfId(conf->getConfID());
conf->add(callId);
// Connect new audio streams together
getRingBufferPool().unBindAll(callId);
std::map<std::string, std::string> callDetails(getCallDetails(callId));
std::string callState(callDetails.find("CALL_STATE")->second);
if (callState == "HOLD") {
conf->bindParticipant(callId);
offHoldCall(callId);
} else if (callState == "INCOMING") {
conf->bindParticipant(callId);
answerCall(callId);
} else if (callState == "CURRENT")
conf->bindParticipant(callId);
ParticipantSet participants(conf->getParticipantList());
if (participants.empty())
RING_ERR("Participant list is empty for this conference");
// Connect stream
addStream(*call);
return true;
}
bool
ManagerImpl::addMainParticipant(const std::string& conference_id)
{
if (hasCurrentCall()) {
std::string current_call_id(getCurrentCallId());
if (isConference(current_call_id))
detachParticipant(RingBufferPool::DEFAULT_ID);
else
onHoldCall(current_call_id);
}
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
ConferenceMap::const_iterator iter = conferenceMap_.find(conference_id);
if (iter == conferenceMap_.end() or iter->second == 0)
return false;
auto conf = iter->second;
ParticipantSet participants(conf->getParticipantList());
for (const auto &item_p : participants) {
getRingBufferPool().bindCallID(item_p, RingBufferPool::DEFAULT_ID);
// Reset ringbuffer's readpointers
getRingBufferPool().flush(item_p);
}
getRingBufferPool().flush(RingBufferPool::DEFAULT_ID);
if (conf->getState() == Conference::ACTIVE_DETACHED)
conf->setState(Conference::ACTIVE_ATTACHED);
else if (conf->getState() == Conference::ACTIVE_DETACHED_REC)
conf->setState(Conference::ACTIVE_ATTACHED_REC);
else
RING_WARN("Invalid conference state while adding main participant");
emitSignal<DRing::CallSignal::ConferenceChanged>(conference_id, conf->getStateStr());
}
switchCall(getCallFromCallID(conference_id));
return true;
}
std::shared_ptr<Call>
ManagerImpl::getCallFromCallID(const std::string& callID)
{
return callFactory.getCall(callID);
}
bool
ManagerImpl::joinParticipant(const std::string& callId1,
const std::string& callId2)
{
if (callId1 == callId2) {
RING_ERR("Cannot join participant %s to itself", callId1.c_str());
return false;
}
// Set corresponding conference ids for call 1
auto call1 = getCallFromCallID(callId1);
if (!call1) {
RING_ERR("Could not find call %s", callId1.c_str());
return false;
}
// Set corresponding conderence details
auto call2 = getCallFromCallID(callId2);
if (!call2) {
RING_ERR("Could not find call %s", callId2.c_str());
return false;
}
// ensure that calls are only in one conference at a time
if (isConferenceParticipant(callId1))
detachParticipant(callId1);
if (isConferenceParticipant(callId2))
detachParticipant(callId2);
std::map<std::string, std::string> call1Details(getCallDetails(callId1));
std::map<std::string, std::string> call2Details(getCallDetails(callId2));
std::string current_call_id(getCurrentCallId());
RING_DBG("Current Call ID %s", current_call_id.c_str());
// detach from the conference and switch to this conference
if ((current_call_id != callId1) and (current_call_id != callId2)) {
// If currently in a conference
if (isConference(current_call_id))
detachParticipant(RingBufferPool::DEFAULT_ID);
else
onHoldCall(current_call_id); // currently in a call
}
auto conf = createConference(callId1, callId2);
call1->setConfId(conf->getConfID());
getRingBufferPool().unBindAll(callId1);
call2->setConfId(conf->getConfID());
getRingBufferPool().unBindAll(callId2);
// Process call1 according to its state
std::string call1_state_str(call1Details.find("CALL_STATE")->second);
RING_DBG("Process call %s state: %s", callId1.c_str(), call1_state_str.c_str());
if (call1_state_str == "HOLD") {
conf->bindParticipant(callId1);
offHoldCall(callId1);
} else if (call1_state_str == "INCOMING") {
conf->bindParticipant(callId1);
answerCall(callId1);
} else if (call1_state_str == "CURRENT") {
conf->bindParticipant(callId1);
} else if (call1_state_str == "INACTIVE") {
conf->bindParticipant(callId1);
answerCall(callId1);
} else
RING_WARN("Call state not recognized");
// Process call2 according to its state
std::string call2_state_str(call2Details.find("CALL_STATE")->second);
RING_DBG("Process call %s state: %s", callId2.c_str(), call2_state_str.c_str());
if (call2_state_str == "HOLD") {
conf->bindParticipant(callId2);
offHoldCall(callId2);
} else if (call2_state_str == "INCOMING") {
conf->bindParticipant(callId2);
answerCall(callId2);
} else if (call2_state_str == "CURRENT") {
conf->bindParticipant(callId2);
} else if (call2_state_str == "INACTIVE") {
conf->bindParticipant(callId2);
answerCall(callId2);
} else
RING_WARN("Call state not recognized");
// Switch current call id to this conference
switchCall(getCallFromCallID(conf->getConfID()));
conf->setState(Conference::ACTIVE_ATTACHED);
// set recording sampling rate
conf->setRecordingFormat(ringbufferpool_->getInternalAudioFormat());
return true;
}
void
ManagerImpl::createConfFromParticipantList(const std::vector< std::string > &participantList)
{
// we must at least have 2 participant for a conference
if (participantList.size() <= 1) {
RING_ERR("Participant number must be higher or equal to 2");
return;
}
auto conf = std::make_shared<Conference>();
int successCounter = 0;
for (const auto &p : participantList) {
std::string numberaccount(p);
std::string tostr(numberaccount.substr(0, numberaccount.find(",")));
std::string account(numberaccount.substr(numberaccount.find(",") + 1, numberaccount.size()));
std::string generatedCallID(getNewCallID());
// Manager methods may behave differently if the call id participates in a conference
conf->add(generatedCallID);
unsetCurrentCall();
// Create call
bool callSuccess = outgoingCall(account, generatedCallID, tostr, conf->getConfID());
// If not able to create call remove this participant from the conference
if (!callSuccess)
conf->remove(generatedCallID);
else {
emitSignal<DRing::CallSignal::NewCallCreated>(account, generatedCallID, tostr);
successCounter++;
}
}
// Create the conference if and only if at least 2 calls have been successfully created
if (successCounter >= 2) {
conferenceMap_[conf->getConfID()] = conf;
emitSignal<DRing::CallSignal::ConferenceCreated>(conf->getConfID());
conf->setRecordingFormat(ringbufferpool_->getInternalAudioFormat());
}
}
bool
ManagerImpl::detachParticipant(const std::string& call_id)
{
const std::string current_call_id(getCurrentCallId());
if (call_id != RingBufferPool::DEFAULT_ID) {
auto call = getCallFromCallID(call_id);
if (!call) {
RING_ERR("Could not find call %s", call_id.c_str());
return false;
}
auto conf = getConferenceFromCallID(call_id);
if (conf == nullptr) {
RING_ERR("Call is not conferencing, cannot detach");
return false;
}
std::map<std::string, std::string> call_details(getCallDetails(call_id));
std::map<std::string, std::string>::iterator iter_details(call_details.find("CALL_STATE"));
if (iter_details == call_details.end()) {
RING_ERR("Could not find CALL_STATE");
return false;
}
// Don't hold ringing calls when detaching them from conferences
if (iter_details->second != "RINGING")
onHoldCall(call_id);
removeParticipant(call_id);
} else {
RING_DBG("Unbind main participant from conference %d");
getRingBufferPool().unBindAll(RingBufferPool::DEFAULT_ID);
if (not isConference(current_call_id)) {
RING_ERR("Current call id (%s) is not a conference", current_call_id.c_str());
return false;
}
ConferenceMap::iterator iter = conferenceMap_.find(current_call_id);
auto conf = iter->second;
if (iter == conferenceMap_.end() or conf == 0) {
RING_DBG("Conference is NULL");
return false;
}
if (conf->getState() == Conference::ACTIVE_ATTACHED)
conf->setState(Conference::ACTIVE_DETACHED);
else if (conf->getState() == Conference::ACTIVE_ATTACHED_REC)
conf->setState(Conference::ACTIVE_DETACHED_REC);
else
RING_WARN("Undefined behavior, invalid conference state in detach participant");
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getConfID(), conf->getStateStr());
unsetCurrentCall();
}
return true;
}
void
ManagerImpl::removeParticipant(const std::string& call_id)
{
RING_DBG("Remove participant %s", call_id.c_str());
// this call is no longer a conference participant
auto call = getCallFromCallID(call_id);
if (!call) {
RING_ERR("Call not found");
return;
}
ConferenceMap::const_iterator iter = conferenceMap_.find(call->getConfId());
auto conf = iter->second;
if (iter == conferenceMap_.end() or conf == 0) {
RING_ERR("No conference with id %s, cannot remove participant", call->getConfId().c_str());
return;
}
conf->remove(call_id);
call->setConfId("");
removeStream(*call);
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getConfID(), conf->getStateStr());
processRemainingParticipants(*conf);
}
void
ManagerImpl::processRemainingParticipants(Conference &conf)
{
const std::string current_call_id(getCurrentCallId());
ParticipantSet participants(conf.getParticipantList());
const size_t n = participants.size();
RING_DBG("Process remaining %d participant(s) from conference %s",
n, conf.getConfID().c_str());
if (n > 1) {
// Reset ringbuffer's readpointers
for (const auto &p : participants)
getRingBufferPool().flush(p);
getRingBufferPool().flush(RingBufferPool::DEFAULT_ID);
} else if (n == 1) {
// this call is the last participant, hence
// the conference is over
ParticipantSet::iterator p = participants.begin();
if (auto call = getCallFromCallID(*p)) {
call->setConfId("");
// if we are not listening to this conference
if (current_call_id != conf.getConfID())
onHoldCall(call->getCallId());
else
switchCall(call);
}
RING_DBG("No remaining participants, remove conference");
removeConference(conf.getConfID());
} else {
RING_DBG("No remaining participants, remove conference");
removeConference(conf.getConfID());
unsetCurrentCall();
}
}
bool
ManagerImpl::joinConference(const std::string& conf_id1,
const std::string& conf_id2)
{
if (conferenceMap_.find(conf_id1) == conferenceMap_.end()) {
RING_ERR("Not a valid conference ID: %s", conf_id1.c_str());
return false;
}
if (conferenceMap_.find(conf_id2) == conferenceMap_.end()) {
RING_ERR("Not a valid conference ID: %s", conf_id2.c_str());
return false;
}
auto conf = conferenceMap_.find(conf_id1)->second;
ParticipantSet participants(conf->getParticipantList());
for (const auto &p : participants)
addParticipant(p, conf_id2);
return true;
}
void
ManagerImpl::addStream(Call& call)
{
const auto call_id = call.getCallId();
RING_DBG("Add audio stream %s", call_id.c_str());
if (isConferenceParticipant(call_id)) {
RING_DBG("Add stream to conference");
// bind to conference participant
ConferenceMap::iterator iter = conferenceMap_.find(call_id);
if (iter != conferenceMap_.end() and iter->second) {
auto conf = iter->second;
conf->bindParticipant(call_id);
}
} else {
RING_DBG("Add stream to call");
// bind to main
getRingBufferPool().bindCallID(call_id, RingBufferPool::DEFAULT_ID);
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (!audiodriver_) {
RING_ERR("Audio driver not initialized");
return;
}
audiodriver_->flushUrgent();
audiodriver_->flushMain();
}
startAudioDriverStream();
}
void
ManagerImpl::removeStream(Call& call)
{
const auto call_id = call.getCallId();
RING_DBG("Remove audio stream %s", call_id.c_str());
getRingBufferPool().unBindAll(call_id);
}
// Not thread-safe, SHOULD be called in same thread that run poolEvents()
void
ManagerImpl::registerEventHandler(uintptr_t handlerId, EventHandler handler)
{
eventHandlerMap_[handlerId] = handler;
}
// Not thread-safe, SHOULD be called in same thread that run poolEvents()
void
ManagerImpl::unregisterEventHandler(uintptr_t handlerId)
{
auto iter = eventHandlerMap_.find(handlerId);
if (iter != eventHandlerMap_.end()) {
if (iter == nextEventHandler_)
nextEventHandler_ = eventHandlerMap_.erase(iter);
else
eventHandlerMap_.erase(iter);
}
}
// Must be invoked periodically by a timer from the main event loop
void ManagerImpl::pollEvents()
{
auto iter = eventHandlerMap_.begin();
while (iter != eventHandlerMap_.end()) {
if (finished_)
return;
// WARN: following callback can do anything and typically
// calls (un)registerEventHandler.
// Think twice before modify this code.
nextEventHandler_ = std::next(iter);
iter->second();
iter = nextEventHandler_;
}
}
//THREAD=Main
void
ManagerImpl::saveConfig()
{
RING_DBG("Saving Configuration to XDG directory %s", path_.c_str());
if (audiodriver_) {
audioPreference.setVolumemic(audiodriver_->getCaptureGain());
audioPreference.setVolumespkr(audiodriver_->getPlaybackGain());
audioPreference.setCaptureMuted(audiodriver_->isCaptureMuted());
audioPreference.setPlaybackMuted(audiodriver_->isPlaybackMuted());
}
try {
YAML::Emitter out;
// FIXME maybe move this into accountFactory?
out << YAML::BeginMap << YAML::Key << "accounts";
out << YAML::Value << YAML::BeginSeq;
for (const auto& account : accountFactory_.getAllAccounts()) {
account->serialize(out);
}
out << YAML::EndSeq;
// FIXME: this is a hack until we get rid of accountOrder
preferences.verifyAccountOrder(getAccountList());
preferences.serialize(out);
voipPreferences.serialize(out);
hookPreference.serialize(out);
audioPreference.serialize(out);
#ifdef RING_VIDEO
getVideoDeviceMonitor().serialize(out);
#endif
shortcutPreferences.serialize(out);
std::ofstream fout(path_);
fout << out.c_str();
} catch (const YAML::Exception &e) {
RING_ERR("%s", e.what());
} catch (const std::runtime_error &e) {
RING_ERR("%s", e.what());
}
}
//THREAD=Main | VoIPLink
void
ManagerImpl::playDtmf(char code)
{
stopTone();
if (not voipPreferences.getPlayDtmf()) {
RING_DBG("Do not have to play a tone...");
return;
}
// length in milliseconds
int pulselen = voipPreferences.getPulseLength();
if (pulselen == 0) {
RING_DBG("Pulse length is not set...");
return;
}
std::lock_guard<std::mutex> lock(audioLayerMutex_);
// numbers of int = length in milliseconds / 1000 (number of seconds)
// = number of seconds * SAMPLING_RATE by SECONDS
// fast return, no sound, so no dtmf
if (not audiodriver_ or not dtmfKey_) {
RING_DBG("No audio layer...");
return;
}
// number of data sampling in one pulselen depends on samplerate
// size (n sampling) = time_ms * sampling/s
// ---------------------
// ms/s
int size = (int)((pulselen * (float) audiodriver_->getSampleRate()) / 1000);
dtmfBuf_.resize(size);
// Handle dtmf
dtmfKey_->startTone(code);
// copy the sound
if (dtmfKey_->generateDTMF(*dtmfBuf_.getChannel(0))) {
// Put buffer to urgentRingBuffer
// put the size in bytes...
// so size * 1 channel (mono) * sizeof (bytes for the data)
// audiolayer->flushUrgent();
audiodriver_->startStream();
// FIXME: do real synchronization
int tries = 10;
while (not audiodriver_->isStarted() and tries--) {
RING_WARN("Audio layer not ready yet");
usleep(10000);
}
audiodriver_->putUrgent(dtmfBuf_);
}
// TODO Cache the DTMF
}
// Multi-thread
bool
ManagerImpl::incomingCallsWaiting()
{
std::lock_guard<std::mutex> m(waitingCallsMutex_);
return not waitingCalls_.empty();
}
void
ManagerImpl::addWaitingCall(const std::string& id)
{
std::lock_guard<std::mutex> m(waitingCallsMutex_);
waitingCalls_.insert(id);
}
void
ManagerImpl::removeWaitingCall(const std::string& id)
{
std::lock_guard<std::mutex> m(waitingCallsMutex_);
waitingCalls_.erase(id);
}
///////////////////////////////////////////////////////////////////////////////
// Management of event peer IP-phone
////////////////////////////////////////////////////////////////////////////////
// SipEvent Thread
void
ManagerImpl::incomingCall(Call &call, const std::string& accountId)
{
stopTone();
const std::string callID(call.getCallId());
if (accountId.empty())
call.setIPToIP(true);
else {
// strip sip: which is not required and bring confusion with ip to ip calls
// when placing new call from history (if call is IAX, do nothing)
std::string peerNumber(call.getPeerNumber());
const char SIP_PREFIX[] = "sip:";
size_t startIndex = peerNumber.find(SIP_PREFIX);
if (startIndex != std::string::npos)
call.setPeerNumber(peerNumber.substr(startIndex + sizeof(SIP_PREFIX) - 1));
}
if (not hasCurrentCall()) {
call.setConnectionState(Call::RINGING);
playRingtone(accountId);
}
addWaitingCall(callID);
std::string number(call.getPeerNumber());
std::string from("<" + number + ">");
emitSignal<DRing::CallSignal::IncomingCall>(accountId, callID, call.getDisplayName() + " " + from);
}
//THREAD=VoIP
#if HAVE_INSTANT_MESSAGING
void
ManagerImpl::incomingMessage(const std::string& callID,
const std::string& from,
const std::string& message)
{
if (isConferenceParticipant(callID)) {
auto conf = getConferenceFromCallID(callID);
ParticipantSet participants(conf->getParticipantList());
for (const auto &item_p : participants) {
if (item_p == callID)
continue;
RING_DBG("Send message to %s", item_p.c_str());
if (auto call = getCallFromCallID(item_p)) {
call->sendTextMessage(message, from);
} else {
RING_ERR("Failed to get call while sending instant message");
return;
}
}
// in case of a conference we must notify client using conference id
emitSignal<DRing::CallSignal::IncomingMessage>(conf->getConfID(), from, message);
} else
emitSignal<DRing::CallSignal::IncomingMessage>(callID, from, message);
}
//THREAD=VoIP
bool
ManagerImpl::sendTextMessage(const std::string& callID,
const std::string& message,
const std::string& from)
{
if (isConference(callID)) {
RING_DBG("Is a conference, send instant message to everyone");
ConferenceMap::iterator it = conferenceMap_.find(callID);
if (it == conferenceMap_.end())
return false;
auto conf = it->second;
if (!conf)
return false;
ParticipantSet participants(conf->getParticipantList());
for (const auto &participant_id : participants) {
if (auto call = getCallFromCallID(participant_id)) {
call->sendTextMessage(message, from);
} else {
RING_ERR("Failed to get call while sending instant message");
return false;
}
}
return true;
}
if (isConferenceParticipant(callID)) {
RING_DBG("Call is participant in a conference, send instant message to everyone");
auto conf = getConferenceFromCallID(callID);
if (!conf)
return false;
ParticipantSet participants(conf->getParticipantList());
for (const auto &participant_id : participants) {
if (auto call = getCallFromCallID(participant_id)) {
call->sendTextMessage(message, from);
} else {
RING_ERR("Failed to get call while sending instant message");
return false;
}
}
} else {
if (auto call = getCallFromCallID(callID)) {
call->sendTextMessage(message, from);
} else {
RING_ERR("Failed to get call while sending instant message");
return false;
}
}
return true;
}
#endif // HAVE_INSTANT_MESSAGING
//THREAD=VoIP CALL=Outgoing
void
ManagerImpl::peerAnsweredCall(Call& call)
{
const auto call_id = call.getCallId();
RING_DBG("Peer answered call %s", call_id.c_str());
// The if statement is usefull only if we sent two calls at the same time.
if (isCurrentCall(call))
stopTone();
// Connect audio streams
addStream(call);
if (audiodriver_) {
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audiodriver_->flushMain();
audiodriver_->flushUrgent();
}
if (audioPreference.getIsAlwaysRecording())
toggleRecordingCall(call_id);
emitSignal<DRing::CallSignal::StateChange>(call_id, "CURRENT");
}
//THREAD=VoIP Call=Outgoing
void
ManagerImpl::peerRingingCall(Call& call)
{
const auto call_id = call.getCallId();
RING_DBG("Peer call %s ringing", call_id.c_str());
if (isCurrentCall(call))
ringback();
emitSignal<DRing::CallSignal::StateChange>(call_id, "RINGING");
}
//THREAD=VoIP Call=Outgoing/Ingoing
void
ManagerImpl::peerHungupCall(Call& call)
{
const auto call_id = call.getCallId();
RING_DBG("Peer hungup call %s", call_id.c_str());
if (isConferenceParticipant(call_id)) {
removeParticipant(call_id);
} else if (isCurrentCall(call)) {
stopTone();
unsetCurrentCall();
}
history_.addCall(&call, preferences.getHistoryLimit());
call.peerHungup();
saveHistory();
emitSignal<DRing::CallSignal::StateChange>(call_id, "HUNGUP");
checkAudio();
removeWaitingCall(call_id);
if (not incomingCallsWaiting())
stopTone();
removeStream(call);
}
//THREAD=VoIP
void
ManagerImpl::callBusy(Call& call)
{
const auto call_id = call.getCallId();
emitSignal<DRing::CallSignal::StateChange>(call_id, "BUSY");
if (isCurrentCall(call)) {
playATone(Tone::TONE_BUSY);
unsetCurrentCall();
}
checkAudio();
removeWaitingCall(call_id);
}
//THREAD=VoIP
void
ManagerImpl::callFailure(Call& call)
{
const auto call_id = call.getCallId();
emitSignal<DRing::CallSignal::StateChange>(call_id, "FAILURE");
if (isCurrentCall(call)) {
playATone(Tone::TONE_BUSY);
unsetCurrentCall();
}
if (isConferenceParticipant(call_id)) {
RING_DBG("Call %s participating in a conference failed", call_id.c_str());
// remove this participant
removeParticipant(call_id);
}
checkAudio();
removeWaitingCall(call_id);
}
//THREAD=VoIP
void
ManagerImpl::startVoiceMessageNotification(const std::string& accountId,
int nb_msg)
{
emitSignal<DRing::CallSignal::VoiceMailNotify>(accountId, nb_msg);
}
/**
* Multi Thread
*/
void
ManagerImpl::playATone(Tone::TONEID toneId)
{
if (not voipPreferences.getPlayTones())
return;
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("Audio layer not initialized");
return;
}
audiodriver_->flushUrgent();
audiodriver_->startStream();
}
{
std::lock_guard<std::mutex> lock(toneMutex_);
if (telephoneTone_)
telephoneTone_->setCurrentTone(toneId);
}
}
/**
* Multi Thread
*/
void
ManagerImpl::stopTone()
{
if (not voipPreferences.getPlayTones())
return;
std::lock_guard<std::mutex> lock(toneMutex_);
if (telephoneTone_)
telephoneTone_->setCurrentTone(Tone::TONE_NULL);
if (audiofile_) {
std::string filepath(audiofile_->getFilePath());
emitSignal<DRing::CallSignal::RecordPlaybackStopped>(filepath);
audiofile_.reset();
}
}
/**
* Multi Thread
*/
void
ManagerImpl::playTone()
{
playATone(Tone::TONE_DIALTONE);
}
/**
* Multi Thread
*/
void
ManagerImpl::playToneWithMessage()
{
playATone(Tone::TONE_CONGESTION);
}
/**
* Multi Thread
*/
void
ManagerImpl::congestion()
{
playATone(Tone::TONE_CONGESTION);
}
/**
* Multi Thread
*/
void
ManagerImpl::ringback()
{
playATone(Tone::TONE_RINGTONE);
}
// Caller must hold toneMutex
void
ManagerImpl::updateAudioFile(const std::string &file, int sampleRate)
{
audiofile_.reset(new AudioFile(file, sampleRate));
}
/**
* Multi Thread
*/
void
ManagerImpl::playRingtone(const std::string& accountID)
{
const auto account = getAccount(accountID);
if (!account) {
RING_WARN("Invalid account in ringtone");
return;
}
if (!account->getRingtoneEnabled()) {
ringback();
return;
}
std::string ringchoice = account->getRingtonePath();
if (ringchoice.find(DIR_SEPARATOR_STR) == std::string::npos) {
// check inside global share directory
static const char * const RINGDIR = "ringtones";
ringchoice = std::string(PROGSHAREDIR) + DIR_SEPARATOR_STR
+ RINGDIR + DIR_SEPARATOR_STR + ringchoice;
}
int audioLayerSmplr = 8000;
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("no audio layer in ringtone");
return;
}
audioLayerSmplr = audiodriver_->getSampleRate();
}
bool doFallback = false;
{
std::lock_guard<std::mutex> m(toneMutex_);
if (audiofile_) {
emitSignal<DRing::CallSignal::RecordPlaybackStopped>(audiofile_->getFilePath());
audiofile_.reset();
}
try {
updateAudioFile(ringchoice, audioLayerSmplr);
} catch (const AudioFileException &e) {
RING_WARN("Ringtone error: %s", e.what());
doFallback = true; // do ringback once lock is out of scope
}
} // leave mutex
if (doFallback) {
ringback();
return;
}
std::lock_guard<std::mutex> lock(audioLayerMutex_);
// start audio if not started AND flush all buffers (main and urgent)
audiodriver_->startStream();
}
AudioLoop*
ManagerImpl::getTelephoneTone()
{
std::lock_guard<std::mutex> m(toneMutex_);
if (telephoneTone_)
return telephoneTone_->getCurrentTone();
else
return nullptr;
}
AudioLoop*
ManagerImpl::getTelephoneFile()
{
std::lock_guard<std::mutex> m(toneMutex_);
return audiofile_.get();
}
///////////////////////////////////////////////////////////////////////////////
// Private functions
///////////////////////////////////////////////////////////////////////////////
/**
* Initialization: Main Thread
*/
std::string
ManagerImpl::retrieveConfigPath() const
{
#ifdef __ANDROID__
std::string configdir = "/data/data/cx.ring";
#else
std::string configdir = fileutils::get_home_dir() + DIR_SEPARATOR_STR +
".config" + DIR_SEPARATOR_STR + PACKAGE;
#endif
const std::string xdg_env(XDG_CONFIG_HOME);
if (not xdg_env.empty())
configdir = xdg_env + DIR_SEPARATOR_STR + PACKAGE;
if (mkdir(configdir.data(), 0700) != 0) {
// If directory creation failed
if (errno != EEXIST)
RING_DBG("Cannot create directory: %s!", configdir.c_str());
}
static const char * const PROGNAME = "dring";
return configdir + DIR_SEPARATOR_STR + PROGNAME + ".yml";
}
/**
* Set input audio plugin
*/
void
ManagerImpl::setAudioPlugin(const std::string& audioPlugin)
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audioPreference.setAlsaPlugin(audioPlugin);
bool wasStarted = audiodriver_->isStarted();
// Recreate audio driver with new settings
audiodriver_.reset(audioPreference.createAudioLayer());
if (audiodriver_ and wasStarted)
audiodriver_->startStream();
else
RING_ERR("No audio layer created, possibly built without audio support");
}
/**
* Set audio output device
*/
void
ManagerImpl::setAudioDevice(int index, DeviceType type)
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("Audio driver not initialized");
return ;
}
const bool wasStarted = audiodriver_->isStarted();
audiodriver_->updatePreference(audioPreference, index, type);
// Recreate audio driver with new settings
audiodriver_.reset(audioPreference.createAudioLayer());
if (audiodriver_ and wasStarted)
audiodriver_->startStream();
}
/**
* Get list of supported audio output device
*/
std::vector<std::string>
ManagerImpl::getAudioOutputDeviceList()
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
return audiodriver_->getPlaybackDeviceList();
}
/**
* Get list of supported audio input device
*/
std::vector<std::string>
ManagerImpl::getAudioInputDeviceList()
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
return audiodriver_->getCaptureDeviceList();
}
/**
* Get string array representing integer indexes of output and input device
*/
std::vector<std::string>
ManagerImpl::getCurrentAudioDevicesIndex()
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
std::vector<std::string> v;
std::stringstream ssi, sso, ssr;
sso << audiodriver_->getIndexPlayback();
v.push_back(sso.str());
ssi << audiodriver_->getIndexCapture();
v.push_back(ssi.str());
ssr << audiodriver_->getIndexRingtone();
v.push_back(ssr.str());
return v;
}
int
ManagerImpl::isRingtoneEnabled(const std::string& id)
{
const auto account = getAccount(id);
if (!account) {
RING_WARN("Invalid account in ringtone enabled");
return 0;
}
return account->getRingtoneEnabled();
}
void
ManagerImpl::ringtoneEnabled(const std::string& id)
{
const auto account = getAccount(id);
if (!account) {
RING_WARN("Invalid account in ringtone enabled");
return;
}
account->getRingtoneEnabled() ? account->setRingtoneEnabled(false) : account->setRingtoneEnabled(true);
}
bool
ManagerImpl::getIsAlwaysRecording() const
{
return audioPreference.getIsAlwaysRecording();
}
void
ManagerImpl::setIsAlwaysRecording(bool isAlwaysRec)
{
return audioPreference.setIsAlwaysRecording(isAlwaysRec);
}
bool
ManagerImpl::toggleRecordingCall(const std::string& id)
{
std::shared_ptr<Recordable> rec;
ConferenceMap::const_iterator it(conferenceMap_.find(id));
if (it == conferenceMap_.end()) {
RING_DBG("toggle recording for call %s", id.c_str());
rec = getCallFromCallID(id);
} else {
RING_DBG("toggle recording for conference %s", id.c_str());
auto conf = it->second;
if (conf) {
rec = conf;
if (conf->isRecording())
conf->setState(Conference::ACTIVE_ATTACHED);
else
conf->setState(Conference::ACTIVE_ATTACHED_REC);
}
}
if (!rec) {
RING_ERR("Could not find recordable instance %s", id.c_str());
return false;
}
const bool result = rec->toggleRecording();
emitSignal<DRing::CallSignal::RecordPlaybackFilepath>(id, rec->getFilename());
emitSignal<DRing::CallSignal::RecordingStateChanged>(id, result);
return result;
}
bool
ManagerImpl::isRecording(const std::string& id)
{
auto call = getCallFromCallID(id);
return call and (static_cast<Recordable*>(call.get()))->isRecording();
}
bool
ManagerImpl::startRecordedFilePlayback(const std::string& filepath)
{
RING_DBG("Start recorded file playback %s", filepath.c_str());
int sampleRate;
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("No audio layer in start recorded file playback");
return false;
}
sampleRate = audiodriver_->getSampleRate();
}
{
std::lock_guard<std::mutex> m(toneMutex_);
if (audiofile_) {
emitSignal<DRing::CallSignal::RecordPlaybackStopped>(audiofile_->getFilePath());
audiofile_.reset();
}
try {
updateAudioFile(filepath, sampleRate);
if (not audiofile_)
return false;
} catch (const AudioFileException &e) {
RING_WARN("Audio file error: %s", e.what());
return false;
}
} // release toneMutex
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audiodriver_->startStream();
}
return true;
}
void ManagerImpl::recordingPlaybackSeek(const double value)
{
std::lock_guard<std::mutex> m(toneMutex_);
if (audiofile_)
audiofile_.get()->seek(value);
}
void ManagerImpl::stopRecordedFilePlayback(const std::string& filepath)
{
RING_DBG("Stop recorded file playback %s", filepath.c_str());
checkAudio();
{
std::lock_guard<std::mutex> m(toneMutex_);
audiofile_.reset();
}
emitSignal<DRing::CallSignal::RecordPlaybackStopped>(filepath);
}
void ManagerImpl::setHistoryLimit(int days)
{
RING_DBG("Set history limit");
preferences.setHistoryLimit(days);
saveConfig();
}
int
ManagerImpl::getHistoryLimit() const
{
return preferences.getHistoryLimit();
}
bool
ManagerImpl::setAudioManager(const std::string &api)
{
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_)
return false;
if (api == audioPreference.getAudioApi()) {
RING_DBG("Audio manager chosen already in use. No changes made. ");
return true;
}
}
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
bool wasStarted = audiodriver_->isStarted();
audioPreference.setAudioApi(api);
audiodriver_.reset(audioPreference.createAudioLayer());
if (audiodriver_ and wasStarted)
audiodriver_->startStream();
}
saveConfig();
// ensure that we completed the transition (i.e. no fallback was used)
return api == audioPreference.getAudioApi();
}
std::string
ManagerImpl::getAudioManager() const
{
return audioPreference.getAudioApi();
}
int
ManagerImpl::getAudioInputDeviceIndex(const std::string &name)
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("Audio layer not initialized");
return 0;
}
return audiodriver_->getAudioDeviceIndex(name, DeviceType::CAPTURE);
}
int
ManagerImpl::getAudioOutputDeviceIndex(const std::string &name)
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
RING_ERR("Audio layer not initialized");
return 0;
}
return audiodriver_->getAudioDeviceIndex(name, DeviceType::PLAYBACK);
}
std::string
ManagerImpl::getCurrentAudioOutputPlugin() const
{
return audioPreference.getAlsaPlugin();
}
bool
ManagerImpl::getNoiseSuppressState() const
{
return audioPreference.getNoiseReduce();
}
void
ManagerImpl::setNoiseSuppressState(bool state)
{
audioPreference.setNoiseReduce(state);
}
bool
ManagerImpl::isAGCEnabled() const
{
return audioPreference.isAGCEnabled();
}
void
ManagerImpl::setAGCState(bool state)
{
audioPreference.setAGCState(state);
}
/**
* Initialization: Main Thread
*/
void
ManagerImpl::initAudioDriver()
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
audiodriver_.reset(audioPreference.createAudioLayer());
}
void
ManagerImpl::hardwareAudioFormatChanged(AudioFormat format)
{
audioFormatUsed(format);
}
void
ManagerImpl::audioFormatUsed(AudioFormat format)
{
AudioFormat currentFormat = ringbufferpool_->getInternalAudioFormat();
format.nb_channels = std::max(currentFormat.nb_channels, std::min(format.nb_channels, 2u)); // max 2 channels.
format.sample_rate = std::max(currentFormat.sample_rate, format.sample_rate);
if (currentFormat == format)
return;
RING_DBG("Audio format changed: %s -> %s", currentFormat.toString().c_str(), format.toString().c_str());
ringbufferpool_->setInternalAudioFormat(format);
{
std::lock_guard<std::mutex> toneLock(toneMutex_);
telephoneTone_.reset(new TelephoneTone(preferences.getZoneToneChoice(), format.sample_rate));
}
dtmfKey_.reset(new DTMF(format.sample_rate));
}
void
ManagerImpl::setAccountsOrder(const std::string& order)
{
RING_DBG("Set accounts order : %s", order.c_str());
// Set the new config
preferences.setAccountOrder(order);
saveConfig();
}
std::vector<std::string>
ManagerImpl::getAccountList() const
{
// TODO: this code looks weird. need further investigation!
using std::vector;
using std::string;
vector<string> account_order(loadAccountOrder());
// The IP2IP profile is always available, and first in the list
vector<string> v;
// Concatenate all account pointers in a single map
const auto& allAccounts = accountFactory_.getAllAccounts();
// If no order has been set, load the default one ie according to the creation date.
if (account_order.empty()) {
for (const auto &account : allAccounts) {
if (account->isIP2IP())
continue;
v.push_back(account->getAccountID());
}
} else {
const auto& ip2ipAccountID = getIP2IPAccount()->getAccountID();
for (const auto& id : account_order) {
if (id.empty() or id == ip2ipAccountID)
continue;
if (accountFactory_.hasAccount(id))
v.push_back(id);
}
}
if (const auto& account = getIP2IPAccount())
v.push_back(account->getAccountID());
else
RING_ERR("could not find IP2IP profile in getAccount list");
return v;
}
std::map<std::string, std::string>
ManagerImpl::getAccountDetails(const std::string& accountID) const
{
const auto account = getAccount(accountID);
if (account) {
return account->getAccountDetails();
} else {
RING_ERR("Could not get account details on a non-existing accountID %s", accountID.c_str());
// return an empty map since we can't throw an exception to D-Bus
return std::map<std::string, std::string>();
}
}
std::map<std::string, std::string>
ManagerImpl::getVolatileAccountDetails(const std::string& accountID) const
{
const auto account = getAccount(accountID);
if (account) {
return account->getVolatileAccountDetails();
} else {
RING_ERR("Could not get volatile account details on a non-existing accountID %s", accountID.c_str());
return {{}};
}
}
// method to reduce the if/else mess.
// Even better, switch to XML !
void
ManagerImpl::setAccountDetails(const std::string& accountID,
const std::map<std::string, std::string>& details)
{
RING_DBG("Set account details for %s", accountID.c_str());
const auto account = getAccount(accountID);
if (account == nullptr) {
RING_ERR("Could not find account %s", accountID.c_str());
return;
}
// Ignore if nothing has changed
if (details == account->getAccountDetails())
return;
// Unregister before modifying any account information
account->doUnregister([&](bool /* transport_free */) {
account->setAccountDetails(details);
// Serialize configuration to disk once it is done
saveConfig();
if (account->isEnabled()) {
account->doRegister();
} else
account->doUnregister();
// Update account details to the client side
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
});
}
std::string
ManagerImpl::addAccount(const std::map<std::string, std::string>& details)
{
/** @todo Deal with both the accountMap_ and the Configuration */
std::string newAccountID;
static std::uniform_int_distribution<uint64_t> rand_acc_id;
const std::vector<std::string> accountList(getAccountList());
do {
std::ostringstream accId;
accId << std::hex << rand_acc_id(rand_);
newAccountID = accId.str();
} while (std::find(accountList.begin(), accountList.end(), newAccountID)
!= accountList.end());
// Get the type
const char* accountType;
if (details.find(Conf::CONFIG_ACCOUNT_TYPE) != details.end())
accountType = (*details.find(Conf::CONFIG_ACCOUNT_TYPE)).second.c_str();
else
accountType = AccountFactory::DEFAULT_ACCOUNT_TYPE;
RING_DBG("Adding account %s", newAccountID.c_str());
auto newAccount = accountFactory_.createAccount(accountType, newAccountID);
if (!newAccount) {
RING_ERR("Unknown %s param when calling addAccount(): %s",
Conf::CONFIG_ACCOUNT_TYPE, accountType);
return "";
}
newAccount->setAccountDetails(details);
preferences.addAccount(newAccountID);
newAccount->doRegister();
saveConfig();
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
return newAccountID;
}
void ManagerImpl::removeAccounts()
{
for (const auto &acc : getAccountList())
removeAccount(acc);
}
void ManagerImpl::removeAccount(const std::string& accountID)
{
// Get it down and dying
if (const auto& remAccount = getAccount(accountID)) {
remAccount->doUnregister();
accountFactory_.removeAccount(*remAccount);
}
preferences.removeAccount(accountID);
saveConfig();
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
}
bool
ManagerImpl::isValidCall(const std::string& callID)
{
return static_cast<bool>(getCallFromCallID(callID));
}
std::string
ManagerImpl::getNewCallID()
{
static std::uniform_int_distribution<uint64_t> rand_call_id;
std::ostringstream random_id;
// generate something like s7ea037947eb9fb2f
do {
random_id.clear();
random_id << rand_call_id(rand_);
} while (isValidCall(random_id.str()));
return random_id.str();
}
std::vector<std::string>
ManagerImpl::loadAccountOrder() const
{
return split_string(preferences.getAccountOrder(), '/');
}
void
ManagerImpl::loadAccount(const YAML::Node &node, int &errorCount,
const std::string &accountOrder)
{
using yaml_utils::parseValue;
std::string accountType;
parseValue(node, "type", accountType);
std::string accountid;
parseValue(node, "id", accountid);
std::string accountAlias;
parseValue(node, "alias", accountAlias);
const auto inAccountOrder = [&](const std::string & id) {
return accountOrder.find(id + "/") != std::string::npos;
};
if (!accountid.empty() and !accountAlias.empty()) {
const auto& ip2ipAccountID = getIP2IPAccount()->getAccountID();
if (not inAccountOrder(accountid) and accountid != ip2ipAccountID) {
RING_WARN("Dropping account %s, which is not in account order", accountid.c_str());
} else if (accountFactory_.isSupportedType(accountType.c_str())) {
std::shared_ptr<Account> a;
if (accountid != ip2ipAccountID)
a = accountFactory_.createAccount(accountType.c_str(), accountid);
else
a = accountFactory_.getIP2IPAccount();
if (a) {
a->unserialize(node);
} else {
RING_ERR("Failed to create account type \"%s\"", accountType.c_str());
++errorCount;
}
} else {
RING_WARN("Ignoring unknown account type \"%s\"", accountType.c_str());
}
}
}
int
ManagerImpl::loadAccountMap(const YAML::Node &node)
{
accountFactory_.initIP2IPAccount();
// build preferences
preferences.unserialize(node);
voipPreferences.unserialize(node);
hookPreference.unserialize(node);
audioPreference.unserialize(node);
shortcutPreferences.unserialize(node);
int errorCount = 0;
try {
#ifdef RING_VIDEO
getVideoDeviceMonitor().unserialize(node);
#endif
} catch (const YAML::Exception &e) {
RING_ERR("%s: No video node in config file", e.what());
++errorCount;
}
const std::string accountOrder = preferences.getAccountOrder();
// load saved preferences for IP2IP account from configuration file
const auto &accountList = node["accounts"];
for (auto &a : accountList) {
loadAccount(a, errorCount, accountOrder);
}
return errorCount;
}
std::map<std::string, std::string>
ManagerImpl::getCallDetails(const std::string &callID)
{
if (auto call = getCallFromCallID(callID)) {
return call->getDetails();
} else {
RING_ERR("Call is NULL");
// FIXME: is this even useful?
return Call::getNullDetails();
}
}
std::vector<std::map<std::string, std::string> >
ManagerImpl::getHistory()
{
return history_.getSerialized();
}
std::vector<std::string>
ManagerImpl::getCallList() const
{
return callFactory.getCallIDs();
}
std::map<std::string, std::string>
ManagerImpl::getConferenceDetails(
const std::string& confID) const
{
std::map<std::string, std::string> conf_details;
ConferenceMap::const_iterator iter_conf = conferenceMap_.find(confID);
if (iter_conf != conferenceMap_.end()) {
conf_details["CONFID"] = confID;
conf_details["CONF_STATE"] = iter_conf->second->getStateStr();
}
return conf_details;
}
std::vector<std::string>
ManagerImpl::getConferenceList() const
{
std::vector<std::string> v;
map_utils::vectorFromMapKeys(conferenceMap_, v);
return v;
}
std::vector<std::string>
ManagerImpl::getDisplayNames(const std::string& confID) const
{
std::vector<std::string> v;
ConferenceMap::const_iterator iter_conf = conferenceMap_.find(confID);
if (iter_conf != conferenceMap_.end()) {
return iter_conf->second->getDisplayNames();
} else {
RING_WARN("Did not find conference %s", confID.c_str());
}
return v;
}
std::vector<std::string>
ManagerImpl::getParticipantList(const std::string& confID) const
{
std::vector<std::string> v;
ConferenceMap::const_iterator iter_conf = conferenceMap_.find(confID);
if (iter_conf != conferenceMap_.end()) {
const ParticipantSet participants(iter_conf->second->getParticipantList());
std::copy(participants.begin(), participants.end(), std::back_inserter(v));;
} else
RING_WARN("Did not find conference %s", confID.c_str());
return v;
}
std::string
ManagerImpl::getConferenceId(const std::string& callID)
{
if (auto call = getCallFromCallID(callID))
return call->getConfId();
RING_ERR("Call is NULL");
return "";
}
void
ManagerImpl::saveHistory()
{
if (!history_.save())
RING_ERR("Could not save history!");
else
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
}
void
ManagerImpl::clearHistory()
{
history_.clear();
}
void
ManagerImpl::startAudioDriverStream()
{
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (!audiodriver_) {
RING_ERR("Audio driver not initialized");
return;
}
audiodriver_->startStream();
}
void
ManagerImpl::registerAccounts()
{
auto allAccounts(getAccountList());
for (auto &item : allAccounts) {
const auto a = getAccount(item);
if (!a)
continue;
a->loadConfig();
if (a->isEnabled()) {
a->doRegister();
}
}
}
void
ManagerImpl::unregisterAccounts()
{
for (const auto& account : getAllAccounts()) {
if (account->isEnabled())
account->doUnregister();
}
}
void
ManagerImpl::sendRegister(const std::string& accountID, bool enable)
{
const auto acc = getAccount(accountID);
if (!acc)
return;
acc->setEnabled(enable);
acc->loadConfig();
Manager::instance().saveConfig();
if (acc->isEnabled()) {
acc->doRegister();
} else
acc->doUnregister();
}
std::shared_ptr<AudioLayer>
ManagerImpl::getAudioDriver()
{
return audiodriver_;
}
std::shared_ptr<Call>
ManagerImpl::newOutgoingCall(const std::string& id,
const std::string& toUrl,
const std::string& preferredAccountId)
{
std::shared_ptr<Account> account = Manager::instance().getIP2IPAccount();
std::string finalToUrl = toUrl;
#if HAVE_DHT
if (toUrl.find("ring:") != std::string::npos) {
RING_WARN("Ring DHT call detected");
auto dhtAcc = getAllAccounts<RingAccount>();
if (not dhtAcc.empty())
return dhtAcc.front()->newOutgoingCall(id, finalToUrl);
}
#endif
// FIXME: have a generic version to remove sip dependency
sip_utils::stripSipUriPrefix(finalToUrl);
if (!IpAddr::isValid(finalToUrl)) {
account = getAccount(preferredAccountId);
if (account)
finalToUrl = toUrl;
else
RING_WARN("Preferred account %s doesn't exist, using IP2IP account",
preferredAccountId.c_str());
} else
RING_WARN("IP Url detected, using IP2IP account");
if (!account) {
RING_ERR("No suitable account found to create outgoing call");
return nullptr;
}
return account->newOutgoingCall(id, finalToUrl);
}
} // namespace ring