Files
jami-daemon/test/unitTest/media_negotiation/media_negotiation.cpp
Mohamed Chibani a887b2dee1 SDP - set media direction according to mute state
Currently, the media attribute in the SDP is always set to
'sendrecv' regardless of the mute state of the media.
In this patch, media direction will be set according to mute
state of the media.
Note that this only applies if the mute/unmute requires media
renegotiation (SIP re-invite with new SDP session). Currently,
this only the case for video media. For audio, mute/unmute is
done locally without SIP re-invite.

References:
RFC-3264
RFC-4317 (non-normative)

Gitlab: #645

Change-Id: I604331255bd25dfe732e192039a673a0980105fa
2021-12-16 15:32:36 -05:00

1030 lines
36 KiB
C++

/*
* 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 "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};
// 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("jami-sample.yml"));
}
~MediaNegotiationTest() { DRing::fini(); }
static std::string name() { return "MediaNegotiationTest"; }
void setUp();
void tearDown();
private:
// 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_answer_muted_video_then_mute_video();
void audio_and_video_then_change_video_source();
CPPUNIT_TEST_SUITE(MediaNegotiationTest);
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_answer_muted_video_then_mute_video);
CPPUNIT_TEST(audio_and_video_then_change_video_source);
CPPUNIT_TEST_SUITE_END();
// 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);
// For backward compatibility test cases
static void onIncomingCall(const std::string& accountId,
const std::string& callId,
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 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 a 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 = {});
private:
CallData aliceData_;
CallData bobData_;
};
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MediaNegotiationTest, MediaNegotiationTest::name());
void
MediaNegotiationTest::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<JamiAccount>(aliceData_.accountId_);
auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData_.accountId_);
wait_for_announcement_of({aliceAccount->getAccountID(), bobAccount->getAccountID()});
}
void
MediaNegotiationTest::tearDown()
{
wait_for_removal_of({aliceData_.accountId_, bobData_.accountId_});
}
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::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
MediaNegotiationTest::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
MediaNegotiationTest::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
MediaNegotiationTest::inferNegotiatedDirection(MediaDirection local, MediaDirection remote)
{
uint8_t val = directionToBitset(local, true) & directionToBitset(remote, false);
return bitsetToDirection(val);
}
bool
MediaNegotiationTest::validateMuteState(std::vector<MediaAttribute> expected,
std::vector<MediaAttribute> actual)
{
CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size());
return std::equal(expected.begin(),
expected.end(),
actual.begin(),
actual.end(),
[](auto const& expAttr, auto const& actAttr) {
return expAttr.muted_ == actAttr.muted_;
});
}
bool
MediaNegotiationTest::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
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::onIncomingCall(const std::string& accountId,
const std::string& callId,
CallData& callData)
{
CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
JAMI_INFO("Signal [%s] - user [%s] - call [%s]",
DRing::CallSignal::IncomingCall::name,
callData.alias_.c_str(),
callId.c_str());
// 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::IncomingCall::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&,
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::IncomingCall>(
[&](const std::string& accountId, const std::string& callId, const std::string&) {
auto user = getUserAlias(callId);
if (not user.empty()) {
onIncomingCall(accountId, callId, 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
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_,
MediaAttribute::mediaAttributesToMediaMaps(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(waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name));
// Answer the call.
{
auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answer_);
Manager::instance().answerCall(bobData.accountId_, bobData.callId_, mediaList);
}
// 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());
// 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_);
DRing::answerMediaChangeRequest(bobData.accountId_, bobData.callId_, mediaList);
}
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
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");
Manager::instance().hangupCall(bobData.accountId_, 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_caller_mute_video()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
configureScenario(aliceData_, bobData_);
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;
scenario.expectMediaChangeRequest_ = false;
testWithScenario(aliceData_, bobData_, scenario);
DRing::unregisterSignalHandlers();
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
void
MediaNegotiationTest::audio_only_then_caller_add_video()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
configureScenario(aliceData_, bobData_);
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;
scenario.expectMediaChangeRequest_ = true;
testWithScenario(aliceData_, bobData_, scenario);
DRing::unregisterSignalHandlers();
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
void
MediaNegotiationTest::audio_and_video_then_caller_mute_audio()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
configureScenario(aliceData_, bobData_);
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;
scenario.expectMediaChangeRequest_ = false;
testWithScenario(aliceData_, bobData_, scenario);
DRing::unregisterSignalHandlers();
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
void
MediaNegotiationTest::audio_and_video_answer_muted_video_then_mute_video()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
configureScenario(aliceData_, bobData_);
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);
video.muted_ = true;
scenario.answer_.emplace_back(audio);
scenario.answer_.emplace_back(video);
// Updated offer/answer
video.muted_ = true;
scenario.offerUpdate_.emplace_back(audio);
scenario.offerUpdate_.emplace_back(video);
video.muted_ = true;
scenario.answerUpdate_.emplace_back(audio);
scenario.answerUpdate_.emplace_back(video);
scenario.expectMediaChangeRequest_ = false;
scenario.expectMediaRenegotiation_ = true;
testWithScenario(aliceData_, bobData_, scenario);
DRing::unregisterSignalHandlers();
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
void
MediaNegotiationTest::audio_and_video_then_change_video_source()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
configureScenario(aliceData_, bobData_);
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;
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())