diff --git a/bin/dbus/cx.ring.Ring.CallManager.xml b/bin/dbus/cx.ring.Ring.CallManager.xml index 0ae9e74ed4996664b6da3a1da1964c6f53c9cfac..83ffb8df0e9ad688365ce8edb4e3591e11e6a68d 100644 --- a/bin/dbus/cx.ring.Ring.CallManager.xml +++ b/bin/dbus/cx.ring.Ring.CallManager.xml @@ -124,6 +124,47 @@ <arg type="b" name="acceptSucceeded" direction="out"/> </method> + <method name="acceptWithMedia" tp:name-for-bindings="acceptWithMedia"> + <tp:added version="9.10.0"/> + <tp:docstring> + Answer an incoming call with the list of media. This version to control + attributes for each media (accepted, muted, ...) + </tp:docstring> + <arg type="s" name="callID" direction="in"> + <tp:docstring> + The callID. + </tp:docstring> + </arg> + <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="VectorMapStringString"/> + <arg type="aa{ss}" name="mediaList" direction="in"> + <tp:docstring> + The list of media. + </tp:docstring> + </arg> + <arg type="b" name="acceptSucceeded" direction="out"/> + </method> + + <method name="answerMediaChangeRequest" tp:name-for-bindings="answerMediaChangeRequest"> + <tp:added version="9.10.0"/> + <tp:docstring> + Answer to a media update request. + </tp:docstring> + <arg type="s" name="callID" direction="in"> + <tp:docstring> + The callID. + </tp:docstring> + </arg> + <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="VectorMapStringString"/> + <arg type="aa{ss}" name="mediaList" direction="in"> + <tp:docstring> + The list of media. Shoudld be of the same size as the media list in + the media update request. An empty media list means that the media + update request was refused. + </tp:docstring> + </arg> + <arg type="b" name="answerMediaChangeRequestSucceeded" direction="out"/> + </method> + <method name="hangUp" tp:name-for-bindings="hangUp"> <tp:docstring> Hangup a call in state "CURRENT" or "HOLD". @@ -628,6 +669,65 @@ </arg> </signal> + <signal name="incomingCallWithMedia" tp:name-for-bindings="incomingCallWithMedia"> + <tp:added version="9.10.0"/> + <tp:docstring> + <p>Notify an incoming call.</p> + <p>This signal is emitted when a new call is received. The list of media + included in the invite is reported. The client can control the media status + of the answer through the media attributes when when accepting the call.</p> + <tp:rationale>The client must subscribe to this signal to handle incoming calls.</tp:rationale> + </tp:docstring> + <arg type="s" name="accountID"> + <tp:docstring> + The account ID of the callee. + </tp:docstring> + </arg> + <arg type="s" name="callID"> + <tp:docstring> + Call ID of the incoming call. + </tp:docstring> + </arg> + <arg type="s" name="from"> + <tp:docstring> + The caller phone number. + </tp:docstring> + </arg> + <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="VectorMapStringString"/> + <arg type="aa{ss}" name="mediaList" direction="in"> + <tp:docstring> + The list of media offered in the incoming call. + </tp:docstring> + </arg> + </signal> + + <signal name="mediaChangeRequested" tp:name-for-bindings="mediaChangeRequested"> + <tp:added version="9.10.0"/> + <tp:docstring> + <p>Notify a media update on the current call.</p> + <p>This signal is emitted when a media update is received from the peer. The list + of media included in the request is reported. The client can control the media status + of the answer through the media attributes when when accepting the request.</p> + <tp:rationale>The client must subscribe to this signal to handle incoming calls.</tp:rationale> + </tp:docstring> + <arg type="s" name="accountID"> + <tp:docstring> + The account ID of the callee. + </tp:docstring> + </arg> + <arg type="s" name="callID"> + <tp:docstring> + Call ID of the incoming call. + </tp:docstring> + </arg> + <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="VectorMapStringString"/> + <arg type="aa{ss}" name="mediaList" direction="in"> + <tp:docstring> + The list of media offered in the incoming call. + </tp:docstring> + </arg> + </signal> + <signal name="incomingMessage" tp:name-for-bindings="incomingMessage"> <tp:docstring> Notify clients that new messages have been received. The key is @@ -928,6 +1028,5 @@ <arg type="s" name="callID" /> <arg type="s" name="event" /> </signal> - </interface> </node> diff --git a/bin/dbus/dbuscallmanager.cpp b/bin/dbus/dbuscallmanager.cpp index 2f47339411aeedf5db0d31522f0db73288afb4b3..baed40d6916613e7905cd8557619e3a053aa7d71 100644 --- a/bin/dbus/dbuscallmanager.cpp +++ b/bin/dbus/dbuscallmanager.cpp @@ -70,6 +70,22 @@ DBusCallManager::accept(const std::string& callID) -> decltype(DRing::accept(cal return DRing::accept(callID); } +auto +DBusCallManager::acceptWithMedia(const std::string& callID, + const std::vector<std::map<std::string, std::string>>& mediaList) + -> decltype(DRing::acceptWithMedia(callID, mediaList)) +{ + return DRing::acceptWithMedia(callID, mediaList); +} + +auto +DBusCallManager::answerMediaChangeRequest( + const std::string& callID, const std::vector<std::map<std::string, std::string>>& mediaList) + -> decltype(DRing::answerMediaChangeRequest(callID, mediaList)) +{ + return DRing::answerMediaChangeRequest(callID, mediaList); +} + auto DBusCallManager::hangUp(const std::string& callID) -> decltype(DRing::hangUp(callID)) { diff --git a/bin/dbus/dbuscallmanager.h b/bin/dbus/dbuscallmanager.h index a213d12df576315f4941e856b16b54960107c741..84222523b5131953ea7e3912fbd403675da36e73 100644 --- a/bin/dbus/dbuscallmanager.h +++ b/bin/dbus/dbuscallmanager.h @@ -68,6 +68,10 @@ public: bool refuse(const std::string& callID); bool accept(const std::string& callID); + bool acceptWithMedia(const std::string& callID, + const std::vector<std::map<std::string, std::string>>& mediaList); + bool answerMediaChangeRequest(const std::string& callID, + const std::vector<std::map<std::string, std::string>>& mediaList); bool hangUp(const std::string& callID); bool hold(const std::string& callID); bool unhold(const std::string& callID); diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index 6724539a512173a3b370a76c602d4dc21eed0bdd..59a1f30312e2ed1fe770f23de65791b99d6bee6c 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -169,6 +169,10 @@ DBusClient::initLibrary(int flags) bind(&DBusCallManager::incomingMessage, callM, _1, _2, _3)), exportable_callback<CallSignal::IncomingCall>( bind(&DBusCallManager::incomingCall, callM, _1, _2, _3)), + exportable_callback<CallSignal::IncomingCallWithMedia>( + bind(&DBusCallManager::incomingCallWithMedia, callM, _1, _2, _3, _4)), + exportable_callback<CallSignal::MediaChangeRequested>( + bind(&DBusCallManager::mediaChangeRequested, callM, _1, _2, _3)), exportable_callback<CallSignal::RecordPlaybackFilepath>( bind(&DBusCallManager::recordPlaybackFilepath, callM, _1, _2)), exportable_callback<CallSignal::ConferenceCreated>( diff --git a/bin/jni/callmanager.i b/bin/jni/callmanager.i index 745dcaad85abd90dffc59dc14c5702978c75b2ec..a7ae0bd43c057eb3734cd04a6247c74ed5c28ecd 100644 --- a/bin/jni/callmanager.i +++ b/bin/jni/callmanager.i @@ -35,6 +35,10 @@ public: virtual void voiceMailNotify(const std::string& accountId, int newCount, int oldCount, int urgentCount){} virtual void incomingMessage(const std::string& id, const std::string& from, const std::map<std::string, std::string>& messages){} virtual void incomingCall(const std::string& account_id, const std::string& call_id, const std::string& from){} + virtual void incomingCallWithMedia(const std::string& account_id, const std::string& call_id, const std::string& from, + const std::vector<std::map<std::string, std::string>>& mediaList){} + virtual void mediaChangeRequested(const std::string& account_id, const std::string& call_id, + const std::vector<std::map<std::string, std::string>>& mediaList){} virtual void recordPlaybackFilepath(const std::string& id, const std::string& filename){} virtual void conferenceCreated(const std::string& conf_id){} virtual void conferenceChanged(const std::string& conf_id, const std::string& state){} @@ -67,6 +71,8 @@ std::string placeCallWithMedia(const std::string& accountID, bool requestMediaChange(const std::string& callID, const std::vector<std::map<std::string, std::string>>& mediaList); bool refuse(const std::string& callID); bool accept(const std::string& callID); +bool acceptWithMedia(const std::string& callID, const std::vector<std::map<std::string, std::string>>& mediaList); +bool answerMediaChangeRequest(const std::string& callID, const std::vector<std::map<std::string, std::string>>& mediaList); bool hangUp(const std::string& callID); bool hold(const std::string& callID); bool unhold(const std::string& callID); @@ -131,6 +137,10 @@ public: virtual void voiceMailNotify(const std::string& accountId, int newCount, int oldCount, int urgentCount){} virtual void incomingMessage(const std::string& id, const std::string& from, const std::map<std::string, std::string>& messages){} virtual void incomingCall(const std::string& account_id, const std::string& call_id, const std::string& from){} + virtual void incomingCallWithMedia(const std::string& account_id, const std::string& call_id, const std::string& from, + const std::vector<std::map<std::string, std::string>>& mediaList){} + virtual void mediaChangeRequested(const std::string& account_id, const std::string& call_id, + const std::vector<std::map<std::string, std::string>>& mediaList){} virtual void recordPlaybackFilepath(const std::string& id, const std::string& filename){} virtual void conferenceCreated(const std::string& conf_id){} virtual void conferenceChanged(const std::string& conf_id, const std::string& state){} diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index da3e951268cf47bd4933c5f87269043fe7aec770..8687220dd5d5aaf3b44bdcfebd022e4eac960879 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -242,6 +242,8 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM exportable_callback<CallSignal::VoiceMailNotify>(bind(&Callback::voiceMailNotify, callM, _1, _2, _3, _4)), exportable_callback<CallSignal::IncomingMessage>(bind(&Callback::incomingMessage, callM, _1, _2, _3)), exportable_callback<CallSignal::IncomingCall>(bind(&Callback::incomingCall, callM, _1, _2, _3)), + exportable_callback<CallSignal::IncomingCallWithMedia>(bind(&Callback::incomingCallWithMedia, callM, _1, _2, _3, _4)), + exportable_callback<CallSignal::MediaChangeRequested>(bind(&Callback::mediaChangeRequested, callM, _1, _2, _3)), exportable_callback<CallSignal::RecordPlaybackFilepath>(bind(&Callback::recordPlaybackFilepath, callM, _1, _2)), exportable_callback<CallSignal::ConferenceCreated>(bind(&Callback::conferenceCreated, callM, _1)), exportable_callback<CallSignal::ConferenceChanged>(bind(&Callback::conferenceChanged, callM, _1, _2)), diff --git a/bin/nodejs/callmanager.i b/bin/nodejs/callmanager.i index dbcd364b5d3f0aa29755701568d121adbd85d57f..8767be2195b143cbcbe6727e7a8bdb7dc1cea551 100644 --- a/bin/nodejs/callmanager.i +++ b/bin/nodejs/callmanager.i @@ -35,6 +35,10 @@ public: virtual void voiceMailNotify(const std::string& accountId, int newCount, int oldCount, int urgentCount){} virtual void incomingMessage(const std::string& id, const std::string& from, const std::map<std::string, std::string>& messages){} virtual void incomingCall(const std::string& account_id, const std::string& call_id, const std::string& from){} + virtual void incomingCallWithMedia(const std::string& account_id, const std::string& call_id, const std::string& from, + const std::vector<std::map<std::string, std::string>>& mediaList){} + virtual void mediaChangeRequested(const std::string& account_id, const std::string& call_id, + const std::vector<std::map<std::string, std::string>>& mediaList){} virtual void recordPlaybackFilepath(const std::string& id, const std::string& filename){} virtual void conferenceCreated(const std::string& conf_id){} virtual void conferenceChanged(const std::string& conf_id, const std::string& state){} @@ -126,6 +130,10 @@ public: virtual void voiceMailNotify(const std::string& accountId, int newCount, int oldCount, int urgentCount){} virtual void incomingMessage(const std::string& id, const std::string& from, const std::map<std::string, std::string>& messages){} virtual void incomingCall(const std::string& account_id, const std::string& call_id, const std::string& from){} + virtual void incomingCallWithMedia(const std::string& account_id, const std::string& call_id, const std::string& from, + const std::vector<std::map<std::string, std::string>>& mediaList){} + virtual void mediaChangeRequested(const std::string& account_id, const std::string& call_id, + const std::vector<std::map<std::string, std::string>>& mediaList){} virtual void recordPlaybackFilepath(const std::string& id, const std::string& filename){} virtual void conferenceCreated(const std::string& conf_id){} virtual void conferenceChanged(const std::string& conf_id, const std::string& state){} diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i index ece8b9c0d808c79e680a2d87e288c849952a275b..589c5d6afdf495dc0334cbf5256dda69a2c8e41c 100644 --- a/bin/nodejs/nodejs_interface.i +++ b/bin/nodejs/nodejs_interface.i @@ -123,6 +123,8 @@ void init(const SWIGV8_VALUE& funcMap){ exportable_callback<CallSignal::StateChange>(bind(&callStateChanged, _1, _2, _3)), exportable_callback<CallSignal::IncomingMessage>(bind(&incomingMessage, _1, _2, _3)), exportable_callback<CallSignal::IncomingCall>(bind(&incomingCall, _1, _2, _3)), + exportable_callback<CallSignal::IncomingCallWithMedia>(bind(&incomingCallWithMedia, _1, _2, _3, _4)), + exportable_callback<CallSignal::MediaChangeRequested>(bind(&mediaChangeRequested, _1, _2, _3) }; const std::map<std::string, SharedCallback> configEvHandlers = { diff --git a/src/account.cpp b/src/account.cpp index 521e566a939cd85cf1bfc7756508640dbd5e6015..0916b1af5ed4fb1e305ab76edcc2b1e4a47ed9a5 100644 --- a/src/account.cpp +++ b/src/account.cpp @@ -123,6 +123,7 @@ Account::Account(const std::string& accountID) , upnpEnabled_(true) , localModeratorsEnabled_(true) , allModeratorsEnabled_(true) + , multiStreamEnabled_(false) { // Initialize the codec order, used when creating a new account loadDefaultCodecs(); diff --git a/src/account.h b/src/account.h index 0282713af431419c6cfac34a0709230f77fbd119..607a798d9a308d930dc55269dc2b1cf485571972 100644 --- a/src/account.h +++ b/src/account.h @@ -165,10 +165,6 @@ public: const std::vector<MediaAttribute>& mediaList) = 0; - /* Note: we forbid incoming call creation from an instance of Account. - * This is why no newIncomingCall() method exist here. - */ - /** * If supported, send a text message from this account. * @return a token to query the message status @@ -350,6 +346,14 @@ public: allModeratorsEnabled_ = isAllModeratorEnabled; } + // Enable/disable multi-stream feature. + // Multi-stream feature changes the callflow of the re-invite process. All + // clients must support this feature before it can be enabled by default. + // These two internal APIs allow controlling the callflow accordingly. They + // should be removed once the multi-stream feature is fully supported. + bool isMultiStreamEnabled() const { return multiStreamEnabled_; } + void enableMultiStream(bool enable) { multiStreamEnabled_ = enable; } + public: // virtual methods that has to be implemented by concrete classes /** @@ -546,6 +550,8 @@ protected: bool localModeratorsEnabled_; bool allModeratorsEnabled_; + bool multiStreamEnabled_; + /** * private account codec searching functions */ diff --git a/src/call.cpp b/src/call.cpp index 006a8f155cc84bc72c4faeda2bda7676a75a491a..c47c194d23c02b4dc5c14c92b5d7bb343442b5b7 100644 --- a/src/call.cpp +++ b/src/call.cpp @@ -59,16 +59,12 @@ namespace jami { /// The predicate should have <code>bool(Call*) signature</code>. template<typename T> inline void -hangupCallsIf(Call::SubcallSet&& calls, - int errcode, - T pred) +hangupCallsIf(Call::SubcallSet&& calls, int errcode, T pred) { for (auto& call : calls) { if (not pred(call.get())) continue; - dht::ThreadPool::io().run([call = std::move(call), errcode] { - call->hangup(errcode); - }); + dht::ThreadPool::io().run([call = std::move(call), errcode] { call->hangup(errcode); }); } } @@ -93,58 +89,58 @@ Call::Call(const std::shared_ptr<Account>& account, { updateDetails(details); - addStateListener( - [this](Call::CallState call_state, Call::ConnectionState cnx_state, UNUSED int code) { - checkPendingIM(); - std::weak_ptr<Call> callWkPtr = shared_from_this(); - runOnMainThread([callWkPtr] { - if (auto call = callWkPtr.lock()) - call->checkAudio(); - }); + addStateListener([this](Call::CallState call_state, + Call::ConnectionState cnx_state, + UNUSED int code) { + checkPendingIM(); + std::weak_ptr<Call> callWkPtr = shared_from_this(); + runOnMainThread([callWkPtr] { + if (auto call = callWkPtr.lock()) + call->checkAudio(); + }); - // if call just started ringing, schedule call timeout - if (type_ == CallType::INCOMING and cnx_state == ConnectionState::RINGING) { - auto timeout = Manager::instance().getRingingTimeout(); - JAMI_DBG("Scheduling call timeout in %d seconds", timeout); - - std::weak_ptr<Call> callWkPtr = shared_from_this(); - Manager::instance().scheduler().scheduleIn( - [callWkPtr] { - if (auto callShPtr = callWkPtr.lock()) { - if (callShPtr->getConnectionState() == Call::ConnectionState::RINGING) { - JAMI_DBG( - "Call %s is still ringing after timeout, setting state to BUSY", - callShPtr->getCallId().c_str()); - callShPtr->hangup(PJSIP_SC_BUSY_HERE); - Manager::instance().callFailure(*callShPtr); - } - } - }, - std::chrono::seconds(timeout)); - } + // if call just started ringing, schedule call timeout + if (type_ == CallType::INCOMING and cnx_state == ConnectionState::RINGING) { + auto timeout = Manager::instance().getRingingTimeout(); + JAMI_DBG("Scheduling call timeout in %d seconds", timeout); - if (!isSubcall() && getCallType() == CallType::OUTGOING) { - if (cnx_state == ConnectionState::CONNECTED && duration_start_ == time_point::min()) - duration_start_ = clock::now(); - else if (cnx_state == ConnectionState::DISCONNECTED && call_state == CallState::OVER) { - if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>( - getAccount().lock())) { - auto duration = duration_start_ == time_point::min() - ? 0 - : std::chrono::duration_cast<std::chrono::milliseconds>( - clock::now() - duration_start_) - .count(); - jamiAccount->addCallHistoryMessage(getPeerNumber(), duration); - - monitor(); + std::weak_ptr<Call> callWkPtr = shared_from_this(); + Manager::instance().scheduler().scheduleIn( + [callWkPtr] { + if (auto callShPtr = callWkPtr.lock()) { + if (callShPtr->getConnectionState() == Call::ConnectionState::RINGING) { + JAMI_DBG( + "Call %s is still ringing after timeout, setting state to BUSY", + callShPtr->getCallId().c_str()); + callShPtr->hangup(PJSIP_SC_BUSY_HERE); + Manager::instance().callFailure(*callShPtr); + } } + }, + std::chrono::seconds(timeout)); + } + + if (!isSubcall() && getCallType() == CallType::OUTGOING) { + if (cnx_state == ConnectionState::CONNECTED && duration_start_ == time_point::min()) + duration_start_ = clock::now(); + else if (cnx_state == ConnectionState::DISCONNECTED && call_state == CallState::OVER) { + if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>(getAccount().lock())) { + auto duration = duration_start_ == time_point::min() + ? 0 + : std::chrono::duration_cast<std::chrono::milliseconds>( + clock::now() - duration_start_) + .count(); + jamiAccount->addCallHistoryMessage(getPeerNumber(), duration); + + monitor(); } } + } - // kill pending subcalls at disconnect - if (call_state == CallState::OVER) - hangupCalls(safePopSubcalls(), 0); - }); + // kill pending subcalls at disconnect + if (call_state == CallState::OVER) + hangupCalls(safePopSubcalls(), 0); + }); time(×tamp_start_); if (auto shared = account_.lock()) @@ -381,6 +377,7 @@ std::map<std::string, std::string> Call::getDetails() const { return { + {DRing::Call::Details::CALL_TYPE, std::to_string((unsigned) type_)}, {DRing::Call::Details::PEER_NUMBER, peerNumber_}, {DRing::Call::Details::DISPLAY_NAME, peerDisplayName_}, @@ -388,9 +385,9 @@ Call::getDetails() const {DRing::Call::Details::CONF_ID, confID_}, {DRing::Call::Details::TIMESTAMP_START, std::to_string(timestamp_start_)}, {DRing::Call::Details::ACCOUNTID, getAccountId()}, - {DRing::Call::Details::AUDIO_MUTED, std::string(bool_to_str(isAudioMuted_))}, - {DRing::Call::Details::VIDEO_MUTED, std::string(bool_to_str(isVideoMuted_))}, - {DRing::Call::Details::AUDIO_ONLY, std::string(bool_to_str(isAudioOnly_))}, + {DRing::Call::Details::AUDIO_MUTED, std::string(bool_to_str(isAudioMuted()))}, + {DRing::Call::Details::VIDEO_MUTED, std::string(bool_to_str(isVideoMuted()))}, + {DRing::Call::Details::AUDIO_ONLY, std::string(bool_to_str(hasVideo()))}, }; } @@ -436,8 +433,12 @@ Call::onTextMessage(std::map<std::string, std::string>&& messages) #ifdef ENABLE_PLUGIN auto& pluginChatManager = jami::Manager::instance().getJamiPluginManager().getChatServicesManager(); - std::shared_ptr<JamiMessage> cm = std::make_shared<JamiMessage>( - getAccountId(), getPeerNumber(), true, const_cast<std::map<std::string, std::string>&>(messages), false); + std::shared_ptr<JamiMessage> cm + = std::make_shared<JamiMessage>(getAccountId(), + getPeerNumber(), + true, + const_cast<std::map<std::string, std::string>&>(messages), + false); pluginChatManager.publishMessage(cm); #endif @@ -695,7 +696,8 @@ Call::setConferenceInfo(const std::string& msg) // confID_ empty -> participant set confInfo with the received one confInfo_ = std::move(newInfo); // Inform client that layout has changed - jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>(id_, confInfo_.toVectorMapStringString()); + jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>( + id_, confInfo_.toVectorMapStringString()); } else if (auto conf = Manager::instance().getConferenceFromID(confID_)) { conf->mergeConfInfo(newInfo, getPeerNumber()); } diff --git a/src/call.h b/src/call.h index f3374befc8d1d96d6304489748ba9ae57711d16a..9ecb37cb78203df64e8a10bd15cf7a93baa2e1d2 100644 --- a/src/call.h +++ b/src/call.h @@ -209,6 +209,25 @@ public: */ virtual void answer() = 0; + /** + * Answer a call with a list of media attributes. + * @param mediaList The list of the media attributes. + * The media attributes set by the caller of this method will + * determine the response sent to the peer and the configuration + * of the local media. + */ + virtual void answer(const std::vector<MediaAttribute>& mediaList) = 0; + + /** + * Answer to a media update request. The media attributes set by the + * caller of this method will determine the response sent to the + * peer and the configuration of the local media. + * @param mediaList The list of media attributes. An empty media + * list means the media update request was not accepted, meaning the + * call continue with the current media. It's up to the implementation + * to determine wether an answer will be sent to the peer. + */ + virtual void answerMediaChangeRequest(const std::vector<MediaAttribute>& mediaList) = 0; /** * Hang up the call * @param reason @@ -335,7 +354,10 @@ public: // media management */ void updateDetails(const std::map<std::string, std::string>& details); - bool hasVideo() const { return not isVideoMuted_; } + // Media status methods + virtual bool hasVideo() const = 0; + virtual bool isAudioMuted() const = 0; + virtual bool isVideoMuted() const = 0; /** * A Call can be in a conference. If this is the case, the other side @@ -380,8 +402,6 @@ protected: // TODO all these members are not protected against multi-thread access const std::string id_ {}; - bool isAudioMuted_ {false}; - bool isVideoMuted_ {false}; ///< MultiDevice: parent call, nullptr otherwise. Access protected by callMutex_. mutable std::shared_ptr<Call> parent_; @@ -414,6 +434,7 @@ private: std::vector<StateListenerCb> stateChangedListeners_ {}; +protected: /** Unique conference ID, used exclusively in case of a conference */ std::string confID_ {}; diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp index 50e58032ff06c5a284ac31f15cdf5ec1e255593b..841327b131b2f92b500e5ecdc9c5882c09dd3a2d 100644 --- a/src/client/callmanager.cpp +++ b/src/client/callmanager.cpp @@ -100,6 +100,18 @@ accept(const std::string& callID) return jami::Manager::instance().answerCall(callID); } +bool +acceptWithMedia(const std::string& callID, const std::vector<DRing::MediaMap>& mediaList) +{ + return jami::Manager::instance().answerCallWithMedia(callID, mediaList); +} + +bool +answerMediaChangeRequest(const std::string& callID, const std::vector<DRing::MediaMap>& mediaList) +{ + return jami::Manager::instance().answerMediaChangeRequest(callID, mediaList); +} + bool hangUp(const std::string& callID) { diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index 4397e6d48280365677073f69d16f6c3b1bf7092a..a306c0765480833bec251157c4cda93543883f19 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -34,6 +34,8 @@ getSignalHandlers() exported_callback<DRing::CallSignal::VoiceMailNotify>(), exported_callback<DRing::CallSignal::IncomingMessage>(), exported_callback<DRing::CallSignal::IncomingCall>(), + exported_callback<DRing::CallSignal::IncomingCallWithMedia>(), + exported_callback<DRing::CallSignal::MediaChangeRequested>(), exported_callback<DRing::CallSignal::RecordPlaybackFilepath>(), exported_callback<DRing::CallSignal::ConferenceCreated>(), exported_callback<DRing::CallSignal::ConferenceChanged>(), diff --git a/src/dring/callmanager_interface.h b/src/dring/callmanager_interface.h index f68d94b2dbea4a578644b2f6efa9dbecdb17e52f..541f5e36351991e674b5c0024539b3be03710e00 100644 --- a/src/dring/callmanager_interface.h +++ b/src/dring/callmanager_interface.h @@ -58,13 +58,17 @@ DRING_PUBLIC bool attendedTransfer(const std::string& transferID, const std::str DRING_PUBLIC std::map<std::string, std::string> getCallDetails(const std::string& callID); DRING_PUBLIC std::vector<std::string> getCallList(); -/* APIs that supports arbitrary number of medias*/ +/* APIs that supports an arbitrary number of media */ DRING_PUBLIC std::string placeCall(const std::string& accountID, const std::string& to, const std::vector<DRing::MediaMap>& mediaList); DRING_PUBLIC bool accept(const std::string& callID, const std::vector<DRing::MediaMap>& mediaList); +DRING_PUBLIC bool acceptWithMedia(const std::string& callID, + const std::vector<DRing::MediaMap>& mediaList); DRING_PUBLIC bool requestMediaChange(const std::string& callID, const std::vector<DRing::MediaMap>& mediaList); +DRING_PUBLIC bool answerMediaChangeRequest(const std::string& callID, + const std::vector<DRing::MediaMap>& mediaList); /* Conference related methods */ DRING_PUBLIC void removeConference(const std::string& conference_id); @@ -164,6 +168,21 @@ struct DRING_PUBLIC CallSignal constexpr static const char* name = "IncomingCall"; using cb_type = void(const std::string&, const std::string&, const std::string&); }; + struct DRING_PUBLIC IncomingCallWithMedia + { + constexpr static const char* name = "IncomingCallWithMedia"; + using cb_type = void(const std::string&, + const std::string&, + const std::string&, + const std::vector<std::map<std::string, std::string>>&); + }; + struct DRING_PUBLIC MediaChangeRequested + { + constexpr static const char* name = "MediaChangeRequested"; + using cb_type = void(const std::string&, + const std::string&, + const std::vector<std::map<std::string, std::string>>&); + }; struct DRING_PUBLIC RecordPlaybackFilepath { constexpr static const char* name = "RecordPlaybackFilepath"; diff --git a/src/ice_transport.cpp b/src/ice_transport.cpp index 613e8ecf75ff17a00702c1b380c17a14b15c2672..7a8e55c53c33d368299c3c7b458689c2d66f1c8b 100644 --- a/src/ice_transport.cpp +++ b/src/ice_transport.cpp @@ -1342,6 +1342,10 @@ IceTransport::packIceMsg(uint8_t version) const bool IceTransport::getCandidateFromSDP(const std::string& line, IceCandidate& cand) const { + // Silently ignore empty lines + if (line.empty()) + return false; + /** Section 4.5, RFC 6544 (https://tools.ietf.org/html/rfc6544) * candidate-attribute = "candidate" ":" foundation SP component-id SP * "TCP" SP @@ -1376,7 +1380,7 @@ IceTransport::getCandidateFromSDP(const std::string& line, IceCandidate& cand) c type, tcp_type); if (cnt != 7 && cnt != 8) { - JAMI_ERR("[ice:%p] Invalid ICE candidate line", pimpl_.get()); + JAMI_ERR("[ice:%p] Invalid ICE candidate line: %s", pimpl_.get(), line.c_str()); return false; } diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 28b3103767d3919e58dd5e6626e18f6639f20788..21bcf1769034d2a5c6c072a1b914ff93d2a5b752 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -423,6 +423,55 @@ JamiAccount::newIncomingCall(const std::string& from, return nullptr; } +std::shared_ptr<SIPCall> +JamiAccount::newIncomingCall(const std::string& from, + const std::vector<MediaAttribute>& mediaList, + const std::shared_ptr<SipTransport>& sipTransp) +{ + JAMI_DBG("New incoming call from %s with %lu media", from.c_str(), mediaList.size()); + + if (sipTransp) { + std::unique_lock<std::mutex> connLock(sipConnsMtx_); + for (auto& [key, value] : sipConns_) { + if (key.first == from) { + // Search for a matching linked SipTransport in connection list. + for (auto conIter = value.rbegin(); conIter != value.rend(); conIter++) { + if (conIter->transport != sipTransp) + continue; + + auto call = Manager::instance().callFactory.newSipCall(shared(), + Call::CallType::INCOMING, + mediaList); + call->setPeerUri(RING_URI_PREFIX + from); + call->setPeerNumber(from); + return call; + } + } + } + } + + std::lock_guard<std::mutex> lock(callsMutex_); + auto call_it = pendingSipCalls_.begin(); + while (call_it != pendingSipCalls_.end()) { + auto call = call_it->call.lock(); + if (not call) { + JAMI_WARN("newIncomingCall: discarding deleted call"); + call_it = pendingSipCalls_.erase(call_it); + } else if (call->getPeerNumber() == from + || (call_it->from_cert and call_it->from_cert->issuer + and call_it->from_cert->issuer->getId().toString() == from)) { + JAMI_DBG("newIncomingCall: found matching call for %s", from.c_str()); + pendingSipCalls_.erase(call_it); + return call; + } else { + call_it++; + } + } + + JAMI_ERR("newIncomingCall: can't find matching call for %s", from.c_str()); + return nullptr; +} + std::shared_ptr<Call> JamiAccount::newOutgoingCall(std::string_view toUrl, const std::map<std::string, std::string>& volatileCallDetails) @@ -464,7 +513,6 @@ JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::st auto suffix = stripPrefix(toUri); JAMI_DBG() << *this << "Calling DHT peer " << suffix; - call->setIPToIP(true); call->setSecure(isTlsEnabled()); try { @@ -580,7 +628,6 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: // cached connection is failing with ICE (close event still not detected). auto dummyCall = createSubCall(call); - dummyCall->setIPToIP(true); dummyCall->setSecure(isTlsEnabled()); call->addSubCall(*dummyCall); @@ -602,7 +649,6 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: return; auto dev_call = createSubCall(call); - dev_call->setIPToIP(true); dev_call->setSecure(isTlsEnabled()); dev_call->setState(Call::ConnectionState::TRYING); call->addStateListener( @@ -647,7 +693,6 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: auto dev_call = createSubCall(call); - dev_call->setIPToIP(true); dev_call->setSecure(isTlsEnabled()); dev_call->setTransport(transport); call->addSubCall(*dev_call); @@ -786,7 +831,7 @@ JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call, bool JamiAccount::SIPStartCall(SIPCall& call, IpAddr target) { - call.setupLocalSDPFromIce(); + call.addLocalIceAttributes(); std::string toUri(getToUri(call.getPeerNumber() + "@" + target.toString(true))); // expecting a fully well formed sip uri @@ -1585,7 +1630,7 @@ JamiAccount::registerName(const std::string& password, const std::string& name) this_->registeredName_ = name; this_->saveConfig(); emitSignal<DRing::ConfigurationSignal::VolatileDetailsChanged>( - this_->accountID_, this_->getVolatileAccountDetails()); + this_->accountID_, this_->getVolatileAccountDetails()); } } emitSignal<DRing::ConfigurationSignal::NameRegistrationEnded>(acc, res, name); diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 4e30c5bdd21c55a2cedb95b19d94d8c580de4bf4..b0756201198a672da1ab6da9c30bf358a58f539f 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -276,18 +276,26 @@ public: const std::map<std::string, std::string>& details = {}, const std::shared_ptr<SipTransport>& sipTr = nullptr) override; + /** + * Create incoming SIPCall. + * @param[in] from The origin of the call + * @param mediaList A list of media + * @param sipTr: SIP Transport + * @return A shared pointer on the created call. + */ + std::shared_ptr<SIPCall> newIncomingCall( + const std::string& from, + const std::vector<MediaAttribute>& mediaList, + const std::shared_ptr<SipTransport>& sipTr = {}) override; + void onTextMessage(const std::string& id, const std::string& from, const std::map<std::string, std::string>& payloads) override; virtual bool isTlsEnabled() const override { return true; } - virtual bool isSrtpEnabled() const { return true; } - - virtual KeyExchangeProtocol getSrtpKeyExchange() const override - { - return KeyExchangeProtocol::SDES; - } + bool isSrtpEnabled() const override { return true; } + KeyExchangeProtocol getSrtpKeyExchange() const { return KeyExchangeProtocol::SDES; } virtual bool getSrtpFallback() const override { return false; } diff --git a/src/manager.cpp b/src/manager.cpp index cb30cc049179c6ca156f24649b6e8bd91b4f6a5c..ead8170ba8bf6d8b51a87b1e7efe277b7163c3c6 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -356,6 +356,9 @@ struct Manager::ManagerPimpl void initAudioDriver(); + void processIncomingCall(Call& incomCall, const std::string& accountId); + static void stripSipPrefix(Call& incomCall); + Manager& base_; // pimpl back-pointer std::shared_ptr<asio::io_context> ioContext_; @@ -1130,6 +1133,91 @@ Manager::answerCall(const std::string& call_id) return result; } +bool +Manager::answerCallWithMedia(const std::string& callId, + const std::vector<DRing::MediaMap>& mediaList) +{ + JAMI_INFO("Answer call %s with %lu media", callId.c_str(), mediaList.size()); + + bool result = true; + + auto call = getCallFromCallID(callId); + + if (not call) { + JAMI_ERR("Call with ID %s does not exist", callId.c_str()); + return false; + } + + if (call->getConnectionState() != Call::ConnectionState::RINGING) { + // The call was already answered + return true; + } + + // If ringing + stopTone(); + + try { + auto mediaAttrList = MediaAttribute::parseMediaList(mediaList); + call->answer(mediaAttrList); + } catch (const std::runtime_error& e) { + JAMI_ERR("%s", e.what()); + result = false; + } + + pimpl_->removeWaitingCall(callId); + + if (not result) { + // Abort on error. + return false; + } + + // Check if the call is part of a conference and + // update accordingly. + if (isConferenceParticipant(callId)) + pimpl_->switchCall(call->getConfId()); + else + pimpl_->switchCall(call); + + addAudio(*call); + + // Start recording if set in preference + if (audioPreference.getIsAlwaysRecording()) + toggleRecordingCall(callId); + + return result; +} + +bool +Manager::answerMediaChangeRequest(const std::string& callId, + const std::vector<DRing::MediaMap>& mediaList) +{ + JAMI_INFO("Answer to media change request on call %s", callId.c_str()); + + bool result = true; + + auto call = getCallFromCallID(callId); + + if (not call) { + JAMI_ERR("Call with ID %s does not exist", callId.c_str()); + return false; + } + + try { + auto mediaAttrList = MediaAttribute::parseMediaList(mediaList); + call->answerMediaChangeRequest(mediaAttrList); + } catch (const std::runtime_error& e) { + JAMI_ERR("%s", e.what()); + result = false; + } + + if (not result) { + // Abort on error. + return false; + } + + return result; +} + // THREAD=Main bool Manager::hangupCall(const std::string& callId) @@ -1932,124 +2020,66 @@ Manager::incomingCallsWaiting() return not pimpl_->waitingCalls_.empty(); } -/////////////////////////////////////////////////////////////////////////////// -// Management of event peer IP-phone -//////////////////////////////////////////////////////////////////////////////// -// SipEvent Thread void Manager::incomingCall(Call& call, const std::string& accountId) { - JAMI_INFO("Incoming call %s on account %s)", call.getCallId().c_str(), accountId.c_str()); - - stopTone(); - const std::string callID(call.getCallId()); - - if (accountId.empty()) - call.setIPToIP(true); - else { - // strip sip: which is not required and bring confusion with ip to ip calls - // when placing new call from history. - std::string peerNumber(call.getPeerNumber()); - - const char SIP_PREFIX[] = "sip:"; - size_t startIndex = peerNumber.find(SIP_PREFIX); - - if (startIndex != std::string::npos) - call.setPeerNumber(peerNumber.substr(startIndex + sizeof(SIP_PREFIX) - 1)); + if (not accountId.empty()) { + pimpl_->stripSipPrefix(call); } - auto w = call.getAccount(); - auto account = w.lock(); - if (!account) { - JAMI_ERR("No account detected"); - return; - } + std::string from("<" + call.getPeerNumber() + ">"); - if (not hasCurrentCall()) { - call.setState(Call::ConnectionState::RINGING); -#if !defined(RING_UWP) && !(defined(TARGET_OS_IOS) && TARGET_OS_IOS) - if (not account->isRendezVous()) - playRingtone(accountId); -#endif + auto const& account = getAccount(accountId); + if (not account) { + JAMI_ERR("Incoming call %s on unknown account %s", + call.getCallId().c_str(), + accountId.c_str()); + return; } - pimpl_->addWaitingCall(callID); + if (account->isMultiStreamEnabled()) { + // Report incoming call using "CallSignal::IncomingCallWithMedia" signal. + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps( + call.getMediaAttributeList()); - std::string number(call.getPeerNumber()); + if (mediaList.empty()) { + JAMI_WARN("Incoming call %s has an empty media list", call.getCallId().c_str()); + } - std::string from("<" + number + ">"); + JAMI_INFO("Incoming call %s on account %s with %lu media", + call.getCallId().c_str(), + accountId.c_str(), + mediaList.size()); - emitSignal<DRing::CallSignal::IncomingCall>(accountId, - callID, - call.getPeerDisplayName() + " " + from); + // Report the call using new API. + emitSignal<DRing::CallSignal::IncomingCallWithMedia>(accountId, + call.getCallId(), + call.getPeerDisplayName() + " " + from, + mediaList); + } else { + JAMI_INFO("Incoming call %s on account %s", call.getCallId().c_str(), accountId.c_str()); - auto currentCall = getCurrentCall(); - if (account->isRendezVous()) { - dht::ThreadPool::io().run([this, callID] { - answerCall(callID); - auto call = getCallFromCallID(callID); - auto accountId = call->getAccountId(); - for (const auto& cid : getCallList()) { - if (auto call = getCallFromCallID(cid)) { - if (call->getState() != Call::CallState::ACTIVE) - continue; - if (call->getAccountId() == accountId) { - if (cid != callID) { - if (call->getConfId().empty()) { - joinParticipant(callID, cid, false); - } else { - addParticipant(callID, call->getConfId()); - } - return; - } - } - } - } - // First call - auto conf = std::make_shared<Conference>(); - pimpl_->conferenceMap_.emplace(conf->getConfID(), conf); + emitSignal<DRing::CallSignal::IncomingCall>(accountId, + call.getCallId(), + call.getPeerDisplayName() + " " + from); + } - // Bind calls according to their state - pimpl_->bindCallToConference(*call, *conf); - conf->detach(); + // Process the call. + pimpl_->processIncomingCall(call, accountId); +} - emitSignal<DRing::CallSignal::ConferenceCreated>(conf->getConfID()); - }); - } else if (pimpl_->autoAnswer_) { - dht::ThreadPool::io().run([this, callID] { answerCall(callID); }); - } else if (currentCall) { - // Test if already calling this person - if (currentCall->getAccountId() == accountId - && currentCall->getPeerNumber() == call.getPeerNumber()) { - auto w = currentCall->getAccount(); - auto account = w.lock(); - if (!account) { - JAMI_ERR("No account detected"); - return; - } - auto device_uid = account->getUsername(); - if (device_uid.find("ring:") == 0) { - // NOTE: in case of a SIP call it's already ready to compare - device_uid = device_uid.substr(5); // after ring: - } - auto answerToCall = false; - auto downgradeToAudioOnly = currentCall->isAudioOnly() != call.isAudioOnly(); - if (downgradeToAudioOnly) - // Accept the incoming audio only - answerToCall = call.isAudioOnly(); - else - // Accept the incoming call from the higher id number - answerToCall = (device_uid.compare(call.getPeerNumber()) < 0); +void +Manager::mediaChangeRequested(const std::string& callId, + const std::string& accountId, + const std::vector<DRing::MediaMap>& mediaList) +{ + JAMI_INFO("Media change request for call %s on account %s with %lu media", + callId.c_str(), + accountId.c_str(), + mediaList.size()); - if (answerToCall) { - auto currentCallID = currentCall->getCallId(); - runOnMainThread([this, currentCallID, callID] { - answerCall(callID); - hangupCall(currentCallID); - }); - } - } - } + // Report the media change request. + emitSignal<DRing::CallSignal::MediaChangeRequested>(accountId, callId, mediaList); } void @@ -2103,10 +2133,12 @@ Manager::sendCallTextMessage(const std::string& callID, JAMI_ERR("no conference associated to call ID %s", callID.c_str()); } } else { - try { + try { call->sendTextMessage(messages, from); } catch (const im::InstantMessageException& e) { - JAMI_ERR("Failed to send message to call %s: %s", call->getCallId().c_str(), e.what()); + JAMI_ERR("Failed to send message to call %s: %s", + call->getCallId().c_str(), + e.what()); } } } else { @@ -2693,6 +2725,118 @@ Manager::ManagerPimpl::initAudioDriver() audiodriver_->startStream(type); } +// Internal helper method +void +Manager::ManagerPimpl::stripSipPrefix(Call& incomCall) +{ + // strip sip: which is not required and bring confusion with ip to ip calls + // when placing new call from history. + std::string peerNumber(incomCall.getPeerNumber()); + + const char SIP_PREFIX[] = "sip:"; + size_t startIndex = peerNumber.find(SIP_PREFIX); + + if (startIndex != std::string::npos) + incomCall.setPeerNumber(peerNumber.substr(startIndex + sizeof(SIP_PREFIX) - 1)); +} + +// Internal helper method +void +Manager::ManagerPimpl::processIncomingCall(Call& incomCall, const std::string& accountId) +{ + auto& mgr = Manager::instance(); + mgr.stopTone(); + + auto incomCallId = incomCall.getCallId(); + auto currentCall = mgr.getCurrentCall(); + + auto w = incomCall.getAccount(); + auto account = w.lock(); + if (!account) { + JAMI_ERR("No account detected"); + return; + } + + if (not mgr.hasCurrentCall()) { + incomCall.setState(Call::ConnectionState::RINGING); +#if !defined(RING_UWP) && !(defined(TARGET_OS_IOS) && TARGET_OS_IOS) + if (not account->isRendezVous()) + mgr.playRingtone(accountId); +#endif + } + + addWaitingCall(incomCallId); + + if (account->isRendezVous()) { + dht::ThreadPool::io().run([this, incomCallId] { + auto& mgr = Manager::instance(); + mgr.answerCall(incomCallId); + auto call = mgr.getCallFromCallID(incomCallId); + auto accountId = call->getAccountId(); + for (const auto& cid : mgr.getCallList()) { + if (auto call = mgr.getCallFromCallID(cid)) { + if (call->getState() != Call::CallState::ACTIVE) + continue; + if (call->getAccountId() == accountId) { + if (cid != incomCallId) { + if (call->getConfId().empty()) { + mgr.joinParticipant(incomCallId, cid, false); + } else { + mgr.addParticipant(incomCallId, call->getConfId()); + } + return; + } + } + } + } + // First call + auto conf = std::make_shared<Conference>(); + conferenceMap_.emplace(conf->getConfID(), conf); + + // Bind calls according to their state + bindCallToConference(*call, *conf); + conf->detach(); + + emitSignal<DRing::CallSignal::ConferenceCreated>(conf->getConfID()); + }); + } else if (autoAnswer_) { + dht::ThreadPool::io().run([incomCallId] { Manager::instance().answerCall(incomCallId); }); + } else if (currentCall) { + // Test if already calling this person + if (currentCall->getAccountId() == account->getAccountID() + && currentCall->getPeerNumber() == incomCall.getPeerNumber()) { + auto w = currentCall->getAccount(); + auto account = w.lock(); + if (!account) { + JAMI_ERR("No account detected"); + return; + } + auto device_uid = account->getUsername(); + if (device_uid.find("ring:") == 0) { + // NOTE: in case of a SIP call it's already ready to compare + device_uid = device_uid.substr(5); // after ring: + } + auto answerToCall = false; + auto downgradeToAudioOnly = currentCall->isAudioOnly() != incomCall.isAudioOnly(); + if (downgradeToAudioOnly) + // Accept the incoming audio only + answerToCall = incomCall.isAudioOnly(); + else + // Accept the incoming call from the higher id number + answerToCall = (device_uid.compare(incomCall.getPeerNumber()) < 0); + + if (answerToCall) { + auto currentCallID = currentCall->getCallId(); + runOnMainThread([currentCallID, incomCallId] { + auto& mgr = Manager::instance(); + mgr.answerCall(incomCallId); + mgr.hangupCall(currentCallID); + }); + } + } + } +} + AudioFormat Manager::hardwareAudioFormatChanged(AudioFormat format) { diff --git a/src/manager.h b/src/manager.h index 0548d499e0547f7cd86e436e13d4891748a9f7d7..e1a29bd9ae6a84a158f9d0e3f521799ee74e92d9 100644 --- a/src/manager.h +++ b/src/manager.h @@ -174,15 +174,53 @@ public: * @param mediaList a list of the new media * @return true on success */ - bool requestMediaChange(const std::string& callID, + bool requestMediaChange(const std::string& callId, const std::vector<DRing::MediaMap>& mediaList); /** * Functions which occur with a user's action * Answer the call - * @param id The call identifier + * @param callId + */ + bool answerCall(const std::string& callId); + + /** + * Answer a call with a list of media + * @param callId + * @param mediaList the list of media attributes. The client can + * control the media through the attributes. The list should have + * the same size as the list reported in the incoming call signal. + */ + bool answerCallWithMedia(const std::string& callId, + const std::vector<DRing::MediaMap>& mediaList); + + /** + * Answer a media change request + * @param callId + * @param mediaList the list of media attributes. The client can + * control the media through the attributes. The list should have + * the same size as the list reported in the media change request. + * The client can ignore the media update request by not calling this + * method, or calling it with an empty media list. + */ + bool answerMediaChangeRequest(const std::string& callId, + const std::vector<DRing::MediaMap>& mediaList = {}); + + /** + * Handle incoming call and notify user + * @param call A call pointer + * @param accountId an account id + */ + void incomingCall(Call& call, const std::string& accountId); + + /** + * Handle a media change request from the peer + * @param callId + * @param accountId */ - bool answerCall(const std::string& id); + void mediaChangeRequested(const std::string& callId, + const std::string& accountId, + const std::vector<DRing::MediaMap>& mediaList); /** * Functions which occur with a user's action @@ -382,13 +420,6 @@ public: */ void stopTone(); - /** - * Handle incoming call and notify user - * @param call A call pointer - * @param accountId an account id - */ - void incomingCall(Call& call, const std::string& accountId); - /** * Notify the user that the recipient of the call has answered and the put the * call in Current state @@ -992,6 +1023,7 @@ private: ~Manager(); friend class AudioDeviceGuard; + // Data members struct ManagerPimpl; std::unique_ptr<ManagerPimpl> pimpl_; }; diff --git a/src/sip/sdes_negotiator.cpp b/src/sip/sdes_negotiator.cpp index fc13ebe20a94a9c0b26c7a48d852cb015a0efcb1..199069e3a2d42ae06c82bfffba21feeee411dae7 100644 --- a/src/sip/sdes_negotiator.cpp +++ b/src/sip/sdes_negotiator.cpp @@ -33,10 +33,6 @@ namespace jami { -SdesNegotiator::SdesNegotiator(const std::vector<CryptoSuiteDefinition>& localCapabilites) - : localCapabilities_(localCapabilites) -{} - std::vector<CryptoAttribute> SdesNegotiator::parse(const std::vector<std::string>& attributes) { @@ -144,12 +140,12 @@ SdesNegotiator::parse(const std::vector<std::string>& attributes) } CryptoAttribute -SdesNegotiator::negotiate(const std::vector<std::string>& attributes) const +SdesNegotiator::negotiate(const std::vector<std::string>& attributes) { try { auto cryptoAttributeVector(parse(attributes)); for (const auto& iter_offer : cryptoAttributeVector) { - for (const auto& iter_local : localCapabilities_) { + for (const auto& iter_local : CryptoSuites) { if (iter_offer.getCryptoSuite() == iter_local.name) return iter_offer; } diff --git a/src/sip/sdes_negotiator.h b/src/sip/sdes_negotiator.h index 14786592a6a4cad6564d095e086b5626869dbc08..051c7688402d38039494abd4650428d84a2ad064 100644 --- a/src/sip/sdes_negotiator.h +++ b/src/sip/sdes_negotiator.h @@ -84,33 +84,14 @@ static std::vector<CryptoSuiteDefinition> CryptoSuites = { class SdesNegotiator { public: - SdesNegotiator() {} + SdesNegotiator(); - /** - * Constructor for an SDES crypto attributes - * negotiator. - * - * @param capabilites - * A vector of crypto attributes as defined in - * RFC4568. This string will be parsed - * and a crypto context will be created - * from it. - */ - SdesNegotiator(const std::vector<CryptoSuiteDefinition>& capabilites); + static CryptoAttribute negotiate(const std::vector<std::string>& attributes); - CryptoAttribute negotiate(const std::vector<std::string>& attributes) const; - - inline explicit operator bool() const { return not localCapabilities_.empty(); } + inline explicit operator bool() const { return not CryptoSuites.empty(); } private: static std::vector<CryptoAttribute> parse(const std::vector<std::string>& attributes); - - /** - * A vector list containing the remote attributes. - * Multiple crypto lines can be sent, and the - * preferred method is then chosen from that list. - */ - std::vector<CryptoSuiteDefinition> localCapabilities_; }; } // namespace jami diff --git a/src/sip/sdp.cpp b/src/sip/sdp.cpp index 69841d41d5bb12c5bb379bd1ccd064daf393bb01..ee587126774e1b1ca9b6101b31271b0e5caf030a 100644 --- a/src/sip/sdp.cpp +++ b/src/sip/sdp.cpp @@ -64,7 +64,6 @@ Sdp::Sdp(const std::string& id) }) , publishedIpAddr_() , publishedIpAddrType_() - , sdesNego_ {CryptoSuites} , telephoneEventPayload_(101) // same as asterisk , sessionName_("Call ID " + id) { @@ -213,6 +212,30 @@ Sdp::getMediaDirection(pjmedia_sdp_media* media) return MediaDirection::UNKNOWN; } +MediaTransport +Sdp::getMediaTransport(pjmedia_sdp_media* media) +{ + if (pj_stricmp2(&media->desc.transport, "RTP/SAVP") == 0) + return MediaTransport::RTP_SAVP; + else if (pj_stricmp2(&media->desc.transport, "RTP/AVP") == 0) + return MediaTransport::RTP_AVP; + + return MediaTransport::UNKNOWN; +} + +std::vector<std::string> +Sdp::getCrypto(pjmedia_sdp_media* media) +{ + std::vector<std::string> crypto; + for (unsigned j = 0; j < media->attr_count; j++) { + const auto attribute = media->attr[j]; + if (pj_stricmp2(&attribute->name, "crypto") == 0) + crypto.emplace_back(attribute->value.ptr, attribute->value.slen); + } + + return crypto; +} + pjmedia_sdp_media* Sdp::addMediaDescription(const MediaAttribute& mediaAttr, bool onHold) { @@ -300,8 +323,8 @@ Sdp::addMediaDescription(const MediaAttribute& mediaAttr, bool onHold) const auto accountVideoCodec = std::static_pointer_cast<AccountVideoCodecInfo>( video_codec_list_[i]); const auto& profileLevelID = accountVideoCodec->parameters.empty() - ? libav_utils::DEFAULT_H264_PROFILE_LEVEL_ID - : accountVideoCodec->parameters; + ? libav_utils::DEFAULT_H264_PROFILE_LEVEL_ID + : accountVideoCodec->parameters; auto value = fmt::format("fmtp:{} {}", payload, profileLevelID); med->attr[med->attr_count++] = pjmedia_sdp_attr_create(memPool_.get(), value.c_str(), @@ -364,7 +387,9 @@ void Sdp::setTelephoneEventRtpmap(pjmedia_sdp_media* med) { ++med->desc.fmt_count; - pj_strdup2(memPool_.get(), &med->desc.fmt[med->desc.fmt_count - 1], std::to_string(telephoneEventPayload_).c_str()); + pj_strdup2(memPool_.get(), + &med->desc.fmt[med->desc.fmt_count - 1], + std::to_string(telephoneEventPayload_).c_str()); pjmedia_sdp_attr* attr_rtpmap = static_cast<pjmedia_sdp_attr*>( pj_pool_zalloc(memPool_.get(), sizeof(pjmedia_sdp_attr))); @@ -413,8 +438,22 @@ Sdp::setLocalMediaCapabilities(MediaType type, } } +const char* +Sdp::getSdpDirectionStr(SdpDirection direction) +{ + if (direction == SdpDirection::LOCAL_OFFER) + return "LOCAL_OFFER"; + if (direction == SdpDirection::LOCAL_ANSWER) + return "LOCAL_ANSWER"; + if (direction == SdpDirection::REMOTE_OFFER) + return "REMOTE_OFFER"; + if (direction == SdpDirection::REMOTE_ANSWER) + return "REMOTE_ANSWER"; + return "NONE"; +} + void -Sdp::printSession(const pjmedia_sdp_session* session, const char* header, bool isOffer) +Sdp::printSession(const pjmedia_sdp_session* session, const char* header, SdpDirection direction) { static constexpr size_t BUF_SZ = 4095; sip_utils::register_thread(); @@ -440,16 +479,17 @@ Sdp::printSession(const pjmedia_sdp_session* session, const char* header, bool i std::array<char, BUF_SZ + 1> buffer; auto size = pjmedia_sdp_print(cloned_session, buffer.data(), BUF_SZ); if (size < 0) { - JAMI_ERR("%sSDP too big for dump", header); + JAMI_ERR("%s SDP too big for dump", header); return; } - JAMI_DBG("[SDP %s] %s%.*s", isOffer ? "offer" : "answer", header, size, buffer.data()); + + JAMI_DBG("[SDP %s] %s\n%.*s", getSdpDirectionStr(direction), header, size, buffer.data()); } void -Sdp::createLocalSession(bool offer) +Sdp::createLocalSession(SdpDirection direction) { - isOffer_ = offer; + sdpDirection_ = direction; localSession_ = PJ_POOL_ZALLOC_T(memPool_.get(), pjmedia_sdp_session); localSession_->conn = PJ_POOL_ZALLOC_T(memPool_.get(), pjmedia_sdp_conn); @@ -498,7 +538,7 @@ Sdp::createOffer(const std::vector<MediaAttribute>& mediaList) JAMI_DBG("Creating SDP offer with %lu medias", mediaList.size()); - createLocalSession(true); + createLocalSession(SdpDirection::LOCAL_OFFER); if (validateSession() != PJ_SUCCESS) { JAMI_ERR("Failed to create initial offer"); @@ -522,26 +562,35 @@ Sdp::createOffer(const std::vector<MediaAttribute>& mediaList) return false; } + Sdp::printSession(localSession_, "Local session (initial):", sdpDirection_); + return true; } void -Sdp::receiveOffer(const pjmedia_sdp_session* remote, const std::vector<MediaAttribute>& mediaList) +Sdp::setReceivedOffer(const pjmedia_sdp_session* remote) { if (remote == nullptr) { JAMI_ERR("Remote session is NULL"); return; } + remoteSession_ = pjmedia_sdp_session_clone(memPool_.get(), remote); +} - JAMI_DBG("Creating SDP answer with %lu media", mediaList.size()); +bool +Sdp::processIncomingOffer(const std::vector<MediaAttribute>& mediaList) +{ + assert(remoteSession_); - printSession(remote, "Remote session:\n", isOffer_); + JAMI_DBG("Processing received offer for [%s] with %lu", sessionName_.c_str(), mediaList.size()); + + printSession(remoteSession_, "Remote session:", SdpDirection::REMOTE_OFFER); if (not localSession_) { - createLocalSession(false); + createLocalSession(SdpDirection::LOCAL_ANSWER); if (validateSession() != PJ_SUCCESS) { JAMI_ERR("Failed to create initial offer"); - return; + return false; } } @@ -551,30 +600,33 @@ Sdp::receiveOffer(const pjmedia_sdp_session* remote, const std::vector<MediaAttr localSession_->media[localSession_->media_count++] = addMediaDescription(media); } - printSession(localSession_, "Local session:\n", not isOffer_); + printSession(localSession_, "Local session:\n", sdpDirection_); if (validateSession() != PJ_SUCCESS) { JAMI_ERR("Failed to add medias"); - return; + return false; } - remoteSession_ = pjmedia_sdp_session_clone(memPool_.get(), remote); - if (pjmedia_sdp_neg_create_w_remote_offer(memPool_.get(), localSession_, remoteSession_, &negotiator_) != PJ_SUCCESS) { JAMI_ERR("Failed to initialize media negotiation"); + return false; } + + return true; } -void +bool Sdp::startNegotiation() { + JAMI_DBG("Starting media negotiation for [%s]", sessionName_.c_str()); + if (negotiator_ == NULL) { JAMI_ERR("Can't start negotiation with invalid negotiator"); - return; + return false; } const pjmedia_sdp_session* active_local; @@ -582,29 +634,34 @@ Sdp::startNegotiation() if (pjmedia_sdp_neg_get_state(negotiator_) != PJMEDIA_SDP_NEG_STATE_WAIT_NEGO) { JAMI_WARN("Negotiator not in right state for negotiation"); - return; + return false; } - if (pjmedia_sdp_neg_negotiate(memPool_.get(), negotiator_, 0) != PJ_SUCCESS) - return; + if (pjmedia_sdp_neg_negotiate(memPool_.get(), negotiator_, 0) != PJ_SUCCESS) { + JAMI_ERR("Failed to start media negotiation"); + return false; + } if (pjmedia_sdp_neg_get_active_local(negotiator_, &active_local) != PJ_SUCCESS) JAMI_ERR("Could not retrieve local active session"); - else { - setActiveLocalSdpSession(active_local); - if (active_local != nullptr) { - Sdp::printSession(active_local, "Local session:\n", isOffer_); - } + + setActiveLocalSdpSession(active_local); + + if (active_local != nullptr) { + Sdp::printSession(active_local, "Local active session:", sdpDirection_); } - if (pjmedia_sdp_neg_get_active_remote(negotiator_, &active_remote) != PJ_SUCCESS) + if (pjmedia_sdp_neg_get_active_remote(negotiator_, &active_remote) != PJ_SUCCESS + or active_remote == nullptr) { JAMI_ERR("Could not retrieve remote active session"); - else { - setActiveRemoteSdpSession(active_remote); - if (active_remote != nullptr) { - Sdp::printSession(active_remote, "Remote active session:\n", not isOffer_); - } + return false; } + + setActiveRemoteSdpSession(active_remote); + + Sdp::printSession(active_remote, "Remote active session:", sdpDirection_); + + return true; } std::string @@ -799,7 +856,7 @@ Sdp::getMediaDescriptions(const pjmedia_sdp_session* session, bool remote) const if (pj_stricmp2(&attribute->name, "crypto") == 0) crypto.emplace_back(attribute->value.ptr, attribute->value.slen); } - descr.crypto = sdesNego_.negotiate(crypto); + descr.crypto = SdesNegotiator::negotiate(crypto); } return ret; } @@ -936,4 +993,51 @@ Sdp::clearIce(pjmedia_sdp_session* session) } } +std::vector<MediaAttribute> +Sdp::getMediaAttributeListFromSdp(const pjmedia_sdp_session* sdpSession) +{ + if (sdpSession == nullptr) { + return {}; + } + + std::vector<MediaAttribute> mediaList; + for (unsigned idx = 0; idx < sdpSession->media_count; idx++) { + mediaList.emplace_back(MediaAttribute {}); + auto& mediaAttr = mediaList.back(); + + auto const& media = sdpSession->media[idx]; + + // Get media type. + if (!pj_stricmp2(&media->desc.media, "audio")) + mediaAttr.type_ = MediaType::MEDIA_AUDIO; + else if (!pj_stricmp2(&media->desc.media, "video")) + mediaAttr.type_ = MediaType::MEDIA_VIDEO; + else { + JAMI_WARN("Media#%u only 'audio' and 'video' types are supported!", idx); + // Keep the media in the list but dont bother parsing the attributes + continue; + } + + // Get mute state. + auto direction = getMediaDirection(media); + mediaAttr.muted_ = direction != MediaDirection::SENDRECV + and direction != MediaDirection::SENDONLY; + + // Get transport. + auto transp = getMediaTransport(media); + if (transp == MediaTransport::UNKNOWN) { + JAMI_WARN("Media#%u could not determine transport type!", idx); + } + + // A media is secure if the transport is of type RTP/SAVP + // and the crypto materials are present. + mediaAttr.secure_ = transp == MediaTransport::RTP_SAVP and not getCrypto(media).empty(); + + mediaAttr.label_ = mediaAttr.type_ == MediaType::MEDIA_AUDIO ? "audio_" : "video_"; + mediaAttr.label_ += std::to_string(idx); + } + + return mediaList; +} + } // namespace jami diff --git a/src/sip/sdp.h b/src/sip/sdp.h index 09bc16849d243e6b877ee84601d9709a505961de..788b14c15efa6bc5591f213f011e08b6ad23022a 100644 --- a/src/sip/sdp.h +++ b/src/sip/sdp.h @@ -61,6 +61,8 @@ public: {} }; +enum class SdpDirection { LOCAL_OFFER, LOCAL_ANSWER, REMOTE_OFFER, REMOTE_ANSWER, NONE }; + class Sdp { public: @@ -119,19 +121,19 @@ public: */ bool createOffer(const std::vector<MediaAttribute>& mediaList); - /* - * On receiving an invite outside a dialog, build the local offer and create the - * SDP negotiator instance with the remote offer. + void setReceivedOffer(const pjmedia_sdp_session* remote); + + /** + * Build a new SDP answer using mediaList. * - * @param remote The remote offer + * @param mediaList The list of media attributes to build the answer */ - void receiveOffer(const pjmedia_sdp_session* remote, - const std::vector<MediaAttribute>& mediaList); + bool processIncomingOffer(const std::vector<MediaAttribute>& mediaList); /** * Start the sdp negotiation. */ - void startNegotiation(); + bool startNegotiation(); /** * Remove all media in the session media vector. @@ -180,6 +182,9 @@ public: std::vector<MediaDescription> getMediaDescriptions(const pjmedia_sdp_session* session, bool remote) const; + static std::vector<MediaAttribute> getMediaAttributeListFromSdp( + const pjmedia_sdp_session* sdpSession); + using MediaSlot = std::pair<MediaDescription, MediaDescription>; std::vector<MediaSlot> getMediaSlots() const; @@ -194,12 +199,14 @@ public: std::vector<std::string> getIceCandidates(unsigned media_index) const; void clearIce(); - // True if the session is an offer and false if it's an answer. - bool isOffer() const { return isOffer_; }; + SdpDirection getSdpDirection() const { return sdpDirection_; }; + static const char* getSdpDirectionStr(SdpDirection direction); /// \brief Log the given session /// \note crypto lines with are removed for security - static void printSession(const pjmedia_sdp_session* session, const char* header, bool offer); + static void printSession(const pjmedia_sdp_session* session, + const char* header, + SdpDirection direction); private: friend class test::SDPTest; @@ -264,15 +271,13 @@ private: uint16_t localVideoDataPort_ {0}; uint16_t localVideoControlPort_ {0}; - SdesNegotiator sdesNego_; - unsigned int telephoneEventPayload_; // The call Id of the SDP owner std::string sessionName_ {}; // Offer/Answer flag. - bool isOffer_ {true}; + SdpDirection sdpDirection_ {SdpDirection::NONE}; /* * Build the sdp media section @@ -281,7 +286,6 @@ private: pjmedia_sdp_media* addMediaDescription(const MediaAttribute& mediaAttr, bool onHold = false); // Determine media direction - char const* mediaDirection(bool enabled, bool onHold, bool muted); char const* mediaDirection(MediaType type, bool onHold); char const* mediaDirection(const MediaAttribute& localAttr, const MediaAttribute& peerAttr); @@ -301,7 +305,7 @@ private: /* * Create a new SDP */ - void createLocalSession(bool offer); + void createLocalSession(SdpDirection direction); /* * Validate SDP diff --git a/src/sip/sipaccount.cpp b/src/sip/sipaccount.cpp index 22cbed8dbd81f05af7b2471f1910773afb3884e2..543c1bfa9612f8c59f00b42e6692d995f4725b75 100644 --- a/src/sip/sipaccount.cpp +++ b/src/sip/sipaccount.cpp @@ -183,6 +183,16 @@ SIPAccount::newIncomingCall(const std::string& from UNUSED, return manager.callFactory.newSipCall(shared(), Call::CallType::INCOMING, details); } +std::shared_ptr<SIPCall> +SIPAccount::newIncomingCall(const std::string& from UNUSED, + const std::vector<MediaAttribute>& mediaAttrList, + const std::shared_ptr<SipTransport>&) +{ + return Manager::instance().callFactory.newSipCall(shared(), + Call::CallType::INCOMING, + mediaAttrList); +} + template<> std::shared_ptr<SIPCall> SIPAccount::newOutgoingCall(std::string_view toUrl, @@ -225,7 +235,6 @@ SIPAccount::newOutgoingCall(std::string_view toUrl, auto toUri = getToUri(to); call->initIceMediaTransport(true); - call->setIPToIP(isIP2IP()); call->setPeerNumber(toUri); call->setPeerUri(toUri); @@ -316,7 +325,6 @@ SIPAccount::newOutgoingCall(std::string_view toUrl, const std::vector<MediaAttri auto toUri = getToUri(to); call->initIceMediaTransport(true); - call->setIPToIP(isIP2IP()); call->setPeerNumber(toUri); call->setPeerUri(toUri); @@ -436,7 +444,7 @@ bool SIPAccount::SIPStartCall(std::shared_ptr<SIPCall>& call) { // Add Ice headers to local SDP if ice transport exist - call->setupLocalSDPFromIce(); + call->addLocalIceAttributes(); const std::string& toUri(call->getPeerNumber()); // expecting a fully well formed sip uri pj_str_t pjTo = sip_utils::CONST_PJ_STR(toUri); @@ -1379,7 +1387,6 @@ SIPAccount::initTlsConfiguration() avail_ciphers.resize(cipherNum); ciphers_.clear(); -#if PJ_VERSION_NUM > (2 << 24 | 2 << 16) std::string_view stream(tlsCiphers_), item; while (jami::getline(stream, item, ' ')) { std::string cipher(item); @@ -1390,7 +1397,7 @@ SIPAccount::initTlsConfiguration() } else JAMI_ERR("Invalid cipher: %s", cipher.c_str()); } -#endif + ciphers_.erase(std::remove_if(ciphers_.begin(), ciphers_.end(), [&](pj_ssl_cipher c) { diff --git a/src/sip/sipaccount.h b/src/sip/sipaccount.h index 15009f1b7afbee24539a13d10442601c8080d59e..5e5484b52363beac42dcfcd6fd88fb9ae60039c0 100644 --- a/src/sip/sipaccount.h +++ b/src/sip/sipaccount.h @@ -373,13 +373,6 @@ public: virtual bool isTlsEnabled() const override { return tlsEnable_; } - virtual KeyExchangeProtocol getSrtpKeyExchange() const override - { - if (tlsEnable_ && srtpKeyExchange_ == KeyExchangeProtocol::NONE) - return KeyExchangeProtocol::SDES; - return srtpKeyExchange_; - } - virtual bool getSrtpFallback() const override { return srtpFallback_; } void setReceivedParameter(const std::string& received) @@ -491,6 +484,18 @@ public: const std::map<std::string, std::string>& details = {}, const std::shared_ptr<SipTransport>& = nullptr) override; + /** + * Create incoming SIPCall. + * @param[in] from The origin of the call + * @param mediaList A list of media + * @param sipTr: SIP Transport + * @return A shared pointer on the created call. + */ + std::shared_ptr<SIPCall> newIncomingCall( + const std::string& from, + const std::vector<MediaAttribute>& mediaList, + const std::shared_ptr<SipTransport>& sipTr = {}) override; + void onRegister(pjsip_regc_cbparam* param); virtual void sendTextMessage(const std::string& to, @@ -541,7 +546,7 @@ private: bool hostnameMatch(std::string_view hostname) const; bool proxyMatch(std::string_view hostname) const; - bool isSrtpEnabled() const { return srtpKeyExchange_ != KeyExchangeProtocol::NONE; } + bool isSrtpEnabled() const override { return srtpKeyExchange_ != KeyExchangeProtocol::NONE; } /** * Callback called by the transport layer when the registration diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 92f5ce85aede17b92bb8163695866c388ae48cbb..9f36efaa815208873d6caeec383a9b1193b09cd6 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -683,8 +683,7 @@ std::vector<MediaAttribute> SIPAccountBase::createDefaultMediaList(bool addVideo, bool onHold) { std::vector<MediaAttribute> mediaList; - bool secure = getSrtpKeyExchange() == KeyExchangeProtocol::SDES; - + bool secure = isSrtpEnabled(); // Add audio and DTMF events mediaList.emplace_back( MediaAttribute(MediaType::MEDIA_AUDIO, onHold, secure, false, "", "main audio")); diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h index 62d734605d65d496f5820ce5234f700e9c7eda69..359df70b9671e326fc843e521ea31192b15d501b 100644 --- a/src/sip/sipaccountbase.h +++ b/src/sip/sipaccountbase.h @@ -145,6 +145,18 @@ public: const std::shared_ptr<SipTransport>& = nullptr) = 0; + /** + * Create incoming SIPCall. + * @param[in] from The origin of the call + * @param mediaList A list of media + * @param sipTr: SIP Transport + * @return A shared pointer on the created call. + */ + virtual std::shared_ptr<SIPCall> newIncomingCall(const std::string& from, + const std::vector<MediaAttribute>& mediaList, + const std::shared_ptr<SipTransport>& sipTr = {}) + = 0; + virtual bool isStunEnabled() const { return false; } virtual pj_str_t getStunServerName() const { return pj_str_t {nullptr, 0}; }; @@ -210,7 +222,7 @@ public: */ bool getPublishedSameasLocal() const { return publishedSameasLocal_; } - virtual KeyExchangeProtocol getSrtpKeyExchange() const = 0; + virtual bool isSrtpEnabled() const = 0; virtual bool getSrtpFallback() const = 0; diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp index b7a18b90c38d79265dedcd7ee1be246549d7f6c0..fd0c5c6b27401550903093d3f46872a776f648df 100644 --- a/src/sip/sipcall.cpp +++ b/src/sip/sipcall.cpp @@ -124,7 +124,21 @@ SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account, account->getActiveAccountCodecInfoList(MEDIA_VIDEO)); #endif - initMediaStreams(mediaAttrList); + std::vector<MediaAttribute> mediaList; + if (mediaAttrList.size() > 0) { + mediaList = mediaAttrList; + } else { + // Handle incoming call without media offer. + JAMI_WARN("[call:%s] No media offered in the incoming invite. Will answer with audio-only", + getCallId().c_str()); + mediaList = getSIPAccount()->createDefaultMediaList(false, getState() == CallState::HOLD); + } + + JAMI_DBG("[call:%s] Create a new SIP call with %lu medias", + getCallId().c_str(), + mediaList.size()); + + initMediaStreams(mediaList); } SIPCall::~SIPCall() @@ -418,19 +432,20 @@ SIPCall::setTransport(const std::shared_ptr<SipTransport>& t) void SIPCall::requestReinvite() { - JAMI_DBG("[call:%s] Requesting a SIP re-invite", getCallId().c_str()); + 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 stream before creating new ones. + // 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; } @@ -452,7 +467,7 @@ SIPCall::SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList) if (not inviteSession_ or inviteSession_->invite_tsx) return PJ_SUCCESS; - JAMI_DBG("[call:%s] Processing reINVITE (state=%s)", + JAMI_DBG("[call:%s] Preparing and sending a re-invite (state=%s)", getCallId().c_str(), pjsip_inv_state_name(inviteSession_->state)); @@ -470,7 +485,7 @@ SIPCall::SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList) return !PJ_SUCCESS; if (initIceMediaTransport(true)) - setupLocalSDPFromIce(); + addLocalIceAttributes(); pjsip_tx_data* tdata; auto local_sdp = sdp_->getLocalSdpSession(); @@ -605,6 +620,8 @@ SIPCall::setMute(bool state) 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) { @@ -720,6 +737,171 @@ SIPCall::answer() setState(CallState::ACTIVE, ConnectionState::CONNECTED); } +void +SIPCall::answer(const std::vector<MediaAttribute>& mediaAttrList) +{ + std::lock_guard<std::recursive_mutex> lk {callMutex_}; + auto account = getSIPAccount(); + if (not account) { + JAMI_ERR("No account detected"); + return; + } + if (mediaAttrList.size() != rtpStreams_.size()) { + JAMI_ERR("Media list size %lu in answer does not match. Expected %lu", + mediaAttrList.size(), + rtpStreams_.size()); + return; + } + + // Apply the media attributes provided by the user. + for (size_t idx = 0; idx < mediaAttrList.size(); idx++) { + rtpStreams_[idx].mediaAttribute_ = std::make_shared<MediaAttribute>(mediaAttrList[idx]); + } + + if (not inviteSession_) + throw VoipLinkException("[call:" + getCallId() + + "] answer: no invite session for this call"); + + if (not inviteSession_->neg) { + JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP", + getCallId().c_str()); + pjmedia_sdp_session* dummy = 0; + Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get(), &dummy); + + if (account->isStunEnabled()) + updateSDPFromSTUN(); + } + + pj_str_t contact(account->getContactHeader(transport_ ? transport_->get() : nullptr)); + setContactHeader(&contact); + + if (!inviteSession_->last_answer) + throw std::runtime_error("Should only be called for initial answer"); + + // TODO. We need a test for this scenario. + // How to Check if this use-case is not broken by the changes. + + // Answer with an SDP offer if the initial invite was empty. + // SIP protocol allows a UA to send a call invite without SDP. + // In this case, if the callee wants to accept the call, it must + // provide an SDP offer in the answer. The caller will then send + // its SDP answer in the SIP OK (200) message. + 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<MediaAttribute>& mediaAttrList) +{ + 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; + } + + 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; + } + + setupLocalIce(); + + 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) { @@ -1257,7 +1439,7 @@ SIPCall::onPeerRinging() } void -SIPCall::setupLocalSDPFromIce() +SIPCall::addLocalIceAttributes() { auto media_tr = getIceMediaTransport(); @@ -1288,9 +1470,9 @@ SIPCall::setupLocalSDPFromIce() unsigned idx = 0; unsigned compId = 1; for (auto const& stream : rtpStreams_) { - JAMI_DBG("[call:%s] add ICE local candidates for media [%s] at index %u", + JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u", getCallId().c_str(), - stream.mediaAttribute_->label_.c_str(), + stream.mediaAttribute_->toString().c_str(), idx); // RTP sdp_->addIceCandidates(idx, media_tr->getLocalCandidates(compId)); @@ -1391,6 +1573,50 @@ SIPCall::initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList) return rtpStreams_.size(); } +bool +SIPCall::isAudioMuted() const +{ + std::function<bool(const RtpStream& stream)> mutedCheck = [](auto const& stream) { + return (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO + and not stream.mediaAttribute_->muted_); + }; + + const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck); + + return iter != rtpStreams_.end(); +} + +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::isVideoMuted() const +{ +#ifdef ENABLE_VIDEO + std::function<bool(const RtpStream& stream)> mutedCheck = [](auto const& stream) { + return (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO + and not stream.mediaAttribute_->muted_); + }; + const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck); + return iter != rtpStreams_.end(); +#else + return true; +#endif +} + void SIPCall::startAllMedia() { @@ -1480,18 +1706,6 @@ SIPCall::startAllMedia() } #ifdef ENABLE_VIDEO - // Check if there is an un-muted video stream - auto const& iter = std::find_if(rtpStreams_.begin(), - rtpStreams_.end(), - [](const RtpStream& stream) { - return stream.mediaAttribute_->type_ - == MediaType::MEDIA_VIDEO - and not stream.mediaAttribute_->muted_; - }); - - // Video is muted if all video streams are muted. - isVideoMuted_ = iter == rtpStreams_.end(); - if (!isVideoEnabled && !getConfId().empty()) { auto conference = Manager::instance().getConferenceFromID(getConfId()); conference->attachVideo(getReceiveVideoFrameActiveWriter().get(), getCallId()); @@ -1609,8 +1823,8 @@ SIPCall::muteMedia(const std::string& mediaType, bool mute) requestMediaChange(mediaList); } -bool -SIPCall::updateMediaStreamInternal(const MediaAttribute& newMediaAttr, size_t streamIdx) +void +SIPCall::updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx) { assert(streamIdx < rtpStreams_.size()); @@ -1620,8 +1834,6 @@ SIPCall::updateMediaStreamInternal(const MediaAttribute& newMediaAttr, size_t st auto const& mediaAttr = rtpStream.mediaAttribute_; assert(mediaAttr); - bool requireReinvite = false; - if (newMediaAttr.muted_ == mediaAttr->muted_) { // Nothing to do. Already in the desired state. JAMI_DBG("[call:%s] [%s] already %s", @@ -1629,7 +1841,7 @@ SIPCall::updateMediaStreamInternal(const MediaAttribute& newMediaAttr, size_t st mediaAttr->label_.c_str(), mediaAttr->muted_ ? "muted " : "un-muted "); - return requireReinvite; + return; } // Update @@ -1646,24 +1858,19 @@ SIPCall::updateMediaStreamInternal(const MediaAttribute& newMediaAttr, size_t st if (not isSubcall()) emitSignal<DRing::CallSignal::AudioMuted>(getCallId(), mediaAttr->muted_); setMute(mediaAttr->muted_); - return requireReinvite; + return; } #ifdef ENABLE_VIDEO if (mediaAttr->type_ == MediaType::MEDIA_VIDEO) { if (not isSubcall()) emitSignal<DRing::CallSignal::VideoMuted>(getCallId(), mediaAttr->muted_); - - // Changes in video attributes always trigger a re-invite. - requireReinvite = true; } #endif - - return requireReinvite; } -bool -SIPCall::requestMediaChange(const std::vector<MediaAttribute>& mediaAttrList) +void +SIPCall::updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList) { JAMI_DBG("[call:%s] New local medias", getCallId().c_str()); @@ -1671,34 +1878,78 @@ SIPCall::requestMediaChange(const std::vector<MediaAttribute>& mediaAttrList) for (auto const& newMediaAttr : mediaAttrList) { JAMI_DBG("[call:%s] Media @%u: %s", getCallId().c_str(), - idx, + idx++, newMediaAttr.toString(true).c_str()); - idx++; } - bool reinviteRequired = false; + JAMI_DBG("[call:%s] Updating local media streams", getCallId().c_str()); + + for (auto const& newAttr : mediaAttrList) { + auto streamIdx = findRtpStreamIndex(newAttr.label_); - for (auto const& newMediaAttr : mediaAttrList) { - auto streamIdx = findRtpStreamIndex(newMediaAttr.label_); if (streamIdx == rtpStreams_.size()) { // Media does not exist, add a new one. - addMediaStream(newMediaAttr); + addMediaStream(newAttr); auto& stream = rtpStreams_.back(); createRtpSession(stream); - JAMI_DBG( - "[call:%s] Added a new media stream - type: %s - @ index %lu. Require a re-invite", - getCallId().c_str(), - stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO ? "AUDIO" : "VIDEO", - streamIdx); - // Needs a new SDP and reinvite. - reinviteRequired = true; + JAMI_DBG("[call:%s] Added a new media stream [%s] @ index %lu", + getCallId().c_str(), + stream.mediaAttribute_->label_.c_str(), + streamIdx); } else { - reinviteRequired |= updateMediaStreamInternal(newMediaAttr, streamIdx); + 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 (reinviteRequired) + 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<MediaAttribute>& mediaAttrList) +{ + 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; } @@ -1720,14 +1971,19 @@ SIPCall::getMediaAttributeList() const /// In case of ICE transport used, the medias streams are launched asynchronously when /// the transport is negotiated. void -SIPCall::onMediaUpdate() +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()) { + emitSignal<DRing::CallSignal::MediaNegotiationStatus>( + this_->getCallId(), MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS); + std::lock_guard<std::recursive_mutex> lk {this_->callMutex_}; - JAMI_WARN("[call:%s] medias changed", this_->getCallId().c_str()); + 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_) @@ -1735,7 +1991,7 @@ SIPCall::onMediaUpdate() // If ICE is not used, start medias now auto rem_ice_attrs = this_->sdp_->getIceAttributes(); if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) { - JAMI_WARN("[call:%s] no remote ICE for medias", this_->getCallId().c_str()); + JAMI_WARN("[call:%s] no remote ICE for media", this_->getCallId().c_str()); this_->stopAllMedia(); this_->startAllMedia(); return; @@ -1808,6 +2064,61 @@ SIPCall::onIceNegoSucceed() 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::REMOTE_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 = getMediaAttributeList(); + answerMediaChangeRequest(localMediaList); + } + + return res; +} + int SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* rdata) { @@ -1820,15 +2131,21 @@ SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* r return !PJ_SUCCESS; } - // TODO. WARNING. If this is an ongoing audio-only call, and the peer added - // video in a subsequent offer, the local user MUST first accept the new media(s). + JAMI_DBG("[call:%s] Received a new offer (re-invite)", getCallId().c_str()); + // This list should be provided by the client. Kept for backward compatibility. auto mediaList = acc->createDefaultMediaList(acc->isVideoEnabled(), getState() == CallState::HOLD); - sdp_->receiveOffer(offer, mediaList); + if (upnp_) { + openPortsUPnP(); + } + + sdp_->setReceivedOffer(offer); + sdp_->processIncomingOffer(mediaList); - setRemoteSdp(offer); + if (offer) + setupLocalIce(); sdp_->startNegotiation(); pjsip_tx_data* tdata = nullptr; @@ -1862,7 +2179,9 @@ SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* r return !PJ_SUCCESS; } - openPortsUPnP(); + if (upnp_) { + openPortsUPnP(); + } return PJ_SUCCESS; } @@ -1870,43 +2189,48 @@ SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* r void SIPCall::openPortsUPnP() { - if (upnp_ and sdp_) { - /** - * 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()); - - upnp_->reserveMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP); - // RTCP port. - upnp_->reserveMapping(sdp_->getLocalAudioControlPort(), upnp::PortType::UDP); + 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); + // 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 details = Call::getDetails(); - details.emplace(DRing::Call::Details::PEER_HOLDING, peerHolding_ ? TRUE_STR : FALSE_STR); - 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) @@ -2069,7 +2393,7 @@ SIPCall::toggleRecording() if (audioRtp) audioRtp->initRecorder(recorder_); #ifdef ENABLE_VIDEO - if (not isAudioOnly_) { + if (hasVideo()) { auto const& videoRtp = getVideoRtp(); if (videoRtp) videoRtp->initRecorder(recorder_); @@ -2091,7 +2415,7 @@ SIPCall::deinitRecorder() if (audioRtp) audioRtp->deinitRecorder(recorder_); #ifdef ENABLE_VIDEO - if (not isAudioOnly_) { + if (hasVideo()) { auto const& videoRtp = getVideoRtp(); if (videoRtp) videoRtp->deinitRecorder(recorder_); @@ -2124,15 +2448,14 @@ SIPCall::InvSessionDeleter::operator()(pjsip_inv_session* inv) const noexcept } bool -SIPCall::initIceMediaTransport(bool master, - std::optional<IceTransportOptions> options, - unsigned channel_num) +SIPCall::initIceMediaTransport(bool master, std::optional<IceTransportOptions> options) { auto acc = getSIPAccount(); if (!acc) { JAMI_ERR("No account detected"); return false; } + JAMI_DBG("[call:%s] create media ICE transport", getCallId().c_str()); auto iceOptions = options == std::nullopt ? acc->getIceOptions() : *options; @@ -2172,9 +2495,11 @@ SIPCall::initIceMediaTransport(bool master, }); }; + // Each RTP stream requires a pair of ICE components (RTP + RTCP). + int compCount = static_cast<int>(rtpStreams_.size() * 2); auto& iceTransportFactory = Manager::instance().getIceTransportFactory(); auto transport = iceTransportFactory.createUTransport(getCallId().c_str(), - channel_num, + compCount, master, iceOptions); std::lock_guard<std::mutex> lk(transportMtx_); @@ -2194,7 +2519,7 @@ SIPCall::initIceMediaTransport(bool master, void SIPCall::merge(Call& call) { - JAMI_DBG("[sipcall:%s] merge subcall %s", getCallId().c_str(), call.getCallId().c_str()); + 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); @@ -2203,7 +2528,8 @@ SIPCall::merge(Call& call) 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_); - inviteSession_->mod_data[Manager::instance().sipVoIPLink().getModId()] = this; + if (inviteSession_) + inviteSession_->mod_data[Manager::instance().sipVoIPLink().getModId()] = this; setTransport(std::move(subcall.transport_)); sdp_ = std::move(subcall.sdp_); peerHolding_ = subcall.peerHolding_; @@ -2222,20 +2548,29 @@ SIPCall::merge(Call& call) startIceMedia(); } -void -SIPCall::setRemoteSdp(const pjmedia_sdp_session* sdp) +bool +SIPCall::remoteHasValidIceAttributes() { - if (!sdp) - return; + if (not sdp_) { + throw std::runtime_error("Must have a valid SDP Session"); + } - // If ICE is not used, start medias now auto rem_ice_attrs = sdp_->getIceAttributes(); - if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) { + return not rem_ice_attrs.ufrag.empty() and not rem_ice_attrs.pwd.empty(); +} + +void +SIPCall::setupLocalIce() +{ + if (not remoteHasValidIceAttributes()) { + // If ICE attributes are not present, skip the ICE initialization + // step (most likely ICE is not used). JAMI_WARN("[call:%s] no ICE data in remote SDP", getCallId().c_str()); return; } if (not initIceMediaTransport(false)) { + JAMI_ERR("[call:%s] ICE initialization failed", getCallId().c_str()); // Fatal condition // TODO: what's SIP rfc says about that? // (same question in startIceMedia) @@ -2244,7 +2579,7 @@ SIPCall::setRemoteSdp(const pjmedia_sdp_session* sdp) } // WARNING: This call blocks! (need ice init done) - setupLocalSDPFromIce(); + addLocalIceAttributes(); } bool diff --git a/src/sip/sipcall.h b/src/sip/sipcall.h index 81ea04ee80daef89742b689f024d341f06ddd60c..a475a8a55cc89657966797098051fa50531035a4 100644 --- a/src/sip/sipcall.h +++ b/src/sip/sipcall.h @@ -126,6 +126,8 @@ private: public: void answer() override; + void answer(const std::vector<MediaAttribute>& mediaList) override; + void answerMediaChangeRequest(const std::vector<MediaAttribute>& mediaList) override; void hangup(int reason) override; void refuse() override; void transfer(const std::string& to) override; @@ -157,6 +159,10 @@ public: return nullptr; } + + bool hasVideo() const override; + bool isAudioMuted() const override; + bool isVideoMuted() const override; // End of override of Call class // Override of Recordable class @@ -174,42 +180,39 @@ public: // Implementation of events reported by SipVoipLink. /** - * Tell the user that the call is ringing + * Call is in ringing state on peer's side */ void onPeerRinging(); - /** - * Tell the user that the call was answered + * Peer answered the call */ void onAnswered(); - /** - * To call in case of server/internal error + * Called to report server/internal errors * @param cause Optional error code */ void onFailure(signed cause = 0); - /** * Peer answered busy */ void onBusyHere(); - /** - * Peer close the connection + * Peer closed the connection */ void onClosed(); - /** * Report a new offer from peer on a existing invite session * (aka re-invite) */ - int onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* rdata); + [[deprecated("Replaced by onReceiveReinvite")]] int onReceiveOffer( + const pjmedia_sdp_session* offer, const pjsip_rx_data* rdata); + pj_status_t onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdata); /** * Called when the media negotiation (SDP offer/answer) has * completed. */ - void onMediaUpdate(); + void onMediaNegotiationComplete(); // End fo SiPVoipLink events void setContactHeader(pj_str_t* contact); @@ -228,13 +231,14 @@ public: void updateSDPFromSTUN(); - void setupLocalSDPFromIce(); + bool remoteHasValidIceAttributes(); + void addLocalIceAttributes(); /** * Give peer SDP to the call for handling * @param sdp pointer on PJSIP sdp structure, could be nullptr (acts as no-op in such case) */ - void setRemoteSdp(const pjmedia_sdp_session* sdp); + void setupLocalIce(); void terminateSipSession(int status); @@ -268,8 +272,7 @@ public: void setPeerUri(const std::string& peerUri) { peerUri_ = peerUri; } bool initIceMediaTransport(bool master, - std::optional<IceTransportOptions> options = std::nullopt, - unsigned channel_num = 4); + std::optional<IceTransportOptions> options = std::nullopt); bool isIceRunning() const; @@ -348,16 +351,27 @@ private: bool hold(); bool unhold(); - /** - * Update the attributes of a media stream - * @param newMediaAttr the new attributes - * @param streamIdx the index of the stream to update - * @return true if the update requires a new SDP and SIP re-invite. - */ - bool updateMediaStreamInternal(const MediaAttribute& newMediaAttr, size_t streamIdx); + // Update the attributes of a media stream + void updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx); + void updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList); + // Check if a SIP re-invite must be sent to negotiate the new media + bool isReinviteRequired(const std::vector<MediaAttribute>& mediaAttrList); void requestReinvite(); int SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList); int SIPSessionReinvite(); + // Add a media stream to the call. + void addMediaStream(const MediaAttribute& mediaAttr); + // Init media streams + size_t initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList); + // Create a new stream from SDP description. + void createRtpSession(RtpStream& rtpStream); + // Configure the RTP session from SDP description. + void configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession, + const std::shared_ptr<MediaAttribute>& mediaAttr, + const MediaDescription& localMedia, + const MediaDescription& remoteMedia); + // Find the stream index with the matching label + size_t findRtpStreamIndex(const std::string& label) const; std::vector<IceCandidate> getAllRemoteCandidates(); @@ -375,30 +389,6 @@ private: } inline std::weak_ptr<SIPCall> weak() { return std::weak_ptr<SIPCall>(shared()); } - // Add a media stream to the call. - void addMediaStream(const MediaAttribute& mediaAttr); - - // Init media streams - size_t initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList); - - // Create a new stream from SDP description. - void createRtpSession(RtpStream& rtpStream); - - // Configure the RTP session from SDP description. - void configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession, - const std::shared_ptr<MediaAttribute>& mediaAttr, - const MediaDescription& localMedia, - const MediaDescription& remoteMedia); - - void dumpMediaAttribute(const MediaAttribute& mediaAttr, size_t idx) const; - - /** - * Find the stream index of the matching label. - * @param label the stream label - * @return the stream index on success, the last index past one otherwise. - */ - size_t findRtpStreamIndex(const std::string& label) const; - // Vector holding the current RTP sessions. std::vector<RtpStream> rtpStreams_; diff --git a/src/sip/siptransport.cpp b/src/sip/siptransport.cpp index b4943392f4cbf855fffcca39e72df0b52b24ac58..87bf03c4a8eb69476edca7d36346ca4dd4349a15 100644 --- a/src/sip/siptransport.cpp +++ b/src/sip/siptransport.cpp @@ -219,11 +219,7 @@ SipTransportBroker::transportStateChanged(pjsip_transport* tp, sipTransport = key->second.lock(); -#if PJ_VERSION_NUM > (2 << 24 | 1 << 16) bool destroyed = state == PJSIP_TP_STATE_DESTROY; -#else - bool destroyed = tp->is_destroying; -#endif // maps cleanup if (destroyed) { diff --git a/src/sip/sipvoiplink.cpp b/src/sip/sipvoiplink.cpp index 80522548bbefa1b9eb722e4c2664143f3232662a..854186c6aab1a79bb3c62ddb6518f9e7e30f1992 100644 --- a/src/sip/sipvoiplink.cpp +++ b/src/sip/sipvoiplink.cpp @@ -61,6 +61,11 @@ #include <pjsip-simple/presence.h> #include <pjsip-simple/publish.h> +// Only PJSIP 2.10 is supported. +#if PJ_VERSION_NUM < (2 << 24 | 10 << 16) +#error "Unsupported PJSIP version (requires version 2.10+)" +#endif + #include <istream> #include <algorithm> #include <regex> @@ -84,6 +89,7 @@ static void outgoing_request_forked_cb(pjsip_inv_session* inv, pjsip_event* e); static void transaction_state_changed_cb(pjsip_inv_session* inv, pjsip_transaction* tsx, pjsip_event* e); + static std::shared_ptr<SIPCall> getCallFromInvite(pjsip_inv_session* inv); #ifdef DEBUG_SIP_REQUEST_MSG static void processInviteResponseHelper(pjsip_inv_session* inv, pjsip_event* e); @@ -334,12 +340,15 @@ transaction_request_cb(pjsip_rx_data* rdata) sip_utils::logMessageHeaders(&rdata->msg_info.msg->hdr); } - pjmedia_sdp_session* r_sdp; + pjmedia_sdp_session* r_sdp {nullptr}; - if (!body - || pjmedia_sdp_parse(rdata->tp_info.pool, (char*) body->data, body->len, &r_sdp) - != PJ_SUCCESS) - r_sdp = NULL; + if (body) { + if (pjmedia_sdp_parse(rdata->tp_info.pool, (char*) body->data, body->len, &r_sdp) + != PJ_SUCCESS) { + JAMI_WARN("Failed to parse the SDP in offer"); + r_sdp = nullptr; + } + } if (not account->hasActiveCodec(MEDIA_AUDIO)) { try_respond_stateless(endpt_, rdata, PJSIP_SC_NOT_ACCEPTABLE_HERE, NULL, NULL, NULL); @@ -355,17 +364,8 @@ transaction_request_cb(pjsip_rx_data* rdata) return PJ_FALSE; } - bool hasVideo = false; - if (r_sdp) { - auto pj_str_video = sip_utils::CONST_PJ_STR("video"); - for (decltype(r_sdp->media_count) i = 0; i < r_sdp->media_count; i++) { - if (pj_strcmp(&r_sdp->media[i]->desc.media, &pj_str_video) == 0) - hasVideo = true; - } - } - auto call = account->newIncomingCall(std::string(remote_user), - {{"AUDIO_ONLY", (hasVideo ? "false" : "true")}}, - transport); + auto const& remoteMediaList = Sdp::getMediaAttributeListFromSdp(r_sdp); + auto call = account->newIncomingCall(std::string(remote_user), remoteMediaList, transport); if (!call) { return PJ_FALSE; @@ -413,12 +413,18 @@ transaction_request_cb(pjsip_rx_data* rdata) if (account->isStunEnabled()) call->updateSDPFromSTUN(); - // This list should be provided by the client. Kept for backward compatibility. - auto mediaList = account->createDefaultMediaList(account->isVideoEnabled() and hasVideo); - - call->getSDP().receiveOffer(r_sdp, mediaList); - - call->setRemoteSdp(r_sdp); + { + auto hasVideo = MediaAttribute::hasMediaType(remoteMediaList, MediaType::MEDIA_VIDEO); + // TODO. + // This list should be built using all the medias in the incoming offer. + // The local media should be set temporarily inactive (and possibly unconfigured) until + // we receive accept(mediaList) from the client. + auto mediaList = account->createDefaultMediaList(account->isVideoEnabled() and hasVideo); + call->getSDP().setReceivedOffer(r_sdp); + call->getSDP().processIncomingOffer(mediaList); + } + if (r_sdp) + call->setupLocalIce(); pjsip_dialog* dialog = nullptr; if (pjsip_dlg_create_uas_and_inc_lock(pjsip_ua_instance(), rdata, nullptr, &dialog) @@ -664,22 +670,18 @@ SIPVoIPLink::SIPVoIPLink() TRY(pjsip_pres_init_module(endpt_, pjsip_evsub_instance())); TRY(pjsip_endpt_register_module(endpt_, &PresSubServer::mod_presence_server)); - static const pjsip_inv_callback inv_cb - = { invite_session_state_changed_cb, - outgoing_request_forked_cb, - transaction_state_changed_cb, - nullptr /* on_rx_offer */, -#if PJ_VERSION_NUM >= (2 << 24 | 7 << 16) - nullptr /* on_rx_offer2 */, -#endif -#if PJ_VERSION_NUM > (2 << 24 | 1 << 16) - reinvite_received_cb, -#endif - sdp_create_offer_cb, - sdp_media_update_cb, - nullptr /* on_send_ack */, - nullptr /* on_redirected */, - }; + static const pjsip_inv_callback inv_cb = { + invite_session_state_changed_cb, + outgoing_request_forked_cb, + transaction_state_changed_cb, + nullptr /* on_rx_offer */, + nullptr /* on_rx_offer2 */, + reinvite_received_cb, + sdp_create_offer_cb, + sdp_media_update_cb, + nullptr /* on_send_ack */, + nullptr /* on_redirected */, + }; TRY(pjsip_inv_usage_init(endpt_, &inv_cb)); static constexpr pj_str_t allowed[] = { @@ -930,9 +932,18 @@ reinvite_received_cb(pjsip_inv_session* inv, const pjmedia_sdp_session* offer, p if (!offer) return !PJ_SUCCESS; if (auto call = getCallFromInvite(inv)) { - return call->onReceiveOffer(offer, rdata); + if (auto const& account = call->getAccount().lock()) { + if (account->isMultiStreamEnabled()) { + return call->onReceiveReinvite(offer, rdata); + } else { + return call->onReceiveOffer(offer, rdata); + } + } } - return !PJ_SUCCESS; + + // Return success if there is no matching call. The re-invite + // should be ignored. + return PJ_SUCCESS; } static void @@ -1051,14 +1062,14 @@ sdp_media_update_cb(pjsip_inv_session* inv, pj_status_t status) auto& sdp = call->getSDP(); sdp.setActiveLocalSdpSession(localSDP); if (localSDP != nullptr) { - Sdp::printSession(localSDP, "Local active session:\n", sdp.isOffer()); + Sdp::printSession(localSDP, "Local active session:", sdp.getSdpDirection()); } sdp.setActiveRemoteSdpSession(remoteSDP); if (remoteSDP != nullptr) { - Sdp::printSession(remoteSDP, "Remote active session:\n", not sdp.isOffer()); + Sdp::printSession(remoteSDP, "Remote active session:", sdp.getSdpDirection()); } - call->onMediaUpdate(); + call->onMediaNegotiationComplete(); } static void @@ -1518,14 +1529,7 @@ SIPVoIPLink::findLocalAddressFromSTUN(pjsip_transport* transport, pj_sockaddr_in mapped_addr; pj_sock_t sipSocket = pjsip_udp_transport_get_socket(transport); const pjstun_setting stunOpt - = { PJ_TRUE, -#if PJ_VERSION_NUM > (2 << 24 | 7 << 16) - localIp.getFamily(), -#endif - *stunServerName, - stunPort, - *stunServerName, - stunPort }; + = {PJ_TRUE, localIp.getFamily(), *stunServerName, stunPort, *stunServerName, stunPort}; const pj_status_t stunStatus = pjstun_get_mapped_addr2(&cp_.factory, &stunOpt, 1, diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 9b60c286ee7b762231f24e45c6ca6d73240c49f0..a19f97bb23c5a21ee8e5e0bdca94f1efe24cc97d 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -146,10 +146,10 @@ ut_conversationRepository_SOURCES = conversationRepository/conversationRepositor check_PROGRAMS += ut_conversation ut_conversation_SOURCES = conversation/conversation.cpp # -# media_control +# media_negotiation # -check_PROGRAMS += ut_media_control -ut_media_control_SOURCES = media_control/media_control.cpp +check_PROGRAMS += ut_media_negotiation +ut_media_negotiation_SOURCES = media_negotiation/media_negotiation.cpp # # syncHistory diff --git a/test/unitTest/media_negotiation/media_negotiation.cpp b/test/unitTest/media_negotiation/media_negotiation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eb265a7a6990ede183d6e417cfaf83ebb98a4238 --- /dev/null +++ b/test/unitTest/media_negotiation/media_negotiation.cpp @@ -0,0 +1,849 @@ +/* + * Copyright (C) 2021 Savoir-faire Linux Inc. + * + * Author: Mohamed Chibani <mohamed.chibani@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, see <https://www.gnu.org/licenses/>. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <string> + +#include "manager.h" +#include "jamidht/connectionmanager.h" +#include "jamidht/jamiaccount.h" +#include "../../test_runner.h" +#include "dring.h" +#include "call_const.h" +#include "account_const.h" +#include "sip/sipcall.h" +#include "sip/sdp.h" + +using namespace DRing::Account; +using namespace DRing::Call; + +namespace jami { +namespace test { + +struct TestScenario +{ + TestScenario(const std::vector<MediaAttribute>& offer, + const std::vector<MediaAttribute>& answer, + const std::vector<MediaAttribute>& offerUpdate, + const std::vector<MediaAttribute>& answerUpdate) + : offer_(std::move(offer)) + , answer_(std::move(answer)) + , offerUpdate_(std::move(offerUpdate)) + , answerUpdate_(std::move(answerUpdate)) + {} + + TestScenario() {}; + + std::vector<MediaAttribute> offer_; + std::vector<MediaAttribute> answer_; + std::vector<MediaAttribute> offerUpdate_; + std::vector<MediaAttribute> answerUpdate_; + // Determine if we should expect the MediaNegotiationStatus signal. + bool expectMediaRenegotiation_ {false}; + // Determine if we should expect the MediaChangeRequested signal. + bool expectMediaChangeRequest_ {false}; +}; + +struct CallData +{ + struct Signal + { + Signal(const std::string& name, const std::string& event = {}) + : name_(std::move(name)) + , event_(std::move(event)) {}; + + std::string name_ {}; + std::string event_ {}; + }; + + std::string accountId_ {}; + std::string userName_ {}; + std::string alias_ {}; + std::string callId_ {}; + std::vector<Signal> signals_; + std::condition_variable cv_ {}; + std::mutex mtx_; +}; + +/** + * Basic tests for media negotiation. + */ +class MediaNegotiationTest : public CppUnit::TestFixture +{ +public: + MediaNegotiationTest() + { + // Init daemon + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(DRing::start("dring-sample.yml")); + } + ~MediaNegotiationTest() { DRing::fini(); } + + static std::string name() { return "MediaNegotiationTest"; } + void setUp(); + void tearDown(); + +private: + // Test cases. + void audio_and_video_then_mute_video(); + void audio_only_then_add_video(); + void audio_and_video_then_mute_audio(); + + CPPUNIT_TEST_SUITE(MediaNegotiationTest); + CPPUNIT_TEST(audio_and_video_then_mute_video); + CPPUNIT_TEST(audio_only_then_add_video); + CPPUNIT_TEST(audio_and_video_then_mute_audio); + CPPUNIT_TEST_SUITE_END(); + + // Event/Signal handlers + static void onCallStateChange(const std::string& callId, + const std::string& state, + CallData& callData); + static void onIncomingCallWithMedia(const std::string& accountId, + const std::string& callId, + const std::vector<DRing::MediaMap> mediaList, + CallData& callData); + static void onMediaChangeRequested(const std::string& accountId, + const std::string& callId, + const std::vector<DRing::MediaMap> mediaList, + CallData& callData); + static void onVideoMuted(const std::string& callId, bool muted, CallData& callData); + static void onMediaNegotiationStatus(const std::string& callId, + const std::string& event, + CallData& callData); + + // Helpers + static void configureScenario(CallData& bob, CallData& alice); + void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario); + static std::string getUserAlias(const std::string& callId); + // Infer media direction from the mute state. + // Note that when processing caller side, local is the caller and + // remote is the callee, and when processing the callee, the local is + // callee and remote is the caller. + static MediaDirection inferDirection(bool localMute, bool remoteMute); + // Wait for a signal from the callbacks. Some signals also report the event that + // triggered the signal a like the StateChange signal. + static bool waitForSignal(CallData& callData, + const std::string& signal, + const std::string& expectedEvent = {}); + +private: + CallData aliceData_; + CallData bobData_; +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MediaNegotiationTest, MediaNegotiationTest::name()); + +void +MediaNegotiationTest::setUp() +{ + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE"; + details[ConfProperties::ALIAS] = "ALICE"; + details[ConfProperties::UPNP_ENABLED] = "false"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + aliceData_.accountId_ = Manager::instance().addAccount(details); + + details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB"; + details[ConfProperties::ALIAS] = "BOB"; + details[ConfProperties::UPNP_ENABLED] = "false"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + bobData_.accountId_ = Manager::instance().addAccount(details); + + JAMI_INFO("Initialize account..."); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData_.accountId_); + aliceAccount->enableMultiStream(true); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData_.accountId_); + bobAccount->enableMultiStream(true); + + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::atomic_bool accountsReady {false}; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + bool ready = false; + auto details = aliceAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready = (daemonStatus == "REGISTERED"); + details = bobAccount->getVolatileAccountDetails(); + daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready &= (daemonStatus == "REGISTERED"); + if (ready) { + accountsReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsReady.load(); })); + DRing::unregisterSignalHandlers(); +} + +void +MediaNegotiationTest::tearDown() +{ + JAMI_INFO("Remove created accounts..."); + + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + auto currentAccSize = Manager::instance().getAccountList().size(); + std::atomic_bool accountsRemoved {false}; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() { + if (Manager::instance().getAccountList().size() <= currentAccSize - 2) { + accountsRemoved = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + Manager::instance().removeAccount(aliceData_.accountId_, true); + Manager::instance().removeAccount(bobData_.accountId_, true); + // Because cppunit is not linked with dbus, just poll if removed + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); })); + + DRing::unregisterSignalHandlers(); +} + +std::string +MediaNegotiationTest::getUserAlias(const std::string& callId) +{ + auto call = Manager::instance().getCallFromCallID(callId); + + if (not call) { + JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str()); + return {}; + } + + auto const& account = call->getAccount().lock(); + if (not account) { + return {}; + } + + return account->getAccountDetails()[ConfProperties::ALIAS]; +} + +MediaDirection +MediaNegotiationTest::inferDirection([[maybe_unused]] bool localMute, + [[maybe_unused]] bool remoteMute) +{ +#if 1 + return MediaDirection::SENDRECV; +#else + // NOTE. The media direction inferred here should be the correct one + // according to the spec (RFC-3264 and RFC-6337). However, the current + // implementation always set 'sendrecv' regardless of the mute state. + if (not localMute and not remoteMute) + return MediaDirection::SENDRECV; + + if (localMute and not remoteMute) + return MediaDirection::RECVONLY; + + if (not localMute and remoteMute) + return MediaDirection::SENDONLY; + + return MediaDirection::INACTIVE; +#endif +} + +void +MediaNegotiationTest::onIncomingCallWithMedia(const std::string& accountId, + const std::string& callId, + const std::vector<DRing::MediaMap> mediaList, + CallData& callData) +{ + CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId); + + JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]", + DRing::CallSignal::IncomingCallWithMedia::name, + callData.alias_.c_str(), + callId.c_str(), + mediaList.size()); + + // NOTE. + // We shouldn't access shared_ptr<Call> as this event is supposed to mimic + // the client, and the client have no access to this type. But here, we only + // needed to check if the call exists. This is the most straightforward and + // reliable way to do it until we add a new API (like hasCall(id)). + if (not Manager::instance().getCallFromCallID(callId)) { + JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str()); + callData.callId_ = {}; + return; + } + + std::unique_lock<std::mutex> lock {callData.mtx_}; + callData.callId_ = callId; + callData.signals_.emplace_back(CallData::Signal(DRing::CallSignal::IncomingCallWithMedia::name)); + + callData.cv_.notify_one(); +} + +void +MediaNegotiationTest::onMediaChangeRequested(const std::string& accountId, + const std::string& callId, + const std::vector<DRing::MediaMap> mediaList, + CallData& callData) +{ + CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId); + + JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]", + DRing::CallSignal::MediaChangeRequested::name, + callData.alias_.c_str(), + callId.c_str(), + mediaList.size()); + + // TODO + // We shouldn't access shared_ptr<Call> as this event is supposed to mimic + // the client, and the client have no access to this type. But here, we only + // needed to check if the call exists. This is the most straightforward and + // reliable way to do it until we add a new API (like hasCall(id)). + if (not Manager::instance().getCallFromCallID(callId)) { + JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str()); + callData.callId_ = {}; + return; + } + + std::unique_lock<std::mutex> lock {callData.mtx_}; + callData.callId_ = callId; + callData.signals_.emplace_back(CallData::Signal(DRing::CallSignal::MediaChangeRequested::name)); + + callData.cv_.notify_one(); +} + +void +MediaNegotiationTest::onCallStateChange(const std::string& callId, + const std::string& state, + CallData& callData) +{ + auto call = Manager::instance().getCallFromCallID(callId); + if (not call) { + JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str()); + return; + } + + auto account = call->getAccount().lock(); + if (not account) { + JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str()); + return; + } + + JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]", + DRing::CallSignal::StateChange::name, + callData.alias_.c_str(), + callId.c_str(), + state.c_str()); + + if (account->getAccountID() != callData.accountId_) + return; + + { + std::unique_lock<std::mutex> lock {callData.mtx_}; + callData.signals_.emplace_back( + CallData::Signal(DRing::CallSignal::StateChange::name, state)); + } + + if (state == "CURRENT" or state == "OVER" or state == "HUNGUP") { + callData.cv_.notify_one(); + } +} + +void +MediaNegotiationTest::onVideoMuted(const std::string& callId, bool muted, CallData& callData) +{ + auto call = Manager::instance().getCallFromCallID(callId); + + if (not call) { + JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str()); + return; + } + + auto account = call->getAccount().lock(); + if (not account) { + JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str()); + return; + } + + JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]", + DRing::CallSignal::VideoMuted::name, + account->getAccountDetails()[ConfProperties::ALIAS].c_str(), + call->getCallId().c_str(), + muted ? "Mute" : "Un-mute"); + + if (account->getAccountID() != callData.accountId_) + return; + + { + std::unique_lock<std::mutex> lock {callData.mtx_}; + callData.signals_.emplace_back( + CallData::Signal(DRing::CallSignal::VideoMuted::name, muted ? "muted" : "un-muted")); + } + + callData.cv_.notify_one(); +} + +void +MediaNegotiationTest::onMediaNegotiationStatus(const std::string& callId, + const std::string& event, + CallData& callData) +{ + auto call = Manager::instance().getCallFromCallID(callId); + if (not call) { + JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str()); + return; + } + + auto account = call->getAccount().lock(); + if (not account) { + JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str()); + return; + } + + JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]", + DRing::CallSignal::MediaNegotiationStatus::name, + account->getAccountDetails()[ConfProperties::ALIAS].c_str(), + call->getCallId().c_str(), + event.c_str()); + + if (account->getAccountID() != callData.accountId_) + return; + + { + std::unique_lock<std::mutex> lock {callData.mtx_}; + callData.signals_.emplace_back( + CallData::Signal(DRing::CallSignal::MediaNegotiationStatus::name, event)); + } + + callData.cv_.notify_one(); +} + +bool +MediaNegotiationTest::waitForSignal(CallData& callData, + const std::string& expectedSignal, + const std::string& expectedEvent) +{ + const std::chrono::seconds TIME_OUT {30}; + std::unique_lock<std::mutex> lock {callData.mtx_}; + + // Combined signal + event (if any). + std::string sigEvent(expectedSignal); + if (not expectedEvent.empty()) + sigEvent += "::" + expectedEvent; + + JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str()); + + auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] { + // Search for the expected signal in list of received signals. + bool pred = false; + for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) { + // The predicate is true if the signal names match, and if the + // expectedEvent is not empty, the events must also match. + if (it->name_ == expectedSignal + and (expectedEvent.empty() or it->event_ == expectedEvent)) { + pred = true; + // Done with this signal. + callData.signals_.erase(it); + break; + } + } + + return pred; + }); + + if (not res) { + JAMI_ERR("[%s] waiting for signal/event [%s] timed-out!", + callData.alias_.c_str(), + sigEvent.c_str()); + + JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str()); + + for (auto const& sig : callData.signals_) { + JAMI_INFO() << "Signal [" << sig.name_ + << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]"; + } + } + + return res; +} + +void +MediaNegotiationTest::configureScenario(CallData& aliceData, CallData& bobData) +{ + { + CPPUNIT_ASSERT(not aliceData.accountId_.empty()); + auto const& account = Manager::instance().getAccount<JamiAccount>(aliceData.accountId_); + aliceData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME]; + aliceData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS]; + } + + { + CPPUNIT_ASSERT(not bobData.accountId_.empty()); + auto const& account = Manager::instance().getAccount<JamiAccount>(bobData.accountId_); + bobData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME]; + bobData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS]; + } + + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> signalHandlers; + + // Insert needed signal handlers. + signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::IncomingCallWithMedia>( + [&](const std::string& accountId, + const std::string& callId, + const std::string&, + const std::vector<DRing::MediaMap> mediaList) { + auto user = getUserAlias(callId); + if (not user.empty()) + onIncomingCallWithMedia(accountId, + callId, + mediaList, + user == aliceData.alias_ ? aliceData : bobData); + })); + + signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaChangeRequested>( + [&](const std::string& accountId, + const std::string& callId, + const std::vector<DRing::MediaMap> mediaList) { + auto user = getUserAlias(callId); + if (not user.empty()) + onMediaChangeRequested(accountId, + callId, + mediaList, + user == aliceData.alias_ ? aliceData : bobData); + })); + + signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::StateChange>( + [&](const std::string& callId, const std::string& state, signed) { + auto user = getUserAlias(callId); + if (not user.empty()) + onCallStateChange(callId, state, user == aliceData.alias_ ? aliceData : bobData); + })); + + signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::VideoMuted>( + [&](const std::string& callId, bool muted) { + auto user = getUserAlias(callId); + if (not user.empty()) + onVideoMuted(callId, muted, user == aliceData.alias_ ? aliceData : bobData); + })); + + signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaNegotiationStatus>( + [&](const std::string& callId, const std::string& event) { + auto user = getUserAlias(callId); + if (not user.empty()) + onMediaNegotiationStatus(callId, + event, + user == aliceData.alias_ ? aliceData : bobData); + })); + + DRing::registerSignalHandlers(signalHandlers); +} + +void +MediaNegotiationTest::testWithScenario(CallData& aliceData, + CallData& bobData, + const TestScenario& scenario) +{ + JAMI_INFO("=== Start a call and validate ==="); + + // The media count of the offer and answer must match (RFC-3264). + auto mediaCount = scenario.offer_.size(); + CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answer_.size()); + + auto const& aliceCall = std::dynamic_pointer_cast<SIPCall>( + (Manager::instance().getAccount<JamiAccount>(aliceData.accountId_)) + ->newOutgoingCall(bobData.userName_, scenario.offer_)); + CPPUNIT_ASSERT(aliceCall); + aliceData.callId_ = aliceCall->getCallId(); + + JAMI_INFO("ALICE [%s] started a call with BOB [%s] and wait for answer", + aliceData.accountId_.c_str(), + bobData.accountId_.c_str()); + + // Wait for incoming call signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name)); + // Answer the call. + { + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answer_); + Manager::instance().answerCallWithMedia(bobData.callId_, mediaList); + } + + // Wait for media negotiation complete signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(bobData, + DRing::CallSignal::MediaNegotiationStatus::name, + MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS)); + // Wait for the StateChange signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(bobData, + DRing::CallSignal::StateChange::name, + StateEvent::CURRENT)); + + JAMI_INFO("BOB answered the call [%s]", bobData.callId_.c_str()); + + // Wait for media negotiation complete signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(aliceData, + DRing::CallSignal::MediaNegotiationStatus::name, + MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS)); + + // Validate Alice's media + { + auto mediaAttr = aliceCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaAttr.size()); + for (size_t idx = 0; idx < mediaCount; idx++) { + CPPUNIT_ASSERT_EQUAL(scenario.offer_[idx].muted_, mediaAttr[idx].muted_); + } + } + + // Validate Bob's media + { + auto const& bobCall = std::dynamic_pointer_cast<SIPCall>( + Manager::instance().getCallFromCallID(bobData.callId_)); + auto mediaAttr = bobCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaAttr.size()); + for (size_t idx = 0; idx < mediaCount; idx++) { + CPPUNIT_ASSERT_EQUAL(scenario.answer_[idx].muted_, mediaAttr[idx].muted_); + } + } + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + JAMI_INFO("=== Request Media Change and validate ==="); + { + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.offerUpdate_); + Manager::instance().requestMediaChange(aliceData.callId_, mediaList); + } + + // Update and validate media count. + mediaCount = scenario.offerUpdate_.size(); + CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answerUpdate_.size()); + + // Not all media change requests requires validation from client. + if (scenario.expectMediaChangeRequest_) { + // Wait for media change request signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(bobData, DRing::CallSignal::MediaChangeRequested::name)); + + // Answer the change request. + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answerUpdate_); + Manager::instance().answerMediaChangeRequest(bobData.callId_, mediaList); + } + + if (scenario.expectMediaRenegotiation_) { + // Wait for media negotiation complete signal. + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(aliceData, + DRing::CallSignal::MediaNegotiationStatus::name, + MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS)); + + // Validate Alice's media + { + auto mediaAttr = aliceCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaAttr.size()); + for (size_t idx = 0; idx < mediaCount; idx++) { + CPPUNIT_ASSERT_EQUAL(scenario.offerUpdate_[idx].muted_, mediaAttr[idx].muted_); + } + } + + // Validate Bob's media + { + auto const& bobCall = std::dynamic_pointer_cast<SIPCall>( + Manager::instance().getCallFromCallID(bobData.callId_)); + auto mediaAttr = bobCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaAttr.size()); + for (size_t idx = 0; idx < mediaCount; idx++) { + CPPUNIT_ASSERT_EQUAL(scenario.answerUpdate_[idx].muted_, mediaAttr[idx].muted_); + } + } + } + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // Bob hang-up. + JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up"); + Manager::instance().hangupCall(bobData.callId_); + + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(aliceData, + DRing::CallSignal::StateChange::name, + StateEvent::HUNGUP)); + + JAMI_INFO("Call terminated on both sides"); +} + +void +MediaNegotiationTest::audio_and_video_then_mute_video() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + JAMI_INFO("Waiting for accounts setup ..."); + // TODO remove. This sleeps is because it take some time for the DHT to be connected + // and account announced + std::this_thread::sleep_for(std::chrono::seconds(10)); + + configureScenario(aliceData_, bobData_); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "main audio"; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "main video"; + + { + MediaAttribute audio(defaultAudio); + MediaAttribute video(defaultVideo); + + TestScenario scenario; + // First offer/answer + scenario.offer_.emplace_back(audio); + scenario.offer_.emplace_back(video); + scenario.answer_.emplace_back(audio); + scenario.answer_.emplace_back(video); + + // Updated offer/answer + scenario.offerUpdate_.emplace_back(audio); + video.muted_ = true; + scenario.offerUpdate_.emplace_back(video); + + scenario.answerUpdate_.emplace_back(audio); + video.muted_ = false; + scenario.answerUpdate_.emplace_back(video); + scenario.expectMediaRenegotiation_ = true; + scenario.expectMediaChangeRequest_ = false; + + testWithScenario(aliceData_, bobData_, scenario); + } + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +void +MediaNegotiationTest::audio_only_then_add_video() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + JAMI_INFO("Waiting for accounts setup ..."); + // TODO remove. This sleeps is because it take some time for the DHT to be connected + // and account announced + std::this_thread::sleep_for(std::chrono::seconds(10)); + + configureScenario(aliceData_, bobData_); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "main audio"; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "main video"; + + { + MediaAttribute audio(defaultAudio); + MediaAttribute video(defaultVideo); + + TestScenario scenario; + // First offer/answer + scenario.offer_.emplace_back(audio); + scenario.answer_.emplace_back(audio); + + // Updated offer/answer + scenario.offerUpdate_.emplace_back(audio); + scenario.offerUpdate_.emplace_back(video); + scenario.answerUpdate_.emplace_back(audio); + scenario.answerUpdate_.emplace_back(video); + scenario.expectMediaRenegotiation_ = true; + scenario.expectMediaChangeRequest_ = true; + + testWithScenario(aliceData_, bobData_, scenario); + } + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +void +MediaNegotiationTest::audio_and_video_then_mute_audio() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + JAMI_INFO("Waiting for accounts setup ..."); + // TODO remove. This sleeps is because it take some time for the DHT to be connected + // and account announced + std::this_thread::sleep_for(std::chrono::seconds(10)); + + configureScenario(aliceData_, bobData_); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "main audio"; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "main video"; + + { + MediaAttribute audio(defaultAudio); + MediaAttribute video(defaultVideo); + + TestScenario scenario; + // First offer/answer + scenario.offer_.emplace_back(audio); + scenario.offer_.emplace_back(video); + scenario.answer_.emplace_back(audio); + scenario.answer_.emplace_back(video); + + // Updated offer/answer + audio.muted_ = true; + scenario.offerUpdate_.emplace_back(audio); + scenario.offerUpdate_.emplace_back(video); + + audio.muted_ = false; + scenario.answerUpdate_.emplace_back(audio); + scenario.answerUpdate_.emplace_back(video); + + scenario.expectMediaRenegotiation_ = false; + scenario.expectMediaChangeRequest_ = false; + + testWithScenario(aliceData_, bobData_, scenario); + } + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +} // namespace test +} // namespace jami + +RING_TEST_RUNNER(jami::test::MediaNegotiationTest::name())