diff options
author | Rohit Sutradhar <rohitsutradhar311@gmail.com> | 2022-10-14 19:19:05 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-14 13:49:05 +0000 |
commit | ac48c332867e773e0e0eb9ad0139b7b625e26851 (patch) | |
tree | 9f4799c650889bb770f72e127441426c9ae42a07 /src | |
parent | Add toggle to disable decrypting notifications (diff) | |
download | nheko-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')
-rw-r--r-- | src/Cache.cpp | 17 | ||||
-rw-r--r-- | src/ChatPage.cpp | 3 | ||||
-rw-r--r-- | src/PowerlevelsEditModels.cpp | 2 | ||||
-rw-r--r-- | src/Utils.cpp | 3 | ||||
-rw-r--r-- | src/Utils.h | 7 | ||||
-rw-r--r-- | src/timeline/TimelineModel.cpp | 65 | ||||
-rw-r--r-- | src/timeline/TimelineModel.h | 6 | ||||
-rw-r--r-- | src/timeline/TimelineViewManager.cpp | 22 | ||||
-rw-r--r-- | src/timeline/TimelineViewManager.h | 3 | ||||
-rw-r--r-- | src/ui/HiddenEvents.cpp | 11 | ||||
-rw-r--r-- | src/voip/CallManager.cpp | 402 | ||||
-rw-r--r-- | src/voip/CallManager.h | 37 | ||||
-rw-r--r-- | src/voip/WebRTCSession.cpp | 15 | ||||
-rw-r--r-- | src/voip/WebRTCSession.h | 1 |
14 files changed, 521 insertions, 73 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp index 863f0683..a83b73f7 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -184,8 +184,18 @@ Cache::isHiddenEvent(lmdb::txn &txn, hiddenEvents.hidden_event_types = std::vector{ EventType::Reaction, EventType::CallCandidates, + EventType::CallNegotiate, EventType::Unsupported, }; + // check if selected answer is from to local user + /* + * localUser accepts/rejects the call and it is selected by caller - No message + * Another User accepts/rejects the call and it is selected by caller - "Call answered/rejected + * elsewhere" + */ + bool callLocalUser_ = true; + if (callLocalUser_) + hiddenEvents.hidden_event_types->push_back(EventType::CallSelectAnswer); if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) { auto h = std::get< @@ -1661,11 +1671,18 @@ isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &) { return true; } + auto isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &) { return true; } + +// auto +// isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &) +// { +// return true; +// } } void diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 3736ec6b..756ef425 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -363,6 +363,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent) connectCallMessage<mtx::events::voip::CallCandidates>(); connectCallMessage<mtx::events::voip::CallAnswer>(); connectCallMessage<mtx::events::voip::CallHangUp>(); + connectCallMessage<mtx::events::voip::CallSelectAnswer>(); + connectCallMessage<mtx::events::voip::CallReject>(); + connectCallMessage<mtx::events::voip::CallNegotiate>(); } void diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp index 8cc2dcc0..2c2d4a7f 100644 --- a/src/PowerlevelsEditModels.cpp +++ b/src/PowerlevelsEditModels.cpp @@ -222,6 +222,8 @@ PowerlevelsTypeListModel::data(const QModelIndex &index, int role) const return tr("Answer a call"); else if (type.type == "m.call.hangup") return tr("Hang up a call"); + else if (type.type == "m.call.reject") + return tr("Reject a call"); else if (type.type == "im.ponies.room_emotes") return tr("Change the room emotes"); return QString::fromStdString(type.type); diff --git a/src/Utils.cpp b/src/Utils.cpp index cedf537a..b92c6cce 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -219,6 +219,7 @@ utils::getMessageDescription(const TimelineEvent &event, using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>; using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>; using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>; + using CallReject = mtx::events::RoomEvent<mtx::events::voip::CallReject>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; if (std::holds_alternative<Audio>(event)) { @@ -241,6 +242,8 @@ utils::getMessageDescription(const TimelineEvent &event, return createDescriptionInfo<CallAnswer>(event, localUser, displayName); } else if (std::holds_alternative<CallHangUp>(event)) { return createDescriptionInfo<CallHangUp>(event, localUser, displayName); + } else if (std::holds_alternative<CallReject>(event)) { + return createDescriptionInfo<CallReject>(event, localUser, displayName); } else if (std::holds_alternative<mtx::events::Sticker>(event)) { return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName); } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) { diff --git a/src/Utils.h b/src/Utils.h index 8f8b9cad..e4c3ccb3 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -110,6 +110,7 @@ messageDescription(const QString &username = QString(), using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>; using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>; using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>; + using CallReject = mtx::events::RoomEvent<mtx::events::voip::CallReject>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; if (std::is_same<T, Audio>::value) { @@ -185,6 +186,12 @@ messageDescription(const QString &username = QString(), else return QCoreApplication::translate("message-description sent:", "%1 ended a call") .arg(username); + } else if (std::is_same<T, CallReject>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You rejected a call"); + else + return QCoreApplication::translate("message-description sent:", "%1 rejected a call") + .arg(username); } else { return QCoreApplication::translate("utils", "Unknown Message Type"); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 0e726bde..6aa81d8b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -138,6 +138,20 @@ struct RoomEventType { return qml_mtx_events::EventType::CallCandidates; } + qml_mtx_events::EventType + operator()(const mtx::events::Event<mtx::events::voip::CallSelectAnswer> &) + { + return qml_mtx_events::EventType::CallSelectAnswer; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::voip::CallReject> &) + { + return qml_mtx_events::EventType::CallReject; + } + qml_mtx_events::EventType + operator()(const mtx::events::Event<mtx::events::voip::CallNegotiate> &) + { + return qml_mtx_events::EventType::CallNegotiate; + } // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return // ::EventType::LocationMessage; } }; @@ -258,6 +272,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) /// m.call.candidates case qml_mtx_events::CallCandidates: return mtx::events::EventType::CallCandidates; + /// m.call.select_answer + case qml_mtx_events::CallSelectAnswer: + return mtx::events::EventType::CallSelectAnswer; + /// m.call.reject + case qml_mtx_events::CallReject: + return mtx::events::EventType::CallReject; + /// m.call.negotiate + case qml_mtx_events::CallNegotiate: + return mtx::events::EventType::CallNegotiate; /// m.room.canonical_alias case qml_mtx_events::CanonicalAlias: return mtx::events::EventType::RoomCanonicalAlias; @@ -922,16 +945,22 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) } if (std::holds_alternative<RoomEvent<voip::CallCandidates>>(e) || + std::holds_alternative<RoomEvent<voip::CallNegotiate>>(e) || std::holds_alternative<RoomEvent<voip::CallInvite>>(e) || std::holds_alternative<RoomEvent<voip::CallAnswer>>(e) || + std::holds_alternative<RoomEvent<voip::CallSelectAnswer>>(e) || + std::holds_alternative<RoomEvent<voip::CallReject>>(e) || std::holds_alternative<RoomEvent<voip::CallHangUp>>(e)) std::visit( [this](auto &event) { event.room_id = room_id_.toStdString(); - if constexpr (std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<voip::CallAnswer>> || - std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<voip::CallHangUp>>) + if constexpr ( + std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallAnswer>> || + std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallInvite>> || + std::is_same_v<std::decay_t<decltype(event)>, + RoomEvent<voip::CallSelectAnswer>> || + std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallReject>> || + std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallHangUp>>) emit newCallEvent(event); else { if (event.sender != http::client()->user_id().to_string()) @@ -1007,6 +1036,17 @@ isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &) return true; } +auto +isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &) +{ + return true; +} +auto +isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &) +{ + return true; +} + // Workaround. We also want to see a room at the top, if we just joined it auto isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e) @@ -1503,6 +1543,23 @@ struct SendMessageVisitor sendRoomEvent<mtx::events::voip::CallHangUp, mtx::events::EventType::CallHangUp>(event); } + void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &event) + { + sendRoomEvent<mtx::events::voip::CallSelectAnswer, + mtx::events::EventType::CallSelectAnswer>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &event) + { + sendRoomEvent<mtx::events::voip::CallReject, mtx::events::EventType::CallReject>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &event) + { + sendRoomEvent<mtx::events::voip::CallNegotiate, mtx::events::EventType::CallNegotiate>( + event); + } + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) { sendRoomEvent<mtx::events::msg::KeyVerificationRequest, diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index a4904f4f..2a04c9c9 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -58,6 +58,12 @@ enum EventType CallHangUp, /// m.call.candidates CallCandidates, + /// m.call.select_answer + CallSelectAnswer, + /// m.call.reject + CallReject, + /// m.call.negotiate + CallNegotiate, /// m.room.canonical_alias CanonicalAlias, /// m.room.create diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index a75a79d1..9c46d201 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -378,6 +378,28 @@ TimelineViewManager::queueCallMessage(const QString &roomid, } void +TimelineViewManager::queueCallMessage(const QString &roomid, + const mtx::events::voip::CallSelectAnswer &callSelectAnswer) +{ + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callSelectAnswer, mtx::events::EventType::CallSelectAnswer); +} +void +TimelineViewManager::queueCallMessage(const QString &roomid, + const mtx::events::voip::CallReject &callReject) +{ + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callReject, mtx::events::EventType::CallReject); +} +void +TimelineViewManager::queueCallMessage(const QString &roomid, + const mtx::events::voip::CallNegotiate &callNegotiate) +{ + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callNegotiate, mtx::events::EventType::CallNegotiate); +} + +void TimelineViewManager::focusMessageInput() { emit focusInput(); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index c0895b2c..c305fe66 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -109,6 +109,9 @@ public slots: void queueCallMessage(const QString &roomid, const mtx::events::voip::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::voip::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::voip::CallHangUp &); + void queueCallMessage(const QString &roomid, const mtx::events::voip::CallSelectAnswer &); + void queueCallMessage(const QString &roomid, const mtx::events::voip::CallReject &); + void queueCallMessage(const QString &roomid, const mtx::events::voip::CallNegotiate &); void setVideoCallItem(); diff --git a/src/ui/HiddenEvents.cpp b/src/ui/HiddenEvents.cpp index 1ebcda3e..4686dfef 100644 --- a/src/ui/HiddenEvents.cpp +++ b/src/ui/HiddenEvents.cpp @@ -17,9 +17,20 @@ HiddenEvents::load() hiddenEvents.hidden_event_types = std::vector{ EventType::Reaction, EventType::CallCandidates, + EventType::CallNegotiate, EventType::Unsupported, }; + // check if selected answer is from to local user + /* + * localUser accepts/rejects the call and it is selected by caller - No message + * Another User accepts/rejects the call and it is selected by caller - "Call answered/rejected + * elsewhere" + */ + bool callLocalUser_ = true; + if (callLocalUser_) + hiddenEvents.hidden_event_types->push_back(EventType::CallSelectAnswer); + if (auto temp = cache::client()->getAccountData(mtx::events::EventType::NhekoHiddenEvents, "")) { auto h = std::get< 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,18 +562,139 @@ 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 || + 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(); + } +} - if (callid_ == callHangUpEvent.content.call_id) +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 @@ -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 diff --git a/src/voip/CallManager.h b/src/voip/CallManager.h index 8f1615f8..3011444f 100644 --- a/src/voip/CallManager.h +++ b/src/voip/CallManager.h @@ -18,6 +18,7 @@ #include "WebRTCSession.h" #include "mtx/events/collections.hpp" #include "mtx/events/voip.hpp" +#include <mtxclient/utils.hpp> namespace mtx::responses { struct TurnServer; @@ -30,6 +31,7 @@ class CallManager final : public QObject Q_OBJECT Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState) Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState) + Q_PROPERTY(bool isOnCallOnOtherDevice READ isOnCallOnOtherDevice NOTIFY newCallDeviceState) Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState) Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState) Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState) @@ -46,7 +48,9 @@ public: CallManager(QObject *); bool haveCallInvite() const { return haveCallInvite_; } - bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; } + bool isOnCall() const { return (session_.state() != webrtc::State::DISCONNECTED); } + bool isOnCallOnOtherDevice() const { return (isOnCallOnOtherDevice_ != ""); } + bool checkSharesRoom(QString roomid_, std::string invitee) const; webrtc::CallType callType() const { return callType_; } webrtc::State callState() const { return session_.state(); } QString callParty() const { return callParty_; } @@ -67,8 +71,9 @@ public slots: void toggleMicMute(); void toggleLocalPiP() { session_.toggleLocalPiP(); } void acceptInvite(); - void - hangUp(mtx::events::voip::CallHangUp::Reason = mtx::events::voip::CallHangUp::Reason::User); + void hangUp( + mtx::events::voip::CallHangUp::Reason = mtx::events::voip::CallHangUp::Reason::UserHangUp); + void rejectInvite(); QStringList windowList(); void previewWindow(unsigned int windowIndex) const; @@ -77,8 +82,12 @@ signals: void newMessage(const QString &roomid, const mtx::events::voip::CallCandidates &); void newMessage(const QString &roomid, const mtx::events::voip::CallAnswer &); void newMessage(const QString &roomid, const mtx::events::voip::CallHangUp &); + void newMessage(const QString &roomid, const mtx::events::voip::CallSelectAnswer &); + void newMessage(const QString &roomid, const mtx::events::voip::CallReject &); + void newMessage(const QString &roomid, const mtx::events::voip::CallNegotiate &); void newInviteState(); void newCallState(); + void newCallDeviceState(); void micMuteChanged(); void devicesChanged(); void turnServerRetrieved(const mtx::responses::TurnServer &); @@ -92,18 +101,23 @@ private: QString callParty_; QString callPartyDisplayName_; QString callPartyAvatarUrl_; + std::string callPartyVersion_ = "1"; std::string callid_; - std::string partyid_ = ""; - std::string invitee_ = ""; - const uint32_t timeoutms_ = 120000; - webrtc::CallType callType_ = webrtc::CallType::VOICE; - bool haveCallInvite_ = false; + std::string partyid_ = mtx::client::utils::random_token(8, false); + std::string selectedpartyid_ = ""; + std::string invitee_ = ""; + const uint32_t timeoutms_ = 120000; + webrtc::CallType callType_ = webrtc::CallType::VOICE; + bool haveCallInvite_ = false; + bool answerSelected_ = false; + std::string isOnCallOnOtherDevice_ = ""; std::string inviteSDP_; std::vector<mtx::events::voip::CallCandidates::Candidate> remoteICECandidates_; std::vector<std::string> turnURIs_; QTimer turnServerTimer_; QMediaPlayer player_; std::vector<std::pair<QString, uint32_t>> windows_; + std::vector<std::string> rejectCallPartyIDs_; template<typename T> bool handleEvent(const mtx::events::collections::TimelineEvents &event); @@ -111,11 +125,14 @@ private: void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallCandidates> &); void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &); void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &); void answerInvite(const mtx::events::voip::CallInvite &, bool isVideo); void generateCallID(); QStringList devices(bool isVideo) const; - void clear(); - void endCall(); + void clear(bool endAllCalls = true); + void endCall(bool endAllCalls = true); void playRingtone(const QUrl &ringtone, bool repeat); void stopRingtone(); }; diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp index 49a5cca0..b3e6bf26 100644 --- a/src/voip/WebRTCSession.cpp +++ b/src/voip/WebRTCSession.cpp @@ -700,6 +700,15 @@ WebRTCSession::acceptOffer(const std::string &sdp) } bool +WebRTCSession::acceptNegotiation(const std::string &sdp) +{ + nhlog::ui()->debug("WebRTC: received negotiation offer:\n{}", sdp); + if (state_ == State::DISCONNECTED) + return false; + return false; +} + +bool WebRTCSession::acceptAnswer(const std::string &sdp) { nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); @@ -1139,6 +1148,12 @@ WebRTCSession::createOffer(webrtc::CallType, uint32_t) // clang-format on bool +WebRTCSession::acceptNegotiation(const std::string &) +{ + return false; +} + +bool WebRTCSession::acceptOffer(const std::string &) { return false; diff --git a/src/voip/WebRTCSession.h b/src/voip/WebRTCSession.h index a0ee9720..081611e5 100644 --- a/src/voip/WebRTCSession.h +++ b/src/voip/WebRTCSession.h @@ -64,6 +64,7 @@ public: bool createOffer(webrtc::CallType, uint32_t shareWindowId); bool acceptOffer(const std::string &sdp); bool acceptAnswer(const std::string &sdp); + bool acceptNegotiation(const std::string &sdp); void acceptICECandidates(const std::vector<mtx::events::voip::CallCandidates::Candidate> &); bool isMicMuted() const; |