mirror of
https://git.jami.net/savoirfairelinux/jami-daemon.git
synced 2025-08-07 22:02:12 +08:00
3275 lines
94 KiB
C++
3275 lines
94 KiB
C++
/*
|
|
* Copyright (C) 2004-2022 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
|
|
|
|
#include "client/videomanager.h"
|
|
|
|
#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"
|
|
#ifdef ENABLE_VIDEO
|
|
#include "video/video_scaler.h"
|
|
#include "video/sinkclient.h"
|
|
#include "video/video_base.h"
|
|
#include "media/video/video_mixer.h"
|
|
#endif
|
|
#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>
|
|
|
|
#ifndef JAMI_DATADIR
|
|
#error "Define the JAMI_DATADIR macro as the data installation prefix of the package"
|
|
#endif
|
|
|
|
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};
|
|
|
|
#if TARGET_OS_IOS
|
|
bool Manager::isIOSExtension = {false};
|
|
#endif
|
|
|
|
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_ {"manager"};
|
|
|
|
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_;
|
|
|
|
std::unique_ptr<VideoManager> videoManager_;
|
|
|
|
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()
|
|
#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)
|
|
{
|
|
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 {
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
JAMI_DBG("[conf:%s] un-holding conference", confId.c_str());
|
|
|
|
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, {});
|
|
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);
|
|
auto account2 = getAccount(account2Id);
|
|
if (not account) {
|
|
JAMI_ERR("Can't find account: %s", accountId.c_str());
|
|
return false;
|
|
}
|
|
if (not account2) {
|
|
JAMI_ERR("Can't find account: %s", account2Id.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 = account2->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, const char* filename, uint32_t linum)
|
|
{
|
|
pimpl_->scheduler_.scheduleAtFixedRate(std::move(task),
|
|
std::chrono::milliseconds(30),
|
|
filename,
|
|
linum);
|
|
}
|
|
|
|
std::shared_ptr<Task>
|
|
Manager::scheduleTask(std::function<void()>&& task,
|
|
std::chrono::steady_clock::time_point when,
|
|
const char* filename,
|
|
uint32_t linum)
|
|
{
|
|
return pimpl_->scheduler_.schedule(std::move(task), when, filename, linum);
|
|
}
|
|
|
|
std::shared_ptr<Task>
|
|
Manager::scheduleTaskIn(std::function<void()>&& task,
|
|
std::chrono::steady_clock::duration timeout,
|
|
const char* filename,
|
|
uint32_t linum)
|
|
{
|
|
return pimpl_->scheduler_.scheduleIn(std::move(task), timeout, filename, linum);
|
|
}
|
|
|
|
// 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
|
|
|
|
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_DEBUG("Incoming call {:s} on account {:s} with {:d} media",
|
|
call.getCallId(),
|
|
accountId,
|
|
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 ringtone = account->getRingtonePath();
|
|
if (ringtone.find(DIR_SEPARATOR_CH) == std::string::npos) {
|
|
// A base file name was provided (such as the default); try to
|
|
// resolve it from Jami's data installation prefix.
|
|
ringtone = std::string(JAMI_DATADIR) + DIR_SEPARATOR_STR + RINGDIR + DIR_SEPARATOR_STR
|
|
+ ringtone;
|
|
}
|
|
|
|
{
|
|
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(ringtone))
|
|
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, false);
|
|
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);
|
|
#ifdef ENABLE_VIDEO
|
|
videoPreferences.unserialize(node);
|
|
#endif
|
|
#ifdef ENABLE_PLUGIN
|
|
pluginPreferences.unserialize(node);
|
|
#endif
|
|
} catch (const YAML::Exception& e) {
|
|
JAMI_ERR("Preferences node unserialize YAML exception: %s", e.what());
|
|
++errorCount;
|
|
} catch (const std::exception& e) {
|
|
JAMI_ERR("Preferences node unserialize standard exception: %s", e.what());
|
|
++errorCount;
|
|
} catch (...) {
|
|
JAMI_ERR("Preferences node unserialize unknown exception");
|
|
++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; });
|
|
|
|
#ifdef ENABLE_PLUGIN
|
|
if (pluginPreferences.getPluginsEnabled()) {
|
|
std::vector<std::string> loadedPlugins = pluginPreferences.getLoadedPlugins();
|
|
for (const std::string& plugin : loadedPlugins) {
|
|
jami::Manager::instance().getJamiPluginManager().loadPlugin(plugin);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
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, bool shutdownConnections)
|
|
{
|
|
const auto acc = getAccount(accountID);
|
|
if (!acc || acc->isActive() == active)
|
|
return;
|
|
acc->setActive(active);
|
|
if (acc->isEnabled()) {
|
|
if (active) {
|
|
acc->doRegister();
|
|
} else {
|
|
acc->doUnregister();
|
|
if (shutdownConnections) {
|
|
if (auto jamiAcc = std::dynamic_pointer_cast<JamiAccount>(acc)) {
|
|
jamiAcc->shutdownConnections();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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::vector<std::shared_ptr<video::VideoFrameActiveWriter>>& videoStreams,
|
|
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 && !participant.videoMuted) {
|
|
auto currentSink = getSinkClient(sinkId);
|
|
if (currentSink) {
|
|
currentSink->setCrop(participant.x, participant.y, 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);
|
|
|
|
for (auto& videoStream : videoStreams)
|
|
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()) {
|
|
for (auto& videoStream : videoStreams)
|
|
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_;
|
|
}
|
|
|
|
VideoManager&
|
|
Manager::getVideoManager() const
|
|
{
|
|
return *pimpl_->videoManager_;
|
|
}
|
|
|
|
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
|