clang-format

master
trilene 4 years ago
parent f14d141cb5
commit e3e7595bab
  1. 123
      src/ActiveCallBar.cpp
  2. 9
      src/ActiveCallBar.h
  3. 621
      src/CallManager.cpp
  4. 37
      src/CallManager.h
  5. 15
      src/ChatPage.cpp
  6. 19
      src/EventAccessors.cpp
  7. 833
      src/WebRTCSession.cpp
  8. 53
      src/WebRTCSession.h
  9. 44
      src/dialogs/AcceptCall.cpp
  10. 11
      src/dialogs/AcceptCall.h
  11. 24
      src/dialogs/PlaceCall.cpp
  12. 11
      src/dialogs/PlaceCall.h
  13. 6
      src/timeline/TimelineModel.cpp

@ -33,8 +33,7 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
layout_ = new QHBoxLayout(this); layout_ = new QHBoxLayout(this);
layout_->setSpacing(widgetMargin); layout_->setSpacing(widgetMargin);
layout_->setContentsMargins( layout_->setContentsMargins(2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
QFont labelFont; QFont labelFont;
labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1); labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1);
@ -56,9 +55,9 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
setMuteIcon(false); setMuteIcon(false);
muteBtn_->setFixedSize(buttonSize_, buttonSize_); muteBtn_->setFixedSize(buttonSize_, buttonSize_);
muteBtn_->setCornerRadius(buttonSize_ / 2); muteBtn_->setCornerRadius(buttonSize_ / 2);
connect(muteBtn_, &FlatButton::clicked, this, [this](){ connect(muteBtn_, &FlatButton::clicked, this, [this]() {
if (WebRTCSession::instance().toggleMuteAudioSrc(muted_)) if (WebRTCSession::instance().toggleMuteAudioSrc(muted_))
setMuteIcon(muted_); setMuteIcon(muted_);
}); });
layout_->addWidget(avatar_, 0, Qt::AlignLeft); layout_->addWidget(avatar_, 0, Qt::AlignLeft);
@ -70,21 +69,21 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
layout_->addSpacing(18); layout_->addSpacing(18);
timer_ = new QTimer(this); timer_ = new QTimer(this);
connect(timer_, &QTimer::timeout, this, connect(timer_, &QTimer::timeout, this, [this]() {
[this](){ auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_;
auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_; int s = seconds % 60;
int s = seconds % 60; int m = (seconds / 60) % 60;
int m = (seconds / 60) % 60; int h = seconds / 3600;
int h = seconds / 3600; char buf[12];
char buf[12]; if (h)
if (h) snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s);
snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s); else
else snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s);
snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s); durationLabel_->setText(buf);
durationLabel_->setText(buf);
}); });
connect(&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update); connect(
&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update);
} }
void void
@ -103,61 +102,59 @@ ActiveCallBar::setMuteIcon(bool muted)
} }
void void
ActiveCallBar::setCallParty( ActiveCallBar::setCallParty(const QString &userid,
const QString &userid, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl)
const QString &avatarUrl)
{ {
callPartyLabel_->setText(" " + callPartyLabel_->setText(" " + (displayName.isEmpty() ? userid : displayName) + " ");
(displayName.isEmpty() ? userid : displayName) + " ");
if (!avatarUrl.isEmpty()) if (!avatarUrl.isEmpty())
avatar_->setImage(avatarUrl); avatar_->setImage(avatarUrl);
else else
avatar_->setLetter(utils::firstChar(roomName)); avatar_->setLetter(utils::firstChar(roomName));
} }
void void
ActiveCallBar::update(WebRTCSession::State state) ActiveCallBar::update(WebRTCSession::State state)
{ {
switch (state) { switch (state) {
case WebRTCSession::State::INITIATING: case WebRTCSession::State::INITIATING:
show(); show();
stateLabel_->setText("Initiating call..."); stateLabel_->setText("Initiating call...");
break; break;
case WebRTCSession::State::INITIATED: case WebRTCSession::State::INITIATED:
show(); show();
stateLabel_->setText("Call initiated..."); stateLabel_->setText("Call initiated...");
break; break;
case WebRTCSession::State::OFFERSENT: case WebRTCSession::State::OFFERSENT:
show(); show();
stateLabel_->setText("Calling..."); stateLabel_->setText("Calling...");
break; break;
case WebRTCSession::State::CONNECTING: case WebRTCSession::State::CONNECTING:
show(); show();
stateLabel_->setText("Connecting..."); stateLabel_->setText("Connecting...");
break; break;
case WebRTCSession::State::CONNECTED: case WebRTCSession::State::CONNECTED:
show(); show();
callStartTime_ = QDateTime::currentSecsSinceEpoch(); callStartTime_ = QDateTime::currentSecsSinceEpoch();
timer_->start(1000); timer_->start(1000);
stateLabel_->setPixmap(QIcon(":/icons/icons/ui/place-call.png"). stateLabel_->setPixmap(
pixmap(QSize(buttonSize_, buttonSize_))); QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(buttonSize_, buttonSize_)));
durationLabel_->setText("00:00"); durationLabel_->setText("00:00");
durationLabel_->show(); durationLabel_->show();
break; break;
case WebRTCSession::State::ICEFAILED: case WebRTCSession::State::ICEFAILED:
case WebRTCSession::State::DISCONNECTED: case WebRTCSession::State::DISCONNECTED:
hide(); hide();
timer_->stop(); timer_->stop();
callPartyLabel_->setText(QString()); callPartyLabel_->setText(QString());
stateLabel_->setText(QString()); stateLabel_->setText(QString());
durationLabel_->setText(QString()); durationLabel_->setText(QString());
durationLabel_->hide(); durationLabel_->hide();
setMuteIcon(false); setMuteIcon(false);
break; break;
default: default:
break; break;
} }
} }

