multi-stream: report the list of negotiated media

Modify the CallSignal::MediaNegotiationStatus signal to report
the list of negotiated media in addition to the status event.

Gitlab: #564

Change-Id: I9ac77656f4f268317ddd87b085f1581ceac0b5b9
This commit is contained in:
Mohamed Chibani
2021-06-04 15:05:48 -04:00
committed by agsantos
parent 53d0ae39d1
commit d35d214bc5
9 changed files with 248 additions and 68 deletions

View File

@ -704,11 +704,14 @@
<signal name="mediaChangeRequested" tp:name-for-bindings="mediaChangeRequested">
<tp:added version="9.10.0"/>
<tp:docstring>
<p>Notify a media update on the current call.</p>
<p>This signal is emitted when a media update is received from the peer. The list
of media included in the request is reported. The client can control the media status
of the answer through the media attributes when when accepting the request.</p>
<tp:rationale>The client must subscribe to this signal to handle incoming calls.</tp:rationale>
<p>Report an incoming media change request.</p>
<p>This signal is emitted when a media change request is received from
the peer. The list of media included in the request is reported. The client
can control the attributes of the media to used in the answer when accepting
the request.</p>
<tp:rationale>The client must subscribe to this signal to handle the media
change requests.
</tp:rationale>
</tp:docstring>
<arg type="s" name="accountID">
<tp:docstring>
@ -1021,20 +1024,26 @@
</signal>
<signal name="mediaNegotiationStatus" tp:name-for-bindings="mediaNegotiationStatus">
<tp:added version="9.10.0"/>
<tp:added version="10.1.0"/>
<tp:docstring>
<p>Report mediation negotation status.</p>
<p>Report mediation negotiation status.</p>
</tp:docstring>
<arg type="s" name="callID" />
<arg type="s" name="event" >
<tp:docstring>
The acceptable states are:
<ul>
<li>NEGOTIATION_SUCCESS: Medias are ready</li>
<li>NEGOTIATION_FAIL: Medias negotion failed</li>
<li>NEGOTIATION_SUCCESS: Media negitiation succeded</li>
<li>NEGOTIATION_FAIL: Media negotiation failed</li>
</ul>
</tp:docstring>
</arg>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="VectorMapStringString"/>
<arg type="aa{ss}" name="mediaList">
<tp:docstring>
The updated attribute list of the negotiated media.
</tp:docstring>
</arg>
</signal>
</interface>
</node>

View File

@ -203,7 +203,7 @@ DBusClient::initLibrary(int flags)
exportable_callback<CallSignal::RemoteRecordingChanged>(
bind(&DBusCallManager::remoteRecordingChanged, callM, _1, _2, _3)),
exportable_callback<CallSignal::MediaNegotiationStatus>(
bind(&DBusCallManager::mediaNegotiationStatus, callM, _1, _2))};
bind(&DBusCallManager::mediaNegotiationStatus, callM, _1, _2, _3))};
// Configuration event handlers
const std::map<std::string, SharedCallback> configEvHandlers = {

View File

@ -53,7 +53,8 @@ public:
virtual void peerHold(const std::string& call_id, bool holding){}
virtual void connectionUpdate(const std::string& id, int state){}
virtual void remoteRecordingChanged(const std::string& call_id, const std::string& peer_number, bool state){}
virtual void mediaNegotiationStatus(const std::string& call_id, const std::string& event){}
virtual void mediaNegotiationStatus(const std::string& call_id, const std::string& event,
const std::vector<std::map<std::string, std::string>>& mediaList){}
};
@ -155,5 +156,6 @@ public:
virtual void peerHold(const std::string& call_id, bool holding){}
virtual void connectionUpdate(const std::string& id, int state){}
virtual void remoteRecordingChanged(const std::string& call_id, const std::string& peer_number, bool state){}
virtual void mediaNegotiationStatus(const std::string& call_id, const std::string& event){}
virtual void mediaNegotiationStatus(const std::string& call_id, const std::string& event,
const std::vector<std::map<std::string, std::string>>& mediaList){}
};

View File

@ -255,7 +255,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
exportable_callback<CallSignal::PeerHold>(bind(&Callback::peerHold, callM, _1, _2)),
exportable_callback<CallSignal::ConnectionUpdate>(bind(&Callback::connectionUpdate, callM, _1, _2)),
exportable_callback<CallSignal::RemoteRecordingChanged>(bind(&Callback::remoteRecordingChanged, callM, _1, _2, _3)),
exportable_callback<CallSignal::MediaNegotiationStatus>(bind(&Callback::mediaNegotiationStatus, callM, _1, _2))
exportable_callback<CallSignal::MediaNegotiationStatus>(bind(&Callback::mediaNegotiationStatus, callM, _1, _2, _3))
};
// Configuration event handlers

