summary refs log tree commit diff
path: root/src/voip/CallManager.cpp
diff options
context:
space:
mode:
authorRohit Sutradhar <rohitsutradhar311@gmail.com>2022-10-14 19:19:05 +0530
committerGitHub <noreply@github.com>2022-10-14 13:49:05 +0000
commitac48c332867e773e0e0eb9ad0139b7b625e26851 (patch)
tree9f4799c650889bb770f72e127441426c9ae42a07 /src/voip/CallManager.cpp
parentAdd toggle to disable decrypting notifications (diff)
downloadnheko-ac48c332867e773e0e0eb9ad0139b7b625e26851.tar.xz
VoIP v1 implementation (#1161)
* Initial commit for VoIP v1 implementation

* Added draft of event handlers for voip methods

* Added event handlers for VoIP events, added rejectCall, added version tracking for call version for V0 and V1 compatibility

* Added call events to the general message pipeline. Modified Call Reject mechanism

* Added message delegates for new events. Modified hidden events. Updated handle events.

* Updated implementation to keep track of calls on other devices

* Fixed linting

* Fixed code warnings

* Fixed minor bugs

* fixed ci

* Added acceptNegotiation method definition when missing gstreamer

* Fixed warnings

* Fixed linting
Diffstat (limited to 'src/voip/CallManager.cpp')
-rw-r--r--src/voip/CallManager.cpp402
1 files changed, 343 insertions, 59 deletions
diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp

index 14f3adf8..5e711499 100644 --- a/src/voip/CallManager.cpp +++ b/src/voip/CallManager.cpp
@@ -24,6 +24,10 @@ #include "mtx/responses/turn_server.hpp" +/* + * Select Answer when one instance of the client supports v0 + */ + #ifdef XCB_AVAILABLE #include <xcb/xcb.h> #include <xcb/xcb_ewmh.h> @@ -67,10 +71,15 @@ CallManager::CallManager(QObject *parent) this, [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_); - emit newMessage( - roomid_, - CallInvite{callid_, partyid_, SDO{sdp, SDO::Type::Offer}, "0", timeoutms_, invitee_}); - emit newMessage(roomid_, CallCandidates{callid_, partyid_, candidates, "0"}); + emit newMessage(roomid_, + CallInvite{callid_, + partyid_, + SDO{sdp, SDO::Type::Offer}, + callPartyVersion_, + timeoutms_, + invitee_}); + emit newMessage(roomid_, + CallCandidates{callid_, partyid_, candidates, callPartyVersion_}); std::string callid(callid_); QTimer::singleShot(timeoutms_, this, [this, callid]() { if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) { @@ -87,8 +96,10 @@ CallManager::CallManager(QObject *parent) this, [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_); - emit newMessage(roomid_, CallAnswer{callid_, partyid_, "0", SDO{sdp, SDO::Type::Answer}}); - emit newMessage(roomid_, CallCandidates{callid_, partyid_, candidates, "0"}); + emit newMessage( + roomid_, CallAnswer{callid_, partyid_, callPartyVersion_, SDO{sdp, SDO::Type::Answer}}); + emit newMessage(roomid_, + CallCandidates{callid_, partyid_, candidates, callPartyVersion_}); }); connect(&session_, @@ -96,7 +107,8 @@ CallManager::CallManager(QObject *parent) this, [this](const CallCandidates::Candidate &candidate) { nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_); - emit newMessage(roomid_, CallCandidates{callid_, partyid_, {candidate}, "0"}); + emit newMessage(roomid_, + CallCandidates{callid_, partyid_, {candidate}, callPartyVersion_}); }); connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); @@ -170,23 +182,14 @@ CallManager::CallManager(QObject *parent) void CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex) { - if (isOnCall()) + if (isOnCall() || isOnCallOnOtherDevice()) { + if (isOnCallOnOtherDevice_ != "") + emit ChatPage::instance()->showNotification( + QStringLiteral("User is already in a call")); return; - if (callType == CallType::SCREEN) { - if (!screenShareSupported()) - return; - if (windows_.empty() || windowIndex >= windows_.size()) { - nhlog::ui()->error("WebRTC: window index out of range"); - return; - } } auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); - if (roomInfo.member_count != 2) { - emit ChatPage::instance()->showNotification( - QStringLiteral("Calls are limited to 1:1 rooms.")); - return; - } std::string errorMessage; if (!session_.havePlugins(false, &errorMessage) || @@ -198,17 +201,60 @@ CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int w callType_ = callType; roomid_ = roomid; - session_.setTurnServers(turnURIs_); generateCallID(); + std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); + const RoomMember *callee; + if (roomInfo.member_count == 1) + callee = &members.front(); + else if (roomInfo.member_count == 2) + callee = members.front().user_id == utils::localUser() ? &members.back() : &members.front(); + else { + emit ChatPage::instance()->showNotification( + QStringLiteral("Calls are limited to rooms with less than two members")); + return; + } + + if (callType == CallType::SCREEN) { + if (!screenShareSupported()) + return; + if (windows_.empty() || windowIndex >= windows_.size()) { + nhlog::ui()->error("WebRTC: window index out of range"); + return; + } + } + + if (haveCallInvite_) { + nhlog::ui()->debug( + "WebRTC: Discarding outbound call for inbound call. localUser is polite party"); + if (callParty_ == callee->user_id) { + if (callType == callType_) + acceptInvite(); + else { + emit ChatPage::instance()->showNotification( + QStringLiteral("Can't place call. Call types do not match")); + emit newMessage( + roomid_, + CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy}); + } + } else { + emit ChatPage::instance()->showNotification( + QStringLiteral("Already on a call with a different user")); + emit newMessage( + roomid_, + CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy}); + } + return; + } + + session_.setTurnServers(turnURIs_); std::string strCallType = callType_ == CallType::VOICE ? "voice" : (callType_ == CallType::VIDEO ? "video" : "screen"); + nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType); - std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); - const RoomMember &callee = - members.front().user_id == utils::localUser() ? members.back() : members.front(); - callParty_ = callee.user_id; - callPartyDisplayName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name; + callParty_ = callee->user_id; + callPartyDisplayName_ = callee->display_name.isEmpty() ? callee->user_id : callee->display_name; callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + invitee_ = callParty_.toStdString(); emit newInviteState(); playRingtone(QUrl(QStringLiteral("qrc:/media/media/ringback.ogg")), true); if (!session_.createOffer(callType, @@ -249,7 +295,7 @@ CallManager::hangUp(CallHangUp::Reason reason) if (!callid_.empty()) { nhlog::ui()->debug( "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason)); - emit newMessage(roomid_, CallHangUp{callid_, partyid_, "0", reason}); + emit newMessage(roomid_, CallHangUp{callid_, partyid_, callPartyVersion_, reason}); endCall(); } } @@ -259,7 +305,9 @@ CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) { #ifdef GSTREAMER_AVAILABLE if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) || - handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event)) + handleEvent<CallNegotiate>(event) || handleEvent<CallSelectAnswer>(event) || + handleEvent<CallAnswer>(event) || handleEvent<CallReject>(event) || + handleEvent<CallHangUp>(event)) return; #else (void)event; @@ -289,41 +337,121 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) [](unsigned char c1, unsigned char c2) { return std::tolower(c1) == std::tolower(c2); }) != sdp.cend(); - - nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}", + nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from ({},{}) ", callInviteEvent.content.call_id, (isVideo ? "video" : "voice"), - callInviteEvent.sender); + callInviteEvent.sender, + callInviteEvent.content.party_id); if (callInviteEvent.content.call_id.empty()) return; - auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); - if (isOnCall() || roomInfo.member_count != 2) { - emit newMessage( - QString::fromStdString(callInviteEvent.room_id), - CallHangUp{ - callInviteEvent.content.call_id, partyid_, "0", CallHangUp::Reason::InviteTimeOut}); - return; + if (callInviteEvent.sender == utils::localUser().toStdString()) { + if (callInviteEvent.content.party_id == partyid_) + return; + else { + if (callInviteEvent.content.invitee != utils::localUser().toStdString()) { + isOnCallOnOtherDevice_ = callInviteEvent.content.call_id; + emit newCallDeviceState(); + nhlog::ui()->debug("WebRTC: User is on call on other device."); + return; + } + } } + auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); + callPartyVersion_ = callInviteEvent.content.version; + const QString &ringtone = UserSettings::instance()->ringtone(); - if (ringtone != QLatin1String("Mute")) + bool sharesRoom = true; + + std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); + const RoomMember &caller = + *std::find_if(members.begin(), members.end(), [&](RoomMember member) { + return member.user_id.toStdString() == callInviteEvent.sender; + }); + if (isOnCall() || isOnCallOnOtherDevice()) { + if (isOnCallOnOtherDevice_ != "") + return; + if (callParty_.toStdString() == callInviteEvent.sender) { + if (session_.state() == webrtc::State::OFFERSENT) { + if (callid_ > callInviteEvent.content.call_id) { + endCall(); + callParty_ = caller.user_id; + callPartyDisplayName_ = + caller.display_name.isEmpty() ? caller.user_id : caller.display_name; + callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + + roomid_ = QString::fromStdString(callInviteEvent.room_id); + callid_ = callInviteEvent.content.call_id; + remoteICECandidates_.clear(); + haveCallInvite_ = true; + callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; + inviteSDP_ = callInviteEvent.content.offer.sdp; + emit newInviteState(); + acceptInvite(); + } + return; + } else if (session_.state() < webrtc::State::OFFERSENT) + endCall(); + else + return; + } else + return; + } + + if (callPartyVersion_ == "0") { + if (roomInfo.member_count != 2) { + emit newMessage(QString::fromStdString(callInviteEvent.room_id), + CallHangUp{callInviteEvent.content.call_id, + partyid_, + callPartyVersion_, + CallHangUp::Reason::InviteTimeOut}); + return; + } + } else { + if (caller.user_id == utils::localUser() && + callInviteEvent.content.party_id == partyid_) // remote echo + return; + + if (roomInfo.member_count == 2 || // general call in room with two members + (roomInfo.member_count == 1 && + partyid_ != + callInviteEvent.content.party_id) || // self call, ring if not the same party_id + callInviteEvent.content.invitee == "" || // empty, meant for everyone + callInviteEvent.content.invitee == + utils::localUser().toStdString()) // meant specifically for local user + { + if (roomInfo.member_count > 2) { + // check if shares room + sharesRoom = checkSharesRoom(QString::fromStdString(callInviteEvent.room_id), + callInviteEvent.content.invitee); + } + } else { + emit newMessage(QString::fromStdString(callInviteEvent.room_id), + CallHangUp{callInviteEvent.content.call_id, + partyid_, + callPartyVersion_, + CallHangUp::Reason::InviteTimeOut}); + return; + } + } + + // ring if not mute or does not have direct message room + if (ringtone != QLatin1String("Mute") && sharesRoom) playRingtone(ringtone == QLatin1String("Default") ? QUrl(QStringLiteral("qrc:/media/media/ring.ogg")) : QUrl::fromLocalFile(ringtone), true); - roomid_ = QString::fromStdString(callInviteEvent.room_id); - callid_ = callInviteEvent.content.call_id; - remoteICECandidates_.clear(); - std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); - const RoomMember &caller = - members.front().user_id == utils::localUser() ? members.back() : members.front(); callParty_ = caller.user_id; callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name; callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + roomid_ = QString::fromStdString(callInviteEvent.room_id); + callid_ = callInviteEvent.content.call_id; + remoteICECandidates_.clear(); + haveCallInvite_ = true; callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; inviteSDP_ = callInviteEvent.content.offer.sdp; @@ -333,6 +461,8 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) void CallManager::acceptInvite() { + // if call was accepted/rejected elsewhere and m.call.select_answer is received + // before acceptInvite if (!haveCallInvite_) return; @@ -341,7 +471,7 @@ CallManager::acceptInvite() if (!session_.havePlugins(false, &errorMessage) || (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) { emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); - hangUp(); + hangUp(CallHangUp::Reason::UserMediaFailed); return; } @@ -358,14 +488,30 @@ CallManager::acceptInvite() } void +CallManager::rejectInvite() +{ + if (callPartyVersion_ == "0") { + hangUp(); + // send m.call.reject after sending hangup as mentioned in MSC2746 + emit newMessage(roomid_, CallReject{callid_, partyid_, callPartyVersion_}); + } + if (!callid_.empty()) { + nhlog::ui()->debug("WebRTC: call id: {} - rejecting call", callid_); + emit newMessage(roomid_, CallReject{callid_, partyid_, callPartyVersion_}); + endCall(false); + } +} + +void CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) { - if (callCandidatesEvent.sender == utils::localUser().toStdString()) + if (callCandidatesEvent.sender == utils::localUser().toStdString() && + callCandidatesEvent.content.party_id == partyid_) return; - - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}", + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from ({}, {})", callCandidatesEvent.content.call_id, - callCandidatesEvent.sender); + callCandidatesEvent.sender, + callCandidatesEvent.content.party_id); if (callid_ == callCandidatesEvent.content.call_id) { if (isOnCall()) @@ -382,20 +528,31 @@ CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) void CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) { - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}", + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from ({}, {})", callAnswerEvent.content.call_id, - callAnswerEvent.sender); + callAnswerEvent.sender, + callAnswerEvent.content.party_id); + if (answerSelected_) + return; if (callAnswerEvent.sender == utils::localUser().toStdString() && callid_ == callAnswerEvent.content.call_id) { + if (partyid_ == callAnswerEvent.content.party_id) + return; + if (!isOnCall()) { emit ChatPage::instance()->showNotification( QStringLiteral("Call answered on another device.")); stopRingtone(); haveCallInvite_ = false; + if (callPartyVersion_ != "1") { + isOnCallOnOtherDevice_ = callid_; + emit newCallDeviceState(); + } emit newInviteState(); } - return; + if (callParty_ != utils::localUser()) + return; } if (isOnCall() && callid_ == callAnswerEvent.content.call_id) { @@ -405,21 +562,142 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) hangUp(); } } + emit newMessage( + roomid_, + CallSelectAnswer{callid_, partyid_, callPartyVersion_, callAnswerEvent.content.party_id}); + selectedpartyid_ = callAnswerEvent.content.party_id; + answerSelected_ = true; } void CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent) { - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}", + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from ({}, {})", callHangUpEvent.content.call_id, callHangUpReasonString(callHangUpEvent.content.reason), - callHangUpEvent.sender); + callHangUpEvent.sender, + callHangUpEvent.content.party_id); - if (callid_ == callHangUpEvent.content.call_id) + if (callid_ == callHangUpEvent.content.call_id || + isOnCallOnOtherDevice_ == callHangUpEvent.content.call_id) endCall(); } void +CallManager::handleEvent(const RoomEvent<CallSelectAnswer> &callSelectAnswerEvent) +{ + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallSelectAnswer from ({}, {})", + callSelectAnswerEvent.content.call_id, + callSelectAnswerEvent.sender, + callSelectAnswerEvent.content.party_id); + if (callSelectAnswerEvent.sender == utils::localUser().toStdString()) { + if (callSelectAnswerEvent.content.party_id != partyid_) { + if (std::find(rejectCallPartyIDs_.begin(), + rejectCallPartyIDs_.begin(), + callSelectAnswerEvent.content.selected_party_id) != + rejectCallPartyIDs_.end()) + endCall(); + else { + if (callSelectAnswerEvent.content.selected_party_id == partyid_) + return; + nhlog::ui()->debug("WebRTC: call id: {} - user is on call with this user!", + callSelectAnswerEvent.content.call_id); + isOnCallOnOtherDevice_ = callSelectAnswerEvent.content.call_id; + emit newCallDeviceState(); + } + } + return; + } else if (callid_ == callSelectAnswerEvent.content.call_id) { + if (callSelectAnswerEvent.content.selected_party_id != partyid_) { + bool endAllCalls = false; + if (std::find(rejectCallPartyIDs_.begin(), + rejectCallPartyIDs_.begin(), + callSelectAnswerEvent.content.selected_party_id) != + rejectCallPartyIDs_.end()) + endAllCalls = true; + else { + isOnCallOnOtherDevice_ = callid_; + emit newCallDeviceState(); + } + endCall(endAllCalls); + } else if (session_.state() == webrtc::State::DISCONNECTED) + endCall(); + } +} + +void +CallManager::handleEvent(const RoomEvent<CallReject> &callRejectEvent) +{ + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallReject from ({}, {})", + callRejectEvent.content.call_id, + callRejectEvent.sender, + callRejectEvent.content.party_id); + if (answerSelected_) + return; + + rejectCallPartyIDs_.push_back(callRejectEvent.content.party_id); + // check remote echo + if (callRejectEvent.sender == utils::localUser().toStdString()) { + if (callRejectEvent.content.party_id != partyid_ && callParty_ != utils::localUser()) + emit ChatPage::instance()->showNotification( + QStringLiteral("Call rejected on another device.")); + endCall(); + return; + } + + if (callRejectEvent.content.call_id == callid_) { + if (session_.state() == webrtc::State::OFFERSENT) { + // only accept reject if webrtc is in OFFERSENT state, else call has been accepted + emit newMessage( + roomid_, + CallSelectAnswer{ + callid_, partyid_, callPartyVersion_, callRejectEvent.content.party_id}); + endCall(); + } + } +} + +void +CallManager::handleEvent(const RoomEvent<CallNegotiate> &callNegotiateEvent) +{ + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallNegotiate from ({}, {})", + callNegotiateEvent.content.call_id, + callNegotiateEvent.sender, + callNegotiateEvent.content.party_id); + + std::string negotiationSDP_ = callNegotiateEvent.content.description.sdp; + if (!session_.acceptNegotiation(negotiationSDP_)) { + emit ChatPage::instance()->showNotification(QStringLiteral("Problem accepting new SDP")); + hangUp(); + return; + } + session_.acceptICECandidates(remoteICECandidates_); + remoteICECandidates_.clear(); +} + +bool +CallManager::checkSharesRoom(QString roomid, std::string invitee) const +{ + /* + IMPLEMENTATION REQUIRED + Check if room is shared to determine whether to ring or not. + Called from handle callInvite event + */ + if (roomid.toStdString() != "") { + if (invitee == "") { + // check all members + return true; + } else { + return true; + // check if invitee shares a direct room with local user + } + return true; + } + + return true; +} + +void CallManager::toggleMicMute() { session_.toggleMicMute(); @@ -467,7 +745,7 @@ CallManager::generateCallID() } void -CallManager::clear() +CallManager::clear(bool endAllCalls) { roomid_.clear(); callParty_.clear(); @@ -476,17 +754,23 @@ CallManager::clear() callid_.clear(); callType_ = CallType::VOICE; haveCallInvite_ = false; + answerSelected_ = false; + if (endAllCalls) { + isOnCallOnOtherDevice_ = ""; + rejectCallPartyIDs_.clear(); + emit newCallDeviceState(); + } emit newInviteState(); inviteSDP_.clear(); remoteICECandidates_.clear(); } void -CallManager::endCall() +CallManager::endCall(bool endAllCalls) { stopRingtone(); session_.end(); - clear(); + clear(endAllCalls); } void