Skip to content
Snippets Groups Projects
CallManager.cpp 16.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • trilene's avatar
    trilene committed
    #include <algorithm>
    
    #include <cctype>
    
    trilene's avatar
    trilene committed
    #include <chrono>
    
    trilene's avatar
    trilene committed
    #include <cstdint>
    
    trilene's avatar
    trilene committed
    
    #include <QMediaPlaylist>
    #include <QUrl>
    
    #include "Cache.h"
    
    trilene's avatar
    trilene committed
    #include "CallManager.h"
    
    trilene's avatar
    trilene committed
    #include "ChatPage.h"
    #include "Logging.h"
    #include "MainWindow.h"
    #include "MatrixClient.h"
    #include "UserSettingsPage.h"
    #include "WebRTCSession.h"
    #include "dialogs/AcceptCall.h"
    
    
    trilene's avatar
    trilene committed
    #include "mtx/responses/turn_server.hpp"
    
    
    trilene's avatar
    trilene committed
    Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
    
    Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
    
    trilene's avatar
    trilene committed
    Q_DECLARE_METATYPE(mtx::responses::TurnServer)
    
    using namespace mtx::events;
    using namespace mtx::events::msg;
    
    
    trilene's avatar
    trilene committed
    // https://github.com/vector-im/riot-web/issues/10173
    
    trilene's avatar
    trilene committed
    #define STUN_SERVER "stun://turn.matrix.org:3478"
    
    
    trilene's avatar
    trilene committed
    namespace {
    std::vector<std::string>
    getTurnURIs(const mtx::responses::TurnServer &turnServer);
    }
    
    
    trilene's avatar
    trilene committed
    CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
    
    trilene's avatar
    trilene committed
      : QObject()
      , session_(WebRTCSession::instance())
      , turnServerTimer_(this)
      , settings_(userSettings)
    
    trilene's avatar
    trilene committed
    {
    
    trilene's avatar
    trilene committed
            qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
            qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
            qRegisterMetaType<mtx::responses::TurnServer>();
    
            connect(
              &session_,
              &WebRTCSession::offerCreated,
              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_, sdp, 0, timeoutms_});
                      emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
                      QTimer::singleShot(timeoutms_, this, [this]() {
                              if (session_.state() == WebRTCSession::State::OFFERSENT) {
                                      hangUp(CallHangUp::Reason::InviteTimeOut);
                                      emit ChatPage::instance()->showNotification(
                                        "The remote side failed to pick up.");
                              }
                      });
              });
    
            connect(
              &session_,
              &WebRTCSession::answerCreated,
              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_, sdp, 0});
                      emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
              });
    
            connect(&session_,
                    &WebRTCSession::newICECandidate,
                    this,
                    [this](const CallCandidates::Candidate &candidate) {
                            nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
                            emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
                    });
    
            connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
    
            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: {} seconds", res.ttl);
                            for (const auto &u : res.uris)
                                    nhlog::net()->info("uri: {}", u);
    
                            // Request new credentials close to expiry
                            // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
                            turnURIs_    = getTurnURIs(res);
                            uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
                            if (res.ttl < 3600)
                                    nhlog::net()->warn("Setting ttl to 1 hour");
                            turnServerTimer_.setInterval(ttl * 1000 * 0.9);
                    });
    
            connect(&session_, &WebRTCSession::stateChanged, this, [this](WebRTCSession::State state) {
                    switch (state) {
                    case WebRTCSession::State::DISCONNECTED:
                            playRingtone("qrc:/media/media/callend.ogg", false);
                            clear();
                            break;
                    case WebRTCSession::State::ICEFAILED: {
                            QString error("Call connection failed.");
                            if (turnURIs_.empty())
                                    error += " Your homeserver has no configured TURN server.";
                            emit ChatPage::instance()->showNotification(error);
                            hangUp(CallHangUp::Reason::ICEFailed);
                            break;
                    }
                    default:
                            break;
                    }
            });
    
            connect(&player_,
                    &QMediaPlayer::mediaStatusChanged,
                    this,
                    [this](QMediaPlayer::MediaStatus status) {
                            if (status == QMediaPlayer::LoadedMedia)
                                    player_.play();
                    });
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::sendInvite(const QString &roomid)
    {
    
    trilene's avatar
    trilene committed
            if (onActiveCall())
                    return;
    
            auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
            if (roomInfo.member_count != 2) {
                    emit ChatPage::instance()->showNotification(
                      "Voice 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;
            session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
            session_.setTurnServers(turnURIs_);
    
            generateCallID();
            nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
            std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
            const RoomMember &callee =
              members.front().user_id == utils::localUser() ? members.back() : members.front();
            emit newCallParty(callee.user_id,
                              callee.display_name,
                              QString::fromStdString(roomInfo.name),
                              QString::fromStdString(roomInfo.avatar_url));
            playRingtone("qrc:/media/media/ringback.ogg", true);
            if (!session_.createOffer()) {
                    emit ChatPage::instance()->showNotification("Problem setting up call.");
                    endCall();
            }
    
    trilene's avatar
    trilene committed
    }
    
    
    trilene's avatar
    trilene committed
    namespace {
    
    trilene's avatar
    trilene committed
    std::string
    callHangUpReasonString(CallHangUp::Reason reason)
    
    trilene's avatar
    trilene committed
    {
    
    trilene's avatar
    trilene committed
            switch (reason) {
            case CallHangUp::Reason::ICEFailed:
                    return "ICE failed";
            case CallHangUp::Reason::InviteTimeOut:
                    return "Invite time out";
            default:
                    return "User";
            }
    
    trilene's avatar
    trilene committed
    void
    
    trilene's avatar
    trilene committed
    CallManager::hangUp(CallHangUp::Reason reason)
    
    trilene's avatar
    trilene committed
    {
    
    trilene's avatar
    trilene committed
            if (!callid_.empty()) {
                    nhlog::ui()->debug(
                      "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
                    emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
                    endCall();
            }
    
    trilene's avatar
    trilene committed
    }
    
    bool
    CallManager::onActiveCall()
    {
    
    trilene's avatar
    trilene committed
            return session_.state() != WebRTCSession::State::DISCONNECTED;
    
    trilene's avatar
    trilene committed
    }
    
    
    trilene's avatar
    trilene committed
    void
    CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
    
    trilene's avatar
    trilene committed
    {
    
    #ifdef GSTREAMER_AVAILABLE
    
    trilene's avatar
    trilene committed
            if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) ||
                handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
                    return;
    
    trilene's avatar
    trilene committed
    }
    
    template<typename T>
    bool
    CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
    {
    
    trilene's avatar
    trilene committed
            if (std::holds_alternative<RoomEvent<T>>(event)) {
                    handleEvent(std::get<RoomEvent<T>>(event));
                    return true;
            }
            return false;
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
    {
    
    trilene's avatar
    trilene committed
            const char video[]     = "m=video";
            const std::string &sdp = callInviteEvent.content.sdp;
            bool isVideo           = std::search(sdp.cbegin(),
                                       sdp.cend(),
                                       std::cbegin(video),
                                       std::cend(video) - 1,
                                       [](unsigned char c1, unsigned char c2) {
                                               return std::tolower(c1) == std::tolower(c2);
                                       }) != sdp.cend();
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
    
    trilene's avatar
    trilene committed
                               callInviteEvent.content.call_id,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    			   (isVideo ? "video" : "voice"),
    
    trilene's avatar
    trilene committed
                               callInviteEvent.sender);
    
            if (callInviteEvent.content.call_id.empty())
                    return;
    
            auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
            if (onActiveCall() || roomInfo.member_count != 2 || isVideo) {
                    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();
    
            std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
            const RoomMember &caller =
              members.front().user_id == utils::localUser() ? members.back() : members.front();
            emit newCallParty(caller.user_id,
                              caller.display_name,
                              QString::fromStdString(roomInfo.name),
                              QString::fromStdString(roomInfo.avatar_url));
    
            auto dialog = new dialogs::AcceptCall(caller.user_id,
                                                  caller.display_name,
                                                  QString::fromStdString(roomInfo.name),
                                                  QString::fromStdString(roomInfo.avatar_url),
    
                                                  settings_,
    
    trilene's avatar
    trilene committed
                                                  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);
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::answerInvite(const CallInvite &invite)
    {
    
    trilene's avatar
    trilene committed
            stopRingtone();
            std::string errorMessage;
            if (!session_.init(&errorMessage)) {
                    emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
                    hangUp();
                    return;
            }
    
            session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
            session_.setTurnServers(turnURIs_);
    
            if (!session_.acceptOffer(invite.sdp)) {
                    emit ChatPage::instance()->showNotification("Problem setting up call.");
                    hangUp();
                    return;
            }
            session_.acceptICECandidates(remoteICECandidates_);
            remoteICECandidates_.clear();
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
    {
    
    trilene's avatar
    trilene committed
            if (callCandidatesEvent.sender == utils::localUser().toStdString())
                    return;
    
            nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
                               callCandidatesEvent.content.call_id,
                               callCandidatesEvent.sender);
    
            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);
                    }
            }
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
    {
    
    trilene's avatar
    trilene committed
            nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
                               callAnswerEvent.content.call_id,
                               callAnswerEvent.sender);
    
            if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
                callid_ == callAnswerEvent.content.call_id) {
                    emit ChatPage::instance()->showNotification("Call answered on another device.");
                    stopRingtone();
                    MainWindow::instance()->hideOverlay();
                    return;
            }
    
            if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
                    stopRingtone();
                    if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
                            emit ChatPage::instance()->showNotification("Problem setting up call.");
                            hangUp();
                    }
            }
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
    {
    
    trilene's avatar
    trilene committed
            nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
                               callHangUpEvent.content.call_id,
                               callHangUpReasonString(callHangUpEvent.content.reason),
                               callHangUpEvent.sender);
    
            if (callid_ == callHangUpEvent.content.call_id) {
                    MainWindow::instance()->hideOverlay();
                    endCall();
            }
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::generateCallID()
    {
    
    trilene's avatar
    trilene committed
            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::clear()
    {
            roomid_.clear();
            callid_.clear();
            remoteICECandidates_.clear();
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::endCall()
    {
    
    trilene's avatar
    trilene committed
            stopRingtone();
            clear();
            session_.end();
    
    trilene's avatar
    trilene committed
    }
    
    
    trilene's avatar
    trilene committed
    void
    CallManager::refreshTurnServer()
    {
    
    trilene's avatar
    trilene committed
            turnURIs_.clear();
            turnServerTimer_.start(2000);
    
    trilene's avatar
    trilene committed
    void
    CallManager::retrieveTurnServer()
    {
    
    trilene's avatar
    trilene committed
            http::client()->get_turn_server(
              [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
                      if (err) {
                              turnServerTimer_.setInterval(5000);
                              return;
                      }
                      emit turnServerRetrieved(res);
              });
    
    trilene's avatar
    trilene committed
    }
    
    void
    
    trilene's avatar
    trilene committed
    CallManager::playRingtone(const QString &ringtone, bool repeat)
    {
    
    trilene's avatar
    trilene committed
            static QMediaPlaylist playlist;
            playlist.clear();
            playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
                                            : QMediaPlaylist::CurrentItemOnce);
            playlist.addMedia(QUrl(ringtone));
            player_.setVolume(100);
            player_.setPlaylist(&playlist);
    
    trilene's avatar
    trilene committed
    }
    
    void
    CallManager::stopRingtone()
    {
    
    trilene's avatar
    trilene committed
            player_.setPlaylist(nullptr);
    
    trilene's avatar
    trilene committed
    }
    
    namespace {
    std::vector<std::string>
    getTurnURIs(const mtx::responses::TurnServer &turnServer)
    
    trilene's avatar
    trilene committed
    {
    
    trilene's avatar
    trilene committed
            // gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
            // where username and password are percent-encoded
            std::vector<std::string> ret;
            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;
                            }
    
                            QString encodedUri =
                              QString::fromStdString(scheme) + "://" +
                              QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
                              ":" +
                              QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
                              "@" + QString::fromStdString(std::string(uri, ++c));
                            ret.push_back(encodedUri.toStdString());
                    }
            }
            return ret;
    
    trilene's avatar
    trilene committed
    }
    }