Skip to content
Snippets Groups Projects
Select Git revision
  • 00f59085c597c70998c957359b3a91fd0b62513f
  • master default protected
  • release/beta-qt-202301101210
  • stable
  • release/beta-qt-202211182015
  • release/beta-qt-202211181752
  • release/beta-qt-202211171508
  • release/beta-qt-202211081754
  • release/beta-qt-202211071518
  • release/beta-qt-202210270957
  • release/beta-qt-202210071648
  • release/beta-qt-202209291549
  • release/beta-qt-202209011129
  • release/beta-qt-202208261640
  • release/beta-qt-202208241511
  • release/beta-qt-202208231849
  • release/beta-qt-202208091525
  • release/beta-qt-202207191241
  • release/beta-qt-202207181708
  • release/beta-qt-202207131914
  • release/beta-qt-202207131513
  • android/release_358
  • android/release_357
  • android/release_356
  • android/release_355
  • android/release_354
  • 20221220.0956.79e1207
  • android/release_353
  • android/release_352
  • android/release_350
  • android/release_349
  • android/release_348
  • android/release_347
  • 20221031.1308.130cc26
  • android/release_346
  • android/release_345
  • android/release_344
  • android/release_343
  • android/release_342
  • android/release_341
  • android/release_340
41 results

build-package-debian.sh