@ -19,11 +19,10 @@ public:
public slots: public slots:
void update(WebRTCSession::State); void update(WebRTCSession::State);
void setCallParty( void setCallParty(const QString &userid,
const QString &userid, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl);
const QString &avatarUrl);
private: private:
QHBoxLayout *layout_ = nullptr; QHBoxLayout *layout_ = nullptr;

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

@ -3,8 +3,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <QObject>
#include <QMediaPlayer> #include <QMediaPlayer>
#include <QObject>
#include <QSharedPointer> #include <QSharedPointer>
#include <QString> #include <QString>
#include <QTimer> #include <QTimer>
@ -27,7 +27,8 @@ public:
CallManager(QSharedPointer<UserSettings>); CallManager(QSharedPointer<UserSettings>);
void sendInvite(const QString &roomid); void sendInvite(const QString &roomid);
void hangUp(mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); void hangUp(
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
bool onActiveCall(); bool onActiveCall();
void refreshTurnServer(); void refreshTurnServer();
@ -35,22 +36,21 @@ public slots:
void syncEvent(const mtx::events::collections::TimelineEvents &event); void syncEvent(const mtx::events::collections::TimelineEvents &event);
signals: signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite&); void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates&); void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer&); void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp&); void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
void turnServerRetrieved(const mtx::responses::TurnServer&); void turnServerRetrieved(const mtx::responses::TurnServer &);
void newCallParty( void newCallParty(const QString &userid,
const QString &userid, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl);
const QString &avatarUrl);
private slots: private slots:
void retrieveTurnServer(); void retrieveTurnServer();
private: private:
WebRTCSession& session_; WebRTCSession &session_;
QString roomid_; QString roomid_;
std::string callid_; std::string callid_;
const uint32_t timeoutms_ = 120000; const uint32_t timeoutms_ = 120000;
@ -62,12 +62,13 @@ private:
template<typename T> template<typename T>
bool handleEvent_(const mtx::events::collections::TimelineEvents &event); bool handleEvent_(const mtx::events::collections::TimelineEvents &event);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite>&); void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates>&); void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer>&); void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp>&); void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
void answerInvite(const mtx::events::msg::CallInvite&); void answerInvite(const mtx::events::msg::CallInvite &);
void generateCallID(); void generateCallID();
void clear();
void endCall(); void endCall();
void playRingtone(const QString &ringtone, bool repeat); void playRingtone(const QString &ringtone, bool repeat);
void stopRingtone(); void stopRingtone();

@ -460,9 +460,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
if (callManager_.onActiveCall()) { if (callManager_.onActiveCall()) {
callManager_.hangUp(); callManager_.hangUp();
} else { } else {
if (auto roomInfo = if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString());
cache::singleRoomInfo(current_room_.toStdString()); roomInfo.member_count != 2) {
roomInfo.member_count != 2) {
showNotification("Voice calls are limited to 1:1 rooms."); showNotification("Voice calls are limited to 1:1 rooms.");
} else { } else {
std::vector<RoomMember> members( std::vector<RoomMember> members(
@ -471,11 +470,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
members.front().user_id == utils::localUser() ? members.back() members.front().user_id == utils::localUser() ? members.back()
: members.front(); : members.front();
auto dialog = new dialogs::PlaceCall( auto dialog = new dialogs::PlaceCall(
callee.user_id, callee.user_id,
callee.display_name, callee.display_name,
QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.name),
QString::fromStdString(roomInfo.avatar_url), QString::fromStdString(roomInfo.avatar_url),
MainWindow::instance()); MainWindow::instance());
connect(dialog, &dialogs::PlaceCall::voice, this, [this]() { connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
callManager_.sendInvite(current_room_); callManager_.sendInvite(current_room_);
}); });

@ -72,12 +72,19 @@ struct CallType
template<class T> template<class T>
std::string operator()(const T &e) std::string operator()(const T &e)
{ {
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) { if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>,
const char video[] = "m=video"; T>) {
const std::string &sdp = e.content.sdp; const char video[] = "m=video";
return std::search(sdp.cbegin(), sdp.cend(), std::cbegin(video), std::cend(video) - 1, const std::string &sdp = e.content.sdp;
[](unsigned char c1, unsigned char c2) {return std::tolower(c1) == std::tolower(c2);}) return std::search(sdp.cbegin(),
!= sdp.cend() ? "video" : "voice"; sdp.cend(),
std::cbegin(video),
std::cend(video) - 1,
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
}) != sdp.cend()
? "video"
: "voice";
} }
return std::string(); return std::string();
} }