View File

@ -268,7 +268,9 @@ struct DRING_PUBLIC CallSignal
struct DRING_PUBLIC MediaNegotiationStatus
{
constexpr static const char* name = "MediaNegotiationStatus";
using cb_type = void(const std::string&, const std::string&);
using cb_type = void(const std::string&,
const std::string&,
const std::vector<std::map<std::string, std::string>>&);
};
};

View File

@ -1004,6 +1004,9 @@ Sdp::getMediaAttributeListFromSdp(const pjmedia_sdp_session* sdpSession)
std::vector<MediaAttribute> mediaList;
for (unsigned idx = 0; idx < sdpSession->media_count; idx++) {
unsigned audioIdx = 0;
unsigned videoIdx = 0;
mediaList.emplace_back(MediaAttribute {});
auto& mediaAttr = mediaList.back();
@ -1035,8 +1038,11 @@ Sdp::getMediaAttributeListFromSdp(const pjmedia_sdp_session* sdpSession)
// and the crypto materials are present.
mediaAttr.secure_ = transp == MediaTransport::RTP_SAVP and not getCrypto(media).empty();
mediaAttr.label_ = mediaAttr.type_ == MediaType::MEDIA_AUDIO ? "audio_" : "video_";
mediaAttr.label_ += std::to_string(idx);
if (mediaAttr.type_ == MediaType::MEDIA_AUDIO) {
mediaAttr.label_ = "audio_" + std::to_string(audioIdx++);
} else if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
mediaAttr.label_ = "video_" + std::to_string(videoIdx++);
}
}
return mediaList;

View File

