From 38981578d5b391079919d1a65bee72825bc2dc71 Mon Sep 17 00:00:00 2001 From: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com> Date: Thu, 18 Nov 2021 09:31:49 -0500 Subject: [PATCH] auto-answer - add unit test Add a media negotiation unit test for auto-answer mode Gitlab: #645 Change-Id: I37f768ce48e078fbd95a2c9b28997877a4dc468d --- src/client/callmanager.cpp | 6 +- src/sip/sipcall.cpp | 34 +- test/test_runner.h | 51 +- test/unitTest/Makefile.am | 2 + .../media_negotiation/auto_answer.cpp | 1030 +++++++++++++++++ .../media_negotiation/media_negotiation.cpp | 10 +- 6 files changed, 1103 insertions(+), 30 deletions(-) create mode 100644 test/unitTest/media_negotiation/auto_answer.cpp diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp index 6c19f172af..847e3bc040 100644 --- a/src/client/callmanager.cpp +++ b/src/client/callmanager.cpp @@ -200,7 +200,7 @@ createConfFromParticipantList(const std::string& accountId, void setConferenceLayout(const std::string& accountId, const std::string& confId, uint32_t layout) { - if (const auto account = jami::Manager::instance().getAccount(accountId)) + if (const auto account = jami::Manager::instance().getAccount(accountId)) { if (auto conf = account->getConference(confId)) { conf->setLayout(layout); } else if (auto call = account->getCall(confId)) { @@ -208,6 +208,7 @@ setConferenceLayout(const std::string& accountId, const std::string& confId, uin root["layout"] = layout; call->sendConfOrder(root); } + } } void @@ -215,7 +216,7 @@ setActiveParticipant(const std::string& accountId, const std::string& confId, const std::string& participant) { - if (const auto account = jami::Manager::instance().getAccount(accountId)) + if (const auto account = jami::Manager::instance().getAccount(accountId)) { if (auto conf = account->getConference(confId)) { conf->setActiveParticipant(participant); } else if (auto call = account->getCall(confId)) { @@ -223,6 +224,7 @@ setActiveParticipant(const std::string& accountId, root["activeParticipant"] = participant; call->sendConfOrder(root); } + } } bool diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp index fff0fc510f..b47dc3f74c 100644 --- a/src/sip/sipcall.cpp +++ b/src/sip/sipcall.cpp @@ -2524,19 +2524,29 @@ SIPCall::handleMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMedi return; } - // If the offered media differ from the current local media, the - // request is reported to the client to be processed. Otherwise, - // it will be processed using the current local media. - - if (checkMediaChangeRequest(remoteMediaList)) { - // Report the media change request. - emitSignal<DRing::CallSignal::MediaChangeRequested>(getAccountId(), - getCallId(), - remoteMediaList); - } else { - auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()); - answerMediaChangeRequest(localMediaList); + // If the offered media does not differ from the current local media, the + // request is answered using the current local media. + if (not checkMediaChangeRequest(remoteMediaList)) { + answerMediaChangeRequest( + MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList())); + return; + } + + if (account->isAutoAnswerEnabled()) { + // NOTE: + // Since the auto-answer is enabled in the account, media change requests + // are automatically accepted too. This also means that if the original + // call was an audio-only call, and the remote added the video, the local + // camera will be enabled, unless the video is disabled in the account + // settings. + answerMediaChangeRequest(remoteMediaList); + return; } + + // Report the media change request. + emitSignal<DRing::CallSignal::MediaChangeRequested>(getAccountId(), + getCallId(), + remoteMediaList); } pj_status_t diff --git a/test/test_runner.h b/test/test_runner.h index 0762b5764c..7b5584c196 100644 --- a/test/test_runner.h +++ b/test/test_runner.h @@ -5,15 +5,42 @@ #include <cppunit/CompilerOutputter.h> #define RING_TEST_RUNNER(suite_name) \ -int main() \ -{ \ - CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(suite_name); \ - CppUnit::Test *suite = registry.makeTest(); \ - if(suite->countTestCases() == 0) { \ - std::cout << "No test cases specified for suite \"" << suite_name << "\"\n"; \ - return 1; \ - } \ - CppUnit::TextUi::TestRunner runner; \ - runner.addTest(suite); \ - return runner.run() ? 0 : 1; \ -} + int main() \ + { \ + CppUnit::TestFactoryRegistry& registry = CppUnit::TestFactoryRegistry::getRegistry( \ + suite_name); \ + CppUnit::Test* suite = registry.makeTest(); \ + if (suite->countTestCases() == 0) { \ + std::cout << "No test cases specified for suite \"" << suite_name << "\"\n"; \ + return 1; \ + } \ + CppUnit::TextUi::TestRunner runner; \ + runner.addTest(suite); \ + return runner.run() ? 0 : 1; \ + } + +// This version of the test runner is similar to RING_TEST_RUNNER but +// can take multiple unit tests. +// It's practical to run a test for diffrent configs, for instance when +// running the same test for both Jami and SIP accounts. + +// The test will abort if a test fails. +#define JAMI_TEST_RUNNER(...) \ + int main() \ + { \ + std::vector<std::string> suite_names {__VA_ARGS__}; \ + for (const std::string& name : suite_names) { \ + CppUnit::TestFactoryRegistry& registry = CppUnit::TestFactoryRegistry::getRegistry( \ + name); \ + CppUnit::Test* suite = registry.makeTest(); \ + if (suite->countTestCases() == 0) { \ + std::cout << "No test cases specified for suite \"" << name << "\"\n"; \ + continue; \ + } \ + CppUnit::TextUi::TestRunner runner; \ + runner.addTest(suite); \ + if (not runner.run()) \ + return 1; \ + } \ + return 0; \ + } diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index ad2bf69252..7cbd07727c 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -157,6 +157,8 @@ check_PROGRAMS += ut_media_negotiation ut_media_negotiation_SOURCES = media_negotiation/media_negotiation.cpp common.cpp check_PROGRAMS += ut_hold_resume ut_hold_resume_SOURCES = media_negotiation/hold_resume.cpp common.cpp +check_PROGRAMS += ut_auto_answer +ut_auto_answer_SOURCES = media_negotiation/auto_answer.cpp common.cpp # # compability diff --git a/test/unitTest/media_negotiation/auto_answer.cpp b/test/unitTest/media_negotiation/auto_answer.cpp new file mode 100644 index 0000000000..33e9331438 --- /dev/null +++ b/test/unitTest/media_negotiation/auto_answer.cpp @@ -0,0 +1,1030 @@ +/* + * 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 "../../test_runner.h" + +#include "manager.h" +#include "jamidht/connectionmanager.h" +#include "account.h" +#include "sip/sipaccount.h" +#include "jami.h" +#include "jami/media_const.h" +#include "call_const.h" +#include "account_const.h" +#include "sip/sipcall.h" +#include "sip/sdp.h" + +#include "common.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}; +}; + +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_ {}; + uint16_t listeningPort_ {0}; + std::string toUri_ {}; + std::vector<Signal> signals_; + std::condition_variable cv_ {}; + std::mutex mtx_; +}; + +class AutoAnswerMediaNegoTest +{ +public: + AutoAnswerMediaNegoTest() + { + // Init daemon + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(DRing::start("jami-sample.yml")); + } + ~AutoAnswerMediaNegoTest() { DRing::fini(); } + +protected: + // Test cases. + void audio_and_video_then_caller_mute_video(); + void audio_only_then_caller_add_video(); + void audio_and_video_then_caller_mute_audio(); + void audio_and_video_then_change_video_source(); + + // Event/Signal handlers + static void onCallStateChange(const std::string& accountId, + 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 + void configureScenario(); + void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario); + static std::string getUserAlias(const std::string& callId); + // Infer media direction of an offer. + static uint8_t directionToBitset(MediaDirection direction, bool isLocal); + static MediaDirection bitsetToDirection(uint8_t val); + static MediaDirection inferInitialDirection(const MediaAttribute& offer); + // Infer media direction of an answer. + static MediaDirection inferNegotiatedDirection(MediaDirection local, MediaDirection answer); + // Wait for a signal from the callbacks. Some signals also report the event that + // triggered the signal like the StateChange signal. + static bool validateMuteState(std::vector<MediaAttribute> expected, + std::vector<MediaAttribute> actual); + static bool validateMediaDirection(std::vector<MediaDescription> descrList, + std::vector<MediaAttribute> listInOffer, + std::vector<MediaAttribute> listInAnswer); + static bool waitForSignal(CallData& callData, + const std::string& signal, + const std::string& expectedEvent = {}); + + bool isSipAccount_ {false}; + CallData aliceData_; + CallData bobData_; +}; + +class AutoAnswerMediaNegoTestSip : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture +{ +public: + AutoAnswerMediaNegoTestSip() { isSipAccount_ = true; }; + + ~AutoAnswerMediaNegoTestSip() {}; + + static std::string name() { return "AutoAnswerMediaNegoTestSip"; } + void setUp() override; + void tearDown() override; + +private: + CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestSip); + CPPUNIT_TEST(audio_and_video_then_caller_mute_video); + CPPUNIT_TEST(audio_only_then_caller_add_video); + CPPUNIT_TEST(audio_and_video_then_caller_mute_audio); + CPPUNIT_TEST(audio_and_video_then_change_video_source); + CPPUNIT_TEST_SUITE_END(); +}; + +void +AutoAnswerMediaNegoTestSip::setUp() +{ + aliceData_.listeningPort_ = 5080; + std::map<std::string, std::string> details = DRing::getAccountTemplate("SIP"); + details[ConfProperties::TYPE] = "SIP"; + details[ConfProperties::DISPLAYNAME] = "ALICE"; + details[ConfProperties::ALIAS] = "ALICE"; + details[ConfProperties::LOCAL_PORT] = std::to_string(aliceData_.listeningPort_); + details[ConfProperties::UPNP_ENABLED] = "false"; + aliceData_.accountId_ = Manager::instance().addAccount(details); + + bobData_.listeningPort_ = 5082; + details = DRing::getAccountTemplate("SIP"); + details[ConfProperties::TYPE] = "SIP"; + details[ConfProperties::DISPLAYNAME] = "BOB"; + details[ConfProperties::ALIAS] = "BOB"; + details[ConfProperties::AUTOANSWER] = "true"; + details[ConfProperties::LOCAL_PORT] = std::to_string(bobData_.listeningPort_); + details[ConfProperties::UPNP_ENABLED] = "false"; + bobData_.accountId_ = Manager::instance().addAccount(details); + + JAMI_INFO("Initialize accounts ..."); + auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_); + auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_); +} + +void +AutoAnswerMediaNegoTestSip::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); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); })); + + DRing::unregisterSignalHandlers(); +} + +class AutoAnswerMediaNegoTestJami : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture +{ +public: + AutoAnswerMediaNegoTestJami() { isSipAccount_ = false; }; + + ~AutoAnswerMediaNegoTestJami() {}; + + static std::string name() { return "AutoAnswerMediaNegoTestJami"; } + void setUp() override; + void tearDown() override; + +private: + CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestJami); + CPPUNIT_TEST(audio_and_video_then_caller_mute_video); + CPPUNIT_TEST(audio_only_then_caller_add_video); + CPPUNIT_TEST(audio_and_video_then_caller_mute_audio); + CPPUNIT_TEST(audio_and_video_then_change_video_source); + CPPUNIT_TEST_SUITE_END(); +}; + +void +AutoAnswerMediaNegoTestJami::setUp() +{ + auto actors = load_actors("actors/alice-bob-no-upnp.yml"); + + aliceData_.accountId_ = actors["alice"]; + bobData_.accountId_ = actors["bob"]; + + JAMI_INFO("Initialize account..."); + auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_); + auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_); + auto details = bobAccount->getAccountDetails(); + details[ConfProperties::AUTOANSWER] = "true"; + bobAccount->setAccountDetails(details); + wait_for_announcement_of({aliceAccount->getAccountID(), bobAccount->getAccountID()}); +} + +void +AutoAnswerMediaNegoTestJami::tearDown() +{ + wait_for_removal_of({aliceData_.accountId_, bobData_.accountId_}); +} + +std::string +AutoAnswerMediaNegoTest::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 +AutoAnswerMediaNegoTest::inferInitialDirection(const MediaAttribute& mediaAttr) +{ + if (not mediaAttr.enabled_) + return MediaDirection::INACTIVE; + + if (mediaAttr.muted_) { + if (mediaAttr.onHold_) + return MediaDirection::INACTIVE; + return MediaDirection::RECVONLY; + } + + if (mediaAttr.onHold_) + return MediaDirection::SENDONLY; + + return MediaDirection::SENDRECV; +} + +uint8_t +AutoAnswerMediaNegoTest::directionToBitset(MediaDirection direction, bool isLocal) +{ + if (direction == MediaDirection::SENDRECV) + return 3; + if (direction == MediaDirection::RECVONLY) + return isLocal ? 2 : 1; + if (direction == MediaDirection::SENDONLY) + return isLocal ? 1 : 2; + return 0; +} + +MediaDirection +AutoAnswerMediaNegoTest::bitsetToDirection(uint8_t val) +{ + if (val == 3) + return MediaDirection::SENDRECV; + if (val == 2) + return MediaDirection::RECVONLY; + if (val == 1) + return MediaDirection::SENDONLY; + return MediaDirection::INACTIVE; +} + +MediaDirection +AutoAnswerMediaNegoTest::inferNegotiatedDirection(MediaDirection local, MediaDirection remote) +{ + uint8_t val = directionToBitset(local, true) & directionToBitset(remote, false); + auto dir = bitsetToDirection(val); + return dir; +} + +bool +AutoAnswerMediaNegoTest::validateMuteState(std::vector<MediaAttribute> expected, + std::vector<MediaAttribute> actual) +{ + CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size()); + + for (size_t idx = 0; idx < expected.size(); idx++) { + if (expected[idx].muted_ != actual[idx].muted_) + return false; + } + + return true; +} + +bool +AutoAnswerMediaNegoTest::validateMediaDirection(std::vector<MediaDescription> descrList, + std::vector<MediaAttribute> localMediaList, + std::vector<MediaAttribute> remoteMediaList) +{ + CPPUNIT_ASSERT_EQUAL(descrList.size(), localMediaList.size()); + CPPUNIT_ASSERT_EQUAL(descrList.size(), remoteMediaList.size()); + + for (size_t idx = 0; idx < descrList.size(); idx++) { + auto local = inferInitialDirection(localMediaList[idx]); + auto remote = inferInitialDirection(remoteMediaList[idx]); + auto negotiated = inferNegotiatedDirection(local, remote); + + if (descrList[idx].direction_ != negotiated) { + JAMI_WARN("Media [%lu] direction mismatch: expected %i - found %i", + idx, + static_cast<int>(negotiated), + static_cast<int>(descrList[idx].direction_)); + return false; + } + } + + return true; +} + +void +AutoAnswerMediaNegoTest::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()); + + 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 +AutoAnswerMediaNegoTest::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()); + + 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 +AutoAnswerMediaNegoTest::onCallStateChange(const std::string& accountId UNUSED, + const std::string& callId, + const std::string& state, + CallData& callData) +{ + // TODO. rewrite me using accountId. + + 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 +AutoAnswerMediaNegoTest::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 +AutoAnswerMediaNegoTest::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 +AutoAnswerMediaNegoTest::waitForSignal(CallData& callData, + const std::string& expectedSignal, + const std::string& expectedEvent) +{ + const std::chrono::seconds TIME_OUT {15}; + 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 +AutoAnswerMediaNegoTest::configureScenario() +{ + // Configure Alice + { + CPPUNIT_ASSERT(not aliceData_.accountId_.empty()); + auto const& account = Manager::instance().getAccount<Account>(aliceData_.accountId_); + aliceData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME]; + aliceData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS]; + account->enableIceForMedia(true); + if (isSipAccount_) { + auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account); + CPPUNIT_ASSERT(sipAccount); + sipAccount->setLocalPort(aliceData_.listeningPort_); + } + } + + // Configure Bob + { + CPPUNIT_ASSERT(not bobData_.accountId_.empty()); + auto const& account = Manager::instance().getAccount<Account>(bobData_.accountId_); + bobData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME]; + bobData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS]; + account->enableIceForMedia(true); + account->isAutoAnswerEnabled(); + + if (isSipAccount_) { + auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account); + CPPUNIT_ASSERT(sipAccount); + sipAccount->setLocalPort(bobData_.listeningPort_); + bobData_.toUri_ = "127.0.0.1:" + std::to_string(bobData_.listeningPort_); + } + } + + 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& accountId, + const std::string& callId, + const std::string& state, + signed) { + auto user = getUserAlias(callId); + if (not user.empty()) + onCallStateChange(accountId, + 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, + const std::vector<std::map<std::string, std::string>>&) { + auto user = getUserAlias(callId); + if (not user.empty()) + onMediaNegotiationStatus(callId, + event, + user == aliceData_.alias_ ? aliceData_ : bobData_); + })); + + DRing::registerSignalHandlers(signalHandlers); +} + +void +AutoAnswerMediaNegoTest::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()); + + aliceData.callId_ = DRing::placeCallWithMedia(aliceData.accountId_, + isSipAccount_ ? bobData.toUri_ + : bobData_.userName_, + MediaAttribute::mediaAttributesToMediaMaps( + scenario.offer_)); + CPPUNIT_ASSERT(not aliceData.callId_.empty()); + auto aliceCall = std::static_pointer_cast<SIPCall>( + Manager::instance().getCallFromCallID(aliceData.callId_)); + + CPPUNIT_ASSERT(aliceCall); + + 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(waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name)); + + // Bob automatically answers the call. + + // Wait for media negotiation complete signal. + CPPUNIT_ASSERT_EQUAL( + true, + waitForSignal(bobData, + DRing::CallSignal::MediaNegotiationStatus::name, + DRing::Media::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, + DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS)); + + // Validate Alice's media + { + auto mediaList = aliceCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size()); + + // Validate mute state + CPPUNIT_ASSERT(validateMuteState(scenario.offer_, mediaList)); + + auto& sdp = aliceCall->getSDP(); + + // Validate local media direction + { + auto descrList = sdp.getActiveMediaDescription(false); + CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size()); + // For Alice, local is the offer and remote is the answer. + CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.offer_, scenario.answer_)); + } + } + + // Validate Bob's media + { + auto const& bobCall = std::dynamic_pointer_cast<SIPCall>( + Manager::instance().getCallFromCallID(bobData.callId_)); + auto mediaList = bobCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size()); + + // Validate mute state + CPPUNIT_ASSERT(validateMuteState(scenario.answer_, mediaList)); + + auto& sdp = bobCall->getSDP(); + + // Validate local media direction + { + auto descrList = sdp.getActiveMediaDescription(false); + CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size()); + // For Bob, local is the answer and remote is the offer. + CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.answer_, scenario.offer_)); + } + } + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + JAMI_INFO("=== Request Media Change and validate ==="); + { + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.offerUpdate_); + DRing::requestMediaChange(aliceData.accountId_, aliceData.callId_, mediaList); + } + + // Update and validate media count. + mediaCount = scenario.offerUpdate_.size(); + CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answerUpdate_.size()); + + if (scenario.expectMediaRenegotiation_) { + // Wait for media negotiation complete signal. + CPPUNIT_ASSERT_EQUAL( + true, + waitForSignal(aliceData, + DRing::CallSignal::MediaNegotiationStatus::name, + DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS)); + + // Validate Alice's media + { + auto mediaList = aliceCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size()); + + // Validate mute state + CPPUNIT_ASSERT(validateMuteState(scenario.offerUpdate_, mediaList)); + + auto& sdp = aliceCall->getSDP(); + + // Validate local media direction + { + auto descrList = sdp.getActiveMediaDescription(false); + CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size()); + CPPUNIT_ASSERT(validateMediaDirection(descrList, + scenario.offerUpdate_, + scenario.answerUpdate_)); + } + // Validate remote media direction + { + auto descrList = sdp.getActiveMediaDescription(true); + CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size()); + CPPUNIT_ASSERT(validateMediaDirection(descrList, + scenario.answerUpdate_, + scenario.offerUpdate_)); + } + } + + // Validate Bob's media + { + auto const& bobCall = std::dynamic_pointer_cast<SIPCall>( + Manager::instance().getCallFromCallID(bobData.callId_)); + auto mediaList = bobCall->getMediaAttributeList(); + CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size()); + + // Validate mute state + CPPUNIT_ASSERT(validateMuteState(scenario.answerUpdate_, mediaList)); + + // NOTE: + // It should be enough to validate media direction on Alice's side + } + } + + 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"); + DRing::hangUp(bobData.accountId_, bobData.callId_); + + CPPUNIT_ASSERT_EQUAL(true, + waitForSignal(aliceData, + DRing::CallSignal::StateChange::name, + StateEvent::HUNGUP)); + + JAMI_INFO("Call terminated on both sides"); +} + +void +AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_video() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + configureScenario(); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "audio_0"; + defaultAudio.enabled_ = true; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "video_0"; + defaultVideo.enabled_ = true; + + 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; + + testWithScenario(aliceData_, bobData_, scenario); + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +void +AutoAnswerMediaNegoTest::audio_only_then_caller_add_video() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + configureScenario(); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "audio_0"; + defaultAudio.enabled_ = true; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "video_0"; + defaultVideo.enabled_ = true; + + 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; + + testWithScenario(aliceData_, bobData_, scenario); + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +void +AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_audio() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + configureScenario(); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "audio_0"; + defaultAudio.enabled_ = true; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "video_0"; + defaultVideo.enabled_ = true; + + 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; + + testWithScenario(aliceData_, bobData_, scenario); + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +void +AutoAnswerMediaNegoTest::audio_and_video_then_change_video_source() +{ + JAMI_INFO("=== Begin test %s ===", __FUNCTION__); + + configureScenario(); + + MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO); + defaultAudio.label_ = "audio_0"; + defaultAudio.enabled_ = true; + + MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO); + defaultVideo.label_ = "video_0"; + defaultVideo.enabled_ = true; + + 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); + // Just change the media source to validate that a new + // media negotiation (re-invite) will be triggered. + video.sourceUri_ = "Fake source"; + scenario.offerUpdate_.emplace_back(video); + + scenario.answerUpdate_.emplace_back(audio); + scenario.answerUpdate_.emplace_back(video); + + scenario.expectMediaRenegotiation_ = true; + + testWithScenario(aliceData_, bobData_, scenario); + + DRing::unregisterSignalHandlers(); + + JAMI_INFO("=== End test %s ===", __FUNCTION__); +} + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestSip, "AutoAnswerMediaNegoTestSip"); +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestJami, "AutoAnswerMediaNegoTestJami"); + +} // namespace test +} // namespace jami + +JAMI_TEST_RUNNER(jami::test::AutoAnswerMediaNegoTestJami::name()) diff --git a/test/unitTest/media_negotiation/media_negotiation.cpp b/test/unitTest/media_negotiation/media_negotiation.cpp index 8d3e8fbef0..bd65ec51c2 100644 --- a/test/unitTest/media_negotiation/media_negotiation.cpp +++ b/test/unitTest/media_negotiation/media_negotiation.cpp @@ -147,7 +147,9 @@ private: // Helpers static void configureScenario(CallData& bob, CallData& alice); - void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario); + static void testWithScenario(CallData& aliceData, + CallData& bobData, + const TestScenario& scenario); static std::string getUserAlias(const std::string& callId); // Infer media direction of an offer. static uint8_t directionToBitset(MediaDirection direction, bool isLocal); @@ -666,7 +668,7 @@ MediaNegotiationTest::testWithScenario(CallData& aliceData, // Answer the call. { auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answer_); - Manager::instance().answerCall(bobData.accountId_, bobData.callId_, mediaList); + DRing::acceptWithMedia(bobData.accountId_, bobData.callId_, mediaList); } // Wait for media negotiation complete signal. @@ -798,7 +800,7 @@ MediaNegotiationTest::testWithScenario(CallData& aliceData, // Bob hang-up. JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up"); - Manager::instance().hangupCall(bobData.accountId_, bobData.callId_); + DRing::hangUp(bobData.accountId_, bobData.callId_); CPPUNIT_ASSERT_EQUAL(true, waitForSignal(aliceData, @@ -1026,4 +1028,4 @@ MediaNegotiationTest::audio_and_video_then_change_video_source() } // namespace test } // namespace jami -RING_TEST_RUNNER(jami::test::MediaNegotiationTest::name()) +JAMI_TEST_RUNNER(jami::test::MediaNegotiationTest::name()) -- GitLab