blob: 01c54c1754914cdf4bbfb047d0d8bb42f4c570e1 [file] [log] [blame]
/*
* Copyright (C) 2004-2021 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: Tristan Matthews <tristan.matthews@savoirfairelinux.com>
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
* Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>
* Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
* Author: Aline Gondim Santos <aline.gondimsantos@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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "manager.h"
#include "logger.h"
#include "account_schema.h"
#include "fileutils.h"
#include "gittransport.h"
#include "map_utils.h"
#include "account.h"
#include "string_utils.h"
#include "jamidht/jamiaccount.h"
#include "sip/sipvoiplink.h"
#include "account.h"
#include <opendht/rng.h>
using random_device = dht::crypto::random_device;
#include "call_factory.h"
#include "sip/sip_utils.h"
#include "sip/sipvoiplink.h"
#include "im/instant_messaging.h"
#include "config/yamlparser.h"
#if HAVE_ALSA
#include "audio/alsa/alsalayer.h"
#endif
#include "media/localrecordermanager.h"
#include "audio/sound/tonelist.h"
#include "audio/sound/dtmf.h"
#include "audio/ringbufferpool.h"
#ifdef ENABLE_PLUGIN
#include "plugin/jamipluginmanager.h"
#include "plugin/streamdata.h"
#endif
#ifdef ENABLE_VIDEO
#include "client/videomanager.h"
#include "video/video_scaler.h"
#endif
#include "conference.h"
#include "ice_transport.h"
#include "client/ring_signal.h"
#include "jami/call_const.h"
#include "jami/account_const.h"
#include "libav_utils.h"
#include "video/sinkclient.h"
#include "video/video_base.h"
#include "media/video/video_mixer.h"
#include "audio/tonecontrol.h"
#include "data_transfer.h"
#include "jami/media_const.h"
#include "upnp/upnp_context.h"
#include <libavutil/ffversion.h>
#include <opendht/thread_pool.h>
#include <asio/io_context.hpp>
#include <asio/executor_work_guard.hpp>
#include <git2.h>
#ifndef WIN32
#include <sys/time.h>
#include <sys/resource.h>
#endif
#ifdef TARGET_OS_IOS
#include <CoreFoundation/CoreFoundation.h>
#endif
#include <cerrno>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <memory>
#include <mutex>
#include <list>
#include <random>
namespace jami {
/** To store uniquely a list of Call ids */
using CallIDSet = std::set<std::string>;
static constexpr const char* PACKAGE_OLD = "ring";
std::atomic_bool Manager::initialized = {false};
static void
copy_over(const std::string& srcPath, const std::string& destPath)
{
std::ifstream src = fileutils::ifstream(srcPath.c_str());
std::ofstream dest = fileutils::ofstream(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);
}
void
check_rename(const std::string& old_dir, const std::string& new_dir)
{
if (old_dir == new_dir or not fileutils::isDirectory(old_dir))
return;
if (not fileutils::isDirectory(new_dir)) {
JAMI_WARN() << "Migrating " << old_dir << " to " << new_dir;
std::rename(old_dir.c_str(), new_dir.c_str());
} else {
for (const auto& file : fileutils::readDirectory(old_dir)) {
auto old_dest = fileutils::getFullPath(old_dir, file);
auto new_dest = fileutils::getFullPath(new_dir, file);
if (fileutils::isDirectory(old_dest) and fileutils::isDirectory(new_dest)) {
check_rename(old_dest, new_dest);
} else {
JAMI_WARN() << "Migrating " << old_dest << " to " << new_dest;
std::rename(old_dest.c_str(), new_dest.c_str());
}
}
fileutils::removeAll(old_dir);
}
}
/**
* Set OpenDHT's log level based on the DHTLOGLEVEL environment variable.
* DHTLOGLEVEL = 0 minimum logging (=disable)
* DHTLOGLEVEL = 1 (=ERROR only)
* DHTLOGLEVEL = 2 (+=WARN)
* DHTLOGLEVEL = 3 maximum logging (+=DEBUG)
*/
/** Environment variable used to set OpenDHT's logging level */
static constexpr const char* DHTLOGLEVEL = "DHTLOGLEVEL";
static void
setDhtLogLevel()
{
#ifndef RING_UWP
char* envvar = getenv(DHTLOGLEVEL);
int level = 0;
if (envvar != nullptr) {
if (not(std::istringstream(envvar) >> level))
level = 0;
// From 0 (min) to 3 (max)
level = std::max(0, std::min(level, 3));
JAMI_DBG("DHTLOGLEVEL=%u", level);
}
Manager::instance().dhtLogLevel = level;
#else
Manager::instance().dhtLogLevel = 0;
#endif
}
/**
* 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()
{
#ifndef RING_UWP
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));
}
#else
int level = 0;
#endif
pj_log_set_level(level);
pj_log_set_log_func([](int level, const char* data, int /*len*/) {
if (level < 2)
JAMI_ERR() << data;
else if (level < 4)
JAMI_WARN() << data;
else
JAMI_DBG() << data;
});
}
/**
* Set gnutls's log level based on the RING_TLS_LOGLEVEL environment variable.
* RING_TLS_LOGLEVEL = 0 minimum logging (default)
* RING_TLS_LOGLEVEL = 9 maximum logging
*/
static constexpr int RING_TLS_LOGLEVEL = 0;
static void
tls_print_logs(int level, const char* msg)
{
JAMI_XDBG("[%d]GnuTLS: %s", level, msg);
}
static void
setGnuTlsLogLevel()
{
#ifndef RING_UWP
char* envvar = getenv("RING_TLS_LOGLEVEL");
int level = RING_TLS_LOGLEVEL;
if (envvar != nullptr) {
int var_level;
if (std::istringstream(envvar) >> var_level)
level = var_level;
// From 0 (min) to 9 (max)
level = std::max(0, std::min(level, 9));
}
gnutls_global_set_log_level(level);
#else
gnutls_global_set_log_level(RING_TLS_LOGLEVEL);
#endif
gnutls_global_set_log_function(tls_print_logs);
}
//==============================================================================
struct Manager::ManagerPimpl
{
explicit ManagerPimpl(Manager& base);
bool parseConfiguration();
/*
* Play one tone
* @return false if the driver is uninitialize
*/
void playATone(Tone::ToneId toneId);
int getCurrentDeviceIndex(AudioDeviceType type);
/**
* Process remaining participant given a conference and the current call id.
* Mainly called when a participant is detached or hagned up
* @param current call id
* @param conference pointer
*/
void processRemainingParticipants(Conference& conf);
/**
* Create config directory in home user and return configuration file path
*/
std::string retrieveConfigPath() const;
void unsetCurrentCall();
void switchCall(const std::string& id);
/**
* Add incoming callid to the waiting list
* @param id std::string to add
*/
void addWaitingCall(const std::string& id);
/**
* Remove incoming callid to the waiting list
* @param id std::string to remove
*/
void removeWaitingCall(const std::string& id);
void loadAccount(const YAML::Node& item, int& errorCount);
void sendTextMessageToConference(const Conference& conf,
const std::map<std::string, std::string>& messages,
const std::string& from) const noexcept;
void bindCallToConference(Call& call, Conference& conf);
void addMainParticipant(Conference& conf);
bool hangupConference(Conference& conf);
template<class T>
std::shared_ptr<T> findAccount(const std::function<bool(const std::shared_ptr<T>&)>&);
void initAudioDriver();
void processIncomingCall(const std::string& accountId, Call& incomCall);
static void stripSipPrefix(Call& incomCall);
Manager& base_; // pimpl back-pointer
std::shared_ptr<asio::io_context> ioContext_;
std::thread ioContextRunner_;
/** Main scheduler */
ScheduledExecutor scheduler_;
std::atomic_bool autoAnswer_ {false};
/** Application wide tone controller */
ToneControl toneCtrl_;
std::unique_ptr<AudioDeviceGuard> toneDeviceGuard_;
/** Current Call ID */
std::string currentCall_;
/** Protected current call access */
std::mutex currentCallMutex_;
/** Protected sinks access */
std::mutex sinksMutex_;
/** Audio layer */
std::shared_ptr<AudioLayer> audiodriver_ {nullptr};
std::array<std::atomic_uint, 3> audioStreamUsers_ {};
// Main thread
std::unique_ptr<DTMF> dtmfKey_;
/** Buffer to generate DTMF */
AudioBuffer dtmfBuf_;
// To handle volume control
// short speakerVolume_;
// short micVolume_;
// End of sound variable
/**
* Mutex used to protect audio layer
*/
std::mutex audioLayerMutex_;
/**
* Waiting Call Vectors
*/
CallIDSet waitingCalls_;
/**
* Protect waiting call list, access by many voip/audio threads
*/
std::mutex waitingCallsMutex_;
/**
* Path of the ConfigFile
*/
std::string path_;
/**
* Instance of the RingBufferPool for the whole application
*
* In order to send signal to other parts of the application, one must pass through the
* RingBufferMananger. Audio instances must be registered into the RingBufferMananger and bound
* together via the Manager.
*
*/
std::unique_ptr<RingBufferPool> ringbufferpool_;
std::atomic_bool finished_ {false};
/* ICE support */
std::unique_ptr<IceTransportFactory> ice_tf_;
/* Sink ID mapping */
std::map<std::string, std::weak_ptr<video::SinkClient>> sinkMap_;
#ifdef ENABLE_VIDEO
std::unique_ptr<VideoManager> videoManager_;
#endif
std::unique_ptr<SIPVoIPLink> sipLink_;
#ifdef ENABLE_PLUGIN
/* Jami Plugin Manager */
JamiPluginManager jami_plugin_manager;
#endif
std::mutex gitTransportsMtx_ {};
std::map<git_smart_subtransport*, std::unique_ptr<P2PSubTransport>> gitTransports_ {};
};
Manager::ManagerPimpl::ManagerPimpl(Manager& base)
: base_(base)
, ioContext_(std::make_shared<asio::io_context>())
, toneCtrl_(base.preferences)
, dtmfBuf_(0, AudioFormat::MONO())
, ringbufferpool_(new RingBufferPool)
#ifdef ENABLE_VIDEO
, videoManager_(new VideoManager)
#endif
{
jami::libav_utils::av_init();
ioContextRunner_ = std::thread([context = ioContext_]() {
try {
auto work = asio::make_work_guard(*context);
context->run();
} catch (const std::exception& ex) {
JAMI_ERR("Unexpected io_context thread exception: %s", ex.what());
}
});
}
bool
Manager::ManagerPimpl::parseConfiguration()
{
bool result = true;
try {
std::ifstream file = fileutils::ifstream(path_);
YAML::Node parsedFile = YAML::Load(file);
file.close();
const int error_count = base_.loadAccountMap(parsedFile);
if (error_count > 0) {
JAMI_WARN("Errors while parsing %s", path_.c_str());
result = false;
}
} catch (const YAML::BadFile& e) {
JAMI_WARN("Could not open configuration file");
result = false;
}
return result;
}
/**
* Multi Thread
*/
void
Manager::ManagerPimpl::playATone(Tone::ToneId toneId)
{
if (not base_.voipPreferences.getPlayTones())
return;
std::lock_guard<std::mutex> lock(audioLayerMutex_);
if (not audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return;
}
auto oldGuard = std::move(toneDeviceGuard_);
toneDeviceGuard_ = base_.startAudioStream(AudioDeviceType::PLAYBACK);
audiodriver_->flushUrgent();
toneCtrl_.play(toneId);
}
int
Manager::ManagerPimpl::getCurrentDeviceIndex(AudioDeviceType type)
{
if (not audiodriver_)
return -1;
switch (type) {
case AudioDeviceType::PLAYBACK:
return audiodriver_->getIndexPlayback();
case AudioDeviceType::RINGTONE:
return audiodriver_->getIndexRingtone();
case AudioDeviceType::CAPTURE:
return audiodriver_->getIndexCapture();
default:
return -1;
}
}
void
Manager::ManagerPimpl::processRemainingParticipants(Conference& conf)
{
const std::string current_callId(base_.getCurrentCallId());
ParticipantSet participants(conf.getParticipantList());
const size_t n = participants.size();
JAMI_DBG("Process remaining %zu participant(s) from conference %s", n, conf.getConfId().c_str());
if (n > 1) {
// Reset ringbuffer's readpointers
for (const auto& p : participants)
base_.getRingBufferPool().flush(p);
base_.getRingBufferPool().flush(RingBufferPool::DEFAULT_ID);
} else if (n == 1) {
// this call is the last participant, hence
// the conference is over
auto p = participants.begin();
if (auto call = base_.getCallFromCallID(*p)) {
// if we are not listening to this conference and not a rendez-vous
auto w = call->getAccount();
auto account = w.lock();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (account->isRendezVous())
return;
if (current_callId != conf.getConfId())
base_.onHoldCall(account->getAccountID(), call->getCallId());
else
switchCall(call->getCallId());
}
JAMI_DBG("No remaining participants, remove conference");
if (auto account = conf.getAccount())
account->removeConference(conf.getConfId());
} else {
JAMI_DBG("No remaining participants, remove conference");
if (auto account = conf.getAccount())
account->removeConference(conf.getConfId());
unsetCurrentCall();
}
}
/**
* Initialization: Main Thread
*/
std::string
Manager::ManagerPimpl::retrieveConfigPath() const
{
// TODO: Migrate config file name from dring.yml to jami.yml.
return fileutils::get_config_dir() + DIR_SEPARATOR_STR + "dring.yml";
}
void
Manager::ManagerPimpl::unsetCurrentCall()
{
currentCall_ = "";
}
void
Manager::ManagerPimpl::switchCall(const std::string& id)
{
std::lock_guard<std::mutex> m(currentCallMutex_);
JAMI_DBG("----- Switch current call id to '%s' -----", not id.empty() ? id.c_str() : "none");
currentCall_ = id;
}
void
Manager::ManagerPimpl::addWaitingCall(const std::string& id)
{
std::lock_guard<std::mutex> m(waitingCallsMutex_);
// Enable incoming call beep if needed.
if (audiodriver_ and waitingCalls_.empty() and not currentCall_.empty())
audiodriver_->playIncomingCallNotification(true);
waitingCalls_.insert(id);
}
void
Manager::ManagerPimpl::removeWaitingCall(const std::string& id)
{
std::lock_guard<std::mutex> m(waitingCallsMutex_);
waitingCalls_.erase(id);
if (audiodriver_ and waitingCalls_.empty())
audiodriver_->playIncomingCallNotification(false);
}
void
Manager::ManagerPimpl::loadAccount(const YAML::Node& node, int& errorCount)
{
using yaml_utils::parseValue;
std::string accountType;
parseValue(node, "type", accountType);
std::string accountid;
parseValue(node, "id", accountid);
if (!accountid.empty()) {
if (base_.accountFactory.isSupportedType(accountType.c_str())) {
if (auto a = base_.accountFactory.createAccount(accountType.c_str(), accountid)) {
a->unserialize(node);
} else {
JAMI_ERR("Failed to create account type \"%s\"", accountType.c_str());
++errorCount;
}
} else {
JAMI_WARN("Ignoring unknown account type \"%s\"", accountType.c_str());
}
}
}
// THREAD=VoIP
void
Manager::ManagerPimpl::sendTextMessageToConference(const Conference& conf,
const std::map<std::string, std::string>& messages,
const std::string& from) const noexcept
{
ParticipantSet participants(conf.getParticipantList());
for (const auto& callId : participants) {
try {
auto call = base_.getCallFromCallID(callId);
if (not call)
throw std::runtime_error("no associated call");
call->sendTextMessage(messages, from);
} catch (const std::exception& e) {
JAMI_ERR("Failed to send message to conference participant %s: %s",
callId.c_str(),
e.what());
}
}
}
void
Manager::ManagerPimpl::bindCallToConference(Call& call, Conference& conf)
{
const auto& callId = call.getCallId();
const auto& confId = conf.getConfId();
const auto& state = call.getStateStr();
// ensure that calls are only in one conference at a time
if (call.isConferenceParticipant())
base_.detachParticipant(callId);
JAMI_DBG("[call:%s] bind to conference %s (callState=%s)",
callId.c_str(),
confId.c_str(),
state.c_str());
base_.getRingBufferPool().unBindAll(callId);
conf.addParticipant(callId);
if (state == "HOLD") {
conf.bindParticipant(callId);
base_.offHoldCall(call.getAccountId(), callId);
} else if (state == "INCOMING") {
conf.bindParticipant(callId);
base_.answerCall(call);
} else if (state == "CURRENT") {
conf.bindParticipant(callId);
} else if (state == "INACTIVE") {
conf.bindParticipant(callId);
base_.answerCall(call);
} else
JAMI_WARN("[call:%s] call state %s not recognized for conference",
callId.c_str(),
state.c_str());
}
//==============================================================================
Manager&
Manager::instance()
{
// Meyers singleton
static Manager instance;
// This will give a warning that can be ignored the first time instance()
// is called...subsequent warnings are more serious
if (not Manager::initialized)
JAMI_DBG("Not initialized");
return instance;
}
Manager::Manager()
: rand_(dht::crypto::getSeededRandomEngine<std::mt19937_64>())
, preferences()
, voipPreferences()
, audioPreference()
, shortcutPreferences()
#ifdef ENABLE_PLUGIN
, pluginPreferences()
#endif
#ifdef ENABLE_VIDEO
, videoPreferences()
#endif
, callFactory(rand_)
, accountFactory()
, pimpl_(new ManagerPimpl(*this))
{}
Manager::~Manager() {}
void
Manager::setAutoAnswer(bool enable)
{
pimpl_->autoAnswer_ = enable;
}
void
Manager::init(const std::string& config_file)
{
// FIXME: this is no good
initialized = true;
git_libgit2_init();
auto res = git_transport_register("git", p2p_transport_cb, nullptr);
if (res < 0) {
const git_error* error = giterr_last();
JAMI_ERR("Unable to initialize git transport %s", error ? error->message : "(unknown)");
}
#ifndef WIN32
// Set the max number of open files.
struct rlimit nofiles;
if (getrlimit(RLIMIT_NOFILE, &nofiles) == 0) {
if (nofiles.rlim_cur < nofiles.rlim_max && nofiles.rlim_cur < 1024u) {
nofiles.rlim_cur = std::min<rlim_t>(nofiles.rlim_max, 8192u);
setrlimit(RLIMIT_NOFILE, &nofiles);
}
}
#endif
#define PJSIP_TRY(ret) \
do { \
if ((ret) != PJ_SUCCESS) \
throw std::runtime_error(#ret " failed"); \
} while (0)
srand(time(nullptr)); // to get random number for RANDOM_PORT
// Initialize PJSIP (SIP and ICE implementation)
PJSIP_TRY(pj_init());
setSipLogLevel();
PJSIP_TRY(pjlib_util_init());
PJSIP_TRY(pjnath_init());
#undef PJSIP_TRY
setGnuTlsLogLevel();
JAMI_DBG("Using PJSIP version %s for %s", pj_get_version(), PJ_OS_NAME);
JAMI_DBG("Using GnuTLS version %s", gnutls_check_version(nullptr));
JAMI_DBG("Using OpenDHT version %s", dht::version());
JAMI_DBG("Using FFmpeg version %s", av_version_info());
int git2_major = 0, git2_minor = 0, git2_rev = 0;
if (git_libgit2_version(&git2_major, &git2_minor, &git2_rev) == 0) {
JAMI_DBG("Using Libgit2 version %d.%d.%d", git2_major, git2_minor, git2_rev);
}
setDhtLogLevel();
// Manager can restart without being recreated (Unit tests)
// So only create the SipLink once
pimpl_->sipLink_ = std::make_unique<SIPVoIPLink>();
check_rename(fileutils::get_cache_dir(PACKAGE_OLD), fileutils::get_cache_dir());
check_rename(fileutils::get_data_dir(PACKAGE_OLD), fileutils::get_data_dir());
check_rename(fileutils::get_config_dir(PACKAGE_OLD), fileutils::get_config_dir());
pimpl_->ice_tf_.reset(new IceTransportFactory());
pimpl_->path_ = config_file.empty() ? pimpl_->retrieveConfigPath() : config_file;
JAMI_DBG("Configuration file path: %s", pimpl_->path_.c_str());
bool no_errors = true;
// manager can restart without being recreated (Unit tests)
pimpl_->finished_ = false;
try {
no_errors = pimpl_->parseConfiguration();
} catch (const YAML::Exception& e) {
JAMI_ERR("%s", e.what());
no_errors = false;
}
// Some VoIP services support SIP/TLS and SRTP, but do not set the
// correct schema in the INVITE request. For more details, see:
// https://trac.pjsip.org/repos/ticket/1735
if (voipPreferences.getDisableSecureDlgCheck()) {
pjsip_cfg()->endpt.disable_secure_dlg_check = PJ_TRUE;
}
// always back up last error-free configuration
if (no_errors) {
make_backup(pimpl_->path_);
} else {
// restore previous configuration
JAMI_WARN("Restoring last working configuration");
try {
// remove accounts from broken configuration
removeAccounts();
restore_backup(pimpl_->path_);
pimpl_->parseConfiguration();
} catch (const YAML::Exception& e) {
JAMI_ERR("%s", e.what());
JAMI_WARN("Restoring backup failed");
}
}
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
pimpl_->initAudioDriver();
if (pimpl_->audiodriver_) {
pimpl_->toneCtrl_.setSampleRate(pimpl_->audiodriver_->getSampleRate());
pimpl_->dtmfKey_.reset(new DTMF(getRingBufferPool().getInternalSamplingRate()));
}
}
registerAccounts();
}
void
Manager::finish() noexcept
{
bool expected = false;
if (not pimpl_->finished_.compare_exchange_strong(expected, true))
return;
try {
// Terminate UPNP context
jami::upnp::UPnPContext::getUPnPContext()->shutdown();
// Forbid call creation
callFactory.forbid();
// Hangup all remaining active calls
JAMI_DBG("Hangup %zu remaining call(s)", callFactory.callCount());
for (const auto& call : callFactory.getAllCalls())
hangupCall(call->getAccountId(), call->getCallId());
callFactory.clear();
for (const auto& account : getAllAccounts<JamiAccount>()) {
if (account->getRegistrationState() == RegistrationState::INITIALIZING)
removeAccount(account->getAccountID(), true);
}
saveConfig();
// Disconnect accounts, close link stacks and free allocated ressources
unregisterAccounts();
accountFactory.clear();
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
pimpl_->audiodriver_.reset();
}
JAMI_DBG("Stopping schedulers and worker threads");
// Flush remaining tasks (free lambda' with capture)
pimpl_->scheduler_.stop();
dht::ThreadPool::io().join();
dht::ThreadPool::computation().join();
// IceTransportFactory should be stopped after the io pool
// as some ICE are destroyed in a ioPool (see ConnectionManager)
// Also, it must be called before pj_shutdown to avoid any problem
pimpl_->ice_tf_.reset();
// NOTE: sipLink_->shutdown() is needed because this will perform
// sipTransportBroker->shutdown(); which will call Manager::instance().sipVoIPLink()
// so the pointer MUST NOT be resetted at this point
if (pimpl_->sipLink_) {
pimpl_->sipLink_->shutdown();
pimpl_->sipLink_.reset();
}
pj_shutdown();
pimpl_->gitTransports_.clear();
git_libgit2_shutdown();
if (!pimpl_->ioContext_->stopped()) {
pimpl_->ioContext_->reset(); // allow to finish
pimpl_->ioContext_->stop(); // make thread stop
}
if (pimpl_->ioContextRunner_.joinable())
pimpl_->ioContextRunner_.join();
} catch (const VoipLinkException& err) {
JAMI_ERR("%s", err.what());
}
}
void
Manager::monitor(bool continuous)
{
Logger::setMonitorLog(true);
JAMI_DBG("############## START MONITORING ##############");
JAMI_DBG("Using PJSIP version %s for %s", pj_get_version(), PJ_OS_NAME);
JAMI_DBG("Using GnuTLS version %s", gnutls_check_version(nullptr));
JAMI_DBG("Using OpenDHT version %s", dht::version());
#ifdef __linux__
#if defined(__ANDROID__)
#else
auto opened_files = fileutils::readDirectory("/proc/" + std::to_string(getpid()) + "/fd").size();
JAMI_DBG("Opened files: %lu", opened_files);
#endif
#endif
for (const auto& call : callFactory.getAllCalls())
call->monitor();
for (const auto& account : getAllAccounts())
if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account))
acc->monitor();
JAMI_DBG("############## END MONITORING ##############");
Logger::setMonitorLog(continuous);
}
bool
Manager::isCurrentCall(const Call& call) const
{
return pimpl_->currentCall_ == call.getCallId();
}
bool
Manager::hasCurrentCall() const
{
for (const auto& call : callFactory.getAllCalls()) {
if (!call->isSubcall() && call->getStateStr() == DRing::Call::StateEvent::CURRENT)
return true;
}
return false;
}
std::shared_ptr<Call>
Manager::getCurrentCall() const
{
return getCallFromCallID(pimpl_->currentCall_);
}
const std::string&
Manager::getCurrentCallId() const
{
return pimpl_->currentCall_;
}
void
Manager::unregisterAccounts()
{
for (const auto& account : getAllAccounts()) {
if (account->isEnabled()) {
if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account)) {
// Note: shutdown the connections as doUnregister will not do it (because the
// account is enabled)
acc->shutdownConnections();
}
account->doUnregister();
}
}
}
///////////////////////////////////////////////////////////////////////////////
// Management of events' IP-phone user
///////////////////////////////////////////////////////////////////////////////
/* Main Thread */
std::string
Manager::outgoingCall(const std::string& account_id,
const std::string& to,
const std::vector<DRing::MediaMap>& mediaList,
std::shared_ptr<Conference> conference)
{
JAMI_DBG() << "try outgoing call to '" << to << "'"
<< " with account '" << account_id << "'";
std::shared_ptr<Call> call;
try {
call = newOutgoingCall(trim(to), account_id, mediaList);
} catch (const std::exception& e) {
JAMI_ERR("%s", e.what());
return {};
}
if (not call)
return {};
stopTone();
pimpl_->switchCall(call->getCallId());
return call->getCallId();
}
// THREAD=Main : for outgoing Call
bool
Manager::answerCall(const std::string& accountId,
const std::string& callId,
const std::vector<DRing::MediaMap>& mediaList)
{
if (auto account = getAccount(accountId)) {
if (auto call = account->getCall(callId)) {
return answerCall(*call, mediaList);
}
}
return false;
}
bool
Manager::answerCall(Call& call, const std::vector<DRing::MediaMap>& mediaList)
{
JAMI_INFO("Answer call %s", call.getCallId().c_str());
if (call.getConnectionState() != Call::ConnectionState::RINGING) {
// The call is already answered
return true;
}
// If ringing
stopTone();
pimpl_->removeWaitingCall(call.getCallId());
try {
if (mediaList.empty())
call.answer();
else
call.answer(mediaList);
} catch (const std::runtime_error& e) {
JAMI_ERR("%s", e.what());
return false;
}
// if we dragged this call into a conference already
if (auto conf = call.getConference())
pimpl_->switchCall(conf->getConfId());
else
pimpl_->switchCall(call.getCallId());
addAudio(call);
// Start recording if set in preference
if (audioPreference.getIsAlwaysRecording()) {
auto recResult = call.toggleRecording();
emitSignal<DRing::CallSignal::RecordPlaybackFilepath>(call.getCallId(), call.getPath());
emitSignal<DRing::CallSignal::RecordingStateChanged>(call.getCallId(), recResult);
}
return true;
}
// THREAD=Main
bool
Manager::hangupCall(const std::string&, const std::string& callId)
{
// store the current call id
const auto& currentCallId(getCurrentCallId());
stopTone();
pimpl_->removeWaitingCall(callId);
/* We often get here when the call was hungup before being created */
auto call = getCallFromCallID(callId);
if (not call) {
JAMI_WARN("Could not hang up non-existant call %s", callId.c_str());
return false;
}
// Disconnect streams
removeAudio(*call);
if (call->isConferenceParticipant()) {
removeParticipant(*call);
} else {
// we are not participating in a conference, current call switched to ""
if (isCurrentCall(*call))
pimpl_->unsetCurrentCall();
}
try {
call->hangup(0);
} catch (const VoipLinkException& e) {
JAMI_ERR("%s", e.what());
return false;
}
return true;
}
bool
Manager::hangupConference(const std::string& accountId, const std::string& confId)
{
if (auto account = getAccount(accountId)) {
if (auto conference = account->getConference(confId)) {
return pimpl_->hangupConference(*conference);
} else {
JAMI_ERR("No such conference %s", confId.c_str());
}
}
return false;
}
// THREAD=Main
bool
Manager::onHoldCall(const std::string&, const std::string& callId)
{
bool result = true;
stopTone();
std::string current_callId(getCurrentCallId());
if (auto call = getCallFromCallID(callId)) {
try {
result = call->onhold([=](bool ok) {
if (!ok) {
JAMI_ERR("hold failed for call %s", callId.c_str());
return;
}
removeAudio(*call); // Unbind calls in main buffer
// Remove call from the queue if it was still there
pimpl_->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_callId == callId)
pimpl_->unsetCurrentCall();
});
} catch (const VoipLinkException& e) {
JAMI_ERR("%s", e.what());
result = false;
}
} else {
JAMI_DBG("CallID %s doesn't exist in call onHold", callId.c_str());
return false;
}
return result;
}
// THREAD=Main
bool
Manager::offHoldCall(const std::string&, const std::string& callId)
{
bool result = true;
stopTone();
std::shared_ptr<Call> call = getCallFromCallID(callId);
if (!call)
return false;
try {
result = call->offhold([=](bool ok) {
if (!ok) {
JAMI_ERR("off hold failed for call %s", callId.c_str());
return;
}
if (auto conf = call->getConference())
pimpl_->switchCall(conf->getConfId());
else
pimpl_->switchCall(call->getCallId());
addAudio(*call);
});
} catch (const VoipLinkException& e) {
JAMI_ERR("%s", e.what());
return false;
}
return result;
}
// THREAD=Main
bool
Manager::transferCall(const std::string& accountId, const std::string& callId, const std::string& to)
{
auto account = getAccount(accountId);
if (not account)
return false;
if (auto call = account->getCall(callId)) {
if (call->isConferenceParticipant()) {
removeParticipant(*call);
} /*else if (not isConference(getCurrentCallId())) {
pimpl_->unsetCurrentCall();
}*/
call->transfer(to);
} else
return false;
// remove waiting call in case we make transfer without even answer
pimpl_->removeWaitingCall(callId);
return true;
}
void
Manager::transferFailed()
{
emitSignal<DRing::CallSignal::TransferFailed>();
}
void
Manager::transferSucceeded()
{
emitSignal<DRing::CallSignal::TransferSucceeded>();
}
// THREAD=Main : Call:Incoming
bool
Manager::refuseCall(const std::string& accountId, const std::string& id)
{
if (auto account = getAccount(accountId)) {
if (auto call = account->getCall(id)) {
stopTone();
call->refuse();
pimpl_->removeWaitingCall(id);
removeAudio(*call);
return true;
}
}
return false;
}
bool
Manager::holdConference(const std::string& accountId, const std::string& confId)
{
JAMI_INFO("Hold conference %s", confId.c_str());
if (const auto account = getAccount(accountId)) {
if (auto conf = account->getConference(confId)) {
conf->detachLocalParticipant();
emitSignal<DRing::CallSignal::ConferenceChanged>(accountId,
conf->getConfId(),
conf->getStateStr());
return true;
}
}
return false;
}
bool
Manager::unHoldConference(const std::string& accountId, const std::string& confId)
{
if (const auto account = getAccount(accountId)) {
if (auto conf = account->getConference(confId)) {
// Unhold conf only if it was in hold state otherwise...
// all participants are restarted
if (conf->getState() == Conference::State::HOLD) {
for (const auto& item : conf->getParticipantList())
offHoldCall(accountId, item);
pimpl_->switchCall(confId);
conf->setState(Conference::State::ACTIVE_ATTACHED);
emitSignal<DRing::CallSignal::ConferenceChanged>(accountId,
conf->getConfId(),
conf->getStateStr());
return true;
} else if (conf->getState() == Conference::State::ACTIVE_DETACHED) {
pimpl_->addMainParticipant(*conf);
}
}
}
return false;
}
bool
Manager::addParticipant(const std::string& accountId,
const std::string& callId,
const std::string& account2Id,
const std::string& conferenceId)
{
auto account = getAccount(accountId);
auto account2 = getAccount(account2Id);
if (account && account2) {
auto call = account->getCall(callId);
auto conf = account2->getConference(conferenceId);
if (!call or !conf)
return false;
auto callConf = call->getConference();
if (callConf != conf)
return addParticipant(*call, *conf);
}
return false;
}
bool
Manager::addParticipant(Call& call, Conference& conference)
{
// No-op if the call is already a conference participant
/*if (call.getConfId() == conference.getConfId()) {
JAMI_WARN("Call %s already participant of conf %s", call.getCallId().c_str(),
conference.getConfId().c_str()); return true;
}*/
JAMI_DBG("Add participant %s to conference %s",
call.getCallId().c_str(),
conference.getConfId().c_str());
// store the current call id (it will change in offHoldCall or in answerCall)
pimpl_->bindCallToConference(call, conference);
// Don't attach current user yet
if (conference.getState() == Conference::State::ACTIVE_DETACHED)
return true;
// 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
// to conference
pimpl_->unsetCurrentCall();
pimpl_->addMainParticipant(conference);
pimpl_->switchCall(conference.getConfId());
addAudio(call);
return true;
}
void
Manager::ManagerPimpl::addMainParticipant(Conference& conf)
{
conf.attachLocalParticipant();
emitSignal<DRing::CallSignal::ConferenceChanged>(conf.getAccountId(),
conf.getConfId(),
conf.getStateStr());
switchCall(conf.getConfId());
}
bool
Manager::ManagerPimpl::hangupConference(Conference& conference)
{
JAMI_DBG("Hangup conference %s", conference.getConfId().c_str());
ParticipantSet participants(conference.getParticipantList());
for (const auto& callId : participants) {
if (auto call = base_.getCallFromCallID(callId))
base_.hangupCall(call->getAccountId(), callId);
}
unsetCurrentCall();
return true;
}
bool
Manager::addMainParticipant(const std::string& accountId, const std::string& conferenceId)
{
JAMI_INFO("Add main participant to conference %s", conferenceId.c_str());
if (auto account = getAccount(accountId)) {
if (auto conf = account->getConference(conferenceId)) {
pimpl_->addMainParticipant(*conf);
JAMI_DBG("Successfully added main participant to conference %s", conferenceId.c_str());
return true;
} else
JAMI_WARN("Failed to add main participant to conference %s", conferenceId.c_str());
}
return false;
}
std::shared_ptr<Call>
Manager::getCallFromCallID(const std::string& callID) const
{
return callFactory.getCall(callID);
}
bool
Manager::joinParticipant(const std::string& accountId,
const std::string& callId1,
const std::string& account2Id,
const std::string& callId2,
bool attached)
{
JAMI_INFO("JoinParticipant(%s, %s, %i)", callId1.c_str(), callId2.c_str(), attached);
auto account = getAccount(accountId);
auto account2 = getAccount(account2Id);
if (not account or not account2) {
return false;
}
JAMI_INFO("Creating conference for participants %s and %s. Attach host [%s]",
callId1.c_str(),
callId2.c_str(),
attached ? "YES" : "NO");
if (callId1 == callId2) {
JAMI_ERR("Cannot join participant %s to itself", callId1.c_str());
return false;
}
// Set corresponding conference ids for call 1
auto call1 = account->getCall(callId1);
if (!call1) {
JAMI_ERR("Could not find call %s", callId1.c_str());
return false;
}
// Set corresponding conference details
auto call2 = account2->getCall(callId2);
if (!call2) {
JAMI_ERR("Could not find call %s", callId2.c_str());
return false;
}
auto conf = std::make_shared<Conference>(account);
account->attach(conf);
emitSignal<DRing::CallSignal::ConferenceCreated>(account->getAccountID(), conf->getConfId());
// Bind calls according to their state
pimpl_->bindCallToConference(*call1, *conf);
pimpl_->bindCallToConference(*call2, *conf);
// Switch current call id to this conference
if (attached) {
pimpl_->switchCall(conf->getConfId());
conf->setState(Conference::State::ACTIVE_ATTACHED);
} else {
conf->detachLocalParticipant();
}
emitSignal<DRing::CallSignal::ConferenceChanged>(account->getAccountID(),
conf->getConfId(),
conf->getStateStr());
return true;
}
void
Manager::createConfFromParticipantList(const std::string& accountId,
const std::vector<std::string>& participantList)
{
auto account = getAccount(accountId);
if (not account) {
JAMI_WARN("Can't find account");
return;
}
// we must at least have 2 participant for a conference
if (participantList.size() <= 1) {
JAMI_ERR("Participant number must be higher or equal to 2");
return;
}
auto conf = std::make_shared<Conference>(account);
unsigned successCounter = 0;
for (const auto& numberaccount : participantList) {
std::string tostr(numberaccount.substr(0, numberaccount.find(',')));
std::string account(numberaccount.substr(numberaccount.find(',') + 1, numberaccount.size()));
pimpl_->unsetCurrentCall();
// Create call
auto callId = outgoingCall(account, tostr, {}, conf);
if (callId.empty())
continue;
// Manager methods may behave differently if the call id participates in a conference
conf->addParticipant(callId);
successCounter++;
}
// Create the conference if and only if at least 2 calls have been successfully created
if (successCounter >= 2) {
account->attach(conf);
emitSignal<DRing::CallSignal::ConferenceCreated>(accountId, conf->getConfId());
}
}
bool
Manager::detachLocalParticipant(const std::shared_ptr<Conference>& conf)
{
if (not conf)
return false;
JAMI_INFO("Detach local participant from conference %s", conf->getConfId().c_str());
conf->detachLocalParticipant();
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getAccountId(),
conf->getConfId(),
conf->getStateStr());
pimpl_->unsetCurrentCall();
return true;
}
bool
Manager::detachParticipant(const std::string& callId)
{
JAMI_DBG("Detach participant %s", callId.c_str());
auto call = getCallFromCallID(callId);
if (!call) {
JAMI_ERR("Could not find call %s", callId.c_str());
return false;
}
// Don't hold ringing calls when detaching them from conferences
if (call->getStateStr() != "RINGING")
onHoldCall(call->getAccountId(), callId);
removeParticipant(*call);
return true;
}
void
Manager::removeParticipant(Call& call)
{
JAMI_DBG("Remove participant %s", call.getCallId().c_str());
auto conf = call.getConference();
if (not conf) {
JAMI_ERR("No conference, cannot remove participant");
return;
}
conf->removeParticipant(call.getCallId());
removeAudio(call);
emitSignal<DRing::CallSignal::ConferenceChanged>(conf->getAccountId(),
conf->getConfId(),
conf->getStateStr());
pimpl_->processRemainingParticipants(*conf);
}
bool
Manager::joinConference(const std::string& accountId,
const std::string& confId1,
const std::string& account2Id,
const std::string& confId2)
{
auto account = getAccount(accountId);
if (not account) {
JAMI_ERR("Can't find account: %s", accountId.c_str());
return false;
}
auto conf = account->getConference(confId1);
if (not conf) {
JAMI_ERR("Not a valid conference ID: %s", confId1.c_str());
return false;
}
auto conf2 = account->getConference(confId2);
if (not conf2) {
JAMI_ERR("Not a valid conference ID: %s", confId2.c_str());
return false;
}
ParticipantSet participants(conf->getParticipantList());
std::vector<std::shared_ptr<Call>> calls;
calls.reserve(participants.size());
// Detach and remove all participant from conf1 before add
// ... to conf2
for (const auto& p : participants) {
JAMI_DBG("Detach participant %s", p.c_str());
if (auto call = account->getCall(p)) {
conf->removeParticipant(p);
removeAudio(*call);
calls.emplace_back(std::move(call));
} else {
JAMI_ERR("Could not find call %s", p.c_str());
}
}
// Remove conf1
account->removeConference(confId1);
for (const auto& c : calls)
addParticipant(*c, *conf2);
return true;
}
void
Manager::addAudio(Call& call)
{
const auto& callId = call.getCallId();
JAMI_INFO("Add audio to call %s", callId.c_str());
if (call.isConferenceParticipant()) {
JAMI_DBG("[conf:%s] Attach local audio", callId.c_str());
// bind to conference participant
/*auto iter = pimpl_->conferenceMap_.find(callId);
if (iter != pimpl_->conferenceMap_.end() and iter->second) {
iter->second->bindParticipant(callId);
}*/
} else {
JAMI_DBG("[call:%s] Attach audio", callId.c_str());
// bind to main
getRingBufferPool().bindCallID(callId, RingBufferPool::DEFAULT_ID);
auto oldGuard = std::move(call.audioGuard);
call.audioGuard = startAudioStream(AudioDeviceType::PLAYBACK);
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (!pimpl_->audiodriver_) {
JAMI_ERR("Audio driver not initialized");
return;
}
pimpl_->audiodriver_->flushUrgent();
getRingBufferPool().flushAllBuffers();
}
}
void
Manager::removeAudio(Call& call)
{
const auto& callId = call.getCallId();
JAMI_DBG("[call:%s] Remove local audio", callId.c_str());
getRingBufferPool().unBindAll(callId);
call.audioGuard.reset();
}
ScheduledExecutor&
Manager::scheduler()
{
return pimpl_->scheduler_;
}
std::shared_ptr<asio::io_context>
Manager::ioContext() const
{
return pimpl_->ioContext_;
}
void
Manager::addTask(std::function<bool()>&& task)
{
pimpl_->scheduler_.scheduleAtFixedRate(std::move(task), std::chrono::milliseconds(30));
}
std::shared_ptr<Task>
Manager::scheduleTask(std::function<void()>&& task, std::chrono::steady_clock::time_point when)
{
return pimpl_->scheduler_.schedule(std::move(task), when);
}
std::shared_ptr<Task>
Manager::scheduleTaskIn(std::function<void()>&& task, std::chrono::steady_clock::duration timeout)
{
return pimpl_->scheduler_.scheduleIn(std::move(task), timeout);
}
// Must be invoked periodically by a timer from the main event loop
void
Manager::pollEvents()
{}
// THREAD=Main
void
Manager::saveConfig(const std::shared_ptr<Account>& acc)
{
if (auto ringAcc = std::dynamic_pointer_cast<JamiAccount>(acc))
ringAcc->saveConfig();
else
saveConfig();
}
void
Manager::saveConfig()
{
JAMI_DBG("Saving Configuration to XDG directory %s", pimpl_->path_.c_str());
if (pimpl_->audiodriver_) {
audioPreference.setVolumemic(pimpl_->audiodriver_->getCaptureGain());
audioPreference.setVolumespkr(pimpl_->audiodriver_->getPlaybackGain());
audioPreference.setCaptureMuted(pimpl_->audiodriver_->isCaptureMuted());
audioPreference.setPlaybackMuted(pimpl_->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()) {
if (auto ringAccount = std::dynamic_pointer_cast<JamiAccount>(account)) {
auto accountConfig = ringAccount->getPath() + DIR_SEPARATOR_STR + "config.yml";
if (not fileutils::isFile(accountConfig)) {
saveConfig(ringAccount);
}
} else {
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);
audioPreference.serialize(out);
#ifdef ENABLE_VIDEO
videoPreferences.serialize(out);
#endif
#ifdef ENABLE_PLUGIN
pluginPreferences.serialize(out);
#endif
shortcutPreferences.serialize(out);
std::lock_guard<std::mutex> lock(fileutils::getFileLock(pimpl_->path_));
std::ofstream fout = fileutils::ofstream(pimpl_->path_);
fout << out.c_str();
} catch (const YAML::Exception& e) {
JAMI_ERR("%s", e.what());
} catch (const std::runtime_error& e) {
JAMI_ERR("%s", e.what());
}
}
// THREAD=Main | VoIPLink
void
Manager::playDtmf(char code)
{
stopTone();
if (not voipPreferences.getPlayDtmf()) {
JAMI_DBG("Do not have to play a tone...");
return;
}
// length in milliseconds
int pulselen = voipPreferences.getPulseLength();
if (pulselen == 0) {
JAMI_DBG("Pulse length is not set...");
return;
}
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
// fast return, no sound, so no dtmf
if (not pimpl_->audiodriver_ or not pimpl_->dtmfKey_) {
JAMI_DBG("No audio layer...");
return;
}
std::shared_ptr<AudioDeviceGuard> audioGuard = startAudioStream(AudioDeviceType::PLAYBACK);
if (not pimpl_->audiodriver_->waitForStart(std::chrono::seconds(1))) {
JAMI_ERR("Failed to start 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) pimpl_->audiodriver_->getSampleRate()) / 1000);
pimpl_->dtmfBuf_.resize(size);
// Handle dtmf
pimpl_->dtmfKey_->startTone(code);
// copy the sound
if (pimpl_->dtmfKey_->generateDTMF(*pimpl_->dtmfBuf_.getChannel(0))) {
// Put buffer to urgentRingBuffer
// put the size in bytes...
// so size * 1 channel (mono) * sizeof (bytes for the data)
// audiolayer->flushUrgent();
pimpl_->audiodriver_->putUrgent(pimpl_->dtmfBuf_);
}
scheduler().scheduleIn([audioGuard] { JAMI_WARN("End of dtmf"); },
std::chrono::milliseconds(pulselen));
// TODO Cache the DTMF
}
// Multi-thread
bool
Manager::incomingCallsWaiting()
{
std::lock_guard<std::mutex> m(pimpl_->waitingCallsMutex_);
return not pimpl_->waitingCalls_.empty();
}
void
Manager::incomingCall(const std::string& accountId, Call& call)
{
if (not accountId.empty()) {
pimpl_->stripSipPrefix(call);
}
std::string from("<" + call.getPeerNumber() + ">");
auto const& account = getAccount(accountId);
if (not account) {
JAMI_ERR("Incoming call %s on unknown account %s",
call.getCallId().c_str(),
accountId.c_str());
return;
}
// Report incoming call using "CallSignal::IncomingCallWithMedia" signal.
auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(call.getMediaAttributeList());
if (mediaList.empty()) {
JAMI_WARN("Incoming call %s has an empty media list", call.getCallId().c_str());
}
JAMI_INFO("Incoming call %s on account %s with %lu media",
call.getCallId().c_str(),
accountId.c_str(),
mediaList.size());
// Report the call using new API.
emitSignal<DRing::CallSignal::IncomingCallWithMedia>(accountId,
call.getCallId(),
call.getPeerDisplayName() + " " + from,
mediaList);
// Process the call.
pimpl_->processIncomingCall(accountId, call);
}
void
Manager::incomingMessage(const std::string& accountId,
const std::string& callId,
const std::string& from,
const std::map<std::string, std::string>& messages)
{
auto account = getAccount(accountId);
if (not account) {
return;
}
if (auto call = account->getCall(callId)) {
if (call->isConferenceParticipant()) {
if (auto conf = call->getConference()) {
JAMI_DBG("Is a conference, send incoming message to everyone");
// filter out vcards messages as they could be resent by master as its own vcard
// TODO. Implement a protocol to handle vcard messages
bool sendToOtherParicipants = true;
for (auto& message : messages) {
if (message.first.find("x-ring/ring.profile.vcard") != std::string::npos) {
sendToOtherParicipants = false;
}
}
if (sendToOtherParicipants) {
pimpl_->sendTextMessageToConference(*conf, messages, from);
}
// in case of a conference we must notify client using conference id
emitSignal<DRing::CallSignal::IncomingMessage>(accountId,
conf->getConfId(),
from,
messages);
} else {
JAMI_ERR("no conference associated to ID %s", callId.c_str());
}
} else {
emitSignal<DRing::CallSignal::IncomingMessage>(accountId, callId, from, messages);
}
}
}
void
Manager::sendCallTextMessage(const std::string& accountId,
const std::string& callID,
const std::map<std::string, std::string>& messages,
const std::string& from,
bool /*isMixed TODO: use it */)
{
auto account = getAccount(accountId);
if (not account) {
return;
}
if (auto conf = account->getConference(callID)) {
JAMI_DBG("Is a conference, send instant message to everyone");
pimpl_->sendTextMessageToConference(*conf, messages, from);
} else if (auto call = account->getCall(callID)) {
if (call->isConferenceParticipant()) {
if (auto conf = call->getConference()) {
JAMI_DBG("Call is participant in a conference, send instant message to everyone");
pimpl_->sendTextMessageToConference(*conf, messages, from);
} else {
JAMI_ERR("no conference associated to call ID %s", callID.c_str());
}
} else {
try {
call->sendTextMessage(messages, from);
} catch (const im::InstantMessageException& e) {
JAMI_ERR("Failed to send message to call %s: %s",
call->getCallId().c_str(),
e.what());
}
}
} else {
JAMI_ERR("Failed to send message to %s: inexistent call ID", callID.c_str());
}
}
// THREAD=VoIP CALL=Outgoing
void
Manager::peerAnsweredCall(Call& call)
{
const auto& callId = call.getCallId();
JAMI_DBG("[call:%s] Peer answered", callId.c_str());
// The if statement is useful only if we sent two calls at the same time.
if (isCurrentCall(call))
stopTone();
addAudio(call);
if (pimpl_->audiodriver_) {
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
getRingBufferPool().flushAllBuffers();
pimpl_->audiodriver_->flushUrgent();
}
if (audioPreference.getIsAlwaysRecording()) {
auto result = call.toggleRecording();
emitSignal<DRing::CallSignal::RecordPlaybackFilepath>(callId, call.getPath());
emitSignal<DRing::CallSignal::RecordingStateChanged>(callId, result);
}
}
// THREAD=VoIP Call=Outgoing
void
Manager::peerRingingCall(Call& call)
{
JAMI_DBG("[call:%s] Peer ringing", call.getCallId().c_str());
if (!hasCurrentCall())
ringback();
}
// THREAD=VoIP Call=Outgoing/Ingoing
void
Manager::peerHungupCall(Call& call)
{
const auto& callId = call.getCallId();
JAMI_DBG("[call:%s] Peer hung up", callId.c_str());
if (call.isConferenceParticipant()) {
removeParticipant(call);
} else if (isCurrentCall(call)) {
stopTone();
pimpl_->unsetCurrentCall();
}
call.peerHungup();
pimpl_->removeWaitingCall(callId);
if (not incomingCallsWaiting())
stopTone();
removeAudio(call);
}
// THREAD=VoIP
void
Manager::callBusy(Call& call)
{
JAMI_DBG("[call:%s] Busy", call.getCallId().c_str());
if (isCurrentCall(call)) {
pimpl_->unsetCurrentCall();
}
pimpl_->removeWaitingCall(call.getCallId());
if (not incomingCallsWaiting())
stopTone();
}
// THREAD=VoIP
void
Manager::callFailure(Call& call)
{
JAMI_DBG("[call:%s] %s failed",
call.getCallId().c_str(),
call.isSubcall() ? "Sub-call" : "Parent call");
if (isCurrentCall(call)) {
pimpl_->unsetCurrentCall();
}
if (call.isConferenceParticipant()) {
JAMI_DBG("[call %s] Participating in a conference. Remove", call.getCallId().c_str());
// remove this participant
removeParticipant(call);
}
pimpl_->removeWaitingCall(call.getCallId());
if (not incomingCallsWaiting())
stopTone();
removeAudio(call);
}
/**
* Multi Thread
*/
void
Manager::stopTone()
{
if (not voipPreferences.getPlayTones())
return;
pimpl_->toneCtrl_.stop();
pimpl_->toneDeviceGuard_.reset();
}
/**
* Multi Thread
*/
void
Manager::playTone()
{
pimpl_->playATone(Tone::ToneId::DIALTONE);
}
/**
* Multi Thread
*/
void
Manager::playToneWithMessage()
{
pimpl_->playATone(Tone::ToneId::CONGESTION);
}
/**
* Multi Thread
*/
void
Manager::congestion()
{
pimpl_->playATone(Tone::ToneId::CONGESTION);
}
/**
* Multi Thread
*/
void
Manager::ringback()
{
pimpl_->playATone(Tone::ToneId::RINGTONE);
}
/**
* Multi Thread
*/
void
Manager::playRingtone(const std::string& accountID)
{
const auto account = getAccount(accountID);
if (!account) {
JAMI_WARN("Invalid account in ringtone");
return;
}
if (!account->getRingtoneEnabled()) {
ringback();
return;
}
std::string ringchoice = account->getRingtonePath();
#if !defined(_WIN32)
if (ringchoice.find(DIR_SEPARATOR_CH) == 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;
}
#endif
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("no audio layer in ringtone");
return;
}
// start audio if not started AND flush all buffers (main and urgent)
auto oldGuard = std::move(pimpl_->toneDeviceGuard_);
pimpl_->toneDeviceGuard_ = startAudioStream(AudioDeviceType::RINGTONE);
pimpl_->toneCtrl_.setSampleRate(pimpl_->audiodriver_->getSampleRate());
}
if (not pimpl_->toneCtrl_.setAudioFile(ringchoice))
ringback();
}
std::shared_ptr<AudioLoop>
Manager::getTelephoneTone()
{
return pimpl_->toneCtrl_.getTelephoneTone();
}
std::shared_ptr<AudioLoop>
Manager::getTelephoneFile()
{
return pimpl_->toneCtrl_.getTelephoneFile();
}
/**
* Set input audio plugin
*/
void
Manager::setAudioPlugin(const std::string& audioPlugin)
{
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
audioPreference.setAlsaPlugin(audioPlugin);
pimpl_->audiodriver_.reset();
pimpl_->initAudioDriver();
}
// Recreate audio driver with new settings
saveConfig();
}
/**
* Set audio output device
*/
void
Manager::setAudioDevice(int index, AudioDeviceType type)
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio driver not initialized");
return;
}
if (pimpl_->getCurrentDeviceIndex(type) == index) {
JAMI_WARN("Audio device already selected ; doing nothing.");
return;
}
pimpl_->audiodriver_->updatePreference(audioPreference, index, type);
// Recreate audio driver with new settings
pimpl_->audiodriver_.reset();
pimpl_->initAudioDriver();
saveConfig();
}
/**
* Get list of supported audio output device
*/
std::vector<std::string>
Manager::getAudioOutputDeviceList()
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return {};
}
return pimpl_->audiodriver_->getPlaybackDeviceList();
}
/**
* Get list of supported audio input device
*/
std::vector<std::string>
Manager::getAudioInputDeviceList()
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return {};
}
return pimpl_->audiodriver_->getCaptureDeviceList();
}
/**
* Get string array representing integer indexes of output and input device
*/
std::vector<std::string>
Manager::getCurrentAudioDevicesIndex()
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return {};
}
return {std::to_string(pimpl_->audiodriver_->getIndexPlayback()),
std::to_string(pimpl_->audiodriver_->getIndexCapture()),
std::to_string(pimpl_->audiodriver_->getIndexRingtone())};
}
void
Manager::startAudio()
{
if (!pimpl_->audiodriver_)
pimpl_->audiodriver_.reset(pimpl_->base_.audioPreference.createAudioLayer());
constexpr std::array<AudioDeviceType, 3> TYPES {AudioDeviceType::CAPTURE,
AudioDeviceType::PLAYBACK,
AudioDeviceType::RINGTONE};
for (const auto& type : TYPES)
if (pimpl_->audioStreamUsers_[(unsigned) type])
pimpl_->audiodriver_->startStream(type);
}
AudioDeviceGuard::AudioDeviceGuard(Manager& manager, AudioDeviceType type)
: manager_(manager)
, type_(type)
{
auto streamId = (unsigned) type;
if (streamId >= manager_.pimpl_->audioStreamUsers_.size())
throw std::invalid_argument("Invalid audio device type");
if (manager_.pimpl_->audioStreamUsers_[(unsigned) type]++ == 0) {
if (auto layer = manager_.getAudioDriver())
layer->startStream(type);
}
}
AudioDeviceGuard::~AudioDeviceGuard()
{
auto streamId = (unsigned) type_;
if (--manager_.pimpl_->audioStreamUsers_[streamId] == 0) {
if (auto layer = manager_.getAudioDriver())
layer->stopStream(type_);
}
}
bool
Manager::getIsAlwaysRecording() const
{
return audioPreference.getIsAlwaysRecording();
}
void
Manager::setIsAlwaysRecording(bool isAlwaysRec)
{
audioPreference.setIsAlwaysRecording(isAlwaysRec);
saveConfig();
}
bool
Manager::toggleRecordingCall(const std::string& accountId, const std::string& id)
{
bool result = false;
if (auto account = getAccount(accountId)) {
std::shared_ptr<Recordable> rec;
if (auto conf = account->getConference(id)) {
JAMI_DBG("toggle recording for conference %s", id.c_str());
rec = conf;
} else if (auto call = account->getCall(id)) {
JAMI_DBG("toggle recording for call %s", id.c_str());
rec = call;
} else {
JAMI_ERR("Could not find recordable instance %s", id.c_str());
return false;
}
result = rec->toggleRecording();
emitSignal<DRing::CallSignal::RecordPlaybackFilepath>(id, rec->getPath());
emitSignal<DRing::CallSignal::RecordingStateChanged>(id, result);
}
return result;
}
bool
Manager::startRecordedFilePlayback(const std::string& filepath)
{
JAMI_DBG("Start recorded file playback %s", filepath.c_str());
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("No audio layer in start recorded file playback");
return false;
}
auto oldGuard = std::move(pimpl_->toneDeviceGuard_);
pimpl_->toneDeviceGuard_ = startAudioStream(AudioDeviceType::RINGTONE);
pimpl_->toneCtrl_.setSampleRate(pimpl_->audiodriver_->getSampleRate());
}
return pimpl_->toneCtrl_.setAudioFile(filepath);
}
void
Manager::recordingPlaybackSeek(const double value)
{
pimpl_->toneCtrl_.seek(value);
}
void
Manager::stopRecordedFilePlayback()
{
JAMI_DBG("Stop recorded file playback");
pimpl_->toneCtrl_.stopAudioFile();
pimpl_->toneDeviceGuard_.reset();
}
void
Manager::setHistoryLimit(int days)
{
JAMI_DBG("Set history limit");
preferences.setHistoryLimit(days);
saveConfig();
}
int
Manager::getHistoryLimit() const
{
return preferences.getHistoryLimit();
}
void
Manager::setRingingTimeout(int timeout)
{
JAMI_DBG("Set ringing timeout");
preferences.setRingingTimeout(timeout);
saveConfig();
}
int
Manager::getRingingTimeout() const
{
return preferences.getRingingTimeout();
}
bool
Manager::setAudioManager(const std::string& api)
{
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_)
return false;
if (api == audioPreference.getAudioApi()) {
JAMI_DBG("Audio manager chosen already in use. No changes made. ");
return true;
}
}
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
audioPreference.setAudioApi(api);
pimpl_->audiodriver_.reset();
pimpl_->initAudioDriver();
}
saveConfig();
// ensure that we completed the transition (i.e. no fallback was used)
return api == audioPreference.getAudioApi();
}
std::string
Manager::getAudioManager() const
{
return audioPreference.getAudioApi();
}
int
Manager::getAudioInputDeviceIndex(const std::string& name)
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return 0;
}
return pimpl_->audiodriver_->getAudioDeviceIndex(name, AudioDeviceType::CAPTURE);
}
int
Manager::getAudioOutputDeviceIndex(const std::string& name)
{
std::lock_guard<std::mutex> lock(pimpl_->audioLayerMutex_);
if (not pimpl_->audiodriver_) {
JAMI_ERR("Audio layer not initialized");
return 0;
}
return pimpl_->audiodriver_->getAudioDeviceIndex(name, AudioDeviceType::PLAYBACK);
}
std::string
Manager::getCurrentAudioOutputPlugin() const
{
return audioPreference.getAlsaPlugin();
}
bool
Manager::getNoiseSuppressState() const
{
return audioPreference.getNoiseReduce();
}
void
Manager::setNoiseSuppressState(bool state)
{
audioPreference.setNoiseReduce(state);
}
bool
Manager::isAGCEnabled() const
{
return audioPreference.isAGCEnabled();
}
void
Manager::setAGCState(bool state)
{
audioPreference.setAGCState(state);
}
/**
* Initialization: Main Thread
*/
void
Manager::ManagerPimpl::initAudioDriver()
{
audiodriver_.reset(base_.audioPreference.createAudioLayer());
constexpr std::array<AudioDeviceType, 3> TYPES {AudioDeviceType::CAPTURE,
AudioDeviceType::PLAYBACK,
AudioDeviceType::RINGTONE};
for (const auto& type : TYPES)
if (audioStreamUsers_[(unsigned) type])
audiodriver_->startStream(type);
}
// Internal helper method
void
Manager::ManagerPimpl::stripSipPrefix(Call& incomCall)
{
// strip sip: which is not required and bring confusion with ip to ip calls
// when placing new call from history.
std::string peerNumber(incomCall.getPeerNumber());
const char SIP_PREFIX[] = "sip:";
size_t startIndex = peerNumber.find(SIP_PREFIX);
if (startIndex != std::string::npos)
incomCall.setPeerNumber(peerNumber.substr(startIndex + sizeof(SIP_PREFIX) - 1));
}
// Internal helper method
void
Manager::ManagerPimpl::processIncomingCall(const std::string& accountId, Call& incomCall)
{
base_.stopTone();
auto incomCallId = incomCall.getCallId();
auto currentCall = base_.getCurrentCall();
auto w = incomCall.getAccount();
auto account = w.lock();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (not base_.hasCurrentCall()) {
incomCall.setState(Call::ConnectionState::RINGING);
#if !defined(RING_UWP) && !(defined(TARGET_OS_IOS) && TARGET_OS_IOS)
if (not account->isRendezVous())
base_.playRingtone(accountId);
#endif
}
addWaitingCall(incomCallId);
if (account->isRendezVous()) {
dht::ThreadPool::io().run([this, account, incomCall = incomCall.shared_from_this()] {
base_.answerCall(*incomCall);
for (const auto& callId : account->getCallList()) {
if (auto call = account->getCall(callId)) {
if (call->getState() != Call::CallState::ACTIVE)
continue;
if (call != incomCall) {
if (auto conf = call->getConference()) {
base_.addParticipant(*incomCall, *conf);
} else {
base_.joinParticipant(account->getAccountID(),
incomCall->getCallId(),
account->getAccountID(),
call->getCallId(),
false);
}
return;
}
}
}
// First call
auto conf = std::make_shared<Conference>(account);
account->attach(conf);
emitSignal<DRing::CallSignal::ConferenceCreated>(account->getAccountID(),
conf->getConfId());
// Bind calls according to their state
bindCallToConference(*incomCall, *conf);
conf->detachLocalParticipant();
emitSignal<DRing::CallSignal::ConferenceChanged>(account->getAccountID(),
conf->getConfId(),
conf->getStateStr());
});
} else if (autoAnswer_ || account->isAutoAnswerEnabled()) {
dht::ThreadPool::io().run(
[this, incomCall = incomCall.shared_from_this()] { base_.answerCall(*incomCall); });
} else if (currentCall && currentCall->getCallId() != incomCallId) {
// Test if already calling this person
if (currentCall->getAccountId() == account->getAccountID()
&& currentCall->getPeerNumber() == incomCall.getPeerNumber()) {
auto device_uid = account->getUsername();
if (device_uid.find("ring:") == 0) {
// NOTE: in case of a SIP call it's already ready to compare
device_uid = device_uid.substr(5); // after ring:
}
auto answerToCall = false;
auto downgradeToAudioOnly = currentCall->isAudioOnly() != incomCall.isAudioOnly();
if (downgradeToAudioOnly)
// Accept the incoming audio only
answerToCall = incomCall.isAudioOnly();
else
// Accept the incoming call from the higher id number
answerToCall = (device_uid.compare(incomCall.getPeerNumber()) < 0);
if (answerToCall) {
runOnMainThread([accountId = currentCall->getAccountId(),
currentCallID = currentCall->getCallId(),
incomCall = incomCall.shared_from_this()] {
auto& mgr = Manager::instance();
mgr.answerCall(*incomCall);
mgr.hangupCall(accountId, currentCallID);
});
}
}
}
}
AudioFormat
Manager::hardwareAudioFormatChanged(AudioFormat format)
{
return audioFormatUsed(format);
}
AudioFormat
Manager::audioFormatUsed(AudioFormat format)
{
AudioFormat currentFormat = pimpl_->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 format;
JAMI_DBG("Audio format changed: %s -> %s",
currentFormat.toString().c_str(),
format.toString().c_str());
pimpl_->ringbufferpool_->setInternalAudioFormat(format);
pimpl_->toneCtrl_.setSampleRate(format.sample_rate);
pimpl_->dtmfKey_.reset(new DTMF(format.sample_rate));
return format;
}
void
Manager::setAccountsOrder(const std::string& order)
{
JAMI_DBG("Set accounts order : %s", order.c_str());
// Set the new config
preferences.setAccountOrder(order);
saveConfig();
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
}
std::vector<std::string>
Manager::getAccountList() const
{
// Concatenate all account pointers in a single map
std::vector<std::string> v;
v.reserve(accountCount());
for (const auto& account : getAllAccounts()) {
v.emplace_back(account->getAccountID());
}
return v;
}
std::map<std::string, std::string>
Manager::getAccountDetails(const std::string& accountID) const
{
const auto account = getAccount(accountID);
if (account) {
return account->getAccountDetails();
} else {
JAMI_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>
Manager::getVolatileAccountDetails(const std::string& accountID) const
{
const auto account = getAccount(accountID);
if (account) {
return account->getVolatileAccountDetails();
} else {
JAMI_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
Manager::setAccountDetails(const std::string& accountID,
const std::map<std::string, std::string>& details)
{
JAMI_DBG("Set account details for %s", accountID.c_str());
const auto account = getAccount(accountID);
if (account == nullptr) {
JAMI_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
// FIXME: inefficient api, don't pass details (not as ref nor copy)
// let client requiests them we needed.
account->doUnregister([&](bool /* transport_free */) {
account->setAccountDetails(details);
// Serialize configuration to disk once it is done
if (auto ringAccount = std::dynamic_pointer_cast<JamiAccount>(account)) {
saveConfig(ringAccount);
} else {
saveConfig();
}
if (account->isUsable())
account->doRegister();
else
account->doUnregister();
// Update account details to the client side
emitSignal<DRing::ConfigurationSignal::AccountDetailsChanged>(accountID, details);
});
}
std::string
Manager::getNewAccountId()
{
std::string random_id;
do {
random_id = to_hex_string(std::uniform_int_distribution<uint64_t>()(rand_));
} while (getAccount(random_id));
return random_id;
}
std::string
Manager::addAccount(const std::map<std::string, std::string>& details, const std::string& accountId)
{
/** @todo Deal with both the accountMap_ and the Configuration */
auto newAccountID = accountId.empty() ? getNewAccountId() : accountId;
// 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;
JAMI_DBG("Adding account %s", newAccountID.c_str());
auto newAccount = accountFactory.createAccount(accountType, newAccountID);
if (!newAccount) {
JAMI_ERR("Unknown %s param when calling addAccount(): %s",
Conf::CONFIG_ACCOUNT_TYPE,
accountType);
return "";
}
newAccount->setAccountDetails(details);
saveConfig(newAccount);
newAccount->doRegister();
preferences.addAccount(newAccountID);
saveConfig();
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
return newAccountID;
}
void
Manager::removeAccount(const std::string& accountID, bool flush)
{
// Get it down and dying
if (const auto& remAccount = getAccount(accountID)) {
// Force stopping connection before doUnregister as it will
// wait for dht threads to finish
if (auto acc = std::dynamic_pointer_cast<JamiAccount>(remAccount)) {
acc->hangupCalls();
acc->shutdownConnections();
}
remAccount->doUnregister();
if (flush)
remAccount->flush();
accountFactory.removeAccount(*remAccount);
}
preferences.removeAccount(accountID);
saveConfig();
emitSignal<DRing::ConfigurationSignal::AccountsChanged>();
}
void
Manager::removeAccounts()
{
for (const auto& acc : getAccountList())
removeAccount(acc);
}
std::vector<std::string_view>
Manager::loadAccountOrder() const
{
return split_string(preferences.getAccountOrder(), '/');
}
int
Manager::loadAccountMap(const YAML::Node& node)
{
int errorCount = 0;
try {
// build preferences
preferences.unserialize(node);
voipPreferences.unserialize(node);
audioPreference.unserialize(node);
shortcutPreferences.unserialize(node);
#ifdef ENABLE_VIDEO
videoPreferences.unserialize(node);
#endif
#ifdef ENABLE_PLUGIN
pluginPreferences.unserialize(node);
if (pluginPreferences.getPluginsEnabled()) {
std::vector<std::string> loadedPlugins = pluginPreferences.getLoadedPlugins();
for (const std::string& plugin : loadedPlugins) {
jami::Manager::instance().getJamiPluginManager().loadPlugin(plugin);
}
}
#endif
} catch (const YAML::Exception& e) {
JAMI_ERR("%s: Preferences node unserialize error: ", 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) {
pimpl_->loadAccount(a, errorCount);
}
auto accountBaseDir = fileutils::get_data_dir();
auto dirs = fileutils::readDirectory(accountBaseDir);
std::condition_variable cv;
std::mutex lock;
size_t remaining {0};
std::unique_lock<std::mutex> l(lock);
for (const auto& dir : dirs) {
if (accountFactory.hasAccount<JamiAccount>(dir)) {
continue;
}
remaining++;
dht::ThreadPool::computation().run([this,
dir,
&cv,
&remaining,
&lock,
configFile = accountBaseDir + DIR_SEPARATOR_STR + dir
+ DIR_SEPARATOR_STR + "config.yml"] {
if (fileutils::isFile(configFile)) {
try {
if (auto a = accountFactory.createAccount(JamiAccount::ACCOUNT_TYPE, dir)) {
std::ifstream file = fileutils::ifstream(configFile);
YAML::Node parsedConfig = YAML::Load(file);
file.close();
a->unserialize(parsedConfig);
}
} catch (const std::exception& e) {
JAMI_ERR("Can't import account %s: %s", dir.c_str(), e.what());
}
}
std::lock_guard<std::mutex> l(lock);
remaining--;
cv.notify_one();
});
}
cv.wait(l, [&remaining] { return remaining == 0; });
return errorCount;
}
std::vector<std::string>
Manager::getCallList() const
{
std::vector<std::string> results;
for (const auto& call : callFactory.getAllCalls()) {
if (!call->isSubcall())
results.push_back(call->getCallId());
}
return results;
}
void
Manager::registerAccounts()
{
auto allAccounts(getAccountList());
for (auto& item : allAccounts) {
const auto a = getAccount(item);
if (!a)
continue;
a->loadConfig();
if (a->isUsable())
a->doRegister();
}
}
void
Manager::sendRegister(const std::string& accountID, bool enable)
{
const auto acc = getAccount(accountID);
if (!acc)
return;
acc->setEnabled(enable);
acc->loadConfig();
saveConfig(acc);
if (acc->isEnabled()) {
acc->doRegister();
} else
acc->doUnregister();
}
bool
Manager::isPasswordValid(const std::string& accountID, const std::string& password)
{
const auto acc = getAccount<JamiAccount>(accountID);
if (!acc)
return false;
return acc->isPasswordValid(password);
}
uint64_t
Manager::sendTextMessage(const std::string& accountID,
const std::string& to,
const std::map<std::string, std::string>& payloads,
const bool fromPlugin)
{
if (const auto acc = getAccount(accountID)) {
try {
#ifdef ENABLE_PLUGIN // modifies send message
auto& pluginChatManager = getJamiPluginManager().getChatServicesManager();
if (pluginChatManager.hasHandlers()) {
auto cm = std::make_shared<JamiMessage>(accountID, to, false, payloads, fromPlugin);
pluginChatManager.publishMessage(cm);
return acc->sendTextMessage(cm->peerId, cm->data);
} else
#endif // ENABLE_PLUGIN
return acc->sendTextMessage(to, payloads);
} catch (const std::exception& e) {
JAMI_ERR("Exception during text message sending: %s", e.what());
}
}
return 0;
}
int
statusFromImStatus(im::MessageStatus status)
{
switch (status) {
case im::MessageStatus::IDLE:
case im::MessageStatus::SENDING:
return static_cast<int>(DRing::Account::MessageStates::SENDING);
case im::MessageStatus::SENT:
return static_cast<int>(DRing::Account::MessageStates::SENT);
case im::MessageStatus::DISPLAYED:
return static_cast<int>(DRing::Account::MessageStates::DISPLAYED);
case im::MessageStatus::FAILURE:
return static_cast<int>(DRing::Account::MessageStates::FAILURE);
default:
return static_cast<int>(DRing::Account::MessageStates::UNKNOWN);
}
}
int
Manager::getMessageStatus(uint64_t id) const
{
const auto& allAccounts = accountFactory.getAllAccounts();
for (const auto& acc : allAccounts) {
auto status = acc->getMessageStatus(id);
if (status != im::MessageStatus::UNKNOWN)
return statusFromImStatus(status);
}
return static_cast<int>(DRing::Account::MessageStates::UNKNOWN);
}
int
Manager::getMessageStatus(const std::string& accountID, uint64_t id) const
{
if (const auto acc = getAccount(accountID))
return statusFromImStatus(acc->getMessageStatus(id));
return static_cast<int>(DRing::Account::MessageStates::UNKNOWN);
}
void
Manager::setAccountActive(const std::string& accountID, bool active)
{
const auto acc = getAccount(accountID);
if (!acc || acc->isActive() == active)
return;
acc->setActive(active);
if (acc->isEnabled()) {
if (active)
acc->doRegister();
else
acc->doUnregister();
}
emitSignal<DRing::ConfigurationSignal::VolatileDetailsChanged>(accountID,
acc->getVolatileAccountDetails());
}
std::shared_ptr<AudioLayer>
Manager::getAudioDriver()
{
return pimpl_->audiodriver_;
}
std::shared_ptr<Call>
Manager::newOutgoingCall(std::string_view toUrl,
const std::string& accountId,
const std::vector<DRing::MediaMap>& mediaList)
{
auto account = getAccount(accountId);
if (not account) {
JAMI_WARN("No account matches ID %s", accountId.c_str());
return {};
}
if (not account->isUsable()) {
JAMI_WARN("Account %s is not usable", accountId.c_str());
return {};
}
return account->newOutgoingCall(toUrl, mediaList);
}
#ifdef ENABLE_VIDEO
std::shared_ptr<video::SinkClient>
Manager::createSinkClient(const std::string& id, bool mixer)
{
const auto& iter = pimpl_->sinkMap_.find(id);
if (iter != std::end(pimpl_->sinkMap_)) {
if (auto sink = iter->second.lock())
return sink;
pimpl_->sinkMap_.erase(iter); // remove expired weak_ptr
}
auto sink = std::make_shared<video::SinkClient>(id, mixer);
pimpl_->sinkMap_.emplace(id, sink);
return sink;
}
void
Manager::createSinkClients(const std::string& callId,
const ConfInfo& infos,
const std::shared_ptr<video::VideoGenerator>& videoStream,
std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap)
{
std::lock_guard<std::mutex> lk(pimpl_->sinksMutex_);
std::set<std::string> sinkIdsList {};
// create video sinks
for (const auto& participant : infos) {
std::string sinkId = participant.sinkId;
if (sinkId.empty()) {
sinkId = callId;
sinkId += string_remove_suffix(participant.uri, '@') + participant.device;
}
if (participant.w && participant.h) {
auto currentSink = getSinkClient(sinkId);
if (currentSink) {
currentSink->setCrop(participant.x, participant.y, participant.w, participant.h);
currentSink->setFrameSize(participant.w, participant.h);
sinkIdsList.emplace(sinkId);
continue;
}
auto newSink = createSinkClient(sinkId);
newSink->start();
newSink->setCrop(participant.x, participant.y, participant.w, participant.h);
newSink->setFrameSize(participant.w, participant.h);
videoStream->attach(newSink.get());
sinksMap.emplace(sinkId, newSink);
sinkIdsList.emplace(sinkId);
} else {
sinkIdsList.erase(sinkId);
}
}
// remove any non used video sink
for (auto it = sinksMap.begin(); it != sinksMap.end();) {
if (sinkIdsList.find(it->first) == sinkIdsList.end()) {
videoStream->detach(it->second.get());
it->second->stop();
it = sinksMap.erase(it);
} else {
it++;
}
}
}
std::shared_ptr<video::SinkClient>
Manager::getSinkClient(const std::string& id)
{
const auto& iter = pimpl_->sinkMap_.find(id);
if (iter != std::end(pimpl_->sinkMap_))
if (auto sink = iter->second.lock())
return sink;
return nullptr;
}
#endif // ENABLE_VIDEO
RingBufferPool&
Manager::getRingBufferPool()
{
return *pimpl_->ringbufferpool_;
}
bool
Manager::hasAccount(const std::string& accountID)
{
return accountFactory.hasAccount(accountID);
}
IceTransportFactory&
Manager::getIceTransportFactory()
{
return *pimpl_->ice_tf_;
}
#ifdef ENABLE_VIDEO
VideoManager&
Manager::getVideoManager() const
{
return *pimpl_->videoManager_;
}
#endif
std::vector<DRing::Message>
Manager::getLastMessages(const std::string& accountID, const uint64_t& base_timestamp)
{
if (const auto acc = getAccount(accountID))
return acc->getLastMessages(base_timestamp);
return {};
}
SIPVoIPLink&
Manager::sipVoIPLink() const
{
return *pimpl_->sipLink_;
}
#ifdef ENABLE_PLUGIN
JamiPluginManager&
Manager::getJamiPluginManager() const
{
return pimpl_->jami_plugin_manager;
}
#endif
std::optional<std::weak_ptr<ChannelSocket>>
Manager::gitSocket(const std::string& accountId,
const std::string& deviceId,
const std::string& conversationId)
{
if (const auto acc = getAccount<JamiAccount>(accountId))
return acc->gitSocket(DeviceId(deviceId), conversationId);
return std::nullopt;
}
std::map<std::string, std::string>
Manager::getNearbyPeers(const std::string& accountID)
{
if (const auto acc = getAccount<JamiAccount>(accountID))
return acc->getNearbyPeers();
return {};
}
void
Manager::setDefaultModerator(const std::string& accountID, const std::string& peerURI, bool state)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to change default moderator, account %s not found", accountID.c_str());
return;
}
if (state)
acc->addDefaultModerator(peerURI);
else
acc->removeDefaultModerator(peerURI);
saveConfig(acc);
}
std::vector<std::string>
Manager::getDefaultModerators(const std::string& accountID)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to get default moderators, account %s not found", accountID.c_str());
return {};
}
auto set = acc->getDefaultModerators();
return std::vector<std::string>(set.begin(), set.end());
}
void
Manager::enableLocalModerators(const std::string& accountID, bool isModEnabled)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to set local moderators, account %s not found", accountID.c_str());
return;
}
acc->enableLocalModerators(isModEnabled);
saveConfig(acc);
}
bool
Manager::isLocalModeratorsEnabled(const std::string& accountID)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to get local moderators, account %s not found", accountID.c_str());
return true; // Default value
}
return acc->isLocalModeratorsEnabled();
}
void
Manager::setAllModerators(const std::string& accountID, bool allModerators)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to set all moderators, account %s not found", accountID.c_str());
return;
}
acc->setAllModerators(allModerators);
saveConfig(acc);
}
bool
Manager::isAllModerators(const std::string& accountID)
{
auto acc = getAccount(accountID);
if (!acc) {
JAMI_ERR("Fail to get all moderators, account %s not found", accountID.c_str());
return true; // Default value
}
return acc->isAllModerators();
}
void
Manager::insertGitTransport(git_smart_subtransport* tr, std::unique_ptr<P2PSubTransport>&& sub)
{
std::lock_guard<std::mutex> lk(pimpl_->gitTransportsMtx_);
pimpl_->gitTransports_[tr] = std::move(sub);
}
void
Manager::eraseGitTransport(git_smart_subtransport* tr)
{
std::lock_guard<std::mutex> lk(pimpl_->gitTransportsMtx_);
pimpl_->gitTransports_.erase(tr);
}
} // namespace jami