summary refs log tree commit diff
path: root/src/CallManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/CallManager.cpp315
1 files changed, 315 insertions, 0 deletions
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
new file mode 100644

index 00000000..67aabced --- /dev/null +++ b/src/CallManager.cpp
@@ -0,0 +1,315 @@ +#include <chrono> + +#include <QMediaPlaylist> +#include <QUrl> + +#include "CallManager.h" +#include "Cache.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "UserSettingsPage.h" +#include "WebRTCSession.h" + +#include "dialogs/AcceptCall.h" + +Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>) +Q_DECLARE_METATYPE(mtx::responses::TurnServer) + +using namespace mtx::events; +using namespace mtx::events::msg; + +// TODO Allow altenative in settings +#define STUN_SERVER "stun://turn.matrix.org:3478" + +CallManager::CallManager(QSharedPointer<UserSettings> userSettings) + : QObject(), + session_(WebRTCSession::instance()), + turnServerTimer_(this), + settings_(userSettings) +{ + qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>(); + qRegisterMetaType<mtx::responses::TurnServer>(); + + connect(&session_, &WebRTCSession::offerCreated, this, + [this](const std::string &sdp, + const std::vector<mtx::events::msg::CallCandidates::Candidate>& candidates) + { + nhlog::ui()->debug("Offer created with callid_ and roomid_: {} {}", callid_, roomid_.toStdString()); + emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_}); + emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); + }); + + connect(&session_, &WebRTCSession::answerCreated, this, + [this](const std::string &sdp, + const std::vector<mtx::events::msg::CallCandidates::Candidate>& candidates) + { + nhlog::ui()->debug("Answer created with callid_ and roomid_: {} {}", callid_, roomid_.toStdString()); + emit newMessage(roomid_, CallAnswer{callid_, sdp, 0}); + emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); + }); + + connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); + turnServerTimer_.start(2000); + + connect(this, &CallManager::turnServerRetrieved, this, + [this](const mtx::responses::TurnServer &res) + { + nhlog::net()->info("TURN server(s) retrieved from homeserver:"); + nhlog::net()->info("username: {}", res.username); + nhlog::net()->info("ttl: {}", res.ttl); + for (const auto &u : res.uris) + nhlog::net()->info("uri: {}", u); + + turnServer_ = res; + turnServerTimer_.setInterval(res.ttl * 1000 * 0.9); + }); + + connect(&session_, &WebRTCSession::pipelineChanged, this, + [this](bool started) { + if (!started) + playRingtone("qrc:/media/media/callend.ogg", false); + }); + + connect(&player_, &QMediaPlayer::mediaStatusChanged, this, + [this](QMediaPlayer::MediaStatus status) { + if (status == QMediaPlayer::LoadedMedia) + player_.play(); + }); +} + +void +CallManager::sendInvite(const QString &roomid) +{ + if (onActiveCall()) + return; + + std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); + if (members.size() != 2) { + emit ChatPage::instance()->showNotification("Voice/Video calls are limited to 1:1 rooms"); + return; + } + + std::string errorMessage; + if (!session_.init(&errorMessage)) { + emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); + return; + } + + roomid_ = roomid; + setTurnServers(); + session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); + + // TODO Add invite timeout + generateCallID(); + const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); + emit newCallParty(callee.user_id, callee.display_name); + playRingtone("qrc:/media/media/ringback.ogg", true); + if (!session_.createOffer()) { + emit ChatPage::instance()->showNotification("Problem setting up call"); + endCall(); + } +} + +void +CallManager::hangUp() +{ + nhlog::ui()->debug("CallManager::hangUp: roomid_: {}", roomid_.toStdString()); + if (!callid_.empty()) { + emit newMessage(roomid_, CallHangUp{callid_, 0, CallHangUp::Reason::User}); + endCall(); + } +} + +bool +CallManager::onActiveCall() +{ + return session_.isActive(); +} + +void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) +{ + if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) + || handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event)) + return; +} + +template<typename T> +bool +CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event) +{ + if (std::holds_alternative<RoomEvent<T>>(event)) { + handleEvent(std::get<RoomEvent<T>>(event)); + return true; + } + return false; +} + +void +CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) +{ + nhlog::ui()->debug("CallManager::incoming CallInvite from {} with id {}", callInviteEvent.sender, callInviteEvent.content.call_id); + + if (callInviteEvent.content.call_id.empty()) + return; + + std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); + if (onActiveCall() || members.size() != 2) { + emit newMessage(QString::fromStdString(callInviteEvent.room_id), + CallHangUp{callInviteEvent.content.call_id, 0, CallHangUp::Reason::InviteTimeOut}); + return; + } + + playRingtone("qrc:/media/media/ring.ogg", true); + roomid_ = QString::fromStdString(callInviteEvent.room_id); + callid_ = callInviteEvent.content.call_id; + remoteICECandidates_.clear(); + + const RoomMember &caller = members.front().user_id == utils::localUser() ? members.back() : members.front(); + emit newCallParty(caller.user_id, caller.display_name); + + auto dialog = new dialogs::AcceptCall(caller.user_id, caller.display_name, MainWindow::instance()); + connect(dialog, &dialogs::AcceptCall::accept, this, + [this, callInviteEvent](){ + MainWindow::instance()->hideOverlay(); + answerInvite(callInviteEvent.content);}); + connect(dialog, &dialogs::AcceptCall::reject, this, + [this](){ + MainWindow::instance()->hideOverlay(); + hangUp();}); + MainWindow::instance()->showSolidOverlayModal(dialog); +} + +void +CallManager::answerInvite(const CallInvite &invite) +{ + stopRingtone(); + std::string errorMessage; + if (!session_.init(&errorMessage)) { + emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); + hangUp(); + return; + } + + setTurnServers(); + session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); + + if (!session_.acceptOffer(invite.sdp)) { + emit ChatPage::instance()->showNotification("Problem setting up call"); + hangUp(); + return; + } + session_.acceptICECandidates(remoteICECandidates_); + remoteICECandidates_.clear(); +} + +void +CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) +{ + nhlog::ui()->debug("CallManager::incoming CallCandidates from {} with id {}", callCandidatesEvent.sender, callCandidatesEvent.content.call_id); + if (callid_ == callCandidatesEvent.content.call_id) { + if (onActiveCall()) + session_.acceptICECandidates(callCandidatesEvent.content.candidates); + else { + // CallInvite has been received and we're awaiting localUser to accept or reject the call + for (const auto &c : callCandidatesEvent.content.candidates) + remoteICECandidates_.push_back(c); + } + } +} + +void +CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) +{ + nhlog::ui()->debug("CallManager::incoming CallAnswer from {} with id {}", callAnswerEvent.sender, callAnswerEvent.content.call_id); + if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) { + stopRingtone(); + if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { + emit ChatPage::instance()->showNotification("Problem setting up call"); + hangUp(); + } + } +} + +void +CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent) +{ + nhlog::ui()->debug("CallManager::incoming CallHangUp from {} with id {}", callHangUpEvent.sender, callHangUpEvent.content.call_id); + if (onActiveCall() && callid_ == callHangUpEvent.content.call_id) + endCall(); +} + +void +CallManager::generateCallID() +{ + using namespace std::chrono; + uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); + callid_ = "c" + std::to_string(ms); +} + +void +CallManager::endCall() +{ + stopRingtone(); + session_.end(); + roomid_.clear(); + callid_.clear(); + remoteICECandidates_.clear(); +} + +void +CallManager::retrieveTurnServer() +{ + http::client()->get_turn_server( + [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) { + if (err) { + turnServerTimer_.setInterval(5000); + return; + } + emit turnServerRetrieved(res); + }); +} + +void +CallManager::setTurnServers() +{ + // gstreamer expects (percent-encoded): turn(s)://username:password@host:port?transport=udp(tcp) + std::vector<std::string> uris; + for (const auto &uri : turnServer_.uris) { + if (auto c = uri.find(':'); c == std::string::npos) { + nhlog::ui()->error("Invalid TURN server uri: {}", uri); + continue; + } + else { + std::string scheme = std::string(uri, 0, c); + if (scheme != "turn" && scheme != "turns") { + nhlog::ui()->error("Invalid TURN server uri: {}", uri); + continue; + } + std::string res = scheme + "://" + turnServer_.username + ":" + turnServer_.password + + "@" + std::string(uri, ++c); + QString encodedUri = QUrl::toPercentEncoding(QString::fromStdString(res)); + uris.push_back(encodedUri.toStdString()); + } + } + if (!uris.empty()) + session_.setTurnServers(uris); +} + +void +CallManager::playRingtone(const QString &ringtone, bool repeat) +{ + static QMediaPlaylist playlist; + playlist.clear(); + playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop : QMediaPlaylist::CurrentItemOnce); + playlist.addMedia(QUrl(ringtone)); + player_.setVolume(100); + player_.setPlaylist(&playlist); +} + +void +CallManager::stopRingtone() +{ + player_.setPlaylist(nullptr); +}