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