Blame
    • Hugo Lefeuvre's avatar
      cd58aca8
      packaging: different releases for website · cd58aca8
      Hugo Lefeuvre authored
      It has been decided that we would have different releases between
      website and apt repository. The version from the website would have
      debian revision -0 and the one from apt repositories -1.
      
      The main difference resides in the fact that the version from the
      website distributes a postinst script which installs key and apt
      source.list so that Ring gets automatically updated when installed
      via one click install. This is not the case with the one from apt
      repos.
      
      Change-Id: I7bbfdfa41b0133ebe93f16bc550aa437c8ddfa6a
      cd58aca8
      History
      packaging: different releases for website
      Hugo Lefeuvre authored
      It has been decided that we would have different releases between
      website and apt repository. The version from the website would have
      debian revision -0 and the one from apt repositories -1.
      
      The main difference resides in the fact that the version from the
      website distributes a postinst script which installs key and apt
      source.list so that Ring gets automatically updated when installed
      via one click install. This is not the case with the one from apt
      repos.
      
      Change-Id: I7bbfdfa41b0133ebe93f16bc550aa437c8ddfa6a
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    sipcall.cpp 40.94 KiB
    /*
     *  Copyright (C) 2004-2018 Savoir-faire Linux Inc.
     *
     *  Author: Emmanuel Milou <emmanuel.milou@savoirfairelinux.com>
     *  Author: Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
     *  Author: Yan Morin <yan.morin@savoirfairelinux.com>
     *  Author: Laurielle Lea <laurielle.lea@savoirfairelinux.com>
     *  Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
     *
     *  This program is free software; you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation; either version 3 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program; if not, write to the Free Software
     *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
     */
    
    #include "call_factory.h"
    #include "sipcall.h"
    #include "sipaccount.h" // for SIPAccount::ACCOUNT_TYPE
    #include "sipaccountbase.h"
    #include "sipvoiplink.h"
    #include "sdes_negotiator.h"
    #include "logger.h" // for _debug
    #include "sdp.h"
    #include "manager.h"
    #include "string_utils.h"
    #include "upnp/upnp_control.h"
    #include "sip_utils.h"
    #include "audio/audio_rtp_session.h"
    #include "system_codec_container.h"
    #include "im/instant_messaging.h"
    #include "dring/call_const.h"
    #include "dring/media_const.h"
    #include "client/ring_signal.h"
    #include "ice_transport.h"
    
    #ifdef RING_VIDEO
    #include "client/videomanager.h"
    #include "video/video_rtp_session.h"
    #include "dring/videomanager_interface.h"
    #include <chrono>
    #endif
    
    #include "errno.h"
    
    #include <opendht/crypto.h>
    
    namespace ring {
    
    using sip_utils::CONST_PJ_STR;
    
    #ifdef RING_VIDEO
    static DeviceParams
    getVideoSettings()
    {
        const auto& videomon = ring::getVideoDeviceMonitor();
        return videomon.getDeviceParams(videomon.getDefaultDevice());
    }
    #endif
    
    static constexpr int DEFAULT_ICE_INIT_TIMEOUT {35}; // seconds
    static constexpr int DEFAULT_ICE_NEGO_TIMEOUT {60}; // seconds
    
    // SDP media Ids
    static constexpr int SDP_AUDIO_MEDIA_ID {0};
    static constexpr int SDP_VIDEO_MEDIA_ID {1};
    
    // ICE components Id used on SIP
    static constexpr int ICE_AUDIO_RTP_COMPID {0};
    static constexpr int ICE_AUDIO_RTCP_COMPID {1};
    static constexpr int ICE_VIDEO_RTP_COMPID {2};
    static constexpr int ICE_VIDEO_RTCP_COMPID {3};
    
    const char* const SIPCall::LINK_TYPE = SIPAccount::ACCOUNT_TYPE;
    
    SIPCall::SIPCall(SIPAccountBase& account, const std::string& id, Call::CallType type,
                     const std::map<std::string, std::string>& details)
        : Call(account, id, type, details)
        , avformatrtp_(new AudioRtpSession(id))
    #ifdef RING_VIDEO
        // The ID is used to associate video streams to calls
        , videortp_(new video::VideoRtpSession(id, getVideoSettings()))
        , mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
    #endif
        , sdp_(new Sdp(id))
    {
        if (account.getUPnPActive())
            upnp_.reset(new upnp::Controller());
    
        setCallMediaLocal();
    }
    
    SIPCall::~SIPCall()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        setTransport({});
        inv.reset(); // prevents callback usage
    }
    
    SIPAccountBase&
    SIPCall::getSIPAccount() const
    {
        return static_cast<SIPAccountBase&>(getAccount());
    }
    
    void
    SIPCall::setCallMediaLocal()
    {
        if (localAudioPort_ == 0
    #ifdef RING_VIDEO
            || localVideoPort_ == 0
    #endif
            )
            generateMediaPorts();
    }
    
    void
    SIPCall::generateMediaPorts()
    {
        auto& account = getSIPAccount();
    
        // Reference: http://www.cs.columbia.edu/~hgs/rtp/faq.html#ports
        // We only want to set ports to new values if they haven't been set
        const unsigned callLocalAudioPort = account.generateAudioPort();
        if (localAudioPort_ != 0)
            account.releasePort(localAudioPort_);
        localAudioPort_ = callLocalAudioPort;
        sdp_->setLocalPublishedAudioPort(callLocalAudioPort);
    
    #ifdef RING_VIDEO
        // https://projects.savoirfairelinux.com/issues/17498
        const unsigned int callLocalVideoPort = account.generateVideoPort();
        if (localVideoPort_ != 0)
            account.releasePort(localVideoPort_);
        // this should already be guaranteed by SIPAccount
        assert(localAudioPort_ != callLocalVideoPort);
        localVideoPort_ = callLocalVideoPort;
        sdp_->setLocalPublishedVideoPort(callLocalVideoPort);
    #endif
    }
    
    void SIPCall::setContactHeader(pj_str_t *contact)
    {
        pj_strcpy(&contactHeader_, contact);
    }
    
    void
    SIPCall::setTransport(const std::shared_ptr<SipTransport>& t)
    {
        if (isSecure() and t and not t->isSecure()) {
            RING_ERR("Can't set unsecure transport to secure call.");
            return;
        }
    
        const auto list_id = reinterpret_cast<uintptr_t>(this);
        if (transport_)
            transport_->removeStateListener(list_id);
        transport_ = t;
    
        if (transport_) {
            setSecure(transport_->isSecure());
            std::weak_ptr<SIPCall> wthis_ = std::static_pointer_cast<SIPCall>(shared_from_this());
    
            // listen for transport destruction
            transport_->addStateListener(list_id,
                [wthis_] (pjsip_transport_state state, const pjsip_transport_state_info*) {
                    if (auto this_ = wthis_.lock()) {
                        // end the call if the SIP transport is shut down
                        if (not SipTransport::isAlive(this_->transport_, state) and this_->getConnectionState() != ConnectionState::DISCONNECTED) {
                            RING_WARN("[call:%s] Ending call because underlying SIP transport was closed",
                                      this_->getCallId().c_str());
                            this_->onFailure(ECONNRESET);
                        }
                    }
                });
        }
    }
    
    /**
     * Send a reINVITE inside an active dialog to modify its state
     * Local SDP session should be modified before calling this method
     */
    
    int
    SIPCall::SIPSessionReinvite()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        // Do nothing if no invitation processed yet
        if (not inv or inv->invite_tsx)
            return PJ_SUCCESS;
    
        RING_DBG("[call:%s] Processing reINVITE (state=%s)", getCallId().c_str(),
                 pjsip_inv_state_name(inv->state));
    
        // Generate new ports to receive the new media stream
        // LibAV doesn't discriminate SSRCs and will be confused about Seq changes on a given port
        generateMediaPorts();
        sdp_->clearIce();
        auto& acc = getSIPAccount();
        if (not sdp_->createOffer(acc.getActiveAccountCodecInfoList(MEDIA_AUDIO),
                                  acc.getActiveAccountCodecInfoList(acc.isVideoEnabled() and not isAudioOnly() ? MEDIA_VIDEO
                                                                                                               : MEDIA_NONE),
                                  acc.getSrtpKeyExchange(),
                                  getState() == CallState::HOLD))
            return !PJ_SUCCESS;
    
        if (initIceMediaTransport(true))
            setupLocalSDPFromIce();
    
        pjsip_tx_data* tdata;
        auto local_sdp = sdp_->getLocalSdpSession();
        auto result = pjsip_inv_reinvite(inv.get(), nullptr, local_sdp, &tdata);
        if (result == PJ_SUCCESS) {
            if (!tdata)
                return PJ_SUCCESS;
            result = pjsip_inv_send_msg(inv.get(), tdata);
            if (result == PJ_SUCCESS)
                return PJ_SUCCESS;
            RING_ERR("[call:%s] Failed to send REINVITE msg (pjsip: %s)", getCallId().c_str(),
                     sip_utils::sip_strerror(result).c_str());
            // Canceling internals without sending (anyways the send has just failed!)
            pjsip_inv_cancel_reinvite(inv.get(), &tdata);
        } else
            RING_ERR("[call:%s] Failed to create REINVITE msg (pjsip: %s)", getCallId().c_str(),
                     sip_utils::sip_strerror(result).c_str());
    
        return !PJ_SUCCESS;
    }
    
    void
    SIPCall::sendSIPInfo(const char *const body, const char *const subtype)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (not inv or not inv->dlg)
            throw VoipLinkException("Couldn't get invite dialog");
    
        pj_str_t methodName = CONST_PJ_STR("INFO");
        const pj_str_t type = CONST_PJ_STR("application");
    
        pjsip_method method;
        pjsip_method_init_np(&method, &methodName);
    
        /* Create request message. */
        pjsip_tx_data *tdata;
    
        if (pjsip_dlg_create_request(inv->dlg, &method, -1, &tdata) != PJ_SUCCESS) {
            RING_ERR("[call:%s] Could not create dialog", getCallId().c_str());
            return;
        }
    
        /* Create "application/<subtype>" message body. */
        pj_str_t content;
        pj_cstr(&content, body);
        pj_str_t pj_subtype;
        pj_cstr(&pj_subtype, subtype);
        tdata->msg->body = pjsip_msg_body_create(tdata->pool, &type, &pj_subtype, &content);
        if (tdata->msg->body == NULL)
            pjsip_tx_data_dec_ref(tdata);
        else
            pjsip_dlg_send_request(inv->dlg, tdata, getSIPVoIPLink()->getModId(), NULL);
    }
    
    void
    SIPCall::updateSDPFromSTUN()
    {
        RING_WARN("[call:%s] SIPCall::updateSDPFromSTUN() not implemented", getCallId().c_str());
    }
    
    void
    SIPCall::terminateSipSession(int status)
    {
        RING_DBG("[call:%s] Terminate SIP session", getCallId().c_str());
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (inv and inv->state != PJSIP_INV_STATE_DISCONNECTED) {
            pjsip_tx_data* tdata = nullptr;
            auto ret = pjsip_inv_end_session(inv.get(), status, nullptr, &tdata);
            if (ret == PJ_SUCCESS) {
                if (tdata) {
                    auto contact = getSIPAccount().getContactHeader(transport_ ? transport_->get() : nullptr);
                    sip_utils::addContactHeader(&contact, tdata);
                    ret = pjsip_inv_send_msg(inv.get(), tdata);
                    if (ret != PJ_SUCCESS)
                        RING_ERR("[call:%s] failed to send terminate msg, SIP error (%s)",
                                 getCallId().c_str(), sip_utils::sip_strerror(ret).c_str());
                }
            } else
                RING_ERR("[call:%s] failed to terminate INVITE@%p, SIP error (%s)",
                         getCallId().c_str(), inv.get(), sip_utils::sip_strerror(ret).c_str());
        }
    
        inv.reset();
    }
    
    void
    SIPCall::answer()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        auto& account = getSIPAccount();
    
        if (not inv)
            throw VoipLinkException("[call:" + getCallId() + "] answer: no invite session for this call");
    
        if (!inv->neg) {
            RING_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
                      getCallId().c_str());
            pjmedia_sdp_session *dummy = 0;
            getSIPVoIPLink()->createSDPOffer(inv.get(), &dummy);
    
            if (account.isStunEnabled())
                updateSDPFromSTUN();
        }
    
        pj_str_t contact(account.getContactHeader(transport_ ? transport_->get() : nullptr));
        setContactHeader(&contact);
    
        pjsip_tx_data *tdata;
        if (!inv->last_answer)
            throw std::runtime_error("Should only be called for initial answer");
    
        // answer with SDP if no SDP was given in initial invite (i.e. inv->neg is NULL)
        if (pjsip_inv_answer(inv.get(), PJSIP_SC_OK, NULL, !inv->neg ? sdp_->getLocalSdpSession() : NULL, &tdata) != PJ_SUCCESS)
            throw std::runtime_error("Could not init invite request answer (200 OK)");
    
        // contactStr must stay in scope as long as tdata
        if (contactHeader_.slen) {
            RING_DBG("[call:%s] Answering with contact header: %.*s",
                     getCallId().c_str(), (int)contactHeader_.slen, contactHeader_.ptr);
            sip_utils::addContactHeader(&contactHeader_, tdata);
        }
    
        if (pjsip_inv_send_msg(inv.get(), tdata) != PJ_SUCCESS) {
            inv.reset();
            throw std::runtime_error("Could not send invite request answer (200 OK)");
        }
    
        setState(CallState::ACTIVE, ConnectionState::CONNECTED);
    }
    
    void
    SIPCall::hangup(int reason)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (inv and inv->dlg) {
            pjsip_route_hdr *route = inv->dlg->route_set.next;
            while (route and route != &inv->dlg->route_set) {
                char buf[1024];
                int printed = pjsip_hdr_print_on(route, buf, sizeof(buf));
                if (printed >= 0) {
                    buf[printed] = '\0';
                    RING_DBG("[call:%s] Route header %s", getCallId().c_str(), buf);
                }
                route = route->next;
            }
            const int status = reason ? reason :
                               inv->state <= PJSIP_INV_STATE_EARLY and inv->role != PJSIP_ROLE_UAC ?
                               PJSIP_SC_CALL_TSX_DOES_NOT_EXIST :
                               inv->state >= PJSIP_INV_STATE_DISCONNECTED ? PJSIP_SC_DECLINE : 0;
            // Notify the peer
            terminateSipSession(status);
        }
        // Stop all RTP streams
        stopAllMedia();
        setState(Call::ConnectionState::DISCONNECTED, reason);
        removeCall();
    }
    
    void
    SIPCall::refuse()
    {
        if (!isIncoming() or getConnectionState() == ConnectionState::CONNECTED or !inv)
            return;
    
        stopAllMedia();
    
        // Notify the peer
        terminateSipSession(PJSIP_SC_DECLINE);
    
        setState(Call::ConnectionState::DISCONNECTED, ECONNABORTED);
        removeCall();
    }
    
    static void
    transfer_client_cb(pjsip_evsub *sub, pjsip_event *event)
    {
        auto link = getSIPVoIPLink();
        if (not link) {
            RING_ERR("no more VoIP link");
            return;
        }
    
        auto mod_ua_id = link->getModId();
    
        switch (pjsip_evsub_get_state(sub)) {
            case PJSIP_EVSUB_STATE_ACCEPTED:
                if (!event)
                    return;
    
                pj_assert(event->type == PJSIP_EVENT_TSX_STATE && event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
                break;
    
            case PJSIP_EVSUB_STATE_TERMINATED:
                pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
                break;
    
            case PJSIP_EVSUB_STATE_ACTIVE: {
                if (!event)
                    return;
    
                pjsip_rx_data* r_data = event->body.rx_msg.rdata;
    
                if (!r_data)
                    return;
    
                std::string request(pjsip_rx_data_get_info(r_data));
    
                pjsip_status_line status_line = { 500, *pjsip_get_status_text(500) };
    
                if (!r_data->msg_info.msg)
                    return;
    
                if (r_data->msg_info.msg->line.req.method.id == PJSIP_OTHER_METHOD and
                        request.find("NOTIFY") != std::string::npos) {
                    pjsip_msg_body *body = r_data->msg_info.msg->body;
    
                    if (!body)
                        return;
    
                    if (pj_stricmp2(&body->content_type.type, "message") or
                            pj_stricmp2(&body->content_type.subtype, "sipfrag"))
                        return;
    
                    if (pjsip_parse_status_line((char*) body->data, body->len, &status_line) != PJ_SUCCESS)
                        return;
                }
    
                if (!r_data->msg_info.cid)
                    return;
    
                auto call = static_cast<SIPCall *>(pjsip_evsub_get_mod_data(sub, mod_ua_id));
                if (!call)
                    return;
    
                if (status_line.code / 100 == 2) {
                    if (call->inv)
                        call->terminateSipSession(PJSIP_SC_GONE);
                    Manager::instance().hangupCall(call->getCallId());
                    pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
                }
    
                break;
            }
    
            case PJSIP_EVSUB_STATE_NULL:
            case PJSIP_EVSUB_STATE_SENT:
            case PJSIP_EVSUB_STATE_PENDING:
            case PJSIP_EVSUB_STATE_UNKNOWN:
            default:
                break;
        }
    }
    
    bool
    SIPCall::transferCommon(pj_str_t *dst)
    {
        if (not inv or not inv->dlg)
            return false;
    
        pjsip_evsub_user xfer_cb;
        pj_bzero(&xfer_cb, sizeof(xfer_cb));
        xfer_cb.on_evsub_state = &transfer_client_cb;
    
        pjsip_evsub *sub;
    
        if (pjsip_xfer_create_uac(inv->dlg, &xfer_cb, &sub) != PJ_SUCCESS)
            return false;
    
        /* Associate this voiplink of call with the client subscription
         * We can not just associate call with the client subscription
         * because after this function, we can no find the cooresponding
         * voiplink from the call any more. But the voiplink is useful!
         */
        pjsip_evsub_set_mod_data(sub, getSIPVoIPLink()->getModId(), this);
    
        /*
         * Create REFER request.
         */
        pjsip_tx_data *tdata;
    
        if (pjsip_xfer_initiate(sub, dst, &tdata) != PJ_SUCCESS)
            return false;
    
        /* Send. */
        if (pjsip_xfer_send_request(sub, tdata) != PJ_SUCCESS)
            return false;
    
        return true;
    }
    
    void
    SIPCall::transfer(const std::string& to)
    {
        auto& account = getSIPAccount();
    
        if (Recordable::isRecording())
            stopRecording();
    
        std::string toUri;
        pj_str_t dst = { 0, 0 };
    
        toUri = account.getToUri(to);
        pj_cstr(&dst, toUri.c_str());
        RING_DBG("[call:%s] Transferring to %.*s", getCallId().c_str(), (int)dst.slen, dst.ptr);
    
        if (!transferCommon(&dst))
            throw VoipLinkException("Couldn't transfer");
    }
    
    bool
    SIPCall::attendedTransfer(const std::string& to)
    {
        const auto toCall = Manager::instance().callFactory.getCall<SIPCall>(to);
        if (!toCall)
            return false;
    
        if (not toCall->inv or not toCall->inv->dlg)
            return false;
    
        pjsip_dialog *target_dlg = toCall->inv->dlg;
        pjsip_uri *uri = (pjsip_uri*) pjsip_uri_get_uri(target_dlg->remote.info->uri);
    
        char str_dest_buf[PJSIP_MAX_URL_SIZE * 2] = { '<' };
        pj_str_t dst = { str_dest_buf, 1 };
    
        dst.slen += pjsip_uri_print(PJSIP_URI_IN_REQ_URI, uri, str_dest_buf + 1, sizeof(str_dest_buf) - 1);
        dst.slen += pj_ansi_snprintf(str_dest_buf + dst.slen,
                                     sizeof(str_dest_buf) - dst.slen,
                                     "?"
                                     "Replaces=%.*s"
                                     "%%3Bto-tag%%3D%.*s"
                                     "%%3Bfrom-tag%%3D%.*s>",
                                     (int)target_dlg->call_id->id.slen,
                                     target_dlg->call_id->id.ptr,
                                     (int)target_dlg->remote.info->tag.slen,
                                     target_dlg->remote.info->tag.ptr,
                                     (int)target_dlg->local.info->tag.slen,
                                     target_dlg->local.info->tag.ptr);
    
        return transferCommon(&dst);
    }
    
    bool
    SIPCall::onhold()
    {
        // If ICE is currently negotiating, we must wait before hold the call
        if (isWaitingForIceAndMedia_) {
            remainingRequest_ = Request::HoldingOn;
            return false;
        }
        if (not setState(CallState::HOLD))
            return false;
    
        stopAllMedia();
    
        if (getConnectionState() == ConnectionState::CONNECTED) {
            if (SIPSessionReinvite() != PJ_SUCCESS) {
                RING_WARN("[call:%s] Reinvite failed", getCallId().c_str());
                return true;
            }
        }
    
        isWaitingForIceAndMedia_ = true;
    
        return true;
    }
    
    bool
    SIPCall::offhold()
    {
        bool success = false;
        // If ICE is currently negotiating, we must wait before unhold the call
        if (isWaitingForIceAndMedia_) {
            remainingRequest_ = Request::HoldingOff;
            return false;
        }
    
        auto& account = getSIPAccount();
    
        try {
            if (account.isStunEnabled())
                success = internalOffHold([&] { updateSDPFromSTUN(); });
            else
                success = internalOffHold([] {});
    
        } catch (const SdpException &e) {
            RING_ERR("[call:%s] %s", getCallId().c_str(), e.what());
            throw VoipLinkException("SDP issue in offhold");
        }
    
        if (success) isWaitingForIceAndMedia_ = true;
    
        return success;
    }
    
    bool
    SIPCall::internalOffHold(const std::function<void()>& sdp_cb)
    {
        if (not setState(CallState::ACTIVE))
            return false;
    
        sdp_cb();
    
        if (getConnectionState() == ConnectionState::CONNECTED) {
            if (SIPSessionReinvite() != PJ_SUCCESS) {
                RING_WARN("[call:%s] resuming hold", getCallId().c_str());
                onhold();
                return false;
            }
        }
    
        return true;
    }
    
    void
    SIPCall::switchInput(const std::string& resource)
    {
        mediaInput_ = resource;
        if (isWaitingForIceAndMedia_) {
            remainingRequest_ = Request::SwitchInput;
        } else {
            if (SIPSessionReinvite() == PJ_SUCCESS) {
                isWaitingForIceAndMedia_ = true;
            }
        }
    }
    
    void
    SIPCall::peerHungup()
    {
        // Stop all RTP streams
        stopAllMedia();
    
        if (inv)
            terminateSipSession(PJSIP_SC_NOT_FOUND);
        else
            RING_ERR("[call:%s] peerHungup: no invite session for this call", getCallId().c_str());
    
        Call::peerHungup();
    }
    
    void
    SIPCall::carryingDTMFdigits(char code)
    {
        int duration = Manager::instance().voipPreferences.getPulseLength();
        char dtmf_body[1000];
    
        // handle flash code
        if (code == '!') {
            snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=16\r\nDuration=%d\r\n", duration);
        } else {
            snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=%c\r\nDuration=%d\r\n", code, duration);
        }
    
        try {
            sendSIPInfo(dtmf_body, "dtmf-relay");
        } catch (const std::exception& e) {
            RING_ERR("Error sending DTMF: %s", e.what());
        }
    }
    
    void
    SIPCall::sendTextMessage(const std::map<std::string, std::string>& messages,
                             const std::string& from)
    {
        //TODO: for now we ignore the "from" (the previous implementation for sending this info was
        //      buggy and verbose), another way to send the original message sender will be implemented
        //      in the future
        if (not subcalls_.empty()) {
            pendingOutMessages_.emplace_back(messages, from);
            for (auto& c : subcalls_)
                c->sendTextMessage(messages, from);
        } else {
            if (inv) {
                try {
                    im::sendSipMessage(inv.get(), messages);
                } catch (...) {}
            } else {
                pendingOutMessages_.emplace_back(messages, from);
                RING_ERR("[call:%s] sendTextMessage: no invite session for this call", getCallId().c_str());
            }
        }
    }
    
    void
    SIPCall::removeCall()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        RING_WARN("[call:%s] removeCall()", getCallId().c_str());
        Call::removeCall();
        mediaTransport_.reset();
        inv.reset();
        setTransport({});
    }
    
    void
    SIPCall::onFailure(signed cause)
    {
        setState(CallState::MERROR, ConnectionState::DISCONNECTED, cause);
        runOnMainThread([w = std::weak_ptr<Call>(shared_from_this())] {
            if (auto shared = w.lock()) {
                auto& call = *shared;
                Manager::instance().callFailure(call);
                call.removeCall();
            }
        });
    }
    
    void
    SIPCall::onBusyHere()
    {
        if (getCallType() == CallType::OUTGOING)
            setState(CallState::PEER_BUSY, ConnectionState::DISCONNECTED);
        else
            setState(CallState::BUSY, ConnectionState::DISCONNECTED);
    
        runOnMainThread([w = std::weak_ptr<Call>(shared_from_this())] {
            if (auto shared = w.lock()) {
                auto& call = *shared;
                Manager::instance().callBusy(call);
                call.removeCall();
            }
        });
    }
    
    void
    SIPCall::onClosed()
    {
        runOnMainThread([w = std::weak_ptr<Call>(shared_from_this())] {
            if (auto shared = w.lock()) {
                auto& call = *shared;
                Manager::instance().peerHungupCall(call);
                call.removeCall();
                Manager::instance().checkAudio();
            }
        });
    }
    
    void
    SIPCall::onAnswered()
    {
        RING_WARN("[call:%s] onAnswered()", getCallId().c_str());
        if (getConnectionState() != ConnectionState::CONNECTED) {
            setState(CallState::ACTIVE, ConnectionState::CONNECTED);
            if (not isSubcall()) {
                runOnMainThread([w = std::weak_ptr<Call>(shared_from_this())] {
                    if (auto shared = w.lock()) {
                        Manager::instance().peerAnsweredCall(*shared);
                    }
                });
            }
        }
    }
    
    void
    SIPCall::onPeerRinging()
    {
        setState(ConnectionState::RINGING);
    }
    
    void
    SIPCall::setupLocalSDPFromIce()
    {
        auto media_tr = getIceMediaTransport();
    
        if (not media_tr) {
            RING_WARN("[call:%s] no media ICE transport, SDP not changed", getCallId().c_str());
            return;
        }
    
        // we need an initialized ICE to progress further
        if (media_tr->waitForInitialization(DEFAULT_ICE_INIT_TIMEOUT) <= 0) {
            RING_ERR("[call:%s] Medias' ICE init failed", getCallId().c_str());
            return;
        }
    
        if (const auto& ip = getSIPAccount().getPublishedIpAddress()) {
            for (unsigned compId = 1; compId <= media_tr->getComponentCount(); ++compId)
                media_tr->registerPublicIP(compId, ip);
        }
    
        RING_WARN("[call:%s] fill SDP with ICE transport %p", getCallId().c_str(), media_tr);
        sdp_->addIceAttributes(media_tr->getLocalAttributes());
    
        // Add video and audio channels
        sdp_->addIceCandidates(SDP_AUDIO_MEDIA_ID, media_tr->getLocalCandidates(ICE_AUDIO_RTP_COMPID));
        sdp_->addIceCandidates(SDP_AUDIO_MEDIA_ID, media_tr->getLocalCandidates(ICE_AUDIO_RTCP_COMPID));
    #ifdef RING_VIDEO
        sdp_->addIceCandidates(SDP_VIDEO_MEDIA_ID, media_tr->getLocalCandidates(ICE_VIDEO_RTP_COMPID));
        sdp_->addIceCandidates(SDP_VIDEO_MEDIA_ID, media_tr->getLocalCandidates(ICE_VIDEO_RTCP_COMPID));
    #endif
    }
    
    std::vector<IceCandidate>
    SIPCall::getAllRemoteCandidates()
    {
        std::vector<IceCandidate> rem_candidates;
        auto media_tr = getIceMediaTransport();
    
        auto addSDPCandidates = [&, this](unsigned sdpMediaId,
                                          std::vector<IceCandidate>& out) {
            IceCandidate cand;
            for (auto& line : sdp_->getIceCandidates(sdpMediaId)) {
                if (media_tr->getCandidateFromSDP(line, cand)) {
                    RING_DBG("[call:%s] add remote ICE candidate: %s", getCallId().c_str(), line.c_str());
                    out.emplace_back(cand);
                }
            }
        };
    
        addSDPCandidates(SDP_AUDIO_MEDIA_ID, rem_candidates);
    #ifdef RING_VIDEO
        addSDPCandidates(SDP_VIDEO_MEDIA_ID, rem_candidates);
    #endif
    
        return rem_candidates;
    }
    
    bool
    SIPCall::useVideoCodec(const AccountVideoCodecInfo* codec) const
    {
    #ifdef RING_VIDEO
        if (videortp_->isSending())
            return videortp_->useCodec(codec);
    #endif
        return false;
    }
    
    void
    SIPCall::startAllMedia()
    {
        RING_WARN("[call:%s] startAllMedia()", getCallId().c_str());
        if (isSecure() && not transport_->isSecure()) {
            RING_ERR("[call:%s] Can't perform secure call over insecure SIP transport",
                     getCallId().c_str());
            onFailure(EPROTONOSUPPORT);
            return;
        }
        auto slots = sdp_->getMediaSlots();
        unsigned ice_comp_id = 0;
        bool peer_holding {true};
        int slotN = -1;
    
        for (const auto& slot : slots) {
            ++slotN;
            const auto& local = slot.first;
            const auto& remote = slot.second;
    
            if (local.type != remote.type) {
                RING_ERR("[call:%s] [SDP:slot#%u] Inconsistent media types between local and remote",
                         getCallId().c_str(), slotN);
                continue;
            }
    
            RtpSession* rtp = local.type == MEDIA_AUDIO
                ? static_cast<RtpSession*>(avformatrtp_.get())
    #ifdef RING_VIDEO
                : static_cast<RtpSession*>(videortp_.get());
    #else
                : nullptr;
    #endif
    
            if (not rtp)
                continue;
    
            if (!local.codec) {
                RING_WARN("[call:%s] [SDP:slot#%u] Missing local codec", getCallId().c_str(),
                          slotN);
                continue;
            }
            if (!remote.codec) {
                RING_WARN("[call:%s] [SDP:slot#%u] Missing remote codec", getCallId().c_str(),
                          slotN);
                continue;
            }
    
            peer_holding &= remote.holding;
    
            if (isSecure() && (not local.crypto || not remote.crypto)) {
                RING_ERR("[call:%s] [SDP:slot#%u] Can't perform secure call over insecure RTP transport",
                         getCallId().c_str(), slotN);
                continue;
            }
    
            auto new_mtu = transport_->getTlsMtu();
            if (local.type & MEDIA_AUDIO)
                avformatrtp_->switchInput(mediaInput_);
            avformatrtp_->setMtu(new_mtu);
    
    #ifdef RING_VIDEO
            if (local.type & MEDIA_VIDEO)
                videortp_->switchInput(mediaInput_);
            videortp_->setMtu(new_mtu);
    #endif
            rtp->updateMedia(remote, local);
    
            // Not restarting media loop on hold as it's a huge waste of CPU ressources
            // because of the audio loop
            if (getState() != CallState::HOLD) {
                if (isIceRunning()) {
                    rtp->start(newIceSocket(ice_comp_id + 0),
                               newIceSocket(ice_comp_id + 1));
                    ice_comp_id += 2;
                } else
                    rtp->start(nullptr, nullptr);
            }
    
            switch (local.type) {
    #ifdef RING_VIDEO
                case MEDIA_VIDEO:
                    isVideoMuted_ = mediaInput_.empty();
                    break;
    #endif
                case MEDIA_AUDIO:
                    isAudioMuted_ = not rtp->isSending();
                    break;
                default: break;
            }
        }
    
        if (not isSubcall() and peerHolding_ != peer_holding) {
            peerHolding_ = peer_holding;
            emitSignal<DRing::CallSignal::PeerHold>(getCallId(), peerHolding_);
        }
    
        // Media is restarted, we can process the last holding request.
        isWaitingForIceAndMedia_ = false;
        if (remainingRequest_ != Request::NoRequest) {
            switch (remainingRequest_) {
            case Request::HoldingOn:
                onhold();
                break;
            case Request::HoldingOff:
                offhold();
                break;
            case Request::SwitchInput:
                SIPSessionReinvite();
                break;
            default:
                break;
            }
            remainingRequest_ = Request::NoRequest;
        }
    }
    
    void
    SIPCall::restartMediaSender()
    {
        RING_DBG("[call:%s] restarting TX media streams", getCallId().c_str());
        avformatrtp_->restartSender();
    #ifdef RING_VIDEO
        videortp_->restartSender();
    #endif
    }
    
    void
    SIPCall::stopAllMedia()
    {
        RING_DBG("[call:%s] stopping all medias", getCallId().c_str());
        if (Recordable::isRecording())
            Recordable::stopRecording(); // if call stops, finish recording
        avformatrtp_->stop();
    #ifdef RING_VIDEO
        videortp_->stop();
    #endif
    }
    
    void
    SIPCall::muteMedia(const std::string& mediaType, bool mute)
    {
        if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_VIDEO) == 0) {
    #ifdef RING_VIDEO
            if (mute == isVideoMuted_) return;
            RING_WARN("[call:%s] video muting %s", getCallId().c_str(), bool_to_str(mute));
            isVideoMuted_ = mute;
            mediaInput_ = isVideoMuted_ ? "" : Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice();
            DRing::switchInput(getCallId(), mediaInput_);
            if (not isSubcall())
                emitSignal<DRing::CallSignal::VideoMuted>(getCallId(), isVideoMuted_);
    #endif
        } else if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_AUDIO) == 0) {
            if (mute == isAudioMuted_) return;
            RING_WARN("[call:%s] audio muting %s", getCallId().c_str(), bool_to_str(mute));
            isAudioMuted_ = mute;
            avformatrtp_->setMuted(isAudioMuted_);
            if (not isSubcall())
                emitSignal<DRing::CallSignal::AudioMuted>(getCallId(), isAudioMuted_);
        }
    }
    
    /// \brief Prepare media transport and launch media stream based on negotiated SDP
    ///
    /// This method has to be called by link (ie SipVoIpLink) when SDP is negotiated and
    /// media streams structures are knows.
    /// In case of ICE transport used, the medias streams are launched asynchonously when
    /// the transport is negotiated.
    void
    SIPCall::onMediaUpdate()
    {
        RING_WARN("[call:%s] medias changed", getCallId().c_str());
    
        // If ICE is not used, start medias now
        auto rem_ice_attrs = sdp_->getIceAttributes();
        if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
            RING_WARN("[call:%s] no remote ICE for medias", getCallId().c_str());
            stopAllMedia();
            startAllMedia();
            return;
        }
    
        // Main call (no subcalls) must wait for ICE now, the rest of code needs to access
        // to a negotiated transport.
        if (not isSubcall())
            waitForIceAndStartMedia();
    }
    
    void
    SIPCall::waitForIceAndStartMedia()
    {
        // Initialization waiting task
        auto weak_call = std::weak_ptr<SIPCall>(std::static_pointer_cast<SIPCall>(shared_from_this()));
        Manager::instance().addTask([weak_call] {
            // TODO: polling algo, to it by event
            if (auto call = weak_call.lock()) {
                auto ice = call->getIceMediaTransport();
    
                if (ice->isFailed()) {
                    RING_ERR("[call:%s] Media ICE init failed", call->getCallId().c_str());
                    call->onFailure(EIO);
                    return false;
                }
    
                if (!ice->isInitialized())
                    return true;
    
                // Start transport on SDP data and wait for negotiation
                auto rem_ice_attrs = call->sdp_->getIceAttributes();
                if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
                    RING_ERR("[call:%s] Media ICE attributes empty", call->getCallId().c_str());
                    call->onFailure(EIO);
                    return false;
                }
                if (not ice->start(rem_ice_attrs, call->getAllRemoteCandidates())) {
                    RING_ERR("[call:%s] Media ICE start failed", call->getCallId().c_str());
                    call->onFailure(EIO);
                    return false;
                }
    
                // Negotiation waiting task
                Manager::instance().addTask([weak_call] {
                    if (auto call = weak_call.lock()) {
                        auto ice = call->getIceMediaTransport();
    
                        if (ice->isFailed()) {
                            RING_ERR("[call:%s] Media ICE negotiation failed", call->getCallId().c_str());
                            call->onFailure(EIO);
                            return false;
                        }
    
                        if (not ice->isRunning())
                            return true;
    
                        // Nego succeed: move to the new media transport
                        call->stopAllMedia();
                        if (call->tmpMediaTransport_)
                            call->mediaTransport_ = std::move(call->tmpMediaTransport_);
                        call->startAllMedia();
                        return false;
                    }
                    return false;
                });
    
                return false;
            }
            return false;
        });
    }
    
    void
    SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer)
    {
        sdp_->clearIce();
        auto& acc = getSIPAccount();
        sdp_->receiveOffer(offer,
            acc.getActiveAccountCodecInfoList(MEDIA_AUDIO),
            acc.getActiveAccountCodecInfoList(acc.isVideoEnabled() ? MEDIA_VIDEO : MEDIA_NONE),
            acc.getSrtpKeyExchange(),
            getState() == CallState::HOLD);
        setRemoteSdp(offer);
        sdp_->startNegotiation();
        pjsip_inv_set_sdp_answer(inv.get(), sdp_->getLocalSdpSession());
        openPortsUPnP();
    }
    
    void
    SIPCall::openPortsUPnP()
    {
        if (upnp_) {
            /**
             * Try to open the desired ports with UPnP,
             * if they are used, use the alternative port and update the SDP session with the newly chosen port(s)
             *
             * TODO: the initial ports were chosen from the list of available ports and were marked as used
             *       the newly selected port should possibly be checked against the list of used ports and marked
             *       as used, the old port should be "released"
             */
            RING_DBG("[call:%s] opening ports via UPNP for SDP session", getCallId().c_str());
            uint16_t audio_port_used;
            if (upnp_->addAnyMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP, true, &audio_port_used)) {
                uint16_t control_port_used;
                if (upnp_->addAnyMapping(sdp_->getLocalAudioControlPort(), upnp::PortType::UDP, true, &control_port_used)) {
                    sdp_->setLocalPublishedAudioPorts(audio_port_used, control_port_used);
                }
            }
    #ifdef RING_VIDEO
            uint16_t video_port_used;
            if (upnp_->addAnyMapping(sdp_->getLocalVideoPort(), upnp::PortType::UDP, true, &video_port_used)) {
                uint16_t control_port_used;
                if (upnp_->addAnyMapping(sdp_->getLocalVideoControlPort(), upnp::PortType::UDP, true, &control_port_used)) {
                    sdp_->setLocalPublishedVideoPorts(video_port_used, control_port_used);
                }
            }
    #endif
        }
    }
    
    std::map<std::string, std::string>
    SIPCall::getDetails() const
    {
        auto details = Call::getDetails();
        details.emplace(DRing::Call::Details::PEER_HOLDING,
                        peerHolding_ ? TRUE_STR : FALSE_STR);
    
        auto& acc = getSIPAccount();
    
    #ifdef RING_VIDEO
        // If Video is not enabled return an empty string
        details.emplace(DRing::Call::Details::VIDEO_SOURCE, acc.isVideoEnabled() ? mediaInput_ : "");
    #endif
    
    #if HAVE_RINGNS
        if (not peerRegistredName_.empty())
            details.emplace(DRing::Call::Details::REGISTERED_NAME, peerRegistredName_);
    #endif
    
        if (transport_ and transport_->isSecure()) {
            const auto& tlsInfos = transport_->getTlsInfos();
            const auto& cipher = pj_ssl_cipher_name(tlsInfos.cipher);
            details.emplace(DRing::TlsTransport::TLS_CIPHER, cipher ? cipher : "");
            if (tlsInfos.peerCert) {
                details.emplace(DRing::TlsTransport::TLS_PEER_CERT,
                                tlsInfos.peerCert->toString());
                auto ca = tlsInfos.peerCert->issuer;
                unsigned n = 0;
                while (ca) {
                    std::ostringstream name_str;
                    name_str << DRing::TlsTransport::TLS_PEER_CA_ << n++;
                    details.emplace(name_str.str(), ca->toString());
                    ca = ca->issuer;
                }
                details.emplace(DRing::TlsTransport::TLS_PEER_CA_NUM,
                                ring::to_string(n));
            } else {
                details.emplace(DRing::TlsTransport::TLS_PEER_CERT, "");
                details.emplace(DRing::TlsTransport::TLS_PEER_CA_NUM, "");
            }
        }
        return details;
    }
    
    bool
    SIPCall::toggleRecording()
    {
        const bool startRecording = Call::toggleRecording();
        if (startRecording) {
            std::stringstream ss;
            ss << "Conversation at %TIMESTAMP between "
                << getSIPAccount().getUserUri() << " and " << peerUri_;
            recorder_->setMetadata(ss.str(), ""); // use default description
            if (avformatrtp_)
                avformatrtp_->initRecorder(recorder_);
    #ifdef RING_VIDEO
            if (!isAudioOnly_ && videortp_)
                videortp_->initRecorder(recorder_);
    #endif
        } else {
            if (avformatrtp_)
                avformatrtp_->deinitRecorder(recorder_);
    #ifdef RING_VIDEO
            if (!isAudioOnly_ && videortp_)
                videortp_->deinitRecorder(recorder_);
    #endif
        }
        return startRecording;
    }
    
    void
    SIPCall::setSecure(bool sec)
    {
        if (srtpEnabled_)
            return;
        if (sec && getConnectionState() != ConnectionState::DISCONNECTED) {
            throw std::runtime_error("Can't enable security since call is already connected");
        }
        srtpEnabled_ = sec;
    }
    
    void
    SIPCall::InvSessionDeleter::operator ()(pjsip_inv_session* inv) const noexcept
    {
        // prevent this from getting accessed in callbacks
        // RING_WARN: this is not thread-safe!
        inv->mod_data[getSIPVoIPLink()->getModId()] = nullptr;
    }
    
    bool
    SIPCall::initIceMediaTransport(bool master, unsigned channel_num)
    {
        RING_DBG("[call:%s] create media ICE transport", getCallId().c_str());
    
        auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
        tmpMediaTransport_ = iceTransportFactory.createTransport(getCallId().c_str(),
                                                                 channel_num, master,
                                                                 getAccount().getIceOptions());
        return static_cast<bool>(tmpMediaTransport_);
    }
    
    void
    SIPCall::merge(Call& call)
    {
        RING_DBG("[sipcall:%s] merge subcall %s", getCallId().c_str(), call.getCallId().c_str());
    
        // This static cast is safe as this method is private and overload Call::merge
        auto& subcall = static_cast<SIPCall&>(call);
    
        inv = std::move(subcall.inv);
        inv->mod_data[getSIPVoIPLink()->getModId()] = this;
        setTransport(subcall.transport_);
        sdp_ = std::move(subcall.sdp_);
        peerHolding_ = subcall.peerHolding_;
        upnp_ = std::move(subcall.upnp_);
        std::copy_n(subcall.contactBuffer_, PJSIP_MAX_URL_SIZE, contactBuffer_);
        pj_strcpy(&contactHeader_, &subcall.contactHeader_);
        localAudioPort_ = subcall.localAudioPort_;
        localVideoPort_ = subcall.localVideoPort_;
        mediaTransport_ = std::move(subcall.mediaTransport_);
        tmpMediaTransport_ = std::move(subcall.tmpMediaTransport_);
    
        Call::merge(subcall);
    
        waitForIceAndStartMedia();
    }
    
    void
    SIPCall::setRemoteSdp(const pjmedia_sdp_session* sdp)
    {
        if (!sdp)
            return;
    
        // If ICE is not used, start medias now
        auto rem_ice_attrs = sdp_->getIceAttributes();
        if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
            RING_WARN("[call:%s] no ICE data in remote SDP", getCallId().c_str());
            return;
        }
    
        if (not initIceMediaTransport(false)) {
            // Fatal condition
            // TODO: what's SIP rfc says about that?
            // (same question in waitForIceAndStartMedia)
            onFailure(EIO);
            return;
        }
    
        // WARNING: This call blocks! (need ice init done)
        setupLocalSDPFromIce();
    }
    
    bool
    SIPCall::isIceRunning() const
    {
        return mediaTransport_ and mediaTransport_->isRunning();
    }
    
    std::unique_ptr<IceSocket>
    SIPCall::newIceSocket(unsigned compId)
    {
        return std::unique_ptr<IceSocket> {new IceSocket(mediaTransport_, compId)};
    }
    
    } // namespace ring