/* * Copyright (C) 2004-2022 Savoir-faire Linux Inc. * * Author: Alexandre Bourget * Author: Yan Morin * Author: Laurielle Lea * Author: Emmanuel Milou * Author: Alexandre Savard * Author: Guillaume Carmel-Archambault * Author: Tristan Matthews * Author: Guillaume Roguez * Author: Adrien Béraud * Author: Philippe Gorley * Author: Aline Gondim Santos * * 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 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 #include #include #include #include #ifndef WIN32 #include #include #endif #ifdef TARGET_OS_IOS #include #endif #include #include #include #include #include #include #include #include #include #include #include #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; 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& messages, const std::string& from) const noexcept; void bindCallToConference(Call& call, Conference& conf); void addMainParticipant(Conference& conf); bool hangupConference(Conference& conf); template std::shared_ptr findAccount(const std::function&)>&); void initAudioDriver(); void processIncomingCall(const std::string& accountId, Call& incomCall); static void stripSipPrefix(Call& incomCall); Manager& base_; // pimpl back-pointer std::shared_ptr ioContext_; std::thread ioContextRunner_; /** Main scheduler */ ScheduledExecutor scheduler_ {"manager"}; std::atomic_bool autoAnswer_ {false}; /** Application wide tone controller */ ToneControl toneCtrl_; std::unique_ptr 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 audiodriver_ {nullptr}; std::array audioStreamUsers_ {}; // Main thread std::unique_ptr 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_; std::atomic_bool finished_ {false}; /* ICE support */ std::unique_ptr ice_tf_; /* Sink ID mapping */ std::map> sinkMap_; std::unique_ptr videoManager_; std::unique_ptr sipLink_; #ifdef ENABLE_PLUGIN /* Jami Plugin Manager */ JamiPluginManager jami_plugin_manager; #endif std::mutex gitTransportsMtx_ {}; std::map> gitTransports_ {}; }; Manager::ManagerPimpl::ManagerPimpl(Manager& base) : base_(base) , ioContext_(std::make_shared()) , 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 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 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 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 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& 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()) , 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(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(); 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 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()) { 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 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(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 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(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& mediaList) { JAMI_DBG() << "try outgoing call to '" << to << "'" << " with account '" << account_id << "'"; std::shared_ptr 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& 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& 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(call.getCallId(), call.getPath()); emitSignal(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 = 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(); } void Manager::transferSucceeded() { emitSignal(); } // 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(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(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(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 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(account); account->attach(conf); emitSignal(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(account->getAccountID(), conf->getConfId(), conf->getStateStr()); return true; } void Manager::createConfFromParticipantList(const std::string& accountId, const std::vector& 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(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(accountId, conf->getConfId()); } } bool Manager::detachLocalParticipant(const std::shared_ptr& conf) { if (not conf) return false; JAMI_INFO("Detach local participant from conference %s", conf->getConfId().c_str()); conf->detachLocalParticipant(); emitSignal(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(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> 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 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 Manager::ioContext() const { return pimpl_->ioContext_; } void Manager::addTask(std::function&& task, const char* filename, uint32_t linum) { pimpl_->scheduler_.scheduleAtFixedRate(std::move(task), std::chrono::milliseconds(30), filename, linum); } std::shared_ptr Manager::scheduleTask(std::function&& 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 Manager::scheduleTaskIn(std::function&& 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& acc) { if (auto ringAcc = std::dynamic_pointer_cast(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(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 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 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 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 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(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& 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(accountId, conf->getConfId(), from, messages); } else { JAMI_ERR("no conference associated to ID %s", callId.c_str()); } } else { emitSignal(accountId, callId, from, messages); } } } void Manager::sendCallTextMessage(const std::string& accountId, const std::string& callID, const std::map& 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 lock(pimpl_->audioLayerMutex_); getRingBufferPool().flushAllBuffers(); pimpl_->audiodriver_->flushUrgent(); } if (audioPreference.getIsAlwaysRecording()) { auto result = call.toggleRecording(); emitSignal(callId, call.getPath()); emitSignal(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 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 Manager::getTelephoneTone() { return pimpl_->toneCtrl_.getTelephoneTone(); } std::shared_ptr Manager::getTelephoneFile() { return pimpl_->toneCtrl_.getTelephoneFile(); } /** * Set input audio plugin */ void Manager::setAudioPlugin(const std::string& audioPlugin) { { std::lock_guard 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 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 Manager::getAudioOutputDeviceList() { std::lock_guard 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 Manager::getAudioInputDeviceList() { std::lock_guard 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 Manager::getCurrentAudioDevicesIndex() { std::lock_guard 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 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 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(id, rec->getPath()); emitSignal(id, result); } return result; } bool Manager::startRecordedFilePlayback(const std::string& filepath) { JAMI_DBG("Start recorded file playback %s", filepath.c_str()); { std::lock_guard 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 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 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 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 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 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(account, false); account->attach(conf); emitSignal(account->getAccountID(), conf->getConfId()); // Bind calls according to their state bindCallToConference(*incomCall, *conf); conf->detachLocalParticipant(); emitSignal(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(); } std::vector Manager::getAccountList() const { // Concatenate all account pointers in a single map std::vector v; v.reserve(accountCount()); for (const auto& account : getAllAccounts()) { v.emplace_back(account->getAccountID()); } return v; } std::map 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::map 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& 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(account)) { saveConfig(ringAccount); } else { saveConfig(); } if (account->isUsable()) account->doRegister(); else account->doUnregister(); // Update account details to the client side emitSignal(accountID, details); }); } std::string Manager::getNewAccountId() { std::string random_id; do { random_id = to_hex_string(std::uniform_int_distribution()(rand_)); } while (getAccount(random_id)); return random_id; } std::string Manager::addAccount(const std::map& 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(); 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(remAccount)) { acc->hangupCalls(); acc->shutdownConnections(); } remAccount->doUnregister(); if (flush) remAccount->flush(); accountFactory.removeAccount(*remAccount); } preferences.removeAccount(accountID); saveConfig(); emitSignal(); } void Manager::removeAccounts() { for (const auto& acc : getAccountList()) removeAccount(acc); } std::vector 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 l(lock); for (const auto& dir : dirs) { if (accountFactory.hasAccount(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 l(lock); remaining--; cv.notify_one(); }); } cv.wait(l, [&remaining] { return remaining == 0; }); #ifdef ENABLE_PLUGIN if (pluginPreferences.getPluginsEnabled()) { std::vector loadedPlugins = pluginPreferences.getLoadedPlugins(); for (const std::string& plugin : loadedPlugins) { jami::Manager::instance().getJamiPluginManager().loadPlugin(plugin); } } #endif return errorCount; } std::vector Manager::getCallList() const { std::vector 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(accountID); if (!acc) return false; return acc->isPasswordValid(password); } uint64_t Manager::sendTextMessage(const std::string& accountID, const std::string& to, const std::map& 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(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(DRing::Account::MessageStates::SENDING); case im::MessageStatus::SENT: return static_cast(DRing::Account::MessageStates::SENT); case im::MessageStatus::DISPLAYED: return static_cast(DRing::Account::MessageStates::DISPLAYED); case im::MessageStatus::FAILURE: return static_cast(DRing::Account::MessageStates::FAILURE); default: return static_cast(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(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(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(acc)) { jamiAcc->shutdownConnections(); } } } } emitSignal(accountID, acc->getVolatileAccountDetails()); } std::shared_ptr Manager::getAudioDriver() { return pimpl_->audiodriver_; } std::shared_ptr Manager::newOutgoingCall(std::string_view toUrl, const std::string& accountId, const std::vector& 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 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(id, mixer); pimpl_->sinkMap_.emplace(id, sink); return sink; } void Manager::createSinkClients( const std::string& callId, const ConfInfo& infos, const std::vector>& videoStreams, std::map>& sinksMap) { std::lock_guard lk(pimpl_->sinksMutex_); std::set 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 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 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> Manager::gitSocket(const std::string& accountId, const std::string& deviceId, const std::string& conversationId) { if (const auto acc = getAccount(accountId)) return acc->gitSocket(DeviceId(deviceId), conversationId); return std::nullopt; } std::map Manager::getNearbyPeers(const std::string& accountID) { if (const auto acc = getAccount(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 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(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&& sub) { std::lock_guard lk(pimpl_->gitTransportsMtx_); pimpl_->gitTransports_[tr] = std::move(sub); } void Manager::eraseGitTransport(git_smart_subtransport* tr) { std::lock_guard lk(pimpl_->gitTransportsMtx_); pimpl_->gitTransports_.erase(tr); } } // namespace jami