@ -1,9 +1,10 @@
#include <cctype> #include <cctype>
#include "WebRTCSession.h"
#include "Logging.h" #include "Logging.h"
#include "WebRTCSession.h"
extern "C" { extern "C"
{
#include "gst/gst.h" #include "gst/gst.h"
#include "gst/sdp/sdp.h" #include "gst/sdp/sdp.h"
@ -13,478 +14,498 @@ extern "C" {
Q_DECLARE_METATYPE(WebRTCSession::State) Q_DECLARE_METATYPE(WebRTCSession::State)
namespace { WebRTCSession::WebRTCSession()
bool isoffering_; : QObject()
std::string localsdp_;
std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data);
GstWebRTCSessionDescription* parseSDP(const std::string &sdp, GstWebRTCSDPType type);
void generateOffer(GstElement *webrtc);
void setLocalDescription(GstPromise *promise, gpointer webrtc);
void addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED);
gboolean onICEGatheringCompletion(gpointer timerid);
void iceConnectionStateChanged(GstElement *webrtcbin, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED);
void createAnswer(GstPromise *promise, gpointer webrtc);
void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
void linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
std::string::const_iterator findName(const std::string &sdp, const std::string &name);
int getPayloadType(const std::string &sdp, const std::string &name);
}
WebRTCSession::WebRTCSession() : QObject()
{ {
qRegisterMetaType<WebRTCSession::State>(); qRegisterMetaType<WebRTCSession::State>();
connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
} }
bool bool
WebRTCSession::init(std::string *errorMessage) WebRTCSession::init(std::string *errorMessage)
{ {
if (initialised_) if (initialised_)
return true; return true;
GError *error = nullptr; GError *error = nullptr;
if (!gst_init_check(nullptr, nullptr, &error)) { if (!gst_init_check(nullptr, nullptr, &error)) {
std::string strError = std::string("WebRTC: failed to initialise GStreamer: "); std::string strError = std::string("WebRTC: failed to initialise GStreamer: ");
if (error) { if (error) {
strError += error->message; strError += error->message;
g_error_free(error); g_error_free(error);
} }
nhlog::ui()->error(strError); nhlog::ui()->error(strError);
if (errorMessage) if (errorMessage)
*errorMessage = strError; *errorMessage = strError;
return false; return false;
} }
gchar *version = gst_version_string(); gchar *version = gst_version_string();
std::string gstVersion(version); std::string gstVersion(version);
g_free(version); g_free(version);
nhlog::ui()->info("WebRTC: initialised " + gstVersion); nhlog::ui()->info("WebRTC: initialised " + gstVersion);
// GStreamer Plugins: // GStreamer Plugins:
// Base: audioconvert, audioresample, opus, playback, volume // Base: audioconvert, audioresample, opus, playback, volume
// Good: autodetect, rtpmanager // Good: autodetect, rtpmanager
// Bad: dtls, srtp, webrtc // Bad: dtls, srtp, webrtc
// libnice [GLib]: nice // libnice [GLib]: nice
initialised_ = true; initialised_ = true;
std::string strError = gstVersion + ": Missing plugins: "; std::string strError = gstVersion + ": Missing plugins: ";
const gchar *needed[] = {"audioconvert", "audioresample", "autodetect", "dtls", "nice", const gchar *needed[] = {"audioconvert",
"opus", "playback", "rtpmanager", "srtp", "volume", "webrtc", nullptr}; "audioresample",
GstRegistry *registry = gst_registry_get(); "autodetect",
for (guint i = 0; i < g_strv_length((gchar**)needed); i++) { "dtls",
GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); "nice",
if (!plugin) { "opus",
strError += std::string(needed[i]) + " "; "playback",
initialised_ = false; "rtpmanager",
continue; "srtp",
} "volume",
gst_object_unref(plugin); "webrtc",
} nullptr};
GstRegistry *registry = gst_registry_get();
if (!initialised_) { for (guint i = 0; i < g_strv_length((gchar **)needed); i++) {
nhlog::ui()->error(strError); GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
if (errorMessage) if (!plugin) {
*errorMessage = strError; strError += std::string(needed[i]) + " ";
} initialised_ = false;
return initialised_; continue;
}
gst_object_unref(plugin);
}
if (!initialised_) {
nhlog::ui()->error(strError);
if (errorMessage)
*errorMessage = strError;
}
return initialised_;
} }
bool namespace {
WebRTCSession::createOffer()
{
isoffering_ = true;
localsdp_.clear();
localcandidates_.clear();
return startPipeline(111); // a dynamic opus payload type
}
bool bool isoffering_;
WebRTCSession::acceptOffer(const std::string &sdp) std::string localsdp_;
std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
gboolean
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
{ {
nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp); WebRTCSession *session = static_cast<WebRTCSession *>(user_data);
if (state_ != State::DISCONNECTED) switch (GST_MESSAGE_TYPE(msg)) {
return false; case GST_MESSAGE_EOS:
nhlog::ui()->error("WebRTC: end of stream");
isoffering_ = false; session->end();
localsdp_.clear(); break;
localcandidates_.clear(); case GST_MESSAGE_ERROR:
GError *error;
int opusPayloadType = getPayloadType(sdp, "opus"); gchar *debug;
if (opusPayloadType == -1) gst_message_parse_error(msg, &error, &debug);
return false; nhlog::ui()->error(
"WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); g_clear_error(&error);
if (!offer) g_free(debug);
return false; session->end();
break;
if (!startPipeline(opusPayloadType)) { default:
gst_webrtc_session_description_free(offer); break;
return false; }
} return TRUE;
// set-remote-description first, then create-answer
GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
gst_webrtc_session_description_free(offer);
return true;
} }
bool GstWebRTCSessionDescription *
WebRTCSession::startPipeline(int opusPayloadType) parseSDP(const std::string &sdp, GstWebRTCSDPType type)
{ {
if (state_ != State::DISCONNECTED) GstSDPMessage *msg;
return false; gst_sdp_message_new(&msg);
if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) {
emit stateChanged(State::INITIATING); return gst_webrtc_session_description_new(type, msg);
} else {
if (!createPipeline(opusPayloadType)) nhlog::ui()->error("WebRTC: failed to parse remote session description");
return false; gst_object_unref(msg);
return nullptr;
webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); }
if (!stunServer_.empty()) {
nhlog::ui()->info("WebRTC: setting STUN server: {}", stunServer_);
g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr);
}
for (const auto &uri : turnServers_) {
nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
gboolean udata;
g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
}
if (turnServers_.empty())
nhlog::ui()->warn("WebRTC: no TURN server provided");
// generate the offer when the pipeline goes to PLAYING
if (isoffering_)
g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(generateOffer), nullptr);
// on-ice-candidate is emitted when a local ICE candidate has been gathered
g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
// capture ICE failure
g_signal_connect(webrtc_, "notify::ice-connection-state",
G_CALLBACK(iceConnectionStateChanged), nullptr);
// incoming streams trigger pad-added
gst_element_set_state(pipe_, GST_STATE_READY);
g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
// webrtcbin lifetime is the same as that of the pipeline
gst_object_unref(webrtc_);
// start the pipeline
GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
nhlog::ui()->error("WebRTC: unable to start pipeline");
end();
return false;
}
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
gst_bus_add_watch(bus, newBusMessage, this);
gst_object_unref(bus);
emit stateChanged(State::INITIATED);
return true;
} }
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload=" void
setLocalDescription(GstPromise *promise, gpointer webrtc)
bool
WebRTCSession::createPipeline(int opusPayloadType)
{ {
std::string pipeline("webrtcbin bundle-policy=max-bundle name=webrtcbin " const GstStructure *reply = gst_promise_get_reply(promise);
"autoaudiosrc ! volume name=srclevel ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! " gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
"queue ! " RTP_CAPS_OPUS + std::to_string(opusPayloadType) + " ! webrtcbin."); GstWebRTCSessionDescription *gstsdp = nullptr;
gst_structure_get(reply,
webrtc_ = nullptr; isAnswer ? "answer" : "offer",
GError *error = nullptr; GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
pipe_ = gst_parse_launch(pipeline.c_str(), &error); &gstsdp,
if (error) { nullptr);
nhlog::ui()->error("WebRTC: failed to parse pipeline: {}", error->message); gst_promise_unref(promise);
g_error_free(error); g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
end();
return false; gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
} localsdp_ = std::string(sdp);
return true; g_free(sdp);
gst_webrtc_session_description_free(gstsdp);
nhlog::ui()->debug(
"WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
} }
bool void
WebRTCSession::acceptAnswer(const std::string &sdp) createOffer(GstElement *webrtc)
{ {
nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); // create-offer first, then set-local-description
if (state_ != State::OFFERSENT) GstPromise *promise =
return false; gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
if (!answer) {
end();
return false;
}
g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
gst_webrtc_session_description_free(answer);
return true;
} }
void void
WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates) createAnswer(GstPromise *promise, gpointer webrtc)
{ {
if (state_ >= State::INITIATED) { // create-answer first, then set-local-description
for (const auto &c : candidates) { gst_promise_unref(promise);
nhlog::ui()->debug("WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str()); g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
}
}
} }
bool gboolean
WebRTCSession::toggleMuteAudioSrc(bool &isMuted) onICEGatheringCompletion(gpointer timerid)
{ {
if (state_ < State::INITIATED) *(guint *)(timerid) = 0;
return false; if (isoffering_) {
emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
if (!srclevel) } else {
return false; emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT);
gboolean muted; }
g_object_get(srclevel, "mute", &muted, nullptr); return FALSE;
g_object_set(srclevel, "mute", !muted, nullptr);
gst_object_unref(srclevel);
isMuted = !muted;
return true;
} }
void void
WebRTCSession::end() addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
guint mlineIndex,
gchar *candidate,
gpointer G_GNUC_UNUSED)
{ {
nhlog::ui()->debug("WebRTC: ending session"); nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
if (pipe_) {
gst_element_set_state(pipe_, GST_STATE_NULL);
gst_object_unref(pipe_);
pipe_ = nullptr;
}
webrtc_ = nullptr;
if (state_ != State::DISCONNECTED)
emit stateChanged(State::DISCONNECTED);
}
namespace { if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) {
emit WebRTCSession::instance().newICECandidate(
{"audio", (uint16_t)mlineIndex, candidate});
return;
}
std::string::const_iterator findName(const std::string &sdp, const std::string &name) localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
{
return std::search(sdp.cbegin(), sdp.cend(), name.cbegin(), name.cend(),
[](unsigned char c1, unsigned char c2) {return std::tolower(c1) == std::tolower(c2);});
}
int getPayloadType(const std::string &sdp, const std::string &name) // GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers
{ // GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early. Fixed in v1.18. Use a 100ms timeout in
// eg a=rtpmap:111 opus/48000/2 // the meantime
auto e = findName(sdp, name); static guint timerid = 0;
if (e == sdp.cend()) { if (timerid)
nhlog::ui()->error("WebRTC: remote offer - " + name + " attribute missing"); g_source_remove(timerid);
return -1;
}
if (auto s = sdp.rfind(':', e - sdp.cbegin()); s == std::string::npos) {
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name + " payload type");
return -1;
}
else {
++s;
try {
return std::stoi(std::string(sdp, s, e - sdp.cbegin() - s));
}
catch(...) {
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name + " payload type");
}
}
return -1;
}
gboolean timerid = g_timeout_add(100, onICEGatheringCompletion, &timerid);
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
{
WebRTCSession *session = (WebRTCSession*)user_data;
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
nhlog::ui()->error("WebRTC: end of stream");
session->end();
break;
case GST_MESSAGE_ERROR:
GError *error;
gchar *debug;
gst_message_parse_error(msg, &error, &debug);
nhlog::ui()->error("WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
g_clear_error(&error);
g_free(debug);
session->end();
break;
default:
break;
}
return TRUE;
} }
GstWebRTCSessionDescription* void
parseSDP(const std::string &sdp, GstWebRTCSDPType type) iceConnectionStateChanged(GstElement *webrtc,
GParamSpec *pspec G_GNUC_UNUSED,
gpointer user_data G_GNUC_UNUSED)
{ {
GstSDPMessage *msg; GstWebRTCICEConnectionState newState;
gst_sdp_message_new(&msg); g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
if (gst_sdp_message_parse_buffer((guint8*)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) { switch (newState) {
return gst_webrtc_session_description_new(type, msg); case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
} nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking");
else { emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING);
nhlog::ui()->error("WebRTC: failed to parse remote session description"); break;
gst_object_unref(msg); case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
return nullptr; nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
} emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ICEFAILED);
break;
default:
break;
}
} }
void void
generateOffer(GstElement *webrtc) linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
{ {
// create-offer first, then set-local-description GstCaps *caps = gst_pad_get_current_caps(newpad);
GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); if (!caps)
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise); return;
const gchar *name = gst_structure_get_name(gst_caps_get_structure(caps, 0));
gst_caps_unref(caps);
GstPad *queuepad = nullptr;
if (g_str_has_prefix(name, "audio")) {
nhlog::ui()->debug("WebRTC: received incoming audio stream");
GstElement *queue = gst_element_factory_make("queue", nullptr);
GstElement *convert = gst_element_factory_make("audioconvert", nullptr);
GstElement *resample = gst_element_factory_make("audioresample", nullptr);
GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr);
gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
gst_element_sync_state_with_parent(queue);
gst_element_sync_state_with_parent(convert);
gst_element_sync_state_with_parent(resample);
gst_element_sync_state_with_parent(sink);
gst_element_link_many(queue, convert, resample, sink, nullptr);
queuepad = gst_element_get_static_pad(queue, "sink");
}
if (queuepad) {
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
nhlog::ui()->error("WebRTC: unable to link new pad");
else {
emit WebRTCSession::instance().stateChanged(
WebRTCSession::State::CONNECTED);
}
gst_object_unref(queuepad);
}
} }
void void
setLocalDescription(GstPromise *promise, gpointer webrtc) addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
{ {
const GstStructure *reply = gst_promise_get_reply(promise); if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC)
gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer")); return;
GstWebRTCSessionDescription *gstsdp = nullptr;
gst_structure_get(reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr); nhlog::ui()->debug("WebRTC: received incoming stream");
gst_promise_unref(promise); GstElement *decodebin = gst_element_factory_make("decodebin", nullptr);
g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
gst_bin_add(GST_BIN(pipe), decodebin);
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); gst_element_sync_state_with_parent(decodebin);
localsdp_ = std::string(sdp); GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink");
g_free(sdp); if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad)))
gst_webrtc_session_description_free(gstsdp); nhlog::ui()->error("WebRTC: unable to link new pad");
gst_object_unref(sinkpad);
nhlog::ui()->debug("WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
} }
void std::string::const_iterator
addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED) findName(const std::string &sdp, const std::string &name)
{ {
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); return std::search(
sdp.cbegin(),
sdp.cend(),
name.cbegin(),
name.cend(),
[](unsigned char c1, unsigned char c2) { return std::tolower(c1) == std::tolower(c2); });
}
if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) { int
emit WebRTCSession::instance().newICECandidate({"audio", (uint16_t)mlineIndex, candidate}); getPayloadType(const std::string &sdp, const std::string &name)
return; {
} // eg a=rtpmap:111 opus/48000/2
auto e = findName(sdp, name);
if (e == sdp.cend()) {
nhlog::ui()->error("WebRTC: remote offer - " + name + " attribute missing");
return -1;
}
if (auto s = sdp.rfind(':', e - sdp.cbegin()); s == std::string::npos) {
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name +
" payload type");
return -1;
} else {
++s;
try {
return std::stoi(std::string(sdp, s, e - sdp.cbegin() - s));
} catch (...) {
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name +
" payload type");
}
}
return -1;
}
localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate}); }
// GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early bool
// fixed in v1.18 WebRTCSession::createOffer()
// use a 100ms timeout in the meantime {
static guint timerid = 0; isoffering_ = true;
if (timerid) localsdp_.clear();
g_source_remove(timerid); localcandidates_.clear();
return startPipeline(111); // a dynamic opus payload type
}
timerid = g_timeout_add(100, onICEGatheringCompletion, &timerid); bool
WebRTCSession::acceptOffer(const std::string &sdp)
{
nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
if (state_ != State::DISCONNECTED)
return false;
isoffering_ = false;
localsdp_.clear();
localcandidates_.clear();
int opusPayloadType = getPayloadType(sdp, "opus");
if (opusPayloadType == -1)
return false;
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
if (!offer)
return false;
if (!startPipeline(opusPayloadType)) {
gst_webrtc_session_description_free(offer);
return false;
}
// set-remote-description first, then create-answer
GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
gst_webrtc_session_description_free(offer);
return true;
} }
gboolean bool
onICEGatheringCompletion(gpointer timerid) WebRTCSession::acceptAnswer(const std::string &sdp)
{ {
*(guint*)(timerid) = 0; nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
if (isoffering_) { if (state_ != State::OFFERSENT)
emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); return false;
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
} GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
else { if (!answer) {
emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); end();
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT); return false;
} }
return FALSE;
g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
gst_webrtc_session_description_free(answer);
return true;
} }
void void
iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) WebRTCSession::acceptICECandidates(
const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
{ {
GstWebRTCICEConnectionState newState; if (state_ >= State::INITIATED) {
g_object_get(webrtc, "ice-connection-state", &newState, nullptr); for (const auto &c : candidates) {
switch (newState) { nhlog::ui()->debug(
case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); g_signal_emit_by_name(
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING); webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
break; }
case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: }
nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ICEFAILED);
break;
default:
break;
}
} }
void bool
createAnswer(GstPromise *promise, gpointer webrtc) WebRTCSession::startPipeline(int opusPayloadType)
{ {
// create-answer first, then set-local-description if (state_ != State::DISCONNECTED)
gst_promise_unref(promise); return false;
promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise); emit stateChanged(State::INITIATING);
if (!createPipeline(opusPayloadType))
return false;
webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
if (!stunServer_.empty()) {
nhlog::ui()->info("WebRTC: setting STUN server: {}", stunServer_);
g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr);
}
for (const auto &uri : turnServers_) {
nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
gboolean udata;
g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
}
if (turnServers_.empty())
nhlog::ui()->warn("WebRTC: no TURN server provided");
// generate the offer when the pipeline goes to PLAYING
if (isoffering_)
g_signal_connect(
webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr);
// on-ice-candidate is emitted when a local ICE candidate has been gathered
g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
// capture ICE failure
g_signal_connect(
webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr);
// incoming streams trigger pad-added
gst_element_set_state(pipe_, GST_STATE_READY);
g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
// webrtcbin lifetime is the same as that of the pipeline
gst_object_unref(webrtc_);
// start the pipeline
GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
nhlog::ui()->error("WebRTC: unable to start pipeline");
end();
return false;
}
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
gst_bus_add_watch(bus, newBusMessage, this);
gst_object_unref(bus);
emit stateChanged(State::INITIATED);
return true;
} }
void #define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
bool
WebRTCSession::createPipeline(int opusPayloadType)
{ {
if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) std::string pipeline("webrtcbin bundle-policy=max-bundle name=webrtcbin "
return; "autoaudiosrc ! volume name=srclevel ! audioconvert ! "
"audioresample ! queue ! opusenc ! rtpopuspay ! "
nhlog::ui()->debug("WebRTC: received incoming stream"); "queue ! " RTP_CAPS_OPUS +
GstElement *decodebin = gst_element_factory_make("decodebin", nullptr); std::to_string(opusPayloadType) + " ! webrtcbin.");
g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
gst_bin_add(GST_BIN(pipe), decodebin); webrtc_ = nullptr;
gst_element_sync_state_with_parent(decodebin); GError *error = nullptr;
GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); pipe_ = gst_parse_launch(pipeline.c_str(), &error);
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) if (error) {
nhlog::ui()->error("WebRTC: unable to link new pad"); nhlog::ui()->error("WebRTC: failed to parse pipeline: {}", error->message);
gst_object_unref(sinkpad); g_error_free(error);
end();
return false;
}
return true;
} }
void bool
linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe) WebRTCSession::toggleMuteAudioSrc(bool &isMuted)
{ {
GstCaps *caps = gst_pad_get_current_caps(newpad); if (state_ < State::INITIATED)
if (!caps) return false;
return;
GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
const gchar *name = gst_structure_get_name(gst_caps_get_structure(caps, 0)); if (!srclevel)
gst_caps_unref(caps); return false;
GstPad *queuepad = nullptr; gboolean muted;
if (g_str_has_prefix(name, "audio")) { g_object_get(srclevel, "mute", &muted, nullptr);
nhlog::ui()->debug("WebRTC: received incoming audio stream"); g_object_set(srclevel, "mute", !muted, nullptr);
GstElement *queue = gst_element_factory_make("queue", nullptr); gst_object_unref(srclevel);
GstElement *convert = gst_element_factory_make("audioconvert", nullptr); isMuted = !muted;
GstElement *resample = gst_element_factory_make("audioresample", nullptr); return true;
GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr);
gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
gst_element_sync_state_with_parent(queue);
gst_element_sync_state_with_parent(convert);
gst_element_sync_state_with_parent(resample);
gst_element_sync_state_with_parent(sink);
gst_element_link_many(queue, convert, resample, sink, nullptr);
queuepad = gst_element_get_static_pad(queue, "sink");
}
if (queuepad) {
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
nhlog::ui()->error("WebRTC: unable to link new pad");
else {
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTED);
}
gst_object_unref(queuepad);
}
} }
void
WebRTCSession::end()
{
nhlog::ui()->debug("WebRTC: ending session");
if (pipe_) {
gst_element_set_state(pipe_, GST_STATE_NULL);
gst_object_unref(pipe_);
pipe_ = nullptr;
}
webrtc_ = nullptr;
if (state_ != State::DISCONNECTED)
emit stateChanged(State::DISCONNECTED);
} }

@ -14,52 +14,55 @@ class WebRTCSession : public QObject
Q_OBJECT Q_OBJECT
public: public:
enum class State { enum class State
ICEFAILED, {
DISCONNECTED, DISCONNECTED,
INITIATING, ICEFAILED,
INITIATED, INITIATING,
OFFERSENT, INITIATED,
ANSWERSENT, OFFERSENT,
CONNECTING, ANSWERSENT,
CONNECTED CONNECTING,
CONNECTED
}; };
static WebRTCSession& instance() static WebRTCSession &instance()
{ {
static WebRTCSession instance; static WebRTCSession instance;
return instance; return instance;
} }
bool init(std::string *errorMessage = nullptr); bool init(std::string *errorMessage = nullptr);
State state() const {return state_;} State state() const { return state_; }
bool createOffer(); bool createOffer();
bool acceptOffer(const std::string &sdp); bool acceptOffer(const std::string &sdp);
bool acceptAnswer(const std::string &sdp); bool acceptAnswer(const std::string &sdp);
void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate>&); void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
bool toggleMuteAudioSrc(bool &isMuted); bool toggleMuteAudioSrc(bool &isMuted);
void end(); void end();
void setStunServer(const std::string &stunServer) {stunServer_ = stunServer;} void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; }
void setTurnServers(const std::vector<std::string> &uris) {turnServers_ = uris;} void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
signals: signals:
void offerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&); void offerCreated(const std::string &sdp,
void answerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&); const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
void newICECandidate(const mtx::events::msg::CallCandidates::Candidate&); void answerCreated(const std::string &sdp,
const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &);
void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt
private slots: private slots:
void setState(State state) {state_ = state;} void setState(State state) { state_ = state; }
private: private:
WebRTCSession(); WebRTCSession();
bool initialised_ = false; bool initialised_ = false;
State state_ = State::DISCONNECTED; State state_ = State::DISCONNECTED;
GstElement *pipe_ = nullptr; GstElement *pipe_ = nullptr;
GstElement *webrtc_ = nullptr; GstElement *webrtc_ = nullptr;
std::string stunServer_; std::string stunServer_;
std::vector<std::string> turnServers_; std::vector<std::string> turnServers_;
@ -68,6 +71,6 @@ private:
bool createPipeline(int opusPayloadType); bool createPipeline(int opusPayloadType);
public: public:
WebRTCSession(WebRTCSession const&) = delete; WebRTCSession(WebRTCSession const &) = delete;
void operator=(WebRTCSession const&) = delete; void operator=(WebRTCSession const &) = delete;
}; };

@ -1,4 +1,5 @@
#include <QLabel> #include <QLabel>
#include <QPixmap>
#include <QPushButton> #include <QPushButton>
#include <QString> #include <QString>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -10,12 +11,12 @@
namespace dialogs { namespace dialogs {
AcceptCall::AcceptCall( AcceptCall::AcceptCall(const QString &caller,
const QString &caller, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl,
const QString &avatarUrl, QWidget *parent)
QWidget *parent) : QWidget(parent) : QWidget(parent)
{ {
setAutoFillBackground(true); setAutoFillBackground(true);
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
@ -39,8 +40,8 @@ AcceptCall::AcceptCall(
if (!displayName.isEmpty() && displayName != caller) { if (!displayName.isEmpty() && displayName != caller) {
displayNameLabel = new QLabel(displayName, this); displayNameLabel = new QLabel(displayName, this);
labelFont.setPointSizeF(f.pointSizeF() * 2); labelFont.setPointSizeF(f.pointSizeF() * 2);
displayNameLabel ->setFont(labelFont); displayNameLabel->setFont(labelFont);
displayNameLabel ->setAlignment(Qt::AlignCenter); displayNameLabel->setAlignment(Qt::AlignCenter);
} }
QLabel *callerLabel = new QLabel(caller, this); QLabel *callerLabel = new QLabel(caller, this);
@ -48,19 +49,23 @@ AcceptCall::AcceptCall(
callerLabel->setFont(labelFont); callerLabel->setFont(labelFont);
callerLabel->setAlignment(Qt::AlignCenter); callerLabel->setAlignment(Qt::AlignCenter);
QLabel *voiceCallLabel = new QLabel("Voice Call", this);
labelFont.setPointSizeF(f.pointSizeF() * 1.1);
voiceCallLabel->setFont(labelFont);
voiceCallLabel->setAlignment(Qt::AlignCenter);
auto avatar = new Avatar(this, QFontMetrics(f).height() * 6); auto avatar = new Avatar(this, QFontMetrics(f).height() * 6);
if (!avatarUrl.isEmpty()) if (!avatarUrl.isEmpty())
avatar->setImage(avatarUrl); avatar->setImage(avatarUrl);
else else
avatar->setLetter(utils::firstChar(roomName)); avatar->setLetter(utils::firstChar(roomName));
const int iconSize = 24;
QLabel *callTypeIndicator = new QLabel(this);
QPixmap callIndicator(":/icons/icons/ui/place-call.png");
callTypeIndicator->setPixmap(callIndicator.scaled(iconSize * 2, iconSize * 2));
QLabel *callTypeLabel = new QLabel("Voice Call", this);
labelFont.setPointSizeF(f.pointSizeF() * 1.1);
callTypeLabel->setFont(labelFont);
callTypeLabel->setAlignment(Qt::AlignCenter);
const int iconSize = 24; auto buttonLayout = new QHBoxLayout;
auto buttonLayout = new QHBoxLayout();
buttonLayout->setSpacing(20); buttonLayout->setSpacing(20);
acceptBtn_ = new QPushButton(tr("Accept"), this); acceptBtn_ = new QPushButton(tr("Accept"), this);
acceptBtn_->setDefault(true); acceptBtn_->setDefault(true);
@ -74,10 +79,11 @@ AcceptCall::AcceptCall(
buttonLayout->addWidget(rejectBtn_); buttonLayout->addWidget(rejectBtn_);
if (displayNameLabel) if (displayNameLabel)
layout->addWidget(displayNameLabel, 0, Qt::AlignCenter); layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
layout->addWidget(callerLabel, 0, Qt::AlignCenter); layout->addWidget(callerLabel, 0, Qt::AlignCenter);
layout->addWidget(voiceCallLabel, 0, Qt::AlignCenter);
layout->addWidget(avatar, 0, Qt::AlignCenter); layout->addWidget(avatar, 0, Qt::AlignCenter);
layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
layout->addLayout(buttonLayout); layout->addLayout(buttonLayout);
connect(acceptBtn_, &QPushButton::clicked, this, [this]() { connect(acceptBtn_, &QPushButton::clicked, this, [this]() {

@ -12,12 +12,11 @@ class AcceptCall : public QWidget
Q_OBJECT Q_OBJECT
public: public:
AcceptCall( AcceptCall(const QString &caller,
const QString &caller, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl,
const QString &avatarUrl, QWidget *parent = nullptr);
QWidget *parent = nullptr);
signals: signals:
void accept(); void accept();

@ -10,12 +10,12 @@
namespace dialogs { namespace dialogs {
PlaceCall::PlaceCall( PlaceCall::PlaceCall(const QString &callee,
const QString &callee, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl,
const QString &avatarUrl, QWidget *parent)
QWidget *parent) : QWidget(parent) : QWidget(parent)
{ {
setAutoFillBackground(true); setAutoFillBackground(true);
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
@ -34,11 +34,13 @@ PlaceCall::PlaceCall(
f.setPointSizeF(f.pointSizeF()); f.setPointSizeF(f.pointSizeF());
auto avatar = new Avatar(this, QFontMetrics(f).height() * 3); auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
if (!avatarUrl.isEmpty()) if (!avatarUrl.isEmpty())
avatar->setImage(avatarUrl); avatar->setImage(avatarUrl);
else else
avatar->setLetter(utils::firstChar(roomName)); avatar->setLetter(utils::firstChar(roomName));
const int iconSize = 24;
voiceBtn_ = new QPushButton(tr("Voice Call"), this); voiceBtn_ = new QPushButton(tr("Voice"), this);
voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
voiceBtn_->setIconSize(QSize(iconSize, iconSize));
voiceBtn_->setDefault(true); voiceBtn_->setDefault(true);
cancelBtn_ = new QPushButton(tr("Cancel"), this); cancelBtn_ = new QPushButton(tr("Cancel"), this);
@ -47,7 +49,7 @@ PlaceCall::PlaceCall(
buttonLayout->addWidget(voiceBtn_); buttonLayout->addWidget(voiceBtn_);
buttonLayout->addWidget(cancelBtn_); buttonLayout->addWidget(cancelBtn_);
QString name = displayName.isEmpty() ? callee : displayName; QString name = displayName.isEmpty() ? callee : displayName;
QLabel *label = new QLabel("Place a call to " + name + "?", this); QLabel *label = new QLabel("Place a call to " + name + "?", this);
layout->addWidget(label); layout->addWidget(label);

@ -12,12 +12,11 @@ class PlaceCall : public QWidget
Q_OBJECT Q_OBJECT
public: public:
PlaceCall( PlaceCall(const QString &callee,
const QString &callee, const QString &displayName,
const QString &displayName, const QString &roomName,
const QString &roomName, const QString &avatarUrl,
const QString &avatarUrl, QWidget *parent = nullptr);
QWidget *parent = nullptr);
signals: signals:
void voice(); void voice();

@ -796,9 +796,11 @@ TimelineModel::internalAddEvents(
} else if (std::holds_alternative<mtx::events::RoomEvent< } else if (std::holds_alternative<mtx::events::RoomEvent<
mtx::events::msg::CallCandidates>>(e_) || mtx::events::msg::CallCandidates>>(e_) ||
std::holds_alternative< std::holds_alternative<
mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>( e_) || mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>(
e_) ||
std::holds_alternative< std::holds_alternative<
mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>( e_)) { mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>(
e_)) {
emit newCallEvent(e_); emit newCallEvent(e_);
} }
} }

Loading…
Cancel
Save