matrixion/src/CallManager.cpp

598 lines
22 KiB
C++
Raw Normal View History

2020-07-31 02:59:54 +03:00
#include <algorithm>
2020-07-30 01:16:52 +03:00
#include <cctype>
2020-07-11 02:19:48 +03:00
#include <chrono>
2020-08-01 21:31:10 +03:00
#include <cstdint>
2021-02-18 23:55:29 +03:00
#include <cstdlib>
#include <memory>
2020-07-11 02:19:48 +03:00
#include <QMediaPlaylist>
#include <QUrl>
#include "Cache.h"
#include "CallDevices.h"
2020-08-01 21:31:10 +03:00
#include "CallManager.h"
2020-07-11 02:19:48 +03:00
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
2020-12-17 19:25:32 +03:00
#include "UserSettingsPage.h"
2020-10-27 19:45:28 +03:00
#include "Utils.h"
2020-07-11 02:19:48 +03:00
2020-07-26 17:59:50 +03:00
#include "mtx/responses/turn_server.hpp"
#ifdef XCB_AVAILABLE
#include <xcb/xcb.h>
#include <xcb/xcb_ewmh.h>
#endif
2020-07-11 02:19:48 +03:00
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
2020-07-11 02:19:48 +03:00
Q_DECLARE_METATYPE(mtx::responses::TurnServer)
using namespace mtx::events;
using namespace mtx::events::msg;
2021-02-18 23:55:29 +03:00
using webrtc::CallType;
2020-07-26 17:59:50 +03:00
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
}
CallManager::CallManager(QObject *parent)
2020-10-27 19:45:28 +03:00
: QObject(parent)
2020-08-01 21:31:10 +03:00
, session_(WebRTCSession::instance())
, turnServerTimer_(this)
2020-07-11 02:19:48 +03:00
{
2020-08-01 21:31:10 +03:00
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});
2021-01-12 23:16:59 +03:00
std::string callid(callid_);
QTimer::singleShot(timeoutms_, this, [this, callid]() {
if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
2020-08-01 21:31:10 +03:00
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);
});
2020-09-22 19:07:36 +03:00
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) {
2020-08-01 21:31:10 +03:00
switch (state) {
2020-09-22 19:07:36 +03:00
case webrtc::State::DISCONNECTED:
playRingtone(QUrl("qrc:/media/media/callend.ogg"), false);
2020-08-01 21:31:10 +03:00
clear();
break;
2020-09-22 19:07:36 +03:00
case webrtc::State::ICEFAILED: {
2020-08-01 21:31:10 +03:00
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;
}
emit newCallState();
2020-08-01 21:31:10 +03:00
});
connect(&CallDevices::instance(),
&CallDevices::devicesChanged,
this,
&CallManager::devicesChanged);
2020-12-17 19:25:32 +03:00
2020-08-01 21:31:10 +03:00
connect(&player_,
&QMediaPlayer::mediaStatusChanged,
this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::LoadedMedia)
player_.play();
});
connect(&player_,
QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
[this](QMediaPlayer::Error error) {
stopRingtone();
switch (error) {
case QMediaPlayer::FormatError:
case QMediaPlayer::ResourceError:
nhlog::ui()->error("WebRTC: valid ringtone file not found");
break;
case QMediaPlayer::AccessDeniedError:
nhlog::ui()->error("WebRTC: access to ringtone file denied");
break;
default:
nhlog::ui()->error("WebRTC: unable to play ringtone");
break;
}
});
2020-07-11 02:19:48 +03:00
}
void
CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
2020-07-11 02:19:48 +03:00
{
if (isOnCall())
2020-08-01 21:31:10 +03:00
return;
if (callType == CallType::SCREEN) {
if (!screenShareSupported())
return;
if (windows_.empty() || windowIndex >= windows_.size()) {
nhlog::ui()->error("WebRTC: window index out of range");
return;
}
}
2020-08-01 21:31:10 +03:00
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (roomInfo.member_count != 2) {
2020-10-27 20:14:06 +03:00
emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
2020-08-01 21:31:10 +03:00
return;
}
std::string errorMessage;
2020-10-27 20:14:06 +03:00
if (!session_.havePlugins(false, &errorMessage) ||
2021-02-18 23:55:29 +03:00
((callType == CallType::VIDEO || callType == CallType::SCREEN) &&
!session_.havePlugins(true, &errorMessage))) {
2020-08-01 21:31:10 +03:00
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
return;
}
2021-02-18 23:55:29 +03:00
callType_ = callType;
roomid_ = roomid;
2020-08-01 21:31:10 +03:00
session_.setTurnServers(turnURIs_);
generateCallID();
2021-02-18 23:55:29 +03:00
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();
2020-12-17 19:25:32 +03:00
callParty_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
2020-12-17 19:25:32 +03:00
emit newInviteState();
playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
if (!session_.createOffer(callType, windows_[windowIndex].second)) {
2020-08-01 21:31:10 +03:00
emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall();
}
2020-07-11 02:19:48 +03:00
}
2020-07-26 17:59:50 +03:00
namespace {
2020-08-01 21:31:10 +03:00
std::string
callHangUpReasonString(CallHangUp::Reason reason)
2020-07-26 17:59:50 +03:00
{
2020-08-01 21:31:10 +03:00
switch (reason) {
case CallHangUp::Reason::ICEFailed:
return "ICE failed";
case CallHangUp::Reason::InviteTimeOut:
return "Invite time out";
default:
return "User";
}
2020-07-26 17:59:50 +03:00
}
}
2020-07-11 02:19:48 +03:00
void
2020-07-26 01:11:11 +03:00
CallManager::hangUp(CallHangUp::Reason reason)
2020-07-11 02:19:48 +03:00
{
2020-08-01 21:31:10 +03:00
if (!callid_.empty()) {
nhlog::ui()->debug(
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
endCall();
}
2020-07-11 02:19:48 +03:00
}
2020-08-01 21:31:10 +03:00
void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
2020-07-11 02:19:48 +03:00
{
#ifdef GSTREAMER_AVAILABLE
2020-08-01 21:31:10 +03:00
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) ||
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
return;
#else
(void)event;
#endif
2020-07-11 02:19:48 +03:00
}
template<typename T>
bool
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
{
2020-08-01 21:31:10 +03:00
if (std::holds_alternative<RoomEvent<T>>(event)) {
handleEvent(std::get<RoomEvent<T>>(event));
return true;
}
return false;
2020-07-11 02:19:48 +03:00
}
void
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
{
2020-08-01 21:31:10 +03:00
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();
2020-08-11 09:51:57 +03:00
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
2020-08-01 21:31:10 +03:00
callInviteEvent.content.call_id,
2020-08-14 10:01:56 +03:00
(isVideo ? "video" : "voice"),
2020-08-01 21:31:10 +03:00
callInviteEvent.sender);
if (callInviteEvent.content.call_id.empty())
return;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
if (isOnCall() || roomInfo.member_count != 2) {
2020-08-01 21:31:10 +03:00
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
0,
CallHangUp::Reason::InviteTimeOut});
return;
}
const QString &ringtone = ChatPage::instance()->userSettings()->ringtone();
if (ringtone != "Mute")
playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg")
: QUrl::fromLocalFile(ringtone),
true);
2020-08-01 21:31:10 +03:00
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();
2020-12-17 19:25:32 +03:00
callParty_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
2020-12-17 19:25:32 +03:00
haveCallInvite_ = true;
2021-02-18 23:55:29 +03:00
callType_ = isVideo ? CallType::VIDEO : CallType::VOICE;
2020-12-17 19:25:32 +03:00
inviteSDP_ = callInviteEvent.content.sdp;
emit newInviteState();
2020-07-11 02:19:48 +03:00
}
void
2020-12-17 19:25:32 +03:00
CallManager::acceptInvite()
2020-07-11 02:19:48 +03:00
{
2020-12-17 19:25:32 +03:00
if (!haveCallInvite_)
return;
2020-08-01 21:31:10 +03:00
stopRingtone();
std::string errorMessage;
2020-10-27 20:14:06 +03:00
if (!session_.havePlugins(false, &errorMessage) ||
2021-02-18 23:55:29 +03:00
(callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
2020-08-01 21:31:10 +03:00
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
hangUp();
return;
}
session_.setTurnServers(turnURIs_);
2020-12-17 19:25:32 +03:00
if (!session_.acceptOffer(inviteSDP_)) {
2020-08-01 21:31:10 +03:00
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
return;
}
session_.acceptICECandidates(remoteICECandidates_);
remoteICECandidates_.clear();
2020-12-17 19:25:32 +03:00
haveCallInvite_ = false;
emit newInviteState();
2020-07-11 02:19:48 +03:00
}
void
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
{
2020-08-01 21:31:10 +03:00
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 (isOnCall())
2020-08-01 21:31:10 +03:00
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);
}
}
2020-07-11 02:19:48 +03:00
}
void
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
{
2020-08-01 21:31:10 +03:00
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
callAnswerEvent.content.call_id,
callAnswerEvent.sender);
2021-01-01 16:46:08 +03:00
if (callAnswerEvent.sender == utils::localUser().toStdString() &&
2020-08-01 21:31:10 +03:00
callid_ == callAnswerEvent.content.call_id) {
2021-01-01 16:46:08 +03:00
if (!isOnCall()) {
emit ChatPage::instance()->showNotification(
"Call answered on another device.");
stopRingtone();
haveCallInvite_ = false;
emit newInviteState();
}
2020-08-01 21:31:10 +03:00
return;
}
if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
2020-08-01 21:31:10 +03:00
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
}
}
2020-07-11 02:19:48 +03:00
}
void
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
{
2020-08-01 21:31:10 +03:00
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
callHangUpEvent.content.call_id,
callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
2020-12-17 19:25:32 +03:00
if (callid_ == callHangUpEvent.content.call_id)
2020-08-01 21:31:10 +03:00
endCall();
2020-07-11 02:19:48 +03:00
}
void
CallManager::toggleMicMute()
{
session_.toggleMicMute();
emit micMuteChanged();
}
bool
2021-02-18 23:55:29 +03:00
CallManager::callsSupported()
{
#ifdef GSTREAMER_AVAILABLE
return true;
#else
return false;
#endif
}
2021-02-18 23:55:29 +03:00
bool
CallManager::screenShareSupported()
{
2021-02-21 01:33:04 +03:00
return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY");
2021-02-18 23:55:29 +03:00
}
2020-12-17 19:25:32 +03:00
QStringList
CallManager::devices(bool isVideo) const
{
QStringList ret;
const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
: ChatPage::instance()->userSettings()->microphone();
std::vector<std::string> devices =
CallDevices::instance().names(isVideo, defaultDevice.toStdString());
2020-12-17 19:25:32 +03:00
ret.reserve(devices.size());
std::transform(devices.cbegin(),
devices.cend(),
std::back_inserter(ret),
[](const auto &d) { return QString::fromStdString(d); });
return ret;
}
2020-07-11 02:19:48 +03:00
void
CallManager::generateCallID()
{
2020-08-01 21:31:10 +03:00
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();
2020-12-17 19:25:32 +03:00
callParty_.clear();
callPartyAvatarUrl_.clear();
2020-08-01 21:31:10 +03:00
callid_.clear();
2021-02-18 23:55:29 +03:00
callType_ = CallType::VOICE;
2020-12-17 19:25:32 +03:00
haveCallInvite_ = false;
emit newInviteState();
inviteSDP_.clear();
2020-08-01 21:31:10 +03:00
remoteICECandidates_.clear();
2020-07-11 02:19:48 +03:00
}
void
CallManager::endCall()
{
2020-08-01 21:31:10 +03:00
stopRingtone();
session_.end();
2020-12-17 19:25:32 +03:00
clear();
2020-07-11 02:19:48 +03:00
}
2020-07-31 02:59:54 +03:00
void
CallManager::refreshTurnServer()
{
2020-08-01 21:31:10 +03:00
turnURIs_.clear();
turnServerTimer_.start(2000);
2020-07-31 02:59:54 +03:00
}
2020-07-11 02:19:48 +03:00
void
CallManager::retrieveTurnServer()
{
2020-08-01 21:31:10 +03:00
http::client()->get_turn_server(
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
if (err) {
turnServerTimer_.setInterval(5000);
return;
}
emit turnServerRetrieved(res);
});
2020-07-11 02:19:48 +03:00
}
void
CallManager::playRingtone(const QUrl &ringtone, bool repeat)
2020-07-26 17:59:50 +03:00
{
2020-08-01 21:31:10 +03:00
static QMediaPlaylist playlist;
playlist.clear();
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
: QMediaPlaylist::CurrentItemOnce);
playlist.addMedia(ringtone);
2020-08-01 21:31:10 +03:00
player_.setVolume(100);
player_.setPlaylist(&playlist);
2020-07-26 17:59:50 +03:00
}
void
CallManager::stopRingtone()
{
2020-08-01 21:31:10 +03:00
player_.setPlaylist(nullptr);
2020-07-26 17:59:50 +03:00
}
QStringList
CallManager::windowList()
{
windows_.clear();
windows_.push_back({"Entire screen", 0});
#ifdef XCB_AVAILABLE
std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection(
xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); });
if (xcb_connection_has_error(connection.get())) {
nhlog::ui()->error("Failed to connect to X server");
return {};
}
xcb_ewmh_connection_t ewmh;
if (!xcb_ewmh_init_atoms_replies(
&ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) {
nhlog::ui()->error("Failed to connect to EWMH server");
return {};
}
std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>>
ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); });
for (int i = 0; i < ewmh.nb_screens; i++) {
xcb_ewmh_get_windows_reply_t clients;
if (!xcb_ewmh_get_client_list_reply(
&ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) {
nhlog::ui()->error("Failed to request window list");
return {};
}
for (uint32_t w = 0; w < clients.windows_len; w++) {
xcb_window_t window = clients.windows[w];
std::string name;
xcb_ewmh_get_utf8_strings_reply_t data;
auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) {
std::string name(r->strings, r->strings_len);
xcb_ewmh_get_utf8_strings_reply_wipe(r);
return name;
};
xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window);
if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window);
if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr))
name = getName(&data);
windows_.push_back({QString::fromStdString(name), window});
}
xcb_ewmh_get_windows_reply_wipe(&clients);
}
#endif
QStringList ret;
ret.reserve(windows_.size());
for (const auto &w : windows_)
ret.append(w.first);
return ret;
}
2020-07-26 17:59:50 +03:00
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer)
2020-07-11 02:19:48 +03:00
{
2020-08-01 21:31:10 +03:00
// 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;
2020-07-11 02:19:48 +03:00
}
}