Skip to content
Snippets Groups Projects
CallManager.cpp 11.9 KiB
Newer Older
trilene's avatar
trilene committed
#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::events::msg::CallCandidates::Candidate)
trilene's avatar
trilene committed
Q_DECLARE_METATYPE(mtx::responses::TurnServer)

using namespace mtx::events;
using namespace mtx::events::msg;

// TODO Allow alterative in settings
trilene's avatar
trilene committed
#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::events::msg::CallCandidates::Candidate>();
trilene's avatar
trilene committed
  qRegisterMetaType<mtx::responses::TurnServer>();

  connect(&session_, &WebRTCSession::offerCreated, this,
      [this](const std::string &sdp,
             const std::vector<CallCandidates::Candidate> &candidates)
trilene's avatar
trilene committed
            {
              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});

              QTimer::singleShot(timeoutms_, this, [this](){
                  if (session_.state() == WebRTCSession::State::OFFERSENT) {
                      emit newMessage(roomid_, CallHangUp{callid_, 0, CallHangUp::Reason::InviteTimeOut});
                      endCall();
                      emit ChatPage::instance()->showNotification("The remote side failed to pick up.");
                  }
              });
trilene's avatar
trilene committed
            });

  connect(&session_, &WebRTCSession::answerCreated, this,
      [this](const std::string &sdp,
             const std::vector<CallCandidates::Candidate> &candidates)
trilene's avatar
trilene committed
            {
              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(&session_, &WebRTCSession::newICECandidate, this,
      [this](const CallCandidates::Candidate &candidate)
            {
              nhlog::ui()->debug("New ICE candidate created with callid_ and roomid_: {} {}", callid_, roomid_.toStdString());
              emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
            });

trilene's avatar
trilene committed
  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);

              // Request new credentials close to expiry
              // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
trilene's avatar
trilene committed
              turnServer_ = res;
              turnServerTimer_.setInterval(res.ttl * 1000 * 0.9);
      });

trilene's avatar
trilene committed
  connect(&session_, &WebRTCSession::stateChanged, this,
      [this](WebRTCSession::State state) {
        if (state == WebRTCSession::State::DISCONNECTED)
trilene's avatar
trilene committed
          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;

trilene's avatar
trilene committed
    auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
    if (roomInfo.member_count != 2) {
      emit ChatPage::instance()->showNotification("Voice calls are limited to 1:1 rooms.");
trilene's avatar
trilene committed
      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 : "");

    generateCallID();
trilene's avatar
trilene committed
    std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
trilene's avatar
trilene committed
    const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front();
trilene's avatar
trilene committed
    emit newCallParty(callee.user_id, callee.display_name,
            QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url));
trilene's avatar
trilene committed
    playRingtone("qrc:/media/media/ringback.ogg", true);
    if (!session_.createOffer()) {
trilene's avatar
trilene committed
      emit ChatPage::instance()->showNotification("Problem setting up call.");
trilene's avatar
trilene committed
      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()
{
trilene's avatar
trilene committed
  return session_.state() != WebRTCSession::State::DISCONNECTED;
trilene's avatar
trilene committed
}

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;

trilene's avatar
trilene committed
  auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
  if (onActiveCall() || roomInfo.member_count != 2) {
trilene's avatar
trilene committed
    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();

trilene's avatar
trilene committed
  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),
      MainWindow::instance());
trilene's avatar
trilene committed
  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)) {
trilene's avatar
trilene committed
    emit ChatPage::instance()->showNotification("Problem setting up call.");
trilene's avatar
trilene committed
    hangUp();
    return;
  }
  session_.acceptICECandidates(remoteICECandidates_);
  remoteICECandidates_.clear();
}

void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
  if (callCandidatesEvent.sender == utils::localUser().toStdString())
    return;

trilene's avatar
trilene committed
  nhlog::ui()->debug("CallManager::incoming CallCandidates from {} with id {}", callCandidatesEvent.sender, callCandidatesEvent.content.call_id);
trilene's avatar
trilene committed
  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() && callAnswerEvent.sender == utils::localUser().toStdString() &&
      callid_ == callAnswerEvent.content.call_id) {
trilene's avatar
trilene committed
    emit ChatPage::instance()->showNotification("Call answered on another device.");
    stopRingtone();
    MainWindow::instance()->hideOverlay();
    return;
  }

trilene's avatar
trilene committed
  if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
    stopRingtone();
    if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
trilene's avatar
trilene committed
      emit ChatPage::instance()->showNotification("Problem setting up call.");
trilene's avatar
trilene committed
      hangUp();
    }
  }
}

void
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
{
  nhlog::ui()->debug("CallManager::incoming CallHangUp from {} with id {}", callHangUpEvent.sender, callHangUpEvent.content.call_id);
trilene's avatar
trilene committed
  if (callid_ == callHangUpEvent.content.call_id) {
    MainWindow::instance()->hideOverlay();
trilene's avatar
trilene committed
    endCall();
trilene's avatar
trilene committed
}

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: turn(s)://username:password@host:port?transport=udp(tcp)
  // where username and password are percent-encoded
trilene's avatar
trilene committed
  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;
      }

      QString encodedUri = QString::fromStdString(scheme) + "://" + 
                           QUrl::toPercentEncoding(QString::fromStdString(turnServer_.username)) + ":" +
                           QUrl::toPercentEncoding(QString::fromStdString(turnServer_.password)) + "@" +
                           QString::fromStdString(std::string(uri, ++c));
trilene's avatar
trilene committed
      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);
}