@ -219,8 +219,9 @@ SIPCall::configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession,
const MediaDescription& localMedia,
const MediaDescription& remoteMedia)
{
assert(rtpSession);
if (not rtpSession)
throw std::runtime_error("Must have a valid Audio RTP Session");
throw std::runtime_error("Must have a valid RTP Session");
// Configure the media stream
auto new_mtu = transport_->getTlsMtu();
@ -1728,15 +1729,12 @@ SIPCall::isVideoMuted() const
}
void
SIPCall::startAllMedia()
SIPCall::updateNegotiatedMedia()
{
if (!transport_ or !sdp_)
return;
JAMI_WARN("[call:%s] startAllMedia()", getCallId().c_str());
if (isSecure() && not transport_->isSecure()) {
JAMI_ERR("[call:%s] Can't perform secure call over insecure SIP transport",
getCallId().c_str());
onFailure(EPROTONOSUPPORT);
JAMI_DBG("[call:%s] updating negotiated media", getCallId().c_str());
if (not transport_ or not sdp_) {
JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
return;
}
@ -1744,16 +1742,34 @@ SIPCall::startAllMedia()
bool peer_holding {true};
int streamIdx = -1;
// reset
readyToRecord_ = false;
resetMediaReady();
bool isVideoEnabled = false;
for (const auto& slot : slots) {
streamIdx++;
const auto& local = slot.first;
const auto& remote = slot.second;
// Skip disabled media
if (not local.enabled) {
JAMI_DBG("[call:%s] [SDP:slot#%u] The media is disabled, skipping",
getCallId().c_str(),
streamIdx);
continue;
}
if (static_cast<size_t>(streamIdx) >= rtpStreams_.size()) {
throw std::runtime_error("Stream index is out-of-range");
}
auto const& rtpStream = rtpStreams_[streamIdx];
if (not rtpStream.mediaAttribute_) {
throw std::runtime_error("Missing media attribute");
}
rtpStream.mediaAttribute_->enabled_ = local.enabled and remote.enabled;
if (not rtpStream.rtpSession_)
throw std::runtime_error("Must have a valid RTP Session");
if (local.type != MEDIA_AUDIO && local.type != MEDIA_VIDEO) {
JAMI_ERR("[call:%s] Unexpected media type %u", getCallId().c_str(), local.type);
throw std::runtime_error("Invalid media attribute");
@ -1766,49 +1782,38 @@ SIPCall::startAllMedia()
continue;
}
if (!local.codec) {
if (local.enabled and not local.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing local codec", getCallId().c_str(), streamIdx);
continue;
}
if (!remote.codec) {
if (remote.enabled and not remote.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing remote codec",
getCallId().c_str(),
streamIdx);
continue;
}
if (isSecure() && (not local.crypto || not remote.crypto)) {
JAMI_WARN(
"[call:%s] [SDP:slot#%u] Secure mode but no crypto attributes. Ignoring the media",
if (isSecure() and local.enabled and not local.crypto) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no local crypto attributes. "
"Ignoring the media",
getCallId().c_str(),
streamIdx);
continue;
}
auto const& rtpStream = rtpStreams_[streamIdx];
if (not rtpStream.mediaAttribute_) {
throw std::runtime_error("Missing media attribute");
}
// Configure the media.
rtpStream.mediaAttribute_->enabled_ = true;
if (rtpStream.mediaAttribute_->type_ == MEDIA_VIDEO)
isVideoEnabled = true;
configureRtpSession(rtpStream.rtpSession_, rtpStream.mediaAttribute_, local, remote);
// Not restarting media loop on hold as it's a huge waste of CPU ressources
// because of the audio loop
if (getState() != CallState::HOLD) {
if (isIceRunning()) {
// Create sockets for RTP and RTCP, and start the session.
auto compId = ICE_COMP_ID_RTP + streamIdx * ICE_COMP_COUNT_PER_STREAM;
rtpStream.rtpSession_->start(newIceSocket(compId), newIceSocket(compId + 1));
} else
rtpStream.rtpSession_->start(nullptr, nullptr);
if (isSecure() and remote.enabled and not remote.crypto) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no crypto remote attributes. "
"Ignoring the media",
getCallId().c_str(),
streamIdx);
continue;
}
// Aggregate holding info over all remote streams
peer_holding &= remote.onHold;
// Configure the media.
configureRtpSession(rtpStream.rtpSession_, rtpStream.mediaAttribute_, local, remote);
}
if (not isSubcall() and peerHolding_ != peer_holding) {
@ -1816,7 +1821,64 @@ SIPCall::startAllMedia()
emitSignal<DRing::CallSignal::PeerHold>(getCallId(), peerHolding_);
}
// Notify using the parent Id if it's a subcall.
auto callId = isSubcall() ? parent_->getCallId() : getCallId();
emitSignal<DRing::CallSignal::MediaNegotiationStatus>(
callId,
MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS,
MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()));
}
void
SIPCall::startAllMedia()
{
JAMI_DBG("[call:%s] starting the media", getCallId().c_str());
if (not transport_ or not sdp_) {
JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
return;
}
if (isSecure() && not transport_->isSecure()) {
JAMI_ERR("[call:%s] Can't perform secure call over insecure SIP transport",
getCallId().c_str());
onFailure(EPROTONOSUPPORT);
return;
}
// reset
readyToRecord_ = false;
resetMediaReady();
#ifdef ENABLE_VIDEO
bool isVideoEnabled = false;
#endif
for (auto iter = rtpStreams_.begin(); iter != rtpStreams_.end(); iter++) {
if (not iter->mediaAttribute_) {
throw std::runtime_error("Missing media attribute");
}
#ifdef ENABLE_VIDEO
if (iter->mediaAttribute_->type_ == MEDIA_VIDEO)
isVideoEnabled = true;
#endif
// Not restarting media loop on hold as it's a huge waste of CPU ressources
// because of the audio loop
if (getState() != CallState::HOLD) {
if (isIceRunning()) {
// Create sockets for RTP and RTCP, and start the session.
auto streamIdx = std::distance(rtpStreams_.begin(), iter);
auto compId = ICE_COMP_ID_RTP + streamIdx * ICE_COMP_COUNT_PER_STREAM;
iter->rtpSession_->start(newIceSocket(compId), newIceSocket(compId + 1));
} else {
iter->rtpSession_->start(nullptr, nullptr);
}
}
}
#ifdef ENABLE_VIDEO
// TODO. Move this elsewhere (when adding participant to conf?)
if (!isVideoEnabled && !getConfId().empty()) {
auto conference = Manager::instance().getConferenceFromID(getConfId());
conference->attachVideo(getReceiveVideoFrameActiveWriter().get(), getCallId());
@ -2100,21 +2162,22 @@ SIPCall::onMediaNegotiationComplete()
// to a negotiated transport.
runOnMainThread([w = weak()] {
if (auto this_ = w.lock()) {
// Notify using the parent Id if it's a subcall.
auto callId = this_->isSubcall() ? this_->parent_->getCallId() : this_->getCallId();
emitSignal<DRing::CallSignal::MediaNegotiationStatus>(
callId, MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS);
std::lock_guard<std::recursive_mutex> lk {this_->callMutex_};
JAMI_WARN("[call:%s] media changed", this_->getCallId().c_str());
// The call is already ended, so we don't need to restart medias
if (not this_->inviteSession_
or this_->inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED or not this_->sdp_)
return;
// If ICE is not used, start medias now
// If ICE is not used, start media now
auto rem_ice_attrs = this_->sdp_->getIceAttributes();
if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
JAMI_WARN("[call:%s] no remote ICE for media", this_->getCallId().c_str());
JAMI_DBG("[call:%s] No ICE, starting media using default ",
this_->getCallId().c_str());
// Update the negotiated media.
this_->updateNegotiatedMedia();
// Start the media.
this_->stopAllMedia();
this_->startAllMedia();
return;
@ -2172,6 +2235,10 @@ SIPCall::onIceNegoSucceed()
// can stop a call. So do not start medias
if (not inviteSession_ or inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED or not sdp_)
return;
// Update the negotiated media.
updateNegotiatedMedia();
// Nego succeed: move to the new media transport
stopAllMedia();
{

View File

@ -335,7 +335,7 @@ private:
void setCallMediaLocal();
void startIceMedia();
void onIceNegoSucceed();
void updateNegotiatedMedia();
void startAllMedia();
void stopAllMedia();

View File

@ -82,6 +82,7 @@ struct CallData
std::string userName_ {};
std::string alias_ {};
std::string callId_ {};
bool enableMultiStream_ {true};
std::vector<Signal> signals_;
std::condition_variable cv_ {};
std::mutex mtx_;
@ -111,11 +112,13 @@ private:
void audio_and_video_then_mute_video();
void audio_only_then_add_video();
void audio_and_video_then_mute_audio();
void audio_only_then_add_video_but_peer_disabled_multistream();
CPPUNIT_TEST_SUITE(MediaNegotiationTest);
CPPUNIT_TEST(audio_and_video_then_mute_video);
CPPUNIT_TEST(audio_only_then_add_video);
CPPUNIT_TEST(audio_and_video_then_mute_audio);
CPPUNIT_TEST(audio_only_then_add_video_but_peer_disabled_multistream);
CPPUNIT_TEST_SUITE_END();
// Event/Signal handlers
@ -126,6 +129,10 @@ private:
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,
@ -253,6 +260,36 @@ MediaNegotiationTest::onIncomingCallWithMedia(const std::string& accountId,
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,
@ -448,6 +485,7 @@ MediaNegotiationTest::configureScenario(CallData& aliceData, CallData& bobData)
auto const& account = Manager::instance().getAccount<JamiAccount>(aliceData.accountId_);
aliceData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
aliceData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
account->enableMultiStream(aliceData.enableMultiStream_);
}
{
@ -455,6 +493,7 @@ MediaNegotiationTest::configureScenario(CallData& aliceData, CallData& bobData)
auto const& account = Manager::instance().getAccount<JamiAccount>(bobData.accountId_);
bobData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
bobData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
account->enableMultiStream(bobData.enableMultiStream_);
}
std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> signalHandlers;
@ -473,6 +512,14 @@ MediaNegotiationTest::configureScenario(CallData& aliceData, CallData& bobData)
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,
@ -500,7 +547,9 @@ MediaNegotiationTest::configureScenario(CallData& aliceData, CallData& bobData)
}));
signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaNegotiationStatus>(
[&](const std::string& callId, const std::string& event) {
[&](const std::string& callId,
const std::string& event,
const std::vector<std::map<std::string, std::string>>& mediaList) {
auto user = getUserAlias(callId);
if (not user.empty())
onMediaNegotiationStatus(callId,
@ -533,8 +582,12 @@ MediaNegotiationTest::testWithScenario(CallData& aliceData,
bobData.accountId_.c_str());
// Wait for incoming call signal.
CPPUNIT_ASSERT_EQUAL(true,
waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name));
if (bobData.enableMultiStream_) {
CPPUNIT_ASSERT(waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name));
} else {
CPPUNIT_ASSERT(waitForSignal(bobData, DRing::CallSignal::IncomingCall::name));
}
// Answer the call.
{
auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answer_);
@ -786,6 +839,47 @@ MediaNegotiationTest::audio_and_video_then_mute_audio()
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
void
MediaNegotiationTest::audio_only_then_add_video_but_peer_disabled_multistream()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
// Disable multi-stream on Bob's side
bobData_.enableMultiStream_ = false;
configureScenario(aliceData_, bobData_);
MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
defaultAudio.label_ = "main audio";
MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
defaultVideo.label_ = "main video";
{
MediaAttribute audio(defaultAudio);
MediaAttribute video(defaultVideo);
TestScenario scenario;
// First offer/answer
scenario.offer_.emplace_back(audio);
scenario.answer_.emplace_back(audio);
// Updated offer/answer
scenario.offerUpdate_.emplace_back(audio);
scenario.offerUpdate_.emplace_back(video);
scenario.answerUpdate_.emplace_back(audio);
scenario.answerUpdate_.emplace_back(video);
scenario.expectMediaRenegotiation_ = false;
scenario.expectMediaChangeRequest_ = false;
testWithScenario(aliceData_, bobData_, scenario);
}
DRing::unregisterSignalHandlers();
JAMI_INFO("=== End test %s ===", __FUNCTION__);
}
} // namespace test
} // namespace jami