Skip to content
Snippets Groups Projects
Select Git revision
  • 6697c86b1ce53bfb4502d6f9aa746ba3feceb634
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/releaseTest
  • release/releaseWindowsTest
  • release/windowsReleaseTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 1.0.0
  • 0.3.0
  • 0.2.1
  • 0.2.0
  • 0.1.0
26 results

collectioneditor.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    sipcall.cpp 100.93 KiB
    /*
     *  Copyright (C) 2004-2021 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>
     *  Author: Philippe Gorley <philippe.gorley@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"
    #include "sipaccountbase.h"
    #include "sipvoiplink.h"
    #include "logger.h"
    #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 "jami/call_const.h"
    #include "jami/media_const.h"
    #include "client/ring_signal.h"
    #include "ice_transport.h"
    #include "pjsip-ua/sip_inv.h"
    
    #ifdef ENABLE_PLUGIN
    #include "plugin/jamipluginmanager.h"
    #endif
    
    #ifdef ENABLE_VIDEO
    #include "client/videomanager.h"
    #include "video/video_rtp_session.h"
    #include "jami/videomanager_interface.h"
    #include <chrono>
    #include <libavutil/display.h>
    #include <video/sinkclient.h>
    #endif
    #include "audio/ringbufferpool.h"
    #include "jamidht/channeled_transport.h"
    
    #include "errno.h"
    
    #include <opendht/crypto.h>
    #include <opendht/thread_pool.h>
    #include <fmt/ranges.h>
    
    namespace jami {
    
    using sip_utils::CONST_PJ_STR;
    using namespace DRing::Call;
    
    #ifdef ENABLE_VIDEO
    static DeviceParams
    getVideoSettings()
    {
        const auto& videomon = jami::getVideoDeviceMonitor();
        return videomon.getDeviceParams(videomon.getDefaultDevice());
    }
    #endif
    
    static constexpr std::chrono::seconds DEFAULT_ICE_INIT_TIMEOUT {35}; // seconds
    static constexpr std::chrono::milliseconds EXPECTED_ICE_INIT_MAX_TIME {5000};
    static constexpr std::chrono::seconds DEFAULT_ICE_NEGO_TIMEOUT {60}; // seconds
    static constexpr std::chrono::milliseconds MS_BETWEEN_2_KEYFRAME_REQUEST {1000};
    static constexpr int ICE_COMP_ID_RTP {1};
    static constexpr int ICE_COMP_COUNT_PER_STREAM {2};
    static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR = "10.0.2"sv;
    static const std::vector<unsigned> MULTISTREAM_REQUIRED_VERSION
        = split_string_to_unsigned(MULTISTREAM_REQUIRED_VERSION_STR, '.');
    
    SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
                     const std::string& callId,
                     Call::CallType type,
                     const std::map<std::string, std::string>& details)
        : Call(account, callId, type, details)
        , sdp_(new Sdp(callId))
        , enableIce_(account->isIceForMediaEnabled())
        , srtpEnabled_(account->isSrtpEnabled())
    {
        if (account->getUPnPActive())
            upnp_.reset(new upnp::Controller());
    
        setCallMediaLocal();
    
        // Set the media caps.
        sdp_->setLocalMediaCapabilities(MediaType::MEDIA_AUDIO,
                                        account->getActiveAccountCodecInfoList(MEDIA_AUDIO));
    #ifdef ENABLE_VIDEO
        sdp_->setLocalMediaCapabilities(MediaType::MEDIA_VIDEO,
                                        account->getActiveAccountCodecInfoList(MEDIA_VIDEO));
    #endif
        auto mediaAttrList = getSIPAccount()->createDefaultMediaList(getSIPAccount()->isVideoEnabled()
                                                                         and not isAudioOnly(),
                                                                     getState() == CallState::HOLD);
        JAMI_DBG("[call:%s] Create a new [%s] SIP call with %lu media",
                 getCallId().c_str(),
                 type == Call::CallType::INCOMING
                     ? "INCOMING"
                     : (type == Call::CallType::OUTGOING ? "OUTGOING" : "MISSED"),
                 mediaAttrList.size());
    
        initMediaStreams(mediaAttrList);
    }
    
    SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
                     const std::string& callId,
                     Call::CallType type,
                     const std::vector<DRing::MediaMap>& mediaList)
        : Call(account, callId, type)
        , peerSupportMultiStream_(false)
        , sdp_(new Sdp(callId))
        , enableIce_(account->isIceForMediaEnabled())
        , srtpEnabled_(account->isSrtpEnabled())
    {
        if (account->getUPnPActive())
            upnp_.reset(new upnp::Controller());
    
        setCallMediaLocal();
    
        // Set the media caps.
        sdp_->setLocalMediaCapabilities(MediaType::MEDIA_AUDIO,
                                        account->getActiveAccountCodecInfoList(MEDIA_AUDIO));
    #ifdef ENABLE_VIDEO
        sdp_->setLocalMediaCapabilities(MediaType::MEDIA_VIDEO,
                                        account->getActiveAccountCodecInfoList(MEDIA_VIDEO));
    #endif
    
        auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
    
        if (mediaAttrList.size() == 0) {
            if (type_ == Call::CallType::INCOMING) {
                // Handle incoming call without media offer.
                JAMI_WARN(
                    "[call:%s] No media offered in the incoming invite. An offer will be provided in "
                    "the answer",
                    getCallId().c_str());
                mediaAttrList = getSIPAccount()->createDefaultMediaList(false,
                                                                        getState() == CallState::HOLD);
            } else {
                JAMI_WARN("[call:%s] Creating an outgoing call with empty offer", getCallId().c_str());
            }
        }
    
        JAMI_DBG("[call:%s] Create a new [%s] SIP call with %lu media",
                 getCallId().c_str(),
                 type == Call::CallType::INCOMING
                     ? "INCOMING"
                     : (type == Call::CallType::OUTGOING ? "OUTGOING" : "MISSED"),
                 mediaList.size());
    
        initMediaStreams(mediaAttrList);
    }
    
    SIPCall::~SIPCall()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
    
        setTransport({});
        setInviteSession(); // prevents callback usage
    }
    
    size_t
    SIPCall::findRtpStreamIndex(const std::string& label) const
    {
        const auto iter = std::find_if(rtpStreams_.begin(),
                                       rtpStreams_.end(),
                                       [&label](const RtpStream& rtp) {
                                           return label == rtp.mediaAttribute_->label_;
                                       });
    
        // Return the index if there is a match.
        if (iter != rtpStreams_.end())
            return std::distance(rtpStreams_.begin(), iter);
    
        // No match found.
        return rtpStreams_.size();
    }
    
    void
    SIPCall::createRtpSession(RtpStream& stream)
    {
        if (not stream.mediaAttribute_)
            throw std::runtime_error("Missing media attribute");
    
        if (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO) {
            stream.rtpSession_ = std::make_shared<AudioRtpSession>(id_);
        }
    #ifdef ENABLE_VIDEO
        else if (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO) {
            stream.rtpSession_ = std::make_shared<video::VideoRtpSession>(id_, getVideoSettings());
        }
    #endif
        else {
            throw std::runtime_error("Unsupported media type");
        }
    
        // Must be valid at this point.
        if (not stream.rtpSession_)
            throw std::runtime_error("Failed to create RTP Session");
        ;
    }
    
    void
    SIPCall::configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession,
                                 const std::shared_ptr<MediaAttribute>& mediaAttr,
                                 const MediaDescription& localMedia,
                                 const MediaDescription& remoteMedia)
    {
        JAMI_DBG("[call:%s] Configuring [%s] rtp session",
                 getCallId().c_str(),
                 MediaAttribute::mediaTypeToString(mediaAttr->type_));
    
        if (not rtpSession)
            throw std::runtime_error("Must have a valid RTP Session");
    
        // Configure the media stream
        auto new_mtu = transport_->getTlsMtu();
        rtpSession->setMtu(new_mtu);
        rtpSession->updateMedia(remoteMedia, localMedia);
    
        // Mute/un-mute media
        if (mediaAttr->muted_) {
            rtpSession->setMuted(true);
            // TODO. Setting mute to true should be enough to mute.
            // Kept for backward compatiblity.
            rtpSession->setMediaSource("");
        } else {
            rtpSession->setMuted(false);
            rtpSession->setMediaSource(mediaAttr->sourceUri_);
        }
    
        rtpSession->setSuccessfulSetupCb([w = weak()](MediaType type, bool isRemote) {
            if (auto thisPtr = w.lock())
                thisPtr->rtpSetupSuccess(type, isRemote);
        });
    
    #ifdef ENABLE_VIDEO
        if (localMedia.type == MediaType::MEDIA_VIDEO) {
            auto videoRtp = std::dynamic_pointer_cast<video::VideoRtpSession>(rtpSession);
            assert(videoRtp);
            videoRtp->setRequestKeyFrameCallback([w = weak()] {
                runOnMainThread([w] {
                    if (auto thisPtr = w.lock())
                        thisPtr->requestKeyframe();
                });
            });
            videoRtp->setChangeOrientationCallback([w = weak()](int angle) {
                runOnMainThread([w, angle] {
                    if (auto thisPtr = w.lock())
                        thisPtr->setVideoOrientation(angle);
                });
            });
        }
    #endif
    }
    
    std::shared_ptr<SIPAccountBase>
    SIPCall::getSIPAccount() const
    {
        return std::static_pointer_cast<SIPAccountBase>(getAccount().lock());
    }
    
    #ifdef ENABLE_PLUGIN
    void
    SIPCall::createCallAVStreams()
    {
        if (hasVideo()) {
            auto videoRtp = getVideoRtp();
            if (not videoRtp)
                return;
            if (videoRtp->hasConference()) {
                clearCallAVStreams();
                return;
            }
        }
    
        auto baseId = getCallId();
        /**
         *   Map: maps the AudioFrame to an AVFrame
         **/
        auto audioMap = [](const std::shared_ptr<jami::MediaFrame>& m) -> AVFrame* {
            return std::static_pointer_cast<AudioFrame>(m)->pointer();
        };
    
        auto const& audioRtp = getAudioRtp();
        if (not audioRtp) {
            throw std::runtime_error("Must have a valid Audio RTP Session");
        }
    
        // Preview
        if (auto& localAudio = audioRtp->getAudioLocal()) {
            auto previewSubject = std::make_shared<MediaStreamSubject>(audioMap);
            StreamData microStreamData {baseId, false, StreamType::audio, getPeerNumber()};
            createCallAVStream(microStreamData, *localAudio, previewSubject);
        }
    
        // Receive
        if (auto& audioReceive = audioRtp->getAudioReceive()) {
            auto receiveSubject = std::make_shared<MediaStreamSubject>(audioMap);
            StreamData phoneStreamData {baseId, true, StreamType::audio, getPeerNumber()};
            createCallAVStream(phoneStreamData, (AVMediaStream&) *audioReceive, receiveSubject);
        }
    #ifdef ENABLE_VIDEO
        if (hasVideo()) {
            auto videoRtp = getVideoRtp();
            if (not videoRtp)
                return;
    
            // Map: maps the VideoFrame to an AVFrame
            auto map = [](const std::shared_ptr<jami::MediaFrame> m) -> AVFrame* {
                return std::static_pointer_cast<VideoFrame>(m)->pointer();
            };
            // Preview
            if (auto& videoPreview = videoRtp->getVideoLocal()) {
                auto previewSubject = std::make_shared<MediaStreamSubject>(map);
                StreamData previewStreamData {getCallId(), false, StreamType::video, getPeerNumber()};
                createCallAVStream(previewStreamData, *videoPreview, previewSubject);
            }
    
            // Receive
            if (auto& videoReceive = videoRtp->getVideoReceive()) {
                auto receiveSubject = std::make_shared<MediaStreamSubject>(map);
                StreamData receiveStreamData {getCallId(), true, StreamType::video, getPeerNumber()};
                createCallAVStream(receiveStreamData, *videoReceive, receiveSubject);
            }
        }
    #endif
    }
    
    void
    SIPCall::createCallAVStream(const StreamData& StreamData,
                                AVMediaStream& streamSource,
                                const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject)
    {
        const std::string AVStreamId = StreamData.id + std::to_string(static_cast<int>(StreamData.type))
                                       + std::to_string(StreamData.direction);
        std::lock_guard<std::mutex> lk(avStreamsMtx_);
        auto it = callAVStreams.find(AVStreamId);
        if (it != callAVStreams.end())
            return;
        it = callAVStreams.insert(it, {AVStreamId, mediaStreamSubject});
        streamSource.attachPriorityObserver(it->second);
        jami::Manager::instance()
            .getJamiPluginManager()
            .getCallServicesManager()
            .createAVSubject(StreamData, it->second);
    }
    
    void
    SIPCall::clearCallAVStreams()
    {
        std::lock_guard<std::mutex> lk(avStreamsMtx_);
        callAVStreams.clear();
    }
    #endif // ENABLE_PLUGIN
    
    void
    SIPCall::setCallMediaLocal()
    {
        if (localAudioPort_ == 0
    #ifdef ENABLE_VIDEO
            || localVideoPort_ == 0
    #endif
        )
            generateMediaPorts();
    }
    
    void
    SIPCall::generateMediaPorts()
    {
        auto account = getSIPAccount();
        if (!account) {
            JAMI_ERR("No account detected");
            return;
        }
    
        // TODO. Setting specfic range for RTP ports is obsolete, in
        // particular in the context of ICE.
    
        // 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_->setLocalPublishedAudioPorts(callLocalAudioPort,
                                          rtcpMuxEnabled_ ? 0 : callLocalAudioPort + 1);
    
    #ifdef ENABLE_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_->setLocalPublishedVideoPorts(callLocalVideoPort,
                                          rtcpMuxEnabled_ ? 0 : callLocalVideoPort + 1);
    #endif
    }
    
    void
    SIPCall::setContactHeader(pj_str_t contact)
    {
        pj_strcpy(&contactHeader_, &contact);
    }
    
    void
    SIPCall::setTransport(const std::shared_ptr<SipTransport>& t)
    {
        if (t != transport_) {
            JAMI_WARN("[call:%s] Setting tranport to [%p]", getCallId().c_str(), t.get());
        }
    
        transport_ = t;
    
        if (not t) {
            return;
        }
    
        if (isSrtpEnabled() and not transport_->isSecure()) {
            JAMI_WARN("[call:%s] Crypto (SRTP) is negotiated over an un-encrypted signaling channel",
                      getCallId().c_str());
        }
    
        if (not isSrtpEnabled() and transport_->isSecure()) {
            JAMI_WARN("[call:%s] The signaling channel is encrypted but the media is not encrypted",
                      getCallId().c_str());
        }
    
        const auto list_id = reinterpret_cast<uintptr_t>(this);
        transport_->removeStateListener(list_id);
    
        // listen for transport destruction
        transport_->addStateListener(
            list_id, [wthis_ = weak()](pjsip_transport_state state, const pjsip_transport_state_info*) {
                if (auto this_ = wthis_.lock()) {
                    JAMI_DBG("[call:%s] SIP transport state [%i] - connection state [%u]",
                             this_->getCallId().c_str(),
                             state,
                             static_cast<unsigned>(this_->getConnectionState()));
    
                    // End the call if the SIP transport was shut down
                    auto isAlive = SipTransport::isAlive(state);
                    if (not isAlive and this_->getConnectionState() != ConnectionState::DISCONNECTED) {
                        JAMI_WARN("[call:%s] Ending call because underlying SIP transport was closed",
                                  this_->getCallId().c_str());
                        this_->stopAllMedia();
                        this_->onFailure(ECONNRESET);
                    }
                }
            });
    }
    
    void
    SIPCall::requestReinvite()
    {
        JAMI_DBG("[call:%s] Sending a SIP re-invite to request media change", getCallId().c_str());
    
        if (isWaitingForIceAndMedia_) {
            remainingRequest_ = Request::SwitchInput;
        } else {
            auto mediaList = getMediaAttributeList();
            assert(not mediaList.empty());
    
            // TODO. We should erase existing streams only after the new
            // ones were successfully negotiated, and make a live switch. But
            // for now, we reset all streams before creating new ones.
            rtpStreams_.clear();
            initMediaStreams(mediaList);
    
            if (SIPSessionReinvite(mediaList) == PJ_SUCCESS) {
                isWaitingForIceAndMedia_ = true;
            }
        }
    }
    
    /**
     * Send a reINVITE inside an active dialog to modify its state
     * Local SDP session should be modified before calling this method
     */
    int
    SIPCall::SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList)
    {
        assert(not mediaAttrList.empty());
    
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
    
        // Do nothing if no invitation processed yet
        if (not inviteSession_ or inviteSession_->invite_tsx)
            return PJ_SUCCESS;
    
        JAMI_DBG("[call:%s] Preparing and sending a re-invite (state=%s)",
                 getCallId().c_str(),
                 pjsip_inv_state_name(inviteSession_->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 acc) {
            JAMI_ERR("No account detected");
            return !PJ_SUCCESS;
        }
    
        if (not sdp_->createOffer(mediaAttrList))
            return !PJ_SUCCESS;
    
        if (isIceEnabled()) {
            createIceMediaTransport();
            if (initIceMediaTransport(true))
                addLocalIceAttributes();
        }
    
        pjsip_tx_data* tdata;
        auto local_sdp = sdp_->getLocalSdpSession();
        auto result = pjsip_inv_reinvite(inviteSession_.get(), nullptr, local_sdp, &tdata);
        if (result == PJ_SUCCESS) {
            if (!tdata)
                return PJ_SUCCESS;
    
            // Add user-agent header
            sip_utils::addUserAgentHeader(acc->getUserAgentName(), tdata);
    
            result = pjsip_inv_send_msg(inviteSession_.get(), tdata);
            if (result == PJ_SUCCESS)
                return PJ_SUCCESS;
            JAMI_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(inviteSession_.get(), &tdata);
        } else
            JAMI_ERR("[call:%s] Failed to create REINVITE msg (pjsip: %s)",
                     getCallId().c_str(),
                     sip_utils::sip_strerror(result).c_str());
    
        return !PJ_SUCCESS;
    }
    
    int
    SIPCall::SIPSessionReinvite()
    {
        auto mediaList = getMediaAttributeList();
        return SIPSessionReinvite(mediaList);
    }
    
    void
    SIPCall::sendSIPInfo(std::string_view body, std::string_view subtype)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (not inviteSession_ or not inviteSession_->dlg)
            throw VoipLinkException("Couldn't get invite dialog");
    
        constexpr pj_str_t methodName = CONST_PJ_STR("INFO");
        constexpr pj_str_t type = CONST_PJ_STR("application");
    
        pjsip_method method;
        pjsip_method_init_np(&method, (pj_str_t*) &methodName);
    
        /* Create request message. */
        pjsip_tx_data* tdata;
        if (pjsip_dlg_create_request(inviteSession_->dlg, &method, -1, &tdata) != PJ_SUCCESS) {
            JAMI_ERR("[call:%s] Could not create dialog", getCallId().c_str());
            return;
        }
    
        /* Create "application/<subtype>" message body. */
        pj_str_t content = CONST_PJ_STR(body);
        pj_str_t pj_subtype = CONST_PJ_STR(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(inviteSession_->dlg,
                                   tdata,
                                   Manager::instance().sipVoIPLink().getModId(),
                                   NULL);
    }
    
    void
    SIPCall::updateRecState(bool state)
    {
        std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                           "<media_control><vc_primitive><to_encoder>"
                           "<recording_state="
                           + std::to_string(state)
                           + "/>"
                             "</to_encoder></vc_primitive></media_control>";
        // see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
    
        JAMI_DBG("Sending recording state via SIP INFO");
    
        try {
            sendSIPInfo(BODY, "media_control+xml");
        } catch (const std::exception& e) {
            JAMI_ERR("Error sending recording state: %s", e.what());
        }
    }
    
    void
    SIPCall::requestKeyframe()
    {
        auto now = clock::now();
        if ((now - lastKeyFrameReq_) < MS_BETWEEN_2_KEYFRAME_REQUEST
            and lastKeyFrameReq_ != time_point::min())
            return;
    
        constexpr auto BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                              "<media_control><vc_primitive><to_encoder>"
                              "<picture_fast_update/>"
                              "</to_encoder></vc_primitive></media_control>"sv;
        JAMI_DBG("Sending video keyframe request via SIP INFO");
        try {
            sendSIPInfo(BODY, "media_control+xml");
        } catch (const std::exception& e) {
            JAMI_ERR("Error sending video keyframe request: %s", e.what());
        }
        lastKeyFrameReq_ = now;
    }
    
    void
    SIPCall::setMute(bool state)
    {
        std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                           "<media_control><vc_primitive><to_encoder>"
                           "<mute_state="
                           + std::to_string(state)
                           + "/>"
                             "</to_encoder></vc_primitive></media_control>";
        // see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
    
        JAMI_DBG("Sending mute state via SIP INFO");
    
        try {
            sendSIPInfo(BODY, "media_control+xml");
        } catch (const std::exception& e) {
            JAMI_ERR("Error sending mute state: %s", e.what());
        }
    }
    
    void
    SIPCall::setInviteSession(pjsip_inv_session* inviteSession)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
    
        if (inviteSession == nullptr and inviteSession_) {
            JAMI_DBG("[call:%s] Delete current invite session", getCallId().c_str());
        } else if (inviteSession != nullptr) {
            // NOTE: The first reference of the invite session is owned by pjsip. If
            // that counter goes down to zero the invite will be destroyed, and the
            // unique_ptr will point freed datas.  To avoid this, we increment the
            // ref counter and let our unique_ptr share the ownership of the session
            // with pjsip.
            if (PJ_SUCCESS != pjsip_inv_add_ref(inviteSession)) {
                JAMI_WARN("[call:%s] trying to set invalid invite session [%p]",
                          getCallId().c_str(),
                          inviteSession);
                inviteSession_.reset(nullptr);
                return;
            }
            JAMI_DBG("[call:%s] Set new invite session [%p]", getCallId().c_str(), inviteSession);
        } else {
            // Nothing to do.
            return;
        }
    
        inviteSession_.reset(inviteSession);
    }
    
    void
    SIPCall::terminateSipSession(int status)
    {
        JAMI_DBG("[call:%s] Terminate SIP session", getCallId().c_str());
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (inviteSession_ and inviteSession_->state != PJSIP_INV_STATE_DISCONNECTED) {
            pjsip_tx_data* tdata = nullptr;
            auto ret = pjsip_inv_end_session(inviteSession_.get(), status, nullptr, &tdata);
            if (ret == PJ_SUCCESS) {
                if (tdata) {
                    auto account = getSIPAccount();
                    if (account) {
                        sip_utils::addContactHeader(account->getContactHeader(
                                                        transport_ ? transport_->get() : nullptr),
                                                    tdata);
                        // Add user-agent header
                        sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
                    } else {
                        JAMI_ERR("No account detected");
                        std::ostringstream msg;
                        msg << "[call:" << getCallId().c_str() << "] "
                            << "The account owning this call is invalid";
                        throw std::runtime_error(msg.str());
                    }
    
                    ret = pjsip_inv_send_msg(inviteSession_.get(), tdata);
                    if (ret != PJ_SUCCESS)
                        JAMI_ERR("[call:%s] failed to send terminate msg, SIP error (%s)",
                                 getCallId().c_str(),
                                 sip_utils::sip_strerror(ret).c_str());
                }
            } else
                JAMI_ERR("[call:%s] failed to terminate INVITE@%p, SIP error (%s)",
                         getCallId().c_str(),
                         inviteSession_.get(),
                         sip_utils::sip_strerror(ret).c_str());
        }
        setInviteSession();
    }
    
    void
    SIPCall::answer()
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        auto account = getSIPAccount();
        if (!account) {
            JAMI_ERR("No account detected");
            return;
        }
    
        if (not inviteSession_)
            throw VoipLinkException("[call:" + getCallId()
                                    + "] answer: no invite session for this call");
    
        if (!inviteSession_->neg) {
            JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
                      getCallId().c_str());
    
            Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
        }
    
        setContactHeader(account->getContactHeader(transport_ ? transport_->get() : nullptr));
    
        pjsip_tx_data* tdata;
        if (!inviteSession_->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(inviteSession_.get(),
                             PJSIP_SC_OK,
                             NULL,
                             !inviteSession_->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) {
            JAMI_DBG("[call:%s] Answering with contact header: %.*s",
                     getCallId().c_str(),
                     (int) contactHeader_.slen,
                     contactHeader_.ptr);
            sip_utils::addContactHeader(contactHeader_, tdata);
        }
    
        // Add user-agent header
        sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
    
        if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
            setInviteSession();
            throw std::runtime_error("Could not send invite request answer (200 OK)");
        }
    
        setState(CallState::ACTIVE, ConnectionState::CONNECTED);
    }
    
    void
    SIPCall::answer(const std::vector<DRing::MediaMap>& mediaList)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        auto account = getSIPAccount();
        if (not account) {
            JAMI_ERR("No account detected");
            return;
        }
    
        auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
    
        if (mediaAttrList.empty()) {
            JAMI_DBG("[call:%s] Media list must not be empty!", getCallId().c_str());
            return;
        }
    
        if (not inviteSession_)
            JAMI_DBG("[call:%s] No invite session for this call", getCallId().c_str());
    
        JAMI_DBG("[call:%s] Answering incoming call with %lu media:",
                 getCallId().c_str(),
                 mediaAttrList.size());
    
        if (mediaAttrList.size() != rtpStreams_.size()) {
            JAMI_ERR("[call:%s] Media list size %lu in answer does not match. Expected %lu",
                     getCallId().c_str(),
                     mediaAttrList.size(),
                     rtpStreams_.size());
            return;
        }
    
        for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
            auto const& mediaAttr = mediaAttrList.at(idx);
            JAMI_DBG("[call:%s] Media @%lu: %s",
                     getCallId().c_str(),
                     idx,
                     mediaAttr.toString(true).c_str());
        }
    
        // Apply the media attributes provided by the user.
        for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
            updateMediaStream(mediaAttrList[idx], idx);
        }
    
        if (not inviteSession_)
            throw VoipLinkException("[call:" + getCallId()
                                    + "] answer: no invite session for this call");
    
        if (not inviteSession_->neg) {
            // We are answering to an INVITE that did not include a media offer (SDP).
            // The SIP specification (RFCs 3261/6337) requires that if a UA wishes to
            // proceed with the call, it must provide a media offer (SDP) if the initial
            // INVITE did not offer one. In this case, the SDP offer will be included in
            // the SIP OK (200) answer. The peer UA will then include its SDP answer in
            // the SIP ACK message.
    
            // TODO. This code should be unified with the code used by accounts to create
            // SDP offers.
    
            JAMI_WARN("[call:%s] No negotiator session, peer sent an empty INVITE (without SDP)",
                      getCallId().c_str());
    
            Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
    
            generateMediaPorts();
    
            // Setup and create ICE offer
            if (isIceEnabled()) {
                sdp_->clearIce();
    
                auto opts = account->getIceOptions();
    
                auto publicAddr = account->getPublishedIpAddress();
    
                if (publicAddr) {
                    opts.accountPublicAddr = publicAddr;
                    if (auto interfaceAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
                                                                        publicAddr.getFamily())) {
                        opts.accountLocalAddr = interfaceAddr;
                        createIceMediaTransport();
                        if (initIceMediaTransport(true, std::move(opts)))
                            addLocalIceAttributes();
                    } else {
                        JAMI_WARN("[call:%s] Cant init ICE transport, missing local address",
                                  getCallId().c_str());
                    }
                } else {
                    JAMI_WARN("[call:%s] Cant init ICE transport, missing public address",
                              getCallId().c_str());
                }
            }
        }
    
        setContactHeader(account->getContactHeader(transport_ ? transport_->get() : nullptr));
    
        if (!inviteSession_->last_answer)
            throw std::runtime_error("Should only be called for initial answer");
    
        // Answer with an SDP offer if the initial invite was empty,
        // otherwise, set the local_sdp session to null to use the
        // current SDP session.
        pjsip_tx_data* tdata;
        if (pjsip_inv_answer(inviteSession_.get(),
                             PJSIP_SC_OK,
                             NULL,
                             not inviteSession_->neg ? sdp_->getLocalSdpSession() : NULL,
                             &tdata)
            != PJ_SUCCESS)
            throw std::runtime_error("Could not init invite request answer (200 OK)");
    
        if (contactHeader_.slen) {
            JAMI_DBG("[call:%s] Answering with contact header: %.*s",
                     getCallId().c_str(),
                     (int) contactHeader_.slen,
                     contactHeader_.ptr);
            sip_utils::addContactHeader(contactHeader_, tdata);
        }
    
        // Add user-agent header
        sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
    
        if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
            setInviteSession();
            throw std::runtime_error("Could not send invite request answer (200 OK)");
        }
    
        setState(CallState::ACTIVE, ConnectionState::CONNECTED);
    }
    
    void
    SIPCall::answerMediaChangeRequest(const std::vector<DRing::MediaMap>& mediaList)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
    
        auto account = getSIPAccount();
        if (not account) {
            JAMI_ERR("[call:%s] No account detected", getCallId().c_str());
            return;
        }
    
        auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
    
        if (mediaAttrList.empty()) {
            JAMI_DBG("[call:%s] Media list size is empty. Ignoring the media change request",
                     getCallId().c_str());
            return;
        }
    
        if (not sdp_) {
            JAMI_ERR("[call:%s] No valid SDP session", getCallId().c_str());
            return;
        }
    
        JAMI_DBG("[call:%s] Current media", getCallId().c_str());
        unsigned idx = 0;
        for (auto const& rtp : rtpStreams_) {
            JAMI_DBG("[call:%s] Media @%u: %s",
                     getCallId().c_str(),
                     idx++,
                     rtp.mediaAttribute_->toString(true).c_str());
        }
    
        JAMI_DBG("[call:%s] Answering to media change request with new media", getCallId().c_str());
        idx = 0;
        for (auto const& newMediaAttr : mediaAttrList) {
            JAMI_DBG("[call:%s] Media @%u: %s",
                     getCallId().c_str(),
                     idx++,
                     newMediaAttr.toString(true).c_str());
        }
    
        updateAllMediaStreams(mediaAttrList);
    
        if (not sdp_->processIncomingOffer(mediaAttrList)) {
            JAMI_WARN("[call:%s] Could not process the new offer, ignoring", getCallId().c_str());
            return;
        }
    
        if (not sdp_->getRemoteSdpSession()) {
            JAMI_ERR("[call:%s] No valid remote SDP session", getCallId().c_str());
            return;
        }
    
        if (isIceEnabled())
            setupIceResponse();
    
        if (not sdp_->startNegotiation()) {
            JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
                     getCallId().c_str());
            return;
        }
    
        if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
            JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
                     getCallId().c_str());
            return;
        }
    
        pjsip_tx_data* tdata;
        if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, NULL, &tdata) != PJ_SUCCESS) {
            JAMI_ERR("[call:%s] Could not init answer to a re-invite request", getCallId().c_str());
            return;
        }
    
        if (contactHeader_.slen) {
            sip_utils::addContactHeader(contactHeader_, tdata);
        }
    
        // Add user-agent header
        sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
    
        if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
            JAMI_ERR("[call:%s] Could not send answer to a re-invite request", getCallId().c_str());
            setInviteSession();
            return;
        }
    
        JAMI_DBG("[call:%s] Successfully answered the media change request", getCallId().c_str());
    }
    
    void
    SIPCall::hangup(int reason)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        pendingRecord_ = false;
        if (inviteSession_ and inviteSession_->dlg) {
            pjsip_route_hdr* route = inviteSession_->dlg->route_set.next;
            while (route and route != &inviteSession_->dlg->route_set) {
                char buf[1024];
                int printed = pjsip_hdr_print_on(route, buf, sizeof(buf));
                if (printed >= 0) {
                    buf[printed] = '\0';
                    JAMI_DBG("[call:%s] Route header %s", getCallId().c_str(), buf);
                }
                route = route->next;
            }
    
            int status = PJSIP_SC_OK;
            if (reason)
                status = reason;
            else if (inviteSession_->state <= PJSIP_INV_STATE_EARLY
                     and inviteSession_->role != PJSIP_ROLE_UAC)
                status = PJSIP_SC_CALL_TSX_DOES_NOT_EXIST;
            else if (inviteSession_->state >= PJSIP_INV_STATE_DISCONNECTED)
                status = PJSIP_SC_DECLINE;
    
            // Notify the peer
            terminateSipSession(status);
        }
    
        // Stop all RTP streams
        stopAllMedia();
        setState(Call::ConnectionState::DISCONNECTED, reason);
        dht::ThreadPool::io().run([w = weak()] {
            if (auto shared = w.lock())
                shared->removeCall();
        });
    }
    
    void
    SIPCall::refuse()
    {
        if (!isIncoming() or getConnectionState() == ConnectionState::CONNECTED or !inviteSession_)
            return;
    
        stopAllMedia();
    
        // Notify the peer
        terminateSipSession(PJSIP_SC_BUSY_HERE);
    
        setState(Call::ConnectionState::DISCONNECTED, ECONNABORTED);
        removeCall();
    }
    
    static void
    transfer_client_cb(pjsip_evsub* sub, pjsip_event* event)
    {
        auto mod_ua_id = Manager::instance().sipVoIPLink().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->inviteSession_)
                    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(const pj_str_t* dst)
    {
        if (not inviteSession_ or not inviteSession_->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(inviteSession_->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, Manager::instance().sipVoIPLink().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 (!account) {
            JAMI_ERR("No account detected");
            return;
        }
    
        if (Recordable::isRecording()) {
            deinitRecorder();
            stopRecording();
        }
    
        std::string toUri = account->getToUri(to);
        const pj_str_t dst(CONST_PJ_STR(toUri));
        JAMI_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)
    {
        auto toCall = Manager::instance().callFactory.getCall<SIPCall>(to);
        if (!toCall)
            return false;
    
        if (not toCall->inviteSession_ or not toCall->inviteSession_->dlg)
            return false;
    
        pjsip_dialog* target_dlg = toCall->inviteSession_->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(OnReadyCb&& cb)
    {
        // If ICE is currently negotiating, we must wait before hold the call
        if (isWaitingForIceAndMedia_) {
            holdCb_ = std::move(cb);
            remainingRequest_ = Request::HoldingOn;
            return false;
        }
    
        auto result = hold();
    
        if (cb)
            cb(result);
    
        return result;
    }
    
    bool
    SIPCall::hold()
    {
        if (not setState(CallState::HOLD))
            return false;
    
        stopAllMedia();
    
        if (getConnectionState() == ConnectionState::CONNECTED) {
            if (SIPSessionReinvite() != PJ_SUCCESS) {
                JAMI_WARN("[call:%s] Reinvite failed", getCallId().c_str());
                return true;
            }
        }
    
        isWaitingForIceAndMedia_ = true;
        return true;
    }
    
    bool
    SIPCall::offhold(OnReadyCb&& cb)
    {
        // If ICE is currently negotiating, we must wait before unhold the call
        if (isWaitingForIceAndMedia_) {
            offHoldCb_ = std::move(cb);
            remainingRequest_ = Request::HoldingOff;
            return false;
        }
    
        auto result = unhold();
    
        if (cb)
            cb(result);
    
        return result;
    }
    
    bool
    SIPCall::unhold()
    {
        auto account = getSIPAccount();
        if (!account) {
            JAMI_ERR("No account detected");
            return false;
        }
    
        bool success = false;
        try {
            success = internalOffHold([] {});
        } catch (const SdpException& e) {
            JAMI_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) {
                JAMI_WARN("[call:%s] resuming hold", getCallId().c_str());
                if (isWaitingForIceAndMedia_) {
                    remainingRequest_ = Request::HoldingOn;
                } else {
                    hold();
                }
                return false;
            }
        }
    
        return true;
    }
    
    void
    SIPCall::switchInput(const std::string& source)
    {
        JAMI_DBG("[call:%s] Set selected source to %s", getCallId().c_str(), source.c_str());
    
        for (auto const& stream : rtpStreams_) {
            auto mediaAttr = stream.mediaAttribute_;
            mediaAttr->sourceUri_ = source;
        }
    
        // Check if the call is being recorded in order to continue
        // ... the recording after the switch
        bool isRec = Call::isRecording();
    
        if (isWaitingForIceAndMedia_) {
            remainingRequest_ = Request::SwitchInput;
        } else {
            if (SIPSessionReinvite() == PJ_SUCCESS) {
                isWaitingForIceAndMedia_ = true;
            }
        }
        if (isRec) {
            readyToRecord_ = false;
            resetMediaReady();
            pendingRecord_ = true;
        }
    }
    
    void
    SIPCall::peerHungup()
    {
        pendingRecord_ = false;
        // Stop all RTP streams
        stopAllMedia();
    
        if (inviteSession_)
            terminateSipSession(PJSIP_SC_NOT_FOUND);
    
        Call::peerHungup();
    }
    
    void
    SIPCall::carryingDTMFdigits(char code)
    {
        int duration = Manager::instance().voipPreferences.getPulseLength();
        char dtmf_body[1000];
        int ret;
    
        // handle flash code
        if (code == '!') {
            ret = snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=16\r\nDuration=%d\r\n", duration);
        } else {
            ret = snprintf(dtmf_body,
                           sizeof dtmf_body - 1,
                           "Signal=%c\r\nDuration=%d\r\n",
                           code,
                           duration);
        }
    
        try {
            sendSIPInfo({dtmf_body, (size_t) ret}, "dtmf-relay");
        } catch (const std::exception& e) {
            JAMI_ERR("Error sending DTMF: %s", e.what());
        }
    }
    
    void
    SIPCall::setVideoOrientation(int rotation)
    {
        std::string sip_body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
                               "<media_control><vc_primitive><to_encoder>"
                               "<device_orientation="
                               + std::to_string(-rotation)
                               + "/>"
                                 "</to_encoder></vc_primitive></media_control>";
    
        JAMI_DBG("Sending device orientation via SIP INFO %d", rotation);
    
        sendSIPInfo(sip_body, "media_control+xml");
    }
    
    void
    SIPCall::sendTextMessage(const std::map<std::string, std::string>& messages, const std::string& from)
    {
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        // 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 (inviteSession_) {
                try {
                    im::sendSipMessage(inviteSession_.get(), messages);
                } catch (...) {
                }
            } else {
                pendingOutMessages_.emplace_back(messages, from);
                JAMI_ERR("[call:%s] sendTextMessage: no invite session for this call",
                         getCallId().c_str());
            }
        }
    }
    
    void
    SIPCall::removeCall()
    {
    #ifdef ENABLE_PLUGIN
        jami::Manager::instance().getJamiPluginManager().getCallServicesManager().clearCallHandlerMaps(
            getCallId());
    #endif
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        JAMI_DBG("[call:%s] removeCall()", getCallId().c_str());
        if (sdp_) {
            sdp_->setActiveLocalSdpSession(nullptr);
            sdp_->setActiveRemoteSdpSession(nullptr);
        }
        Call::removeCall();
    
        {
            std::lock_guard<std::mutex> lk(transportMtx_);
            resetTransport(std::move(mediaTransport_));
        }
    
        setInviteSession();
        setTransport({});
    }
    
    void
    SIPCall::onFailure(signed cause)
    {
        if (setState(CallState::MERROR, ConnectionState::DISCONNECTED, cause)) {
            runOnMainThread([w = weak()] {
                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 = weak()] {
            if (auto shared = w.lock()) {
                auto& call = *shared;
                Manager::instance().callBusy(call);
                call.removeCall();
            }
        });
    }
    
    void
    SIPCall::onClosed()
    {
        runOnMainThread([w = weak()] {
            if (auto shared = w.lock()) {
                auto& call = *shared;
                Manager::instance().peerHungupCall(call);
                call.removeCall();
            }
        });
    }
    
    void
    SIPCall::onAnswered()
    {
        JAMI_WARN("[call:%s] onAnswered()", getCallId().c_str());
        runOnMainThread([w = weak()] {
            if (auto shared = w.lock()) {
                if (shared->getConnectionState() != ConnectionState::CONNECTED) {
                    shared->setState(CallState::ACTIVE, ConnectionState::CONNECTED);
                    if (not shared->isSubcall()) {
                        Manager::instance().peerAnsweredCall(*shared);
                    }
                }
            }
        });
    }
    
    void
    SIPCall::sendKeyframe()
    {
    #ifdef ENABLE_VIDEO
        dht::ThreadPool::computation().run([w = weak()] {
            if (auto sthis = w.lock()) {
                JAMI_DBG("handling picture fast update request");
                if (auto const& videoRtp = sthis->getVideoRtp()) {
                    videoRtp->forceKeyFrame();
                }
            }
        });
    #endif
    }
    
    bool
    SIPCall::isIceEnabled() const
    {
        return enableIce_;
    }
    
    void
    SIPCall::setPeerUaVersion(std::string_view ua)
    {
        if (peerUserAgent_ == ua or ua.empty()) {
            // Silently ignore if it did not change or empty.
            return;
        }
    
        if (peerUserAgent_.empty()) {
            JAMI_DBG("[call:%s] Set peer's User-Agent to [%.*s]",
                     getCallId().c_str(),
                     (int) ua.size(),
                     ua.data());
        } else if (not peerUserAgent_.empty()) {
            // Unlikely, but should be handled since we dont have control over the peer.
            // Even if it's unexpected, we still try to parse the UA version.
            JAMI_WARN("[call:%s] Peer's User-Agent unexpectedly changed from [%s] to [%.*s]",
                      getCallId().c_str(),
                      peerUserAgent_.c_str(),
                      (int) ua.size(),
                      ua.data());
        }
    
        peerUserAgent_ = ua;
    
        // User-agent parsing
        constexpr std::string_view PACK_NAME(PACKAGE_NAME " ");
        auto pos = ua.find(PACK_NAME);
        if (pos == std::string_view::npos) {
            // Must have the expected package name.
            JAMI_WARN("Could not find the expected package name in peer's User-Agent");
            return;
        }
    
        ua = ua.substr(pos + PACK_NAME.length());
    
        std::string_view version;
        // Unstable (un-released) versions has a hiphen + commit Id after
        // the version number. Find the commit Id if any, and ignore it.
        pos = ua.find('-');
        if (pos != std::string_view::npos) {
            // Get the version and ignore the commit ID.
            version = ua.substr(0, pos);
        } else {
            // Extract the version number.
            pos = ua.find(' ');
            if (pos != std::string_view::npos) {
                version = ua.substr(0, pos);
            }
        }
    
        if (version.empty()) {
            JAMI_DBG("[call:%s] Could not parse peer's version", getCallId().c_str());
            return;
        }
    
        auto peerVersion = split_string_to_unsigned(version, '.');
        if (peerVersion.size() > 4u) {
            JAMI_WARN("[call:%s] Could not parse peer's version", getCallId().c_str());
            return;
        }
    
        // Check if peer's version is at least 10.0.2 to enable multi-stream.
        peerSupportMultiStream_ = Account::meetMinimumRequiredVersion(peerVersion,
                                                                      MULTISTREAM_REQUIRED_VERSION);
        if (not peerSupportMultiStream_) {
            JAMI_DBG(
                "Peer's version [%.*s] does not support multi-stream. Min required version: [%.*s]",
                (int) version.size(),
                version.data(),
                (int) MULTISTREAM_REQUIRED_VERSION_STR.size(),
                MULTISTREAM_REQUIRED_VERSION_STR.data());
        }
    }
    
    void
    SIPCall::onPeerRinging()
    {
        JAMI_DBG("[call:%s] Peer ringing", getCallId().c_str());
        setState(ConnectionState::RINGING);
    }
    
    void
    SIPCall::addLocalIceAttributes()
    {
        if (not isIceEnabled())
            return;
    
        auto mediaTransport = getIceMedia();
        if (not mediaTransport) {
            JAMI_ERR("[call:%s] Invalid ICE instance", getCallId().c_str());
            return;
        }
    
        auto start = std::chrono::steady_clock::now();
    
        if (not mediaTransport->isInitialized()) {
            JAMI_DBG("[call:%s] Waiting for ICE initialization", getCallId().c_str());
            // we need an initialized ICE to progress further
            if (mediaTransport->waitForInitialization(DEFAULT_ICE_INIT_TIMEOUT) <= 0) {
                JAMI_ERR("[call:%s] ICE initialization timed out", getCallId().c_str());
                return;
            }
            // ICE initialization may take longer than usual in some cases,
            // for instance when TURN servers do not respond in time (DNS
            // resolution or other issues).
            auto duration = std::chrono::steady_clock::now() - start;
            if (duration > EXPECTED_ICE_INIT_MAX_TIME) {
                JAMI_WARN("[call:%s] ICE initialization time was unexpectedly high (%ld ms)",
                          getCallId().c_str(),
                          std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
            }
        }
    
        // Check the state of ICE instance, the initialization may have failed.
        if (not mediaTransport->isInitialized()) {
            JAMI_ERR("[call:%s] ICE session is not initialized", getCallId().c_str());
            return;
        }
    
        // Check the state, the call might have been canceled while waiting.
        // for initialization.
        if (getState() == Call::CallState::OVER) {
            JAMI_WARN("[call:%s] The call was terminated while waiting for ICE initialization",
                      getCallId().c_str());
            return;
        }
    
        auto account = getSIPAccount();
        if (not account) {
            JAMI_ERR("No account detected");
            return;
        }
        if (not sdp_) {
            JAMI_ERR("No sdp detected");
            return;
        }
    
        JAMI_DBG("[call:%s] Add local attributes for ICE instance [%p]",
                 getCallId().c_str(),
                 mediaTransport.get());
    
        sdp_->addIceAttributes(mediaTransport->getLocalAttributes());
    
        if (account->isIceCompIdRfc5245Compliant()) {
            unsigned streamIdx = 0;
            for (auto const& stream : rtpStreams_) {
                if (not stream.mediaAttribute_->enabled_) {
                    // Dont add ICE candidates if the media is disabled
                    JAMI_DBG("[call:%s] media [%s] @ %u is disabled, dont add local candidates",
                             getCallId().c_str(),
                             stream.mediaAttribute_->toString().c_str(),
                             streamIdx);
                    continue;
                }
                JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
                         getCallId().c_str(),
                         stream.mediaAttribute_->toString().c_str(),
                         streamIdx);
                // RTP
                sdp_->addIceCandidates(streamIdx,
                                       mediaTransport->getLocalCandidates(streamIdx, ICE_COMP_ID_RTP));
                // RTCP if it has its own port
                if (not rtcpMuxEnabled_) {
                    sdp_->addIceCandidates(streamIdx,
                                           mediaTransport->getLocalCandidates(streamIdx,
                                                                              ICE_COMP_ID_RTP + 1));
                }
    
                streamIdx++;
            }
        } else {
            unsigned idx = 0;
            unsigned compId = 1;
            for (auto const& stream : rtpStreams_) {
                if (not stream.mediaAttribute_->enabled_) {
                    // Skipping local ICE candidates if the media is disabled
                    continue;
                }
                JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
                         getCallId().c_str(),
                         stream.mediaAttribute_->toString().c_str(),
                         idx);
                // RTP
                sdp_->addIceCandidates(idx, mediaTransport->getLocalCandidates(compId));
                compId++;
    
                // RTCP if it has its own port
                if (not rtcpMuxEnabled_) {
                    sdp_->addIceCandidates(idx, mediaTransport->getLocalCandidates(compId));
                    compId++;
                }
    
                idx++;
            }
        }
    }
    
    std::vector<IceCandidate>
    SIPCall::getAllRemoteCandidates(IceTransport& transport) const
    {
        std::vector<IceCandidate> rem_candidates;
        for (unsigned mediaIdx = 0; mediaIdx < static_cast<unsigned>(rtpStreams_.size()); mediaIdx++) {
            IceCandidate cand;
            for (auto& line : sdp_->getIceCandidates(mediaIdx)) {
                if (transport.parseIceAttributeLine(mediaIdx, line, cand)) {
                    JAMI_DBG("[call:%s] Add remote ICE candidate: %s",
                             getCallId().c_str(),
                             line.c_str());
                    rem_candidates.emplace_back(std::move(cand));
                }
            }
        }
        return rem_candidates;
    }
    
    std::shared_ptr<AccountCodecInfo>
    SIPCall::getVideoCodec() const
    {
    #ifdef ENABLE_VIDEO
        if (auto const& videoRtp = getVideoRtp())
            return videoRtp->getCodec();
    #endif
        return {};
    }
    
    std::shared_ptr<AccountCodecInfo>
    SIPCall::getAudioCodec() const
    {
        if (auto const& audioRtp = getAudioRtp())
            return audioRtp->getCodec();
        return {};
    }
    
    void
    SIPCall::addMediaStream(const MediaAttribute& mediaAttr)
    {
        // Create and add the media stream with the provided attribute.
        // Do not create the RTP sessions yet.
        RtpStream stream;
        stream.mediaAttribute_ = std::make_shared<MediaAttribute>(mediaAttr);
    
        // Set default media source if empty. Kept for backward compatibility.
    #ifdef ENABLE_VIDEO
        if (stream.mediaAttribute_->sourceUri_.empty()) {
            stream.mediaAttribute_->sourceUri_
                = Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice();
        }
    #endif
    
        if (stream.mediaAttribute_->sourceType_ == MediaSourceType::NONE) {
            stream.mediaAttribute_->sourceType_ = MediaSourceType::CAPTURE_DEVICE;
        }
    
        rtpStreams_.emplace_back(std::move(stream));
    }
    
    size_t
    SIPCall::initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
    {
        for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
            auto const& mediaAttr = mediaAttrList.at(idx);
            if (mediaAttr.type_ != MEDIA_AUDIO && mediaAttr.type_ != MEDIA_VIDEO) {
                JAMI_ERR("[call:%s] Unexpected media type %u", getCallId().c_str(), mediaAttr.type_);
                assert(false);
            }
    
            addMediaStream(mediaAttr);
            auto& stream = rtpStreams_.back();
            createRtpSession(stream);
    
            JAMI_DBG("[call:%s] Added media @%lu: %s",
                     getCallId().c_str(),
                     idx,
                     stream.mediaAttribute_->toString(true).c_str());
        }
    
        JAMI_DBG("[call:%s] Created %lu Media streams", getCallId().c_str(), rtpStreams_.size());
    
        return rtpStreams_.size();
    }
    
    bool
    SIPCall::hasVideo() const
    {
    #ifdef ENABLE_VIDEO
        std::function<bool(const RtpStream& stream)> videoCheck = [](auto const& stream) {
            return stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO;
        };
    
        const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), videoCheck);
    
        return iter != rtpStreams_.end();
    #else
        return false;
    #endif
    }
    
    bool
    SIPCall::isCaptureDeviceMuted(const MediaType& mediaType) const
    {
        // Return true only if all media of type 'mediaType' that use capture devices
        // source, are muted.
        std::function<bool(const RtpStream& stream)> mutedCheck = [&mediaType](auto const& stream) {
            return (stream.mediaAttribute_->type_ == mediaType
                    and stream.mediaAttribute_->sourceType_ == MediaSourceType::CAPTURE_DEVICE
                    and not stream.mediaAttribute_->muted_);
        };
        const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck);
        return iter == rtpStreams_.end();
    }
    
    void
    SIPCall::updateNegotiatedMedia()
    {
        JAMI_DBG("[call:%s] updating negotiated media", getCallId().c_str());
    
        if (not transport_ or not sdp_) {
            JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
            return;
        }
    
        auto slots = sdp_->getMediaSlots();
        bool peer_holding {true};
        int streamIdx = -1;
    
        for (const auto& slot : slots) {
            streamIdx++;
            const auto& local = slot.first;
            const auto& remote = slot.second;
    
            // Skip disabled media
            if (not local.enabled) {
                JAMI_DBG("[call:%s] [SDP:slot#%u] The media is disabled, skipping",
                         getCallId().c_str(),
                         streamIdx);
                continue;
            }
    
            if (static_cast<size_t>(streamIdx) >= rtpStreams_.size()) {
                throw std::runtime_error("Stream index is out-of-range");
            }
    
            auto const& rtpStream = rtpStreams_[streamIdx];
    
            if (not rtpStream.mediaAttribute_) {
                throw std::runtime_error("Missing media attribute");
            }
    
            // To enable a media, it must be enabled on both sides.
            rtpStream.mediaAttribute_->enabled_ = local.enabled and remote.enabled;
    
            if (not rtpStream.rtpSession_)
                throw std::runtime_error("Must have a valid RTP Session");
    
            if (local.type != MEDIA_AUDIO && local.type != MEDIA_VIDEO) {
                JAMI_ERR("[call:%s] Unexpected media type %u", getCallId().c_str(), local.type);
                throw std::runtime_error("Invalid media attribute");
            }
    
            if (local.type != remote.type) {
                JAMI_ERR("[call:%s] [SDP:slot#%u] Inconsistent media type between local and remote",
                         getCallId().c_str(),
                         streamIdx);
                continue;
            }
    
            if (local.enabled and not local.codec) {
                JAMI_WARN("[call:%s] [SDP:slot#%u] Missing local codec", getCallId().c_str(), streamIdx);
                continue;
            }
    
            if (remote.enabled and not remote.codec) {
                JAMI_WARN("[call:%s] [SDP:slot#%u] Missing remote codec",
                          getCallId().c_str(),
                          streamIdx);
                continue;
            }
    
            if (isSrtpEnabled() and local.enabled and not local.crypto) {
                JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no local crypto attributes. "
                          "Ignoring the media",
                          getCallId().c_str(),
                          streamIdx);
                continue;
            }
    
            if (isSrtpEnabled() and remote.enabled and not remote.crypto) {
                JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no crypto remote attributes. "
                          "Ignoring the media",
                          getCallId().c_str(),
                          streamIdx);
                continue;
            }
    
            // Aggregate holding info over all remote streams
            peer_holding &= remote.onHold;
    
            configureRtpSession(rtpStream.rtpSession_, rtpStream.mediaAttribute_, local, remote);
        }
    
        if (not isSubcall() and peerHolding_ != peer_holding) {
            peerHolding_ = peer_holding;
            emitSignal<DRing::CallSignal::PeerHold>(getCallId(), peerHolding_);
        }
    
        // Notify using the parent Id if it's a subcall.
        auto callId = isSubcall() ? parent_->getCallId() : getCallId();
        emitSignal<DRing::CallSignal::MediaNegotiationStatus>(
            callId,
            DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS,
            MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()));
    }
    
    void
    SIPCall::startAllMedia()
    {
        JAMI_DBG("[call:%s] starting the media", getCallId().c_str());
    
        if (not transport_ or not sdp_) {
            JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
            return;
        }
    
        if (isSrtpEnabled() && not transport_->isSecure()) {
            JAMI_WARN("[call:%s] Crypto (SRTP) is negotiated over an insecure signaling transport",
                      getCallId().c_str());
        }
    
        // reset
        readyToRecord_ = false;
        resetMediaReady();
    #ifdef ENABLE_VIDEO
        bool isVideoEnabled = false;
    #endif
    
        int currentCompId = 1;
    
        for (auto iter = rtpStreams_.begin(); iter != rtpStreams_.end(); iter++) {
            if (not iter->mediaAttribute_) {
                throw std::runtime_error("Missing media attribute");
            }
    
    #ifdef ENABLE_VIDEO
            if (iter->mediaAttribute_->type_ == MEDIA_VIDEO)
                isVideoEnabled |= iter->mediaAttribute_->enabled_;
    #endif
    
            // 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()) {
                    // Create sockets for RTP and RTCP, and start the session.
                    auto iceRtpSocket = newIceSocket(currentCompId++);
    
                    std::unique_ptr<IceSocket> iceRtcpSocket;
                    if (not rtcpMuxEnabled_) {
                        iceRtcpSocket = newIceSocket(currentCompId++);
                    }
                    iter->rtpSession_->start(std::move(iceRtpSocket), std::move(iceRtcpSocket));
                } else {
                    iter->rtpSession_->start(nullptr, nullptr);
                }
            }
        }
    
    #ifdef ENABLE_VIDEO
        // TODO. Move this elsewhere (when adding participant to conf?)
        if (!isVideoEnabled && !getConfId().empty()) {
            auto conference = Manager::instance().getConferenceFromID(getConfId());
            conference->attachVideo(getReceiveVideoFrameActiveWriter().get(), getCallId());
        }
    #endif
    
        // Media is restarted, we can process the last holding request.
        isWaitingForIceAndMedia_ = false;
        if (remainingRequest_ != Request::NoRequest) {
            bool result = true;
            switch (remainingRequest_) {
            case Request::HoldingOn:
                result = hold();
                if (holdCb_) {
                    holdCb_(result);
                    holdCb_ = nullptr;
                }
                break;
            case Request::HoldingOff:
                result = unhold();
                if (offHoldCb_) {
                    offHoldCb_(result);
                    offHoldCb_ = nullptr;
                }
                break;
            case Request::SwitchInput:
                SIPSessionReinvite();
                break;
            default:
                break;
            }
            remainingRequest_ = Request::NoRequest;
        }
    
    #ifdef ENABLE_PLUGIN
        // Create AVStreams associated with the call
        createCallAVStreams();
    #endif
    }
    
    void
    SIPCall::restartMediaSender()
    {
        JAMI_DBG("[call:%s] restarting TX media streams", getCallId().c_str());
        auto const& audioRtp = getAudioRtp();
        if (audioRtp)
            audioRtp->restartSender();
    
    #ifdef ENABLE_VIDEO
        if (hasVideo()) {
            auto const& videoRtp = getVideoRtp();
            if (videoRtp)
                videoRtp->restartSender();
        }
    #endif
    }
    
    void
    SIPCall::stopAllMedia()
    {
        JAMI_DBG("[call:%s] stopping all medias", getCallId().c_str());
        if (Recordable::isRecording()) {
            deinitRecorder();
            stopRecording(); // if call stops, finish recording
        }
        auto const& audioRtp = getAudioRtp();
        if (audioRtp)
            audioRtp->stop();
    #ifdef ENABLE_VIDEO
        auto const& videoRtp = getVideoRtp();
    
        {
            std::lock_guard<std::mutex> lk(sinksMtx_);
            for (auto it = callSinksMap_.begin(); it != callSinksMap_.end();) {
                auto& videoReceive = videoRtp->getVideoReceive();
                if (videoReceive) {
                    videoReceive->detach(it->second.get());
                }
    
                it->second->stop();
                it = callSinksMap_.erase(it);
            }
        }
    
        if (videoRtp)
            videoRtp->stop();
    #endif
    
    #ifdef ENABLE_PLUGIN
        {
            clearCallAVStreams();
            std::lock_guard<std::mutex> lk(avStreamsMtx_);
            Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject(
                getCallId());
        }
    #endif
    }
    
    void
    SIPCall::muteMedia(const std::string& mediaType, bool mute)
    {
        auto type = MediaAttribute::stringToMediaType(mediaType);
    
        if (type == MediaType::MEDIA_AUDIO) {
            JAMI_WARN("[call:%s] %s all audio medias",
                      getCallId().c_str(),
                      mute ? "muting " : "un-muting ");
    
        } else if (type == MediaType::MEDIA_VIDEO) {
            JAMI_WARN("[call:%s] %s all video medias",
                      getCallId().c_str(),
                      mute ? "muting" : "un-muting");
        } else {
            JAMI_ERR("[call:%s] invalid media type %s", getCallId().c_str(), mediaType.c_str());
            assert(false);
        }
    
        // Get the current media attributes.
        auto mediaList = getMediaAttributeList();
    
        // Mute/Un-mute all medias with matching type.
        for (auto& mediaAttr : mediaList) {
            if (mediaAttr.type_ == type) {
                mediaAttr.muted_ = mute;
            }
        }
    
        // Apply
        requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList));
    }
    
    void
    SIPCall::updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx)
    {
        assert(streamIdx < rtpStreams_.size());
    
        auto const& rtpStream = rtpStreams_[streamIdx];
        assert(rtpStream.rtpSession_);
    
        auto const& mediaAttr = rtpStream.mediaAttribute_;
        assert(mediaAttr);
    
        bool notify = false;
    
        if (newMediaAttr.muted_ == mediaAttr->muted_) {
            // Nothing to do. Already in the desired state.
            JAMI_DBG("[call:%s] [%s] already %s",
                     getCallId().c_str(),
                     mediaAttr->label_.c_str(),
                     mediaAttr->muted_ ? "muted " : "un-muted ");
    
        } else {
            notify = true;
        }
    
        // Update
        mediaAttr->muted_ = newMediaAttr.muted_;
        // Only update source and type if actually set.
        if (not newMediaAttr.sourceUri_.empty())
            mediaAttr->sourceUri_ = newMediaAttr.sourceUri_;
        if (newMediaAttr.sourceType_ != MediaSourceType::NONE)
            mediaAttr->sourceType_ = newMediaAttr.sourceType_;
    
        JAMI_DBG("[call:%s] %s [%s]",
                 getCallId().c_str(),
                 mediaAttr->muted_ ? "muting" : "un-muting",
                 mediaAttr->label_.c_str());
    
        if (notify and mediaAttr->type_ == MediaType::MEDIA_AUDIO) {
            rtpStream.rtpSession_->setMuted(mediaAttr->muted_);
            setMute(mediaAttr->muted_);
            if (not isSubcall())
                emitSignal<DRing::CallSignal::AudioMuted>(getCallId(), mediaAttr->muted_);
            return;
        }
    
    #ifdef ENABLE_VIDEO
        if (notify and mediaAttr->type_ == MediaType::MEDIA_VIDEO and not isSubcall()) {
            emitSignal<DRing::CallSignal::VideoMuted>(getCallId(), mediaAttr->muted_);
        }
    #endif
    }
    
    void
    SIPCall::updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
    {
        JAMI_DBG("[call:%s] New local medias", getCallId().c_str());
    
        unsigned idx = 0;
        for (auto const& newMediaAttr : mediaAttrList) {
            JAMI_DBG("[call:%s] Media @%u: %s",
                     getCallId().c_str(),
                     idx++,
                     newMediaAttr.toString(true).c_str());
        }
    
        JAMI_DBG("[call:%s] Updating local media streams", getCallId().c_str());
    
        for (auto const& newAttr : mediaAttrList) {
            auto streamIdx = findRtpStreamIndex(newAttr.label_);
    
            if (streamIdx == rtpStreams_.size()) {
                // Media does not exist, add a new one.
                addMediaStream(newAttr);
                auto& stream = rtpStreams_.back();
                createRtpSession(stream);
                JAMI_DBG("[call:%s] Added a new media stream [%s] @ index %lu",
                         getCallId().c_str(),
                         stream.mediaAttribute_->label_.c_str(),
                         streamIdx);
            } else {
                updateMediaStream(newAttr, streamIdx);
            }
        }
    }
    
    bool
    SIPCall::isReinviteRequired(const std::vector<MediaAttribute>& mediaAttrList)
    {
        if (mediaAttrList.size() != rtpStreams_.size())
            return true;
    
        for (auto const& newAttr : mediaAttrList) {
            auto streamIdx = findRtpStreamIndex(newAttr.label_);
    
            if (streamIdx == rtpStreams_.size()) {
                // Always needs a reinvite when a new media is added.
                return true;
            }
    
    #ifdef ENABLE_VIDEO
            if (newAttr.type_ == MediaType::MEDIA_VIDEO) {
                assert(rtpStreams_[streamIdx].mediaAttribute_);
                // Changes in video attributes always trigger a re-invite.
                return newAttr.muted_ != rtpStreams_[streamIdx].mediaAttribute_->muted_;
            }
    #endif
        }
    
        return false;
    }
    
    bool
    SIPCall::requestMediaChange(const std::vector<DRing::MediaMap>& mediaList)
    {
        auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
    
        // If the peer does not support multi-stream and the size of the new
        // media list is different from the current media list, the media
        // change request will be ignored.
        if (not peerSupportMultiStream_ and rtpStreams_.size() != mediaAttrList.size()) {
            JAMI_WARN("[call:%s] Peer does not support multi-stream. Media change request ignored",
                      getCallId().c_str());
            return false;
        }
    
        JAMI_DBG("[call:%s] Requesting media change. List of new media:", getCallId().c_str());
    
        unsigned idx = 0;
        for (auto const& newMediaAttr : mediaAttrList) {
            JAMI_DBG("[call:%s] Media @%u: %s",
                     getCallId().c_str(),
                     idx++,
                     newMediaAttr.toString(true).c_str());
        }
    
        auto needReinvite = isReinviteRequired(mediaAttrList);
    
        updateAllMediaStreams(mediaAttrList);
    
        if (needReinvite) {
            JAMI_DBG("[call:%s] Media change requires a new negotiation (re-invite)",
                     getCallId().c_str());
            requestReinvite();
        }
    
        return true;
    }
    
    std::vector<MediaAttribute>
    SIPCall::getMediaAttributeList() const
    {
        std::vector<MediaAttribute> mediaList;
        mediaList.reserve(rtpStreams_.size());
        for (auto const& stream : rtpStreams_) {
            mediaList.emplace_back(*stream.mediaAttribute_);
        }
        return mediaList;
    }
    
    /// \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 asynchronously when
    /// the transport is negotiated.
    void
    SIPCall::onMediaNegotiationComplete()
    {
        JAMI_WARN("[call:%s] Media negotiation complete", getCallId().c_str());
    
        // Main call (no subcalls) must wait for ICE now, the rest of code needs to access
        // to a negotiated transport.
        runOnMainThread([w = weak()] {
            if (auto this_ = w.lock()) {
                std::lock_guard<std::recursive_mutex> lk {this_->callMutex_};
                JAMI_WARN("[call:%s] media changed", this_->getCallId().c_str());
                // The call is already ended, so we don't need to restart medias
                if (not this_->inviteSession_
                    or this_->inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED
                    or not this_->sdp_) {
                    return;
                }
    
                bool hasIce = this_->isIceEnabled();
                if (hasIce) {
                    // If ICE is not used, start medias now
                    auto rem_ice_attrs = this_->sdp_->getIceAttributes();
                    hasIce = not rem_ice_attrs.ufrag.empty() and not rem_ice_attrs.pwd.empty();
                }
                if (hasIce) {
                    if (not this_->isSubcall()) {
                        // Start ICE checks. Media will be started once ICE checks complete.
                        this_->startIceMedia();
                    }
                } else {
                    // No ICE, start media now.
                    JAMI_WARN("[call:%s] ICE media disabled, using default media ports",
                              this_->getCallId().c_str());
                    // Update the negotiated media.
                    this_->updateNegotiatedMedia();
    
                    // Start the media.
                    this_->stopAllMedia();
                    this_->startAllMedia();
                }
            }
        });
    }
    
    void
    SIPCall::startIceMedia()
    {
        JAMI_DBG("[call:%s] Starting ICE", getCallId().c_str());
        auto mediaTransport = getIceMedia();
    
        if (not mediaTransport or mediaTransport->isFailed()) {
            JAMI_ERR("[call:%s] Media ICE init failed", getCallId().c_str());
            onFailure(EIO);
            return;
        }
    
        if (mediaTransport->isStarted()) {
            // NOTE: for incoming calls, the ice is already there and running
            if (mediaTransport->isRunning())
                onIceNegoSucceed();
            return;
        }
    
        if (not mediaTransport->isInitialized()) {
            // In this case, onInitDone will occurs after the startIceMedia
            waitForIceInit_ = true;
            return;
        }
    
        // Start transport on SDP data and wait for negotiation
        if (!sdp_)
            return;
        auto rem_ice_attrs = sdp_->getIceAttributes();
        if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
            JAMI_ERR("[call:%s] Media ICE attributes empty", getCallId().c_str());
            onFailure(EIO);
            return;
        }
        if (not mediaTransport->startIce(rem_ice_attrs, getAllRemoteCandidates(*mediaTransport))) {
            JAMI_ERR("[call:%s] Media ICE start failed", getCallId().c_str());
            onFailure(EIO);
        }
    }
    
    void
    SIPCall::onIceNegoSucceed()
    {
        JAMI_DBG("[call:%s] ICE negotiation succeeded", getCallId().c_str());
    
        // Check if the call is already ended, so we don't need to restart medias
        // This is typically the case in a multi-device context where one device
        // can stop a call. So do not start medias
        if (not inviteSession_ or inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED or not sdp_) {
            JAMI_ERR("[call:%s] ICE negotiation succeeded, but call is in invalid state",
                     getCallId().c_str());
            return;
        }
    
        // Update the negotiated media.
        updateNegotiatedMedia();
    
        // Nego succeed: move to the new media transport
        stopAllMedia();
    
        startAllMedia();
    }
    
    pj_status_t
    SIPCall::onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdata)
    {
        JAMI_DBG("[call:%s] Received a re-invite", getCallId().c_str());
    
        pj_status_t res = PJ_SUCCESS;
    
        if (not sdp_) {
            JAMI_ERR("SDP session is invalid");
            return res;
        }
    
        sdp_->clearIce();
        auto acc = getSIPAccount();
        if (not acc) {
            JAMI_ERR("No account detected");
            return res;
        }
    
        Sdp::printSession(offer, "Remote session (media change request)", SdpDirection::OFFER);
    
        sdp_->setReceivedOffer(offer);
    
        auto const& mediaAttrList = Sdp::getMediaAttributeListFromSdp(offer);
        if (mediaAttrList.empty()) {
            JAMI_WARN("[call:%s] Media list is empty, ignoring", getCallId().c_str());
            return res;
        }
    
        if (upnp_) {
            openPortsUPnP();
        }
    
        pjsip_tx_data* tdata = nullptr;
        if (pjsip_inv_initial_answer(inviteSession_.get(), rdata, PJSIP_SC_TRYING, NULL, NULL, &tdata)
            != PJ_SUCCESS) {
            JAMI_ERR("Could not create answer TRYING");
            return res;
        }
    
        // Report the change request.
        auto const& remoteMediaList = MediaAttribute::mediaAttributesToMediaMaps(mediaAttrList);
        // TODO_MC. Validate this assessment.
        // Report re-invites only if the number of media changed, otherwise answer
        // using the current local attributes.
        if (acc->isMultiStreamEnabled() and remoteMediaList.size() != rtpStreams_.size()) {
            Manager::instance().mediaChangeRequested(getCallId(), getAccountId(), remoteMediaList);
        } else {
            auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
            answerMediaChangeRequest(localMediaList);
        }
    
        return res;
    }
    
    int
    SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* rdata)
    {
        if (!sdp_)
            return !PJ_SUCCESS;
        sdp_->clearIce();
        auto acc = getSIPAccount();
        if (!acc) {
            JAMI_ERR("No account detected");
            return !PJ_SUCCESS;
        }
    
        JAMI_DBG("[call:%s] Received a new offer (re-invite)", getCallId().c_str());
    
        sdp_->setReceivedOffer(offer);
    
        // Use current media list.
        sdp_->processIncomingOffer(getMediaAttributeList());
    
        if (isIceEnabled() and offer != nullptr) {
            setupIceResponse();
        }
    
        sdp_->startNegotiation();
    
        pjsip_tx_data* tdata = nullptr;
    
        if (pjsip_inv_initial_answer(inviteSession_.get(),
                                     const_cast<pjsip_rx_data*>(rdata),
                                     PJSIP_SC_OK,
                                     NULL,
                                     NULL,
                                     &tdata)
            != PJ_SUCCESS) {
            JAMI_ERR("Could not create initial answer OK");
            return !PJ_SUCCESS;
        }
    
        // Add user-agent header
        sip_utils::addUserAgentHeader(getSIPAccount()->getUserAgentName(), tdata);
    
        if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, sdp_->getLocalSdpSession(), &tdata)
            != PJ_SUCCESS) {
            JAMI_ERR("Could not create answer OK");
            return !PJ_SUCCESS;
        }
    
        // ContactStr must stay in scope as long as tdata
        sip_utils::addContactHeader(getSIPAccount()->getContactHeader(getTransport()->get()), tdata);
    
        if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
            JAMI_ERR("Could not send msg OK");
            return !PJ_SUCCESS;
        }
    
        if (upnp_) {
            openPortsUPnP();
        }
    
        return PJ_SUCCESS;
    }
    
    void
    SIPCall::onReceiveOfferIn200OK(const pjmedia_sdp_session* offer)
    {
        if (not rtpStreams_.empty()) {
            JAMI_ERR("[call:%s] Unexpected offer in '200 OK' answer", getCallId().c_str());
            return;
        }
    
        auto acc = getSIPAccount();
        if (not acc) {
            JAMI_ERR("No account detected");
            return;
        }
    
        if (not sdp_) {
            JAMI_ERR("invalid SDP session");
            return;
        }
    
        JAMI_DBG("[call:%s] Received an offer in '200 OK' answer", getCallId().c_str());
    
        auto mediaList = Sdp::getMediaAttributeListFromSdp(offer);
        // If this method is called, it means we are expecting an offer
        // in the 200OK answer.
        if (mediaList.empty()) {
            JAMI_WARN("[call:%s] Remote media list is empty, ignoring", getCallId().c_str());
            return;
        }
    
        Sdp::printSession(offer, "Remote session (offer in 200 OK answer)", SdpDirection::OFFER);
        sdp_->clearIce();
        sdp_->setReceivedOffer(offer);
    
        // If we send an empty offer, video will be accepted only if locally
        // enabled by the user.
        for (auto& mediaAttr : mediaList) {
            if (mediaAttr.type_ == MediaType::MEDIA_VIDEO and not acc->isVideoEnabled()) {
                mediaAttr.enabled_ = false;
            }
        }
    
        initMediaStreams(mediaList);
    
        sdp_->processIncomingOffer(mediaList);
    
        if (upnp_) {
            openPortsUPnP();
        }
    
        if (isIceEnabled()) {
            setupIceResponse();
        }
    
        sdp_->startNegotiation();
    
        if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
            JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
                     getCallId().c_str());
        }
    }
    
    void
    SIPCall::openPortsUPnP()
    {
        if (not sdp_) {
            JAMI_ERR("[call:%s] Current SDP instance is invalid", getCallId().c_str());
            return;
        }
    
        /**
         * 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:
         * No need to request mappings for specfic port numbers. Set the port to '0' to
         * request the first available port (faster and more likely to succeed).
         */
        JAMI_DBG("[call:%s] opening ports via UPNP for SDP session", getCallId().c_str());
    
        // RTP port.
        upnp_->reserveMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP);
        // RTCP port.
        upnp_->reserveMapping(sdp_->getLocalAudioControlPort(), upnp::PortType::UDP);
    
    #ifdef ENABLE_VIDEO
        // RTP port.
        upnp_->reserveMapping(sdp_->getLocalVideoPort(), upnp::PortType::UDP);
        // RTCP port.
        upnp_->reserveMapping(sdp_->getLocalVideoControlPort(), upnp::PortType::UDP);
    #endif
    }
    
    std::map<std::string, std::string>
    SIPCall::getDetails() const
    {
        auto acc = getSIPAccount();
        if (!acc) {
            JAMI_ERR("No account detected");
            return {};
        }
    
        auto details = Call::getDetails();
    
        details.emplace(DRing::Call::Details::PEER_HOLDING, peerHolding_ ? TRUE_STR : FALSE_STR);
    
    #ifdef ENABLE_VIDEO
        for (auto const& stream : rtpStreams_) {
            if (stream.mediaAttribute_->type_ != MediaType::MEDIA_VIDEO)
                continue;
            details.emplace(DRing::Call::Details::VIDEO_SOURCE, stream.mediaAttribute_->sourceUri_);
            if (auto const& rtpSession = stream.rtpSession_) {
                if (auto codec = rtpSession->getCodec())
                    details.emplace(DRing::Call::Details::VIDEO_CODEC, codec->systemCodecInfo.name);
                else
                    details.emplace(DRing::Call::Details::VIDEO_CODEC, "");
            }
        }
    #endif
    
    #if HAVE_RINGNS
        if (not peerRegisteredName_.empty())
            details.emplace(DRing::Call::Details::REGISTERED_NAME, peerRegisteredName_);
    #endif
    
    #ifdef ENABLE_CLIENT_CERT
        std::lock_guard<std::recursive_mutex> lk {callMutex_};
        if (transport_ and transport_->isSecure()) {
            const auto& tlsInfos = transport_->getTlsInfos();
            if (tlsInfos.cipher != PJ_TLS_UNKNOWN_CIPHER) {
                const auto& cipher = pj_ssl_cipher_name(tlsInfos.cipher);
                details.emplace(DRing::TlsTransport::TLS_CIPHER, cipher ? cipher : "");
            } else {
                details.emplace(DRing::TlsTransport::TLS_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, std::to_string(n));
            } else {
                details.emplace(DRing::TlsTransport::TLS_PEER_CERT, "");
                details.emplace(DRing::TlsTransport::TLS_PEER_CA_NUM, "");
            }
        }
    #endif
        return details;
    }
    
    void
    SIPCall::enterConference(const std::string& confId)
    {
    #ifdef ENABLE_VIDEO
        auto conf = Manager::instance().getConferenceFromID(confId);
        if (conf == nullptr) {
            JAMI_ERR("Unknown conference [%s]", confId.c_str());
            return;
        }
    
        auto videoRtp = getVideoRtp();
        if (not videoRtp) {
            // In conference, we need to have a video RTP session even
            // if it's an audio only call
            videoRtp = addDummyVideoRtpSession();
            if (not videoRtp) {
                throw std::runtime_error("Failed to create dummy RTP video session");
            }
        }
    
        videoRtp->enterConference(conf.get());
    #endif
    
    #ifdef ENABLE_PLUGIN
        clearCallAVStreams();
    #endif
    }
    
    void
    SIPCall::exitConference()
    {
        auto const& audioRtp = getAudioRtp();
        if (audioRtp && !isCaptureDeviceMuted(MediaType::MEDIA_AUDIO)) {
            auto& rbPool = Manager::instance().getRingBufferPool();
            rbPool.bindCallID(getCallId(), RingBufferPool::DEFAULT_ID);
            rbPool.flush(RingBufferPool::DEFAULT_ID);
        }
    #ifdef ENABLE_VIDEO
        auto const& videoRtp = getVideoRtp();
        if (videoRtp)
            videoRtp->exitConference();
    #endif
    #ifdef ENABLE_PLUGIN
        createCallAVStreams();
    #endif
    }
    
    std::shared_ptr<Observable<std::shared_ptr<MediaFrame>>>
    SIPCall::getReceiveVideoFrameActiveWriter()
    {
    #ifdef ENABLE_VIDEO
        auto videoRtp = getVideoRtp();
        if (videoRtp)
            return videoRtp->getReceiveVideoFrameActiveWriter();
    #endif
    
        return {};
    }
    
    std::shared_ptr<video::VideoRtpSession>
    SIPCall::addDummyVideoRtpSession()
    {
    #ifdef ENABLE_VIDEO
        MediaAttribute mediaAttr(MediaType::MEDIA_VIDEO, true, true, false, "", "dummy video session");
        addMediaStream(mediaAttr);
        auto& stream = rtpStreams_.back();
        createRtpSession(stream);
        if (stream.rtpSession_) {
            return std::dynamic_pointer_cast<video::VideoRtpSession>(stream.rtpSession_);
        }
    #endif
    
        return {};
    }
    
    void
    SIPCall::createSinks(const ConfInfo& infos)
    {
    #ifdef ENABLE_VIDEO
        if (!hasVideo())
            return;
    
        std::lock_guard<std::mutex> lk(sinksMtx_);
        auto videoRtp = getVideoRtp();
        auto& videoReceive = videoRtp->getVideoReceive();
        if (!videoReceive)
            return;
        auto id = getConfId().empty() ? getCallId() : getConfId();
        Manager::instance().createSinkClients(id,
                                              infos,
                                              std::static_pointer_cast<video::VideoGenerator>(
                                                  videoReceive),
                                              callSinksMap_);
    #endif
    }
    
    std::shared_ptr<AudioRtpSession>
    SIPCall::getAudioRtp() const
    {
        // For the moment, the clients support only one audio stream, so we
        // return the first audio stream.
        for (auto const& stream : rtpStreams_) {
            auto rtp = stream.rtpSession_;
            if (rtp->getMediaType() == MediaType::MEDIA_AUDIO) {
                return std::dynamic_pointer_cast<AudioRtpSession>(rtp);
            }
        }
    
        return nullptr;
    }
    
    #ifdef ENABLE_VIDEO
    std::shared_ptr<video::VideoRtpSession>
    SIPCall::getVideoRtp() const
    {
        for (auto const& stream : rtpStreams_) {
            auto rtp = stream.rtpSession_;
            if (rtp->getMediaType() == MediaType::MEDIA_VIDEO) {
                return std::dynamic_pointer_cast<video::VideoRtpSession>(rtp);
            }
        }
        return nullptr;
    }
    #endif
    
    std::vector<std::shared_ptr<RtpSession>>
    SIPCall::getRtpSessionList() const
    {
        std::vector<std::shared_ptr<RtpSession>> rtpList;
        rtpList.reserve(rtpStreams_.size());
        for (auto const& stream : rtpStreams_) {
            rtpList.emplace_back(stream.rtpSession_);
        }
        return rtpList;
    }
    
    void
    SIPCall::monitor() const
    {
        if (isSubcall())
            return;
        auto acc = getSIPAccount();
        if (!acc) {
            JAMI_ERR("No account detected");
            return;
        }
        JAMI_DBG("- Call %s with %s:", getCallId().c_str(), getPeerNumber().c_str());
        JAMI_DBG("\t- Duration: %s", dht::print_duration(getCallDuration()).c_str());
        for (const auto& stream : rtpStreams_)
            JAMI_DBG("\t- Media: %s", stream.mediaAttribute_->toString(true).c_str());
    #ifdef ENABLE_VIDEO
        if (auto codec = getVideoCodec())
            JAMI_DBG("\t- Video codec: %s", codec->systemCodecInfo.name.c_str());
    #endif
        if (auto transport = getIceMedia()) {
            JAMI_DBG("\t- Medias: %s", transport->link().c_str());
        }
    }
    
    bool
    SIPCall::toggleRecording()
    {
        pendingRecord_ = true;
        if (not readyToRecord_)
            return true;
    
        // add streams to recorder before starting the record
        if (not Call::isRecording()) {
            updateRecState(true);
            auto account = getSIPAccount();
            if (!account) {
                JAMI_ERR("No account detected");
                return false;
            }
            auto title = fmt::format("Conversation at %TIMESTAMP between {} and {}",
                                     account->getUserUri(),
                                     peerUri_);
            recorder_->setMetadata(title, ""); // use default description
            auto const& audioRtp = getAudioRtp();
            if (audioRtp)
                audioRtp->initRecorder(recorder_);
    #ifdef ENABLE_VIDEO
            if (hasVideo()) {
                auto const& videoRtp = getVideoRtp();
                if (videoRtp)
                    videoRtp->initRecorder(recorder_);
            }
    #endif
        } else {
            updateRecState(false);
            deinitRecorder();
        }
        pendingRecord_ = false;
        return Call::toggleRecording();
    }
    
    void
    SIPCall::deinitRecorder()
    {
        if (Call::isRecording()) {
            auto const& audioRtp = getAudioRtp();
            if (audioRtp)
                audioRtp->deinitRecorder(recorder_);
    #ifdef ENABLE_VIDEO
            if (hasVideo()) {
                auto const& videoRtp = getVideoRtp();
                if (videoRtp)
                    videoRtp->deinitRecorder(recorder_);
            }
    #endif
        }
    }
    
    void
    SIPCall::InvSessionDeleter::operator()(pjsip_inv_session* inv) const noexcept
    {
        // prevent this from getting accessed in callbacks
        // JAMI_WARN: this is not thread-safe!
        if (!inv)
            return;
        inv->mod_data[Manager::instance().sipVoIPLink().getModId()] = nullptr;
        // NOTE: the counter is incremented by sipvoiplink (transaction_request_cb)
        pjsip_inv_dec_ref(inv);
    }
    
    void
    SIPCall::createIceMediaTransport()
    {
        auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
        std::lock_guard<std::mutex> lk(transportMtx_);
        resetTransport(std::move(mediaTransport_));
        mediaTransport_ = iceTransportFactory.createTransport(getCallId().c_str());
        if (mediaTransport_) {
            JAMI_DBG("[call:%s] Successfully created media ICE transport [ice:%p]",
                     getCallId().c_str(),
                     mediaTransport_.get());
        } else {
            JAMI_ERR("[call:%s] Failed to create media ICE transport", getCallId().c_str());
        }
    }
    
    bool
    SIPCall::initIceMediaTransport(bool master, std::optional<IceTransportOptions> options)
    {
        auto acc = getSIPAccount();
        if (!acc) {
            JAMI_ERR("No account detected");
            return false;
        }
    
        JAMI_DBG("[call:%s] Init media ICE transport", getCallId().c_str());
        auto mediaTransport = getIceMedia();
        if (not mediaTransport) {
            JAMI_ERR("[call:%s] Failed to create media ICE transport", getCallId().c_str());
            return false;
        }
    
        auto iceOptions = options == std::nullopt ? acc->getIceOptions() : *options;
    
        auto optOnInitDone = std::move(iceOptions.onInitDone);
        auto optOnNegoDone = std::move(iceOptions.onNegoDone);
        iceOptions.onInitDone = [w = weak(), cb = std::move(optOnInitDone)](bool ok) {
            runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
                auto call = w.lock();
                if (cb)
                    cb(ok);
                if (!ok or !call or !call->waitForIceInit_.exchange(false))
                    return;
    
                std::lock_guard<std::recursive_mutex> lk {call->callMutex_};
                auto rem_ice_attrs = call->sdp_->getIceAttributes();
                // Init done but no remote_ice_attributes, the ice->start will be triggered later
                if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty())
                    return;
                call->startIceMedia();
            });
        };
        iceOptions.onNegoDone = [w = weak(), cb = std::move(optOnNegoDone)](bool ok) {
            runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
                if (cb)
                    cb(ok);
                if (auto call = w.lock()) {
                    // The ICE is related to subcalls, but medias are handled by parent call
                    std::lock_guard<std::recursive_mutex> lk {call->callMutex_};
                    call = call->isSubcall() ? std::dynamic_pointer_cast<SIPCall>(call->parent_) : call;
                    if (!ok) {
                        JAMI_ERR("[call:%s] Media ICE negotiation failed", call->getCallId().c_str());
                        call->onFailure(EIO);
                        return;
                    }
                    call->onIceNegoSucceed();
                }
            });
        };
    
        iceOptions.master = master;
        iceOptions.streamsCount = static_cast<unsigned>(rtpStreams_.size());
        // Each RTP stream requires a pair of ICE components (RTP + RTCP).
        iceOptions.compCountPerStream = ICE_COMP_COUNT_PER_STREAM;
    
        // Init ICE.
        mediaTransport->initIceInstance(iceOptions);
    
        return true;
    }
    
    std::vector<std::string>
    SIPCall::getLocalIceCandidates(unsigned compId) const
    {
        std::lock_guard<std::mutex> lk(transportMtx_);
        if (not mediaTransport_) {
            JAMI_WARN("[call:%s] no media ICE transport", getCallId().c_str());
            return {};
        }
        return mediaTransport_->getLocalCandidates(compId);
    }
    
    void
    SIPCall::resetTransport(std::shared_ptr<IceTransport>&& transport)
    {
        // Move the transport to another thread and destroy it there if possible
        if (transport) {
            dht::ThreadPool::io().run(
                [transport = std::move(transport)]() mutable { transport.reset(); });
        }
    }
    
    void
    SIPCall::merge(Call& call)
    {
        JAMI_DBG("[call:%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);
    
        std::lock(callMutex_, subcall.callMutex_);
        std::lock_guard<std::recursive_mutex> lk1 {callMutex_, std::adopt_lock};
        std::lock_guard<std::recursive_mutex> lk2 {subcall.callMutex_, std::adopt_lock};
        inviteSession_ = std::move(subcall.inviteSession_);
        if (inviteSession_)
            inviteSession_->mod_data[Manager::instance().sipVoIPLink().getModId()] = this;
        setTransport(std::move(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_;
        peerUserAgent_ = subcall.peerUserAgent_;
        peerSupportMultiStream_ = subcall.peerSupportMultiStream_;
    
        Call::merge(subcall);
        if (isIceEnabled())
            startIceMedia();
    }
    
    bool
    SIPCall::remoteHasValidIceAttributes()
    {
        if (not sdp_) {
            throw std::runtime_error("Must have a valid SDP Session");
        }
    
        auto rem_ice_attrs = sdp_->getIceAttributes();
        if (rem_ice_attrs.ufrag.empty()) {
            JAMI_WARN("[call:%s] Missing ICE username fragment attribute in remote SDP",
                      getCallId().c_str());
            return false;
        }
    
        if (rem_ice_attrs.pwd.empty()) {
            JAMI_WARN("[call:%s] Missing ICE password attribute in remote SDP", getCallId().c_str());
            return false;
        }
    
        return true;
    }
    
    void
    SIPCall::setIceMedia(std::shared_ptr<IceTransport> ice)
    {
        JAMI_DBG("[call:%s] Setting ICE session [%p]", getCallId().c_str(), ice.get());
    
        std::lock_guard<std::mutex> lk(transportMtx_);
        if (not isSubcall()) {
            JAMI_ERR("[call:%s] The call is expected to be a sub-call", getCallId().c_str());
        }
        mediaTransport_ = std::move(ice);
    }
    
    void
    SIPCall::setupIceResponse()
    {
        JAMI_DBG("[call:%s] Setup ICE response", getCallId().c_str());
    
        auto account = getSIPAccount();
        if (not account) {
            JAMI_ERR("No account detected");
        }
    
        if (not remoteHasValidIceAttributes()) {
            // If ICE attributes are not present, skip the ICE initialization
            // step (most likely ICE is not used).
            JAMI_ERR("[call:%s] no ICE data in remote SDP", getCallId().c_str());
            return;
        }
    
        auto opt = account->getIceOptions();
    
        // Try to use the discovered public address. If not available,
        // fallback on local address.
        opt.accountPublicAddr = account->getPublishedIpAddress();
        if (opt.accountLocalAddr) {
            opt.accountLocalAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
                                                              opt.accountPublicAddr.getFamily());
        } else {
            // Just set the local address for both, most likely the account is not
            // registered.
            opt.accountLocalAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(), AF_INET);
            opt.accountPublicAddr = opt.accountLocalAddr;
        }
    
        if (not opt.accountLocalAddr) {
            JAMI_ERR("[call:%s] No local address, ICE can't be initialized", getCallId().c_str());
            onFailure(EIO);
            return;
        }
        createIceMediaTransport();
        if (not initIceMediaTransport(false, opt)) {
            JAMI_ERR("[call:%s] ICE initialization failed", getCallId().c_str());
            // Fatal condition
            // TODO: what's SIP rfc says about that?
            // (same question in startIceMedia)
            onFailure(EIO);
            return;
        }
    
        // WARNING: This call blocks! (need ice init done)
        addLocalIceAttributes();
    }
    
    bool
    SIPCall::isIceRunning() const
    {
        std::lock_guard<std::mutex> lk(transportMtx_);
        return mediaTransport_ and mediaTransport_->isRunning();
    }
    
    std::unique_ptr<IceSocket>
    SIPCall::newIceSocket(unsigned compId)
    {
        return std::unique_ptr<IceSocket> {new IceSocket(mediaTransport_, compId)};
    }
    
    void
    SIPCall::rtpSetupSuccess(MediaType type, bool isRemote)
    {
        std::lock_guard<std::mutex> lk {setupSuccessMutex_};
        if (type == MEDIA_AUDIO) {
            if (isRemote)
                mediaReady_.at("a:remote") = true;
            else
                mediaReady_.at("a:local") = true;
        } else {
            if (isRemote)
                mediaReady_.at("v:remote") = true;
            else
                mediaReady_.at("v:local") = true;
        }
    
        if (mediaReady_.at("a:local") and mediaReady_.at("a:remote") and mediaReady_.at("v:remote")) {
            if (Manager::instance().videoPreferences.getRecordPreview() or mediaReady_.at("v:local"))
                readyToRecord_ = true;
        }
    
        if (pendingRecord_ && readyToRecord_)
            toggleRecording();
    }
    
    void
    SIPCall::peerRecording(bool state)
    {
        const std::string& id = getConfId().empty() ? getCallId() : getConfId();
        if (state) {
            JAMI_WARN("Peer is recording");
            emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), true);
        } else {
            JAMI_WARN("Peer stopped recording");
            emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), false);
        }
        peerRecording_ = state;
    }
    
    void
    SIPCall::peerMuted(bool muted)
    {
        if (muted) {
            JAMI_WARN("Peer muted");
        } else {
            JAMI_WARN("Peer un-muted");
        }
        peerMuted_ = muted;
        if (auto conf = Manager::instance().getConferenceFromID(getConfId())) {
            conf->updateMuted();
        }
    }
    
    void
    SIPCall::resetMediaReady()
    {
        for (auto& m : mediaReady_)
            m.second = false;
    }
    
    } // namespace jami