Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "../../test_runner.h"
19 :
20 : #include "manager.h"
21 : #include "account.h"
22 : #include "sip/sipaccount.h"
23 : #include "jami.h"
24 : #include "jami/media_const.h"
25 : #include "call_const.h"
26 : #include "account_const.h"
27 : #include "sip/sipcall.h"
28 : #include "sip/sdp.h"
29 : #include "common.h"
30 :
31 : #include <dhtnet/connectionmanager.h>
32 :
33 : #include <cppunit/TestAssert.h>
34 : #include <cppunit/TestFixture.h>
35 : #include <cppunit/extensions/HelperMacros.h>
36 :
37 : #include <condition_variable>
38 : #include <string>
39 :
40 : using namespace libjami::Account;
41 : using namespace libjami::Call;
42 :
43 : namespace jami {
44 : namespace test {
45 :
46 : struct TestScenario
47 : {
48 : TestScenario(const std::vector<MediaAttribute>& offer,
49 : const std::vector<MediaAttribute>& answer,
50 : const std::vector<MediaAttribute>& offerUpdate,
51 : const std::vector<MediaAttribute>& answerUpdate)
52 : : offer_(std::move(offer))
53 : , answer_(std::move(answer))
54 : , offerUpdate_(std::move(offerUpdate))
55 : , answerUpdate_(std::move(answerUpdate))
56 : {}
57 :
58 4 : TestScenario() {};
59 :
60 : std::vector<MediaAttribute> offer_;
61 : std::vector<MediaAttribute> answer_;
62 : std::vector<MediaAttribute> offerUpdate_;
63 : std::vector<MediaAttribute> answerUpdate_;
64 : // Determine if we should expect the MediaNegotiationStatus signal.
65 : bool expectMediaRenegotiation_ {false};
66 : };
67 :
68 : struct CallData
69 : {
70 : struct Signal
71 : {
72 56 : Signal(const std::string& name, const std::string& event = {})
73 56 : : name_(std::move(name))
74 56 : , event_(std::move(event)) {};
75 :
76 : std::string name_ {};
77 : std::string event_ {};
78 : };
79 :
80 : std::string accountId_ {};
81 : std::string userName_ {};
82 : std::string alias_ {};
83 : std::string callId_ {};
84 : uint16_t listeningPort_ {0};
85 : std::string toUri_ {};
86 : std::vector<Signal> signals_;
87 : std::condition_variable cv_ {};
88 : std::mutex mtx_;
89 : };
90 :
91 : class AutoAnswerMediaNegoTest
92 : {
93 : public:
94 4 : AutoAnswerMediaNegoTest()
95 4 : {
96 : // Init daemon
97 4 : libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
98 4 : if (not Manager::instance().initialized)
99 1 : CPPUNIT_ASSERT(libjami::start("jami-sample.yml"));
100 4 : }
101 4 : ~AutoAnswerMediaNegoTest() { libjami::fini(); }
102 :
103 : protected:
104 : // Test cases.
105 : void audio_and_video_then_caller_mute_video();
106 : void audio_only_then_caller_add_video();
107 : void audio_and_video_then_caller_mute_audio();
108 : void audio_and_video_then_change_video_source();
109 :
110 : // Event/Signal handlers
111 : static void onCallStateChange(const std::string& accountId,
112 : const std::string& callId,
113 : const std::string& state,
114 : CallData& callData);
115 : static void onIncomingCallWithMedia(const std::string& accountId,
116 : const std::string& callId,
117 : const std::vector<libjami::MediaMap> mediaList,
118 : CallData& callData);
119 : static void onMediaChangeRequested(const std::string& accountId,
120 : const std::string& callId,
121 : const std::vector<libjami::MediaMap> mediaList,
122 : CallData& callData);
123 : static void onVideoMuted(const std::string& callId, bool muted, CallData& callData);
124 : static void onMediaNegotiationStatus(const std::string& callId,
125 : const std::string& event,
126 : CallData& callData);
127 :
128 : // Helpers
129 : void configureScenario();
130 : void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario);
131 : static std::string getUserAlias(const std::string& callId);
132 : // Infer media direction of an offer.
133 : static uint8_t directionToBitset(MediaDirection direction, bool isLocal);
134 : static MediaDirection bitsetToDirection(uint8_t val);
135 : static MediaDirection inferInitialDirection(const MediaAttribute& offer);
136 : // Infer media direction of an answer.
137 : static MediaDirection inferNegotiatedDirection(MediaDirection local, MediaDirection answer);
138 : // Wait for a signal from the callbacks. Some signals also report the event that
139 : // triggered the signal like the StateChange signal.
140 : static bool validateMuteState(std::vector<MediaAttribute> expected,
141 : std::vector<MediaAttribute> actual);
142 : static bool validateMediaDirection(std::vector<MediaDescription> descrList,
143 : std::vector<MediaAttribute> listInOffer,
144 : std::vector<MediaAttribute> listInAnswer);
145 : static bool waitForSignal(CallData& callData,
146 : const std::string& signal,
147 : const std::string& expectedEvent = {});
148 :
149 : bool isSipAccount_ {false};
150 : CallData aliceData_;
151 : CallData bobData_;
152 : };
153 :
154 : class AutoAnswerMediaNegoTestSip : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture
155 : {
156 : public:
157 0 : AutoAnswerMediaNegoTestSip() { isSipAccount_ = true; };
158 :
159 0 : ~AutoAnswerMediaNegoTestSip() {};
160 :
161 1 : static std::string name() { return "AutoAnswerMediaNegoTestSip"; }
162 : void setUp() override;
163 : void tearDown() override;
164 :
165 : private:
166 0 : CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestSip);
167 0 : CPPUNIT_TEST(audio_and_video_then_caller_mute_video);
168 0 : CPPUNIT_TEST(audio_only_then_caller_add_video);
169 0 : CPPUNIT_TEST(audio_and_video_then_caller_mute_audio);
170 0 : CPPUNIT_TEST(audio_and_video_then_change_video_source);
171 0 : CPPUNIT_TEST_SUITE_END();
172 : };
173 :
174 : void
175 0 : AutoAnswerMediaNegoTestSip::setUp()
176 : {
177 0 : aliceData_.listeningPort_ = 5080;
178 0 : std::map<std::string, std::string> details = libjami::getAccountTemplate("SIP");
179 0 : details[ConfProperties::TYPE] = "SIP";
180 0 : details[ConfProperties::DISPLAYNAME] = "ALICE";
181 0 : details[ConfProperties::ALIAS] = "ALICE";
182 0 : details[ConfProperties::LOCAL_PORT] = std::to_string(aliceData_.listeningPort_);
183 0 : details[ConfProperties::UPNP_ENABLED] = "false";
184 0 : aliceData_.accountId_ = Manager::instance().addAccount(details);
185 :
186 0 : bobData_.listeningPort_ = 5082;
187 0 : details = libjami::getAccountTemplate("SIP");
188 0 : details[ConfProperties::TYPE] = "SIP";
189 0 : details[ConfProperties::DISPLAYNAME] = "BOB";
190 0 : details[ConfProperties::ALIAS] = "BOB";
191 0 : details[ConfProperties::AUTOANSWER] = "true";
192 0 : details[ConfProperties::LOCAL_PORT] = std::to_string(bobData_.listeningPort_);
193 0 : details[ConfProperties::UPNP_ENABLED] = "false";
194 0 : bobData_.accountId_ = Manager::instance().addAccount(details);
195 :
196 0 : JAMI_INFO("Initialize accounts ...");
197 0 : auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_);
198 0 : auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_);
199 0 : }
200 :
201 : void
202 0 : AutoAnswerMediaNegoTestSip::tearDown()
203 : {
204 0 : JAMI_INFO("Remove created accounts...");
205 :
206 0 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
207 0 : std::mutex mtx;
208 0 : std::unique_lock lk {mtx};
209 0 : std::condition_variable cv;
210 0 : auto currentAccSize = Manager::instance().getAccountList().size();
211 0 : std::atomic_bool accountsRemoved {false};
212 0 : confHandlers.insert(
213 0 : libjami::exportable_callback<libjami::ConfigurationSignal::AccountsChanged>([&]() {
214 0 : if (Manager::instance().getAccountList().size() <= currentAccSize - 2) {
215 0 : accountsRemoved = true;
216 0 : cv.notify_one();
217 : }
218 0 : }));
219 0 : libjami::registerSignalHandlers(confHandlers);
220 :
221 0 : Manager::instance().removeAccount(aliceData_.accountId_, true);
222 0 : Manager::instance().removeAccount(bobData_.accountId_, true);
223 0 : CPPUNIT_ASSERT(
224 : cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); }));
225 :
226 0 : libjami::unregisterSignalHandlers();
227 0 : }
228 :
229 : class AutoAnswerMediaNegoTestJami : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture
230 : {
231 : public:
232 4 : AutoAnswerMediaNegoTestJami() { isSipAccount_ = false; };
233 :
234 8 : ~AutoAnswerMediaNegoTestJami() {};
235 :
236 2 : static std::string name() { return "AutoAnswerMediaNegoTestJami"; }
237 : void setUp() override;
238 : void tearDown() override;
239 :
240 : private:
241 2 : CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestJami);
242 1 : CPPUNIT_TEST(audio_and_video_then_caller_mute_video);
243 1 : CPPUNIT_TEST(audio_only_then_caller_add_video);
244 1 : CPPUNIT_TEST(audio_and_video_then_caller_mute_audio);
245 1 : CPPUNIT_TEST(audio_and_video_then_change_video_source);
246 4 : CPPUNIT_TEST_SUITE_END();
247 : };
248 :
249 : void
250 4 : AutoAnswerMediaNegoTestJami::setUp()
251 : {
252 4 : auto actors = load_actors("actors/alice-bob-no-upnp.yml");
253 :
254 4 : aliceData_.accountId_ = actors["alice"];
255 4 : bobData_.accountId_ = actors["bob"];
256 :
257 4 : JAMI_INFO("Initialize account...");
258 4 : auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_);
259 4 : auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_);
260 4 : auto details = bobAccount->getAccountDetails();
261 4 : details[ConfProperties::AUTOANSWER] = "true";
262 4 : bobAccount->setAccountDetails(details);
263 12 : wait_for_announcement_of({aliceAccount->getAccountID(), bobAccount->getAccountID()});
264 4 : }
265 :
266 : void
267 4 : AutoAnswerMediaNegoTestJami::tearDown()
268 : {
269 12 : wait_for_removal_of({aliceData_.accountId_, bobData_.accountId_});
270 4 : }
271 :
272 : std::string
273 60 : AutoAnswerMediaNegoTest::getUserAlias(const std::string& callId)
274 : {
275 60 : auto call = Manager::instance().getCallFromCallID(callId);
276 :
277 60 : if (not call) {
278 4 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
279 4 : return {};
280 : }
281 :
282 56 : auto const& account = call->getAccount().lock();
283 56 : if (not account) {
284 0 : return {};
285 : }
286 :
287 56 : return account->getAccountDetails()[ConfProperties::ALIAS];
288 60 : }
289 :
290 : MediaDirection
291 44 : AutoAnswerMediaNegoTest::inferInitialDirection(const MediaAttribute& mediaAttr)
292 : {
293 44 : if (not mediaAttr.enabled_)
294 0 : return MediaDirection::INACTIVE;
295 :
296 44 : if (mediaAttr.muted_) {
297 2 : if (mediaAttr.onHold_)
298 0 : return MediaDirection::INACTIVE;
299 2 : return MediaDirection::RECVONLY;
300 : }
301 :
302 42 : if (mediaAttr.onHold_)
303 0 : return MediaDirection::SENDONLY;
304 :
305 42 : return MediaDirection::SENDRECV;
306 : }
307 :
308 : uint8_t
309 44 : AutoAnswerMediaNegoTest::directionToBitset(MediaDirection direction, bool isLocal)
310 : {
311 44 : if (direction == MediaDirection::SENDRECV)
312 42 : return 3;
313 2 : if (direction == MediaDirection::RECVONLY)
314 2 : return isLocal ? 2 : 1;
315 0 : if (direction == MediaDirection::SENDONLY)
316 0 : return isLocal ? 1 : 2;
317 0 : return 0;
318 : }
319 :
320 : MediaDirection
321 22 : AutoAnswerMediaNegoTest::bitsetToDirection(uint8_t val)
322 : {
323 22 : if (val == 3)
324 20 : return MediaDirection::SENDRECV;
325 2 : if (val == 2)
326 1 : return MediaDirection::RECVONLY;
327 1 : if (val == 1)
328 1 : return MediaDirection::SENDONLY;
329 0 : return MediaDirection::INACTIVE;
330 : }
331 :
332 : MediaDirection
333 22 : AutoAnswerMediaNegoTest::inferNegotiatedDirection(MediaDirection local, MediaDirection remote)
334 : {
335 22 : uint8_t val = directionToBitset(local, true) & directionToBitset(remote, false);
336 22 : auto dir = bitsetToDirection(val);
337 22 : return dir;
338 : }
339 :
340 : bool
341 12 : AutoAnswerMediaNegoTest::validateMuteState(std::vector<MediaAttribute> expected,
342 : std::vector<MediaAttribute> actual)
343 : {
344 12 : CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size());
345 :
346 34 : for (size_t idx = 0; idx < expected.size(); idx++) {
347 22 : if (expected[idx].muted_ != actual[idx].muted_)
348 0 : return false;
349 : }
350 :
351 12 : return true;
352 : }
353 :
354 : bool
355 12 : AutoAnswerMediaNegoTest::validateMediaDirection(std::vector<MediaDescription> descrList,
356 : std::vector<MediaAttribute> localMediaList,
357 : std::vector<MediaAttribute> remoteMediaList)
358 : {
359 12 : CPPUNIT_ASSERT_EQUAL(descrList.size(), localMediaList.size());
360 12 : CPPUNIT_ASSERT_EQUAL(descrList.size(), remoteMediaList.size());
361 :
362 34 : for (size_t idx = 0; idx < descrList.size(); idx++) {
363 22 : auto local = inferInitialDirection(localMediaList[idx]);
364 22 : auto remote = inferInitialDirection(remoteMediaList[idx]);
365 22 : auto negotiated = inferNegotiatedDirection(local, remote);
366 :
367 22 : if (descrList[idx].direction_ != negotiated) {
368 0 : JAMI_WARN("Media [%lu] direction mismatch: expected %i - found %i",
369 : idx,
370 : static_cast<int>(negotiated),
371 : static_cast<int>(descrList[idx].direction_));
372 0 : return false;
373 : }
374 : }
375 :
376 12 : return true;
377 : }
378 :
379 : void
380 4 : AutoAnswerMediaNegoTest::onIncomingCallWithMedia(const std::string& accountId,
381 : const std::string& callId,
382 : const std::vector<libjami::MediaMap> mediaList,
383 : CallData& callData)
384 : {
385 4 : CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
386 :
387 4 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
388 : libjami::CallSignal::IncomingCallWithMedia::name,
389 : callData.alias_.c_str(),
390 : callId.c_str(),
391 : mediaList.size());
392 :
393 4 : if (not Manager::instance().getCallFromCallID(callId)) {
394 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
395 0 : callData.callId_ = {};
396 0 : return;
397 : }
398 :
399 4 : std::unique_lock lock {callData.mtx_};
400 4 : callData.callId_ = callId;
401 4 : callData.signals_.emplace_back(CallData::Signal(libjami::CallSignal::IncomingCallWithMedia::name));
402 :
403 4 : callData.cv_.notify_one();
404 4 : }
405 :
406 : void
407 0 : AutoAnswerMediaNegoTest::onMediaChangeRequested(const std::string& accountId,
408 : const std::string& callId,
409 : const std::vector<libjami::MediaMap> mediaList,
410 : CallData& callData)
411 : {
412 0 : CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
413 :
414 0 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
415 : libjami::CallSignal::MediaChangeRequested::name,
416 : callData.alias_.c_str(),
417 : callId.c_str(),
418 : mediaList.size());
419 :
420 0 : if (not Manager::instance().getCallFromCallID(callId)) {
421 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
422 0 : callData.callId_ = {};
423 0 : return;
424 : }
425 :
426 0 : std::unique_lock lock {callData.mtx_};
427 0 : callData.callId_ = callId;
428 0 : callData.signals_.emplace_back(CallData::Signal(libjami::CallSignal::MediaChangeRequested::name));
429 :
430 0 : callData.cv_.notify_one();
431 0 : }
432 :
433 : void
434 36 : AutoAnswerMediaNegoTest::onCallStateChange(const std::string& accountId UNUSED,
435 : const std::string& callId,
436 : const std::string& state,
437 : CallData& callData)
438 : {
439 : // TODO. rewrite me using accountId.
440 :
441 36 : auto call = Manager::instance().getCallFromCallID(callId);
442 36 : if (not call) {
443 0 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
444 0 : return;
445 : }
446 :
447 36 : auto account = call->getAccount().lock();
448 36 : if (not account) {
449 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
450 0 : return;
451 : }
452 :
453 36 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
454 : libjami::CallSignal::StateChange::name,
455 : callData.alias_.c_str(),
456 : callId.c_str(),
457 : state.c_str());
458 :
459 36 : if (account->getAccountID() != callData.accountId_)
460 0 : return;
461 :
462 : {
463 36 : std::unique_lock lock {callData.mtx_};
464 36 : callData.signals_.emplace_back(
465 72 : CallData::Signal(libjami::CallSignal::StateChange::name, state));
466 36 : }
467 :
468 36 : if (state == "CURRENT" or state == "OVER" or state == "HUNGUP") {
469 16 : callData.cv_.notify_one();
470 : }
471 36 : }
472 :
473 : void
474 1 : AutoAnswerMediaNegoTest::onVideoMuted(const std::string& callId, bool muted, CallData& callData)
475 : {
476 1 : auto call = Manager::instance().getCallFromCallID(callId);
477 :
478 1 : if (not call) {
479 0 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
480 0 : return;
481 : }
482 :
483 1 : auto account = call->getAccount().lock();
484 1 : if (not account) {
485 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
486 0 : return;
487 : }
488 :
489 1 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
490 : libjami::CallSignal::VideoMuted::name,
491 : account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
492 : call->getCallId().c_str(),
493 : muted ? "Mute" : "Un-mute");
494 :
495 1 : if (account->getAccountID() != callData.accountId_)
496 0 : return;
497 :
498 : {
499 1 : std::unique_lock lock {callData.mtx_};
500 1 : callData.signals_.emplace_back(
501 2 : CallData::Signal(libjami::CallSignal::VideoMuted::name, muted ? "muted" : "un-muted"));
502 1 : }
503 :
504 1 : callData.cv_.notify_one();
505 1 : }
506 :
507 : void
508 15 : AutoAnswerMediaNegoTest::onMediaNegotiationStatus(const std::string& callId,
509 : const std::string& event,
510 : CallData& callData)
511 : {
512 15 : auto call = Manager::instance().getCallFromCallID(callId);
513 15 : if (not call) {
514 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
515 0 : return;
516 : }
517 :
518 15 : auto account = call->getAccount().lock();
519 15 : if (not account) {
520 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
521 0 : return;
522 : }
523 :
524 15 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
525 : libjami::CallSignal::MediaNegotiationStatus::name,
526 : account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
527 : call->getCallId().c_str(),
528 : event.c_str());
529 :
530 15 : if (account->getAccountID() != callData.accountId_)
531 0 : return;
532 :
533 : {
534 15 : std::unique_lock lock {callData.mtx_};
535 15 : callData.signals_.emplace_back(
536 30 : CallData::Signal(libjami::CallSignal::MediaNegotiationStatus::name, event));
537 15 : }
538 :
539 15 : callData.cv_.notify_one();
540 15 : }
541 :
542 : bool
543 22 : AutoAnswerMediaNegoTest::waitForSignal(CallData& callData,
544 : const std::string& expectedSignal,
545 : const std::string& expectedEvent)
546 : {
547 22 : const std::chrono::seconds TIME_OUT {15};
548 22 : std::unique_lock lock {callData.mtx_};
549 :
550 : // Combined signal + event (if any).
551 22 : std::string sigEvent(expectedSignal);
552 22 : if (not expectedEvent.empty())
553 18 : sigEvent += "::" + expectedEvent;
554 :
555 22 : JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str());
556 :
557 22 : auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] {
558 : // Search for the expected signal in list of received signals.
559 42 : bool pred = false;
560 171 : for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) {
561 : // The predicate is true if the signal names match, and if the
562 : // expectedEvent is not empty, the events must also match.
563 151 : if (it->name_ == expectedSignal
564 151 : and (expectedEvent.empty() or it->event_ == expectedEvent)) {
565 22 : pred = true;
566 : // Done with this signal.
567 22 : callData.signals_.erase(it);
568 22 : break;
569 : }
570 : }
571 :
572 42 : return pred;
573 : });
574 :
575 22 : if (not res) {
576 0 : JAMI_ERR("[%s] waiting for signal/event [%s] timed-out!",
577 : callData.alias_.c_str(),
578 : sigEvent.c_str());
579 :
580 0 : JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str());
581 :
582 0 : for (auto const& sig : callData.signals_) {
583 0 : JAMI_INFO() << "Signal [" << sig.name_
584 0 : << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]";
585 : }
586 : }
587 :
588 22 : return res;
589 22 : }
590 :
591 : void
592 4 : AutoAnswerMediaNegoTest::configureScenario()
593 : {
594 : // Configure Alice
595 : {
596 4 : CPPUNIT_ASSERT(not aliceData_.accountId_.empty());
597 4 : auto const& account = Manager::instance().getAccount<Account>(aliceData_.accountId_);
598 4 : aliceData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
599 4 : aliceData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
600 4 : if (isSipAccount_) {
601 0 : auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account);
602 0 : CPPUNIT_ASSERT(sipAccount);
603 0 : sipAccount->setLocalPort(aliceData_.listeningPort_);
604 0 : }
605 4 : }
606 :
607 : // Configure Bob
608 : {
609 4 : CPPUNIT_ASSERT(not bobData_.accountId_.empty());
610 4 : auto const& account = Manager::instance().getAccount<Account>(bobData_.accountId_);
611 4 : bobData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
612 4 : bobData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
613 4 : CPPUNIT_ASSERT(account->isAutoAnswerEnabled());
614 :
615 4 : if (isSipAccount_) {
616 0 : auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account);
617 0 : CPPUNIT_ASSERT(sipAccount);
618 0 : sipAccount->setLocalPort(bobData_.listeningPort_);
619 0 : bobData_.toUri_ = "127.0.0.1:" + std::to_string(bobData_.listeningPort_);
620 0 : }
621 4 : }
622 :
623 4 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> signalHandlers;
624 :
625 : // Insert needed signal handlers.
626 4 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::IncomingCallWithMedia>(
627 4 : [&](const std::string& accountId,
628 : const std::string& callId,
629 : const std::string&,
630 : const std::vector<libjami::MediaMap> mediaList) {
631 4 : auto user = getUserAlias(callId);
632 4 : if (not user.empty())
633 8 : onIncomingCallWithMedia(accountId,
634 : callId,
635 : mediaList,
636 8 : user == aliceData_.alias_ ? aliceData_ : bobData_);
637 4 : }));
638 :
639 4 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::MediaChangeRequested>(
640 0 : [&](const std::string& accountId,
641 : const std::string& callId,
642 : const std::vector<libjami::MediaMap> mediaList) {
643 0 : auto user = getUserAlias(callId);
644 0 : if (not user.empty())
645 0 : onMediaChangeRequested(accountId,
646 : callId,
647 : mediaList,
648 0 : user == aliceData_.alias_ ? aliceData_ : bobData_);
649 0 : }));
650 :
651 4 : signalHandlers.insert(
652 8 : libjami::exportable_callback<libjami::CallSignal::StateChange>([&](const std::string& accountId,
653 : const std::string& callId,
654 : const std::string& state,
655 : signed) {
656 40 : auto user = getUserAlias(callId);
657 40 : if (not user.empty())
658 72 : onCallStateChange(accountId,
659 : callId,
660 : state,
661 72 : user == aliceData_.alias_ ? aliceData_ : bobData_);
662 40 : }));
663 :
664 4 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::VideoMuted>(
665 1 : [&](const std::string& callId, bool muted) {
666 1 : auto user = getUserAlias(callId);
667 1 : if (not user.empty())
668 1 : onVideoMuted(callId, muted, user == aliceData_.alias_ ? aliceData_ : bobData_);
669 1 : }));
670 :
671 4 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::MediaNegotiationStatus>(
672 15 : [&](const std::string& callId,
673 : const std::string& event,
674 : const std::vector<std::map<std::string, std::string>>&) {
675 15 : auto user = getUserAlias(callId);
676 15 : if (not user.empty())
677 30 : onMediaNegotiationStatus(callId,
678 : event,
679 30 : user == aliceData_.alias_ ? aliceData_ : bobData_);
680 15 : }));
681 :
682 4 : libjami::registerSignalHandlers(signalHandlers);
683 4 : }
684 :
685 : void
686 4 : AutoAnswerMediaNegoTest::testWithScenario(CallData& aliceData,
687 : CallData& bobData,
688 : const TestScenario& scenario)
689 : {
690 4 : JAMI_INFO("=== Start a call and validate ===");
691 :
692 : // The media count of the offer and answer must match (RFC-3264).
693 4 : auto mediaCount = scenario.offer_.size();
694 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answer_.size());
695 :
696 8 : aliceData.callId_ = libjami::placeCallWithMedia(aliceData.accountId_,
697 4 : isSipAccount_ ? bobData.toUri_
698 : : bobData_.userName_,
699 8 : MediaAttribute::mediaAttributesToMediaMaps(
700 8 : scenario.offer_));
701 4 : CPPUNIT_ASSERT(not aliceData.callId_.empty());
702 : auto aliceCall = std::static_pointer_cast<SIPCall>(
703 4 : Manager::instance().getCallFromCallID(aliceData.callId_));
704 :
705 4 : CPPUNIT_ASSERT(aliceCall);
706 :
707 4 : JAMI_INFO("ALICE [%s] started a call with BOB [%s] and wait for answer",
708 : aliceData.accountId_.c_str(),
709 : bobData.accountId_.c_str());
710 :
711 : // Wait for incoming call signal.
712 4 : CPPUNIT_ASSERT(waitForSignal(bobData, libjami::CallSignal::IncomingCallWithMedia::name));
713 :
714 : // Bob automatically answers the call.
715 :
716 : // Wait for media negotiation complete signal.
717 4 : CPPUNIT_ASSERT_EQUAL(
718 : true,
719 : waitForSignal(bobData,
720 : libjami::CallSignal::MediaNegotiationStatus::name,
721 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
722 : // Wait for the StateChange signal.
723 4 : CPPUNIT_ASSERT_EQUAL(true,
724 : waitForSignal(bobData,
725 : libjami::CallSignal::StateChange::name,
726 : StateEvent::CURRENT));
727 :
728 4 : JAMI_INFO("BOB answered the call [%s]", bobData.callId_.c_str());
729 :
730 : // Wait for media negotiation complete signal.
731 4 : CPPUNIT_ASSERT_EQUAL(
732 : true,
733 : waitForSignal(aliceData,
734 : libjami::CallSignal::MediaNegotiationStatus::name,
735 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
736 :
737 : // Validate Alice's media
738 : {
739 4 : auto mediaList = aliceCall->getMediaAttributeList();
740 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
741 :
742 : // Validate mute state
743 4 : CPPUNIT_ASSERT(validateMuteState(scenario.offer_, mediaList));
744 :
745 4 : auto& sdp = aliceCall->getSDP();
746 :
747 : // Validate local media direction
748 : {
749 4 : auto descrList = sdp.getActiveMediaDescription(false);
750 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
751 : // For Alice, local is the offer and remote is the answer.
752 4 : CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.offer_, scenario.answer_));
753 4 : }
754 4 : }
755 :
756 : // Validate Bob's media
757 : {
758 : auto const& bobCall = std::dynamic_pointer_cast<SIPCall>(
759 4 : Manager::instance().getCallFromCallID(bobData.callId_));
760 4 : auto mediaList = bobCall->getMediaAttributeList();
761 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
762 :
763 : // Validate mute state
764 4 : CPPUNIT_ASSERT(validateMuteState(scenario.answer_, mediaList));
765 :
766 4 : auto& sdp = bobCall->getSDP();
767 :
768 : // Validate local media direction
769 : {
770 4 : auto descrList = sdp.getActiveMediaDescription(false);
771 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
772 : // For Bob, local is the answer and remote is the offer.
773 4 : CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.answer_, scenario.offer_));
774 4 : }
775 4 : }
776 :
777 4 : std::this_thread::sleep_for(std::chrono::seconds(3));
778 :
779 4 : JAMI_INFO("=== Request Media Change and validate ===");
780 : {
781 4 : auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.offerUpdate_);
782 4 : libjami::requestMediaChange(aliceData.accountId_, aliceData.callId_, mediaList);
783 4 : }
784 :
785 : // Update and validate media count.
786 4 : mediaCount = scenario.offerUpdate_.size();
787 4 : CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answerUpdate_.size());
788 :
789 4 : if (scenario.expectMediaRenegotiation_) {
790 : // Wait for media negotiation complete signal.
791 2 : CPPUNIT_ASSERT_EQUAL(
792 : true,
793 : waitForSignal(aliceData,
794 : libjami::CallSignal::MediaNegotiationStatus::name,
795 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
796 :
797 : // Validate Alice's media
798 : {
799 2 : auto mediaList = aliceCall->getMediaAttributeList();
800 2 : CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
801 :
802 : // Validate mute state
803 2 : CPPUNIT_ASSERT(validateMuteState(scenario.offerUpdate_, mediaList));
804 :
805 2 : auto& sdp = aliceCall->getSDP();
806 :
807 : // Validate local media direction
808 : {
809 2 : auto descrList = sdp.getActiveMediaDescription(false);
810 2 : CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
811 2 : CPPUNIT_ASSERT(validateMediaDirection(descrList,
812 : scenario.offerUpdate_,
813 : scenario.answerUpdate_));
814 2 : }
815 : // Validate remote media direction
816 : {
817 2 : auto descrList = sdp.getActiveMediaDescription(true);
818 2 : CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
819 2 : CPPUNIT_ASSERT(validateMediaDirection(descrList,
820 : scenario.answerUpdate_,
821 : scenario.offerUpdate_));
822 2 : }
823 2 : }
824 :
825 : // Validate Bob's media
826 : {
827 : auto const& bobCall = std::dynamic_pointer_cast<SIPCall>(
828 2 : Manager::instance().getCallFromCallID(bobData.callId_));
829 2 : auto mediaList = bobCall->getMediaAttributeList();
830 2 : CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
831 :
832 : // Validate mute state
833 2 : CPPUNIT_ASSERT(validateMuteState(scenario.answerUpdate_, mediaList));
834 :
835 : // NOTE:
836 : // It should be enough to validate media direction on Alice's side
837 2 : }
838 : }
839 :
840 4 : std::this_thread::sleep_for(std::chrono::seconds(3));
841 :
842 : // Bob hang-up.
843 4 : JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
844 4 : libjami::hangUp(bobData.accountId_, bobData.callId_);
845 :
846 4 : CPPUNIT_ASSERT_EQUAL(true,
847 : waitForSignal(aliceData,
848 : libjami::CallSignal::StateChange::name,
849 : StateEvent::HUNGUP));
850 :
851 4 : JAMI_INFO("Call terminated on both sides");
852 4 : }
853 :
854 : void
855 1 : AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_video()
856 : {
857 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
858 :
859 1 : configureScenario();
860 :
861 1 : MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
862 1 : defaultAudio.label_ = "audio_0";
863 1 : defaultAudio.enabled_ = true;
864 :
865 1 : MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
866 1 : defaultVideo.label_ = "video_0";
867 1 : defaultVideo.enabled_ = true;
868 :
869 1 : MediaAttribute audio(defaultAudio);
870 1 : MediaAttribute video(defaultVideo);
871 :
872 1 : TestScenario scenario;
873 : // First offer/answer
874 1 : scenario.offer_.emplace_back(audio);
875 1 : scenario.offer_.emplace_back(video);
876 1 : scenario.answer_.emplace_back(audio);
877 1 : scenario.answer_.emplace_back(video);
878 :
879 : // Updated offer/answer
880 1 : scenario.offerUpdate_.emplace_back(audio);
881 1 : video.muted_ = true;
882 1 : scenario.offerUpdate_.emplace_back(video);
883 :
884 1 : scenario.answerUpdate_.emplace_back(audio);
885 1 : video.muted_ = false;
886 1 : scenario.answerUpdate_.emplace_back(video);
887 1 : scenario.expectMediaRenegotiation_ = true;
888 :
889 1 : testWithScenario(aliceData_, bobData_, scenario);
890 :
891 1 : libjami::unregisterSignalHandlers();
892 :
893 1 : JAMI_INFO("=== End test %s ===", __FUNCTION__);
894 1 : }
895 :
896 : void
897 1 : AutoAnswerMediaNegoTest::audio_only_then_caller_add_video()
898 : {
899 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
900 :
901 1 : configureScenario();
902 :
903 1 : MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
904 1 : defaultAudio.label_ = "audio_0";
905 1 : defaultAudio.enabled_ = true;
906 :
907 1 : MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
908 1 : defaultVideo.label_ = "video_0";
909 1 : defaultVideo.enabled_ = true;
910 :
911 1 : MediaAttribute audio(defaultAudio);
912 1 : MediaAttribute video(defaultVideo);
913 :
914 1 : TestScenario scenario;
915 : // First offer/answer
916 1 : scenario.offer_.emplace_back(audio);
917 1 : scenario.answer_.emplace_back(audio);
918 :
919 : // Updated offer/answer
920 1 : scenario.offerUpdate_.emplace_back(audio);
921 1 : scenario.offerUpdate_.emplace_back(video);
922 1 : scenario.answerUpdate_.emplace_back(audio);
923 1 : scenario.answerUpdate_.emplace_back(video);
924 :
925 1 : testWithScenario(aliceData_, bobData_, scenario);
926 :
927 1 : libjami::unregisterSignalHandlers();
928 :
929 1 : JAMI_INFO("=== End test %s ===", __FUNCTION__);
930 1 : }
931 :
932 : void
933 1 : AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_audio()
934 : {
935 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
936 :
937 1 : configureScenario();
938 :
939 1 : MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
940 1 : defaultAudio.label_ = "audio_0";
941 1 : defaultAudio.enabled_ = true;
942 :
943 1 : MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
944 1 : defaultVideo.label_ = "video_0";
945 1 : defaultVideo.enabled_ = true;
946 :
947 1 : MediaAttribute audio(defaultAudio);
948 1 : MediaAttribute video(defaultVideo);
949 :
950 1 : TestScenario scenario;
951 : // First offer/answer
952 1 : scenario.offer_.emplace_back(audio);
953 1 : scenario.offer_.emplace_back(video);
954 1 : scenario.answer_.emplace_back(audio);
955 1 : scenario.answer_.emplace_back(video);
956 :
957 : // Updated offer/answer
958 1 : audio.muted_ = true;
959 1 : scenario.offerUpdate_.emplace_back(audio);
960 1 : scenario.offerUpdate_.emplace_back(video);
961 :
962 1 : audio.muted_ = false;
963 1 : scenario.answerUpdate_.emplace_back(audio);
964 1 : scenario.answerUpdate_.emplace_back(video);
965 :
966 1 : scenario.expectMediaRenegotiation_ = false;
967 :
968 1 : testWithScenario(aliceData_, bobData_, scenario);
969 :
970 1 : libjami::unregisterSignalHandlers();
971 :
972 1 : JAMI_INFO("=== End test %s ===", __FUNCTION__);
973 1 : }
974 :
975 : void
976 1 : AutoAnswerMediaNegoTest::audio_and_video_then_change_video_source()
977 : {
978 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
979 :
980 1 : configureScenario();
981 :
982 1 : MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
983 1 : defaultAudio.label_ = "audio_0";
984 1 : defaultAudio.enabled_ = true;
985 :
986 1 : MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
987 1 : defaultVideo.label_ = "video_0";
988 1 : defaultVideo.enabled_ = true;
989 :
990 1 : MediaAttribute audio(defaultAudio);
991 1 : MediaAttribute video(defaultVideo);
992 :
993 1 : TestScenario scenario;
994 : // First offer/answer
995 1 : scenario.offer_.emplace_back(audio);
996 1 : scenario.offer_.emplace_back(video);
997 1 : scenario.answer_.emplace_back(audio);
998 1 : scenario.answer_.emplace_back(video);
999 :
1000 : // Updated offer/answer
1001 1 : scenario.offerUpdate_.emplace_back(audio);
1002 : // Just change the media source to validate that a new
1003 : // media negotiation (re-invite) will be triggered.
1004 1 : video.sourceUri_ = "Fake source";
1005 1 : scenario.offerUpdate_.emplace_back(video);
1006 :
1007 1 : scenario.answerUpdate_.emplace_back(audio);
1008 1 : scenario.answerUpdate_.emplace_back(video);
1009 :
1010 1 : scenario.expectMediaRenegotiation_ = true;
1011 :
1012 1 : testWithScenario(aliceData_, bobData_, scenario);
1013 :
1014 1 : libjami::unregisterSignalHandlers();
1015 :
1016 1 : JAMI_INFO("=== End test %s ===", __FUNCTION__);
1017 1 : }
1018 :
1019 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestSip,
1020 : AutoAnswerMediaNegoTestSip::name());
1021 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestJami,
1022 : AutoAnswerMediaNegoTestJami::name());
1023 :
1024 : } // namespace test
1025 : } // namespace jami
1026 :
1027 5 : JAMI_TEST_RUNNER(jami::test::AutoAnswerMediaNegoTestJami::name())
|