Polish voice call UI

This commit is contained in:
trilene 2020-07-22 21:15:45 -04:00
parent da9995fc3d
commit 88cfa3a8fa
18 changed files with 348 additions and 146 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

View file

@ -70,6 +70,11 @@
<file>icons/ui/mail-reply.png</file> <file>icons/ui/mail-reply.png</file>
<file>icons/ui/place-call.png</file>
<file>icons/ui/end-call.png</file>
<file>icons/ui/microphone-mute.png</file>
<file>icons/ui/microphone-unmute.png</file>
<file>icons/emoji-categories/people.png</file> <file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/people@2x.png</file> <file>icons/emoji-categories/people@2x.png</file>
<file>icons/emoji-categories/nature.png</file> <file>icons/emoji-categories/nature.png</file>

View file

@ -1,10 +1,17 @@
#include <cstdio>
#include <QDateTime>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QIcon> #include <QIcon>
#include <QLabel> #include <QLabel>
#include <QString> #include <QString>
#include <QTimer>
#include "ActiveCallBar.h" #include "ActiveCallBar.h"
#include "ChatPage.h"
#include "Utils.h"
#include "WebRTCSession.h" #include "WebRTCSession.h"
#include "ui/Avatar.h"
#include "ui/FlatButton.h" #include "ui/FlatButton.h"
ActiveCallBar::ActiveCallBar(QWidget *parent) ActiveCallBar::ActiveCallBar(QWidget *parent)
@ -12,7 +19,7 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
{ {
setAutoFillBackground(true); setAutoFillBackground(true);
auto p = palette(); auto p = palette();
p.setColor(backgroundRole(), Qt::green); p.setColor(backgroundRole(), QColorConstants::Svg::limegreen);
setPalette(p); setPalette(p);
QFont f; QFont f;
@ -24,51 +31,126 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
setFixedHeight(contentHeight + widgetMargin); setFixedHeight(contentHeight + widgetMargin);
topLayout_ = new QHBoxLayout(this); layout_ = new QHBoxLayout(this);
topLayout_->setSpacing(widgetMargin); layout_->setSpacing(widgetMargin);
topLayout_->setContentsMargins( layout_->setContentsMargins(
2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
topLayout_->setSizeConstraint(QLayout::SetMinimumSize);
QFont labelFont; QFont labelFont;
labelFont.setPointSizeF(labelFont.pointSizeF() * 1.2); labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1);
labelFont.setWeight(QFont::Medium); labelFont.setWeight(QFont::Medium);
avatar_ = new Avatar(this, QFontMetrics(f).height() * 2.5);
callPartyLabel_ = new QLabel(this); callPartyLabel_ = new QLabel(this);
callPartyLabel_->setFont(labelFont); callPartyLabel_->setFont(labelFont);
// TODO microphone mute/unmute icons stateLabel_ = new QLabel(this);
stateLabel_->setFont(labelFont);
durationLabel_ = new QLabel(this);
durationLabel_->setFont(labelFont);
durationLabel_->hide();
muteBtn_ = new FlatButton(this); muteBtn_ = new FlatButton(this);
QIcon muteIcon; setMuteIcon(false);
muteIcon.addFile(":/icons/icons/ui/do-not-disturb-rounded-sign.png");
muteBtn_->setIcon(muteIcon);
muteBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
muteBtn_->setToolTip(tr("Mute Mic"));
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_))
QIcon icon; setMuteIcon(muted_);
if (muted_) {
muteBtn_->setToolTip("Unmute Mic");
icon.addFile(":/icons/icons/ui/round-remove-button.png");
} else {
muteBtn_->setToolTip("Mute Mic");
icon.addFile(":/icons/icons/ui/do-not-disturb-rounded-sign.png");
}
muteBtn_->setIcon(icon);
}
}); });
topLayout_->addWidget(callPartyLabel_, 0, Qt::AlignLeft); layout_->addWidget(avatar_, 0, Qt::AlignLeft);
topLayout_->addWidget(muteBtn_, 0, Qt::AlignRight); layout_->addWidget(callPartyLabel_, 0, Qt::AlignLeft);
layout_->addWidget(stateLabel_, 0, Qt::AlignLeft);
layout_->addWidget(durationLabel_, 0, Qt::AlignLeft);
layout_->addStretch();
layout_->addWidget(muteBtn_, 0, Qt::AlignCenter);
layout_->addSpacing(18);
timer_ = new QTimer(this);
connect(timer_, &QTimer::timeout, this,
[this](){
auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_;
int s = seconds % 60;
int m = (seconds / 60) % 60;
int h = seconds / 3600;
char buf[12];
if (h)
snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s);
else
snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s);
durationLabel_->setText(buf);
});
connect(&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update);
} }
void void
ActiveCallBar::setCallParty(const QString &userid, const QString &displayName) ActiveCallBar::setMuteIcon(bool muted)
{ {
if (!displayName.isEmpty() && displayName != userid) QIcon icon;
callPartyLabel_->setText("Active Call: " + displayName + " (" + userid + ")"); if (muted) {
else muteBtn_->setToolTip("Unmute Mic");
callPartyLabel_->setText("Active Call: " + userid); icon.addFile(":/icons/icons/ui/microphone-unmute.png");
} else {
muteBtn_->setToolTip("Mute Mic");
icon.addFile(":/icons/icons/ui/microphone-mute.png");
}
muteBtn_->setIcon(icon);
muteBtn_->setIconSize(QSize(buttonSize_, buttonSize_));
}
void
ActiveCallBar::setCallParty(
const QString &userid,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl)
{
callPartyLabel_->setText(
(displayName.isEmpty() ? userid : displayName) + " -");
if (!avatarUrl.isEmpty())
avatar_->setImage(avatarUrl);
else
avatar_->setLetter(utils::firstChar(roomName));
}
void
ActiveCallBar::update(WebRTCSession::State state)
{
switch (state) {
case WebRTCSession::State::INITIATING:
stateLabel_->setText("Initiating call...");
break;
case WebRTCSession::State::INITIATED:
stateLabel_->setText("Call initiated...");
break;
case WebRTCSession::State::OFFERSENT:
stateLabel_->setText("Calling...");
break;
case WebRTCSession::State::CONNECTING:
stateLabel_->setText("Connecting...");
break;
case WebRTCSession::State::CONNECTED:
callStartTime_ = QDateTime::currentSecsSinceEpoch();
timer_->start(1000);
stateLabel_->setText("Active call:");
durationLabel_->setText("00:00");
durationLabel_->show();
muteBtn_->show();
break;
case WebRTCSession::State::DISCONNECTED:
timer_->stop();
callPartyLabel_->setText(QString());
stateLabel_->setText(QString());
durationLabel_->setText(QString());
durationLabel_->hide();
setMuteIcon(false);
break;
default:
break;
}
} }

View file

@ -2,9 +2,12 @@
#include <QWidget> #include <QWidget>
#include "WebRTCSession.h"
class QHBoxLayout; class QHBoxLayout;
class QLabel; class QLabel;
class QString; class QTimer;
class Avatar;
class FlatButton; class FlatButton;
class ActiveCallBar : public QWidget class ActiveCallBar : public QWidget
@ -15,12 +18,24 @@ public:
ActiveCallBar(QWidget *parent = nullptr); ActiveCallBar(QWidget *parent = nullptr);
public slots: public slots:
void setCallParty(const QString &userid, const QString &displayName); void update(WebRTCSession::State);
void setCallParty(
const QString &userid,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl);
private: private:
QHBoxLayout *topLayout_ = nullptr; QHBoxLayout *layout_ = nullptr;
Avatar *avatar_ = nullptr;
QLabel *callPartyLabel_ = nullptr; QLabel *callPartyLabel_ = nullptr;
QLabel *stateLabel_ = nullptr;
QLabel *durationLabel_ = nullptr;
FlatButton *muteBtn_ = nullptr; FlatButton *muteBtn_ = nullptr;
int buttonSize_ = 32; int buttonSize_ = 22;
bool muted_ = false; bool muted_ = false;
qint64 callStartTime_ = 0;
QTimer *timer_ = nullptr;
void setMuteIcon(bool muted);
}; };

View file

@ -68,9 +68,9 @@ CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
turnServerTimer_.setInterval(res.ttl * 1000 * 0.9); turnServerTimer_.setInterval(res.ttl * 1000 * 0.9);
}); });
connect(&session_, &WebRTCSession::pipelineChanged, this, connect(&session_, &WebRTCSession::stateChanged, this,
[this](bool started) { [this](WebRTCSession::State state) {
if (!started) if (state == WebRTCSession::State::DISCONNECTED)
playRingtone("qrc:/media/media/callend.ogg", false); playRingtone("qrc:/media/media/callend.ogg", false);
}); });
@ -87,9 +87,9 @@ CallManager::sendInvite(const QString &roomid)
if (onActiveCall()) if (onActiveCall())
return; return;
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
if (members.size() != 2) { if (roomInfo.member_count != 2) {
emit ChatPage::instance()->showNotification("Voice/Video calls are limited to 1:1 rooms"); emit ChatPage::instance()->showNotification("Voice calls are limited to 1:1 rooms.");
return; return;
} }
@ -105,11 +105,13 @@ CallManager::sendInvite(const QString &roomid)
// TODO Add invite timeout // TODO Add invite timeout
generateCallID(); generateCallID();
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front();
emit newCallParty(callee.user_id, callee.display_name); emit newCallParty(callee.user_id, callee.display_name,
QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url));
playRingtone("qrc:/media/media/ringback.ogg", true); playRingtone("qrc:/media/media/ringback.ogg", true);
if (!session_.createOffer()) { if (!session_.createOffer()) {
emit ChatPage::instance()->showNotification("Problem setting up call"); emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall(); endCall();
} }
} }
@ -127,7 +129,7 @@ CallManager::hangUp()
bool bool
CallManager::onActiveCall() CallManager::onActiveCall()
{ {
return session_.isActive(); return session_.state() != WebRTCSession::State::DISCONNECTED;
} }
void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
@ -156,8 +158,8 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
if (callInviteEvent.content.call_id.empty()) if (callInviteEvent.content.call_id.empty())
return; return;
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
if (onActiveCall() || members.size() != 2) { if (onActiveCall() || roomInfo.member_count != 2) {
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, 0, CallHangUp::Reason::InviteTimeOut});
return; return;
@ -168,10 +170,18 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
callid_ = callInviteEvent.content.call_id; callid_ = callInviteEvent.content.call_id;
remoteICECandidates_.clear(); 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 =
members.front().user_id == utils::localUser() ? members.back() : members.front();
emit newCallParty(caller.user_id, caller.display_name,
QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url));
auto dialog = new dialogs::AcceptCall(caller.user_id, caller.display_name, MainWindow::instance()); auto dialog = new dialogs::AcceptCall(
caller.user_id,
caller.display_name,
QString::fromStdString(roomInfo.name),
QString::fromStdString(roomInfo.avatar_url),
MainWindow::instance());
connect(dialog, &dialogs::AcceptCall::accept, this, connect(dialog, &dialogs::AcceptCall::accept, this,
[this, callInviteEvent](){ [this, callInviteEvent](){
MainWindow::instance()->hideOverlay(); MainWindow::instance()->hideOverlay();
@ -198,7 +208,7 @@ CallManager::answerInvite(const CallInvite &invite)
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
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;
} }
@ -232,6 +242,7 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() && if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) { callid_ == callAnswerEvent.content.call_id) {
emit ChatPage::instance()->showNotification("Call answered on another device.");
stopRingtone(); stopRingtone();
MainWindow::instance()->hideOverlay(); MainWindow::instance()->hideOverlay();
return; return;
@ -240,7 +251,7 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) { if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone(); stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call"); emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp(); hangUp();
} }
} }

View file

@ -36,7 +36,11 @@ signals:
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(const QString &userid, const QString& displayName); void newCallParty(
const QString &userid,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl);
private slots: private slots:
void retrieveTurnServer(); void retrieveTurnServer();

View file

@ -138,13 +138,13 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect( connect(
&callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty); &callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty);
connect(&WebRTCSession::instance(), connect(&WebRTCSession::instance(),
&WebRTCSession::pipelineChanged, &WebRTCSession::stateChanged,
this, this,
[this](bool callStarted) { [this](WebRTCSession::State state) {
if (callStarted) if (state == WebRTCSession::State::DISCONNECTED)
activeCallBar_->show();
else
activeCallBar_->hide(); activeCallBar_->hide();
else
activeCallBar_->show();
}); });
// Splitter // Splitter
@ -469,22 +469,28 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
if (callManager_.onActiveCall()) { if (callManager_.onActiveCall()) {
callManager_.hangUp(); callManager_.hangUp();
} else { } else {
if (cache::singleRoomInfo(current_room_.toStdString()).member_count != 2) { if (auto roomInfo =
showNotification("Voice/Video calls are limited to 1:1 rooms"); cache::singleRoomInfo(current_room_.toStdString());
roomInfo.member_count != 2) {
showNotification("Voice calls are limited to 1:1 rooms.");
} else { } else {
std::vector<RoomMember> members( std::vector<RoomMember> members(
cache::getMembers(current_room_.toStdString())); cache::getMembers(current_room_.toStdString()));
const RoomMember &callee = const RoomMember &callee =
members.front().user_id == utils::localUser() ? members.back() members.front().user_id == utils::localUser() ? members.back()
: members.front(); : members.front();
auto dialog = auto dialog = new dialogs::PlaceCall(
new dialogs::PlaceCall(callee.user_id, callee.display_name, MainWindow::instance()); callee.user_id,
callee.display_name,
QString::fromStdString(roomInfo.name),
QString::fromStdString(roomInfo.avatar_url),
MainWindow::instance());
connect(dialog, &dialogs::PlaceCall::voice, this, [this]() { connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
callManager_.sendInvite(current_room_); callManager_.sendInvite(current_room_);
}); });
connect(dialog, &dialogs::PlaceCall::video, this, [this]() { /*connect(dialog, &dialogs::PlaceCall::video, this, [this]() {
showNotification("Video calls not yet implemented"); showNotification("Video calls not yet implemented.");
}); });*/
utils::centerWidget(dialog, MainWindow::instance()); utils::centerWidget(dialog, MainWindow::instance());
dialog->show(); dialog->show();
} }

View file

@ -31,7 +31,6 @@
#include "Logging.h" #include "Logging.h"
#include "TextInputWidget.h" #include "TextInputWidget.h"
#include "Utils.h" #include "Utils.h"
#include "WebRTCSession.h"
#include "ui/FlatButton.h" #include "ui/FlatButton.h"
#include "ui/LoadingIndicator.h" #include "ui/LoadingIndicator.h"
@ -455,9 +454,9 @@ TextInputWidget::TextInputWidget(QWidget *parent)
topLayout_->setContentsMargins(13, 1, 13, 0); topLayout_->setContentsMargins(13, 1, 13, 0);
callBtn_ = new FlatButton(this); callBtn_ = new FlatButton(this);
changeCallButtonState(false); changeCallButtonState(WebRTCSession::State::DISCONNECTED);
connect(&WebRTCSession::instance(), connect(&WebRTCSession::instance(),
&WebRTCSession::pipelineChanged, &WebRTCSession::stateChanged,
this, this,
&TextInputWidget::changeCallButtonState); &TextInputWidget::changeCallButtonState);
@ -664,17 +663,16 @@ TextInputWidget::paintEvent(QPaintEvent *)
} }
void void
TextInputWidget::changeCallButtonState(bool callStarted) TextInputWidget::changeCallButtonState(WebRTCSession::State state)
{ {
// TODO Telephone and HangUp icons - co-opt the ones below for now
QIcon icon; QIcon icon;
if (callStarted) { if (state == WebRTCSession::State::DISCONNECTED) {
callBtn_->setToolTip(tr("Hang up"));
icon.addFile(":/icons/icons/ui/remove-symbol.png");
} else {
callBtn_->setToolTip(tr("Place a call")); callBtn_->setToolTip(tr("Place a call"));
icon.addFile(":/icons/icons/ui/speech-bubbles-comment-option.png"); icon.addFile(":/icons/icons/ui/place-call.png");
} else {
callBtn_->setToolTip(tr("Hang up"));
icon.addFile(":/icons/icons/ui/end-call.png");
} }
callBtn_->setIcon(icon); callBtn_->setIcon(icon);
callBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); callBtn_->setIconSize(QSize(ButtonHeight * 1.1, ButtonHeight * 1.1));
} }

View file

@ -26,6 +26,7 @@
#include <QTextEdit> #include <QTextEdit>
#include <QWidget> #include <QWidget>
#include "WebRTCSession.h"
#include "dialogs/PreviewUploadOverlay.h" #include "dialogs/PreviewUploadOverlay.h"
#include "emoji/PickButton.h" #include "emoji/PickButton.h"
#include "popups/SuggestionsPopup.h" #include "popups/SuggestionsPopup.h"
@ -149,7 +150,7 @@ public slots:
void openFileSelection(); void openFileSelection();
void hideUploadSpinner(); void hideUploadSpinner();
void focusLineEdit() { input_->setFocus(); } void focusLineEdit() { input_->setFocus(); }
void changeCallButtonState(bool callStarted); void changeCallButtonState(WebRTCSession::State);
private slots: private slots:
void addSelectedEmoji(const QString &emoji); void addSelectedEmoji(const QString &emoji);

View file

@ -11,6 +11,8 @@ extern "C" {
#include "gst/webrtc/webrtc.h" #include "gst/webrtc/webrtc.h"
} }
Q_DECLARE_METATYPE(WebRTCSession::State)
namespace { namespace {
bool gisoffer; bool gisoffer;
std::string glocalsdp; std::string glocalsdp;
@ -29,6 +31,12 @@ std::string::const_iterator findName(const std::string &sdp, const std::string
int getPayloadType(const std::string &sdp, const std::string &name); int getPayloadType(const std::string &sdp, const std::string &name);
} }
WebRTCSession::WebRTCSession() : QObject()
{
qRegisterMetaType<WebRTCSession::State>();
connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
}
bool bool
WebRTCSession::init(std::string *errorMessage) WebRTCSession::init(std::string *errorMessage)
{ {
@ -54,14 +62,14 @@ WebRTCSession::init(std::string *errorMessage)
nhlog::ui()->info("Initialised " + gstVersion); nhlog::ui()->info("Initialised " + gstVersion);
// GStreamer Plugins: // GStreamer Plugins:
// Base: audioconvert, audioresample, opus, playback, videoconvert, volume // Base: audioconvert, audioresample, opus, playback, volume
// Good: autodetect, rtpmanager, vpx // Good: autodetect, rtpmanager, vpx
// 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", "audioresample", "autodetect", "dtls", "nice",
"opus", "playback", "rtpmanager", "srtp", "videoconvert", "vpx", "volume", "webrtc", nullptr}; "opus", "playback", "rtpmanager", "srtp", "vpx", "volume", "webrtc", nullptr};
GstRegistry *registry = gst_registry_get(); GstRegistry *registry = gst_registry_get();
for (guint i = 0; i < g_strv_length((gchar**)needed); i++) { for (guint i = 0; i < g_strv_length((gchar**)needed); i++) {
GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
@ -91,17 +99,19 @@ WebRTCSession::createOffer()
} }
bool bool
WebRTCSession::acceptOffer(const std::string& sdp) WebRTCSession::acceptOffer(const std::string &sdp)
{ {
nhlog::ui()->debug("Received offer:\n{}", sdp); nhlog::ui()->debug("Received offer:\n{}", sdp);
if (state_ != State::DISCONNECTED)
return false;
gisoffer = false; gisoffer = false;
glocalsdp.clear(); glocalsdp.clear();
gcandidates.clear(); gcandidates.clear();
int opusPayloadType = getPayloadType(sdp, "opus"); int opusPayloadType = getPayloadType(sdp, "opus");
if (opusPayloadType == -1) { if (opusPayloadType == -1)
return false; return false;
}
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
if (!offer) if (!offer)
@ -120,9 +130,11 @@ WebRTCSession::acceptOffer(const std::string& sdp)
bool bool
WebRTCSession::startPipeline(int opusPayloadType) WebRTCSession::startPipeline(int opusPayloadType)
{ {
if (isActive()) if (state_ != State::DISCONNECTED)
return false; return false;
emit stateChanged(State::INITIATING);
if (!createPipeline(opusPayloadType)) if (!createPipeline(opusPayloadType))
return false; return false;
@ -132,7 +144,12 @@ WebRTCSession::startPipeline(int opusPayloadType)
nhlog::ui()->info("WebRTC: Setting STUN server: {}", stunServer_); nhlog::ui()->info("WebRTC: Setting STUN server: {}", stunServer_);
g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr); g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr);
} }
addTurnServers();
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));
}
// generate the offer when the pipeline goes to PLAYING // generate the offer when the pipeline goes to PLAYING
if (gisoffer) if (gisoffer)
@ -152,16 +169,14 @@ WebRTCSession::startPipeline(int opusPayloadType)
GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) { if (ret == GST_STATE_CHANGE_FAILURE) {
nhlog::ui()->error("WebRTC: unable to start pipeline"); nhlog::ui()->error("WebRTC: unable to start pipeline");
gst_object_unref(pipe_); end();
pipe_ = nullptr;
webrtc_ = nullptr;
return false; return false;
} }
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
gst_bus_add_watch(bus, newBusMessage, this); gst_bus_add_watch(bus, newBusMessage, this);
gst_object_unref(bus); gst_object_unref(bus);
emit pipelineChanged(true); emit stateChanged(State::INITIATED);
return true; return true;
} }
@ -180,10 +195,7 @@ WebRTCSession::createPipeline(int opusPayloadType)
if (error) { if (error) {
nhlog::ui()->error("WebRTC: Failed to parse pipeline: {}", error->message); nhlog::ui()->error("WebRTC: Failed to parse pipeline: {}", error->message);
g_error_free(error); g_error_free(error);
if (pipe_) { end();
gst_object_unref(pipe_);
pipe_ = nullptr;
}
return false; return false;
} }
return true; return true;
@ -193,7 +205,7 @@ bool
WebRTCSession::acceptAnswer(const std::string &sdp) WebRTCSession::acceptAnswer(const std::string &sdp)
{ {
nhlog::ui()->debug("WebRTC: Received sdp:\n{}", sdp); nhlog::ui()->debug("WebRTC: Received sdp:\n{}", sdp);
if (!isActive()) if (state_ != State::OFFERSENT)
return false; return false;
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
@ -206,18 +218,20 @@ WebRTCSession::acceptAnswer(const std::string &sdp)
} }
void void
WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate>& candidates) WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
{ {
if (isActive()) { if (state_ >= State::INITIATED) {
for (const auto& c : candidates) for (const auto &c : candidates)
g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str()); g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
} }
if (state_ < State::CONNECTED)
emit stateChanged(State::CONNECTING);
} }
bool bool
WebRTCSession::toggleMuteAudioSrc(bool &isMuted) WebRTCSession::toggleMuteAudioSrc(bool &isMuted)
{ {
if (!isActive()) if (state_ < State::INITIATED)
return false; return false;
GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
@ -241,20 +255,7 @@ WebRTCSession::end()
pipe_ = nullptr; pipe_ = nullptr;
} }
webrtc_ = nullptr; webrtc_ = nullptr;
emit pipelineChanged(false); emit stateChanged(State::DISCONNECTED);
}
void
WebRTCSession::addTurnServers()
{
if (!webrtc_)
return;
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));
}
} }
namespace { namespace {
@ -373,8 +374,10 @@ gboolean
onICEGatheringCompletion(gpointer timerid) onICEGatheringCompletion(gpointer timerid)
{ {
*(guint*)(timerid) = 0; *(guint*)(timerid) = 0;
if (gisoffer) if (gisoffer) {
emit WebRTCSession::instance().offerCreated(glocalsdp, gcandidates); emit WebRTCSession::instance().offerCreated(glocalsdp, gcandidates);
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
}
else else
emit WebRTCSession::instance().answerCreated(glocalsdp, gcandidates); emit WebRTCSession::instance().answerCreated(glocalsdp, gcandidates);
@ -445,6 +448,9 @@ linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe
if (queuepad) { if (queuepad) {
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
nhlog::ui()->error("WebRTC: Unable to link new pad"); nhlog::ui()->error("WebRTC: Unable to link new pad");
else {
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTED);
}
gst_object_unref(queuepad); gst_object_unref(queuepad);
} }
} }

View file

@ -14,6 +14,15 @@ class WebRTCSession : public QObject
Q_OBJECT Q_OBJECT
public: public:
enum class State {
DISCONNECTED,
INITIATING,
INITIATED,
OFFERSENT,
CONNECTING,
CONNECTED
};
static WebRTCSession& instance() static WebRTCSession& instance()
{ {
static WebRTCSession instance; static WebRTCSession instance;
@ -27,7 +36,7 @@ public:
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 isActive() { return pipe_ != nullptr; } State state() const {return state_;}
bool toggleMuteAudioSrc(bool &isMuted); bool toggleMuteAudioSrc(bool &isMuted);
void end(); void end();
@ -37,12 +46,16 @@ public:
signals: signals:
void offerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&); void offerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&);
void answerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&); void answerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&);
void pipelineChanged(bool started); void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt
private slots:
void setState(State state) {state_ = state;}
private: private:
WebRTCSession() : QObject() {} WebRTCSession();
bool initialised_ = false; bool initialised_ = false;
State state_ = State::DISCONNECTED;
GstElement *pipe_ = nullptr; GstElement *pipe_ = nullptr;
GstElement *webrtc_ = nullptr; GstElement *webrtc_ = nullptr;
std::string stunServer_; std::string stunServer_;
@ -50,7 +63,6 @@ private:
bool startPipeline(int opusPayloadType); bool startPipeline(int opusPayloadType);
bool createPipeline(int opusPayloadType); bool createPipeline(int opusPayloadType);
void addTurnServers();
public: public:
WebRTCSession(WebRTCSession const&) = delete; WebRTCSession(WebRTCSession const&) = delete;

View file

@ -1,43 +1,83 @@
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QString>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "Config.h" #include "Config.h"
#include "Utils.h"
#include "dialogs/AcceptCall.h" #include "dialogs/AcceptCall.h"
#include "ui/Avatar.h"
namespace dialogs { namespace dialogs {
AcceptCall::AcceptCall(const QString &caller, const QString &displayName, QWidget *parent) AcceptCall::AcceptCall(
: QWidget(parent) const QString &caller,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl,
QWidget *parent) : QWidget(parent)
{ {
setAutoFillBackground(true); setAutoFillBackground(true);
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
setWindowModality(Qt::WindowModal); setWindowModality(Qt::WindowModal);
setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, true);
setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
auto layout = new QVBoxLayout(this); auto layout = new QVBoxLayout(this);
layout->setSpacing(conf::modals::WIDGET_SPACING); layout->setSpacing(conf::modals::WIDGET_SPACING);
layout->setMargin(conf::modals::WIDGET_MARGIN); layout->setMargin(conf::modals::WIDGET_MARGIN);
auto buttonLayout = new QHBoxLayout(); QFont f;
buttonLayout->setSpacing(15); f.setPointSizeF(f.pointSizeF());
buttonLayout->setMargin(0);
QFont labelFont;
labelFont.setWeight(QFont::Medium);
QLabel *displayNameLabel = nullptr;
if (!displayName.isEmpty() && displayName != caller) {
displayNameLabel = new QLabel(displayName, this);
labelFont.setPointSizeF(f.pointSizeF() * 2);
displayNameLabel ->setFont(labelFont);
displayNameLabel ->setAlignment(Qt::AlignCenter);
}
QLabel *callerLabel = new QLabel(caller, this);
labelFont.setPointSizeF(f.pointSizeF() * 1.2);
callerLabel->setFont(labelFont);
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);
if (!avatarUrl.isEmpty())
avatar->setImage(avatarUrl);
else
avatar->setLetter(utils::firstChar(roomName));
const int iconSize = 24;
auto buttonLayout = new QHBoxLayout();
buttonLayout->setSpacing(20);
acceptBtn_ = new QPushButton(tr("Accept"), this); acceptBtn_ = new QPushButton(tr("Accept"), this);
acceptBtn_->setDefault(true); acceptBtn_->setDefault(true);
rejectBtn_ = new QPushButton(tr("Reject"), this); acceptBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
acceptBtn_->setIconSize(QSize(iconSize, iconSize));
buttonLayout->addStretch(1); rejectBtn_ = new QPushButton(tr("Reject"), this);
rejectBtn_->setIcon(QIcon(":/icons/icons/ui/end-call.png"));
rejectBtn_->setIconSize(QSize(iconSize, iconSize));
buttonLayout->addWidget(acceptBtn_); buttonLayout->addWidget(acceptBtn_);
buttonLayout->addWidget(rejectBtn_); buttonLayout->addWidget(rejectBtn_);
QLabel *label; if (displayNameLabel)
if (!displayName.isEmpty() && displayName != caller) layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
label = new QLabel("Accept call from " + displayName + " (" + caller + ")?", this); layout->addWidget(callerLabel, 0, Qt::AlignCenter);
else layout->addWidget(voiceCallLabel, 0, Qt::AlignCenter);
label = new QLabel("Accept call from " + caller + "?", this); layout->addWidget(avatar, 0, Qt::AlignCenter);
layout->addWidget(label);
layout->addLayout(buttonLayout); layout->addLayout(buttonLayout);
connect(acceptBtn_, &QPushButton::clicked, this, [this]() { connect(acceptBtn_, &QPushButton::clicked, this, [this]() {

View file

@ -1,9 +1,9 @@
#pragma once #pragma once
#include <QString>
#include <QWidget> #include <QWidget>
class QPushButton; class QPushButton;
class QString;
namespace dialogs { namespace dialogs {
@ -12,7 +12,12 @@ class AcceptCall : public QWidget
Q_OBJECT Q_OBJECT
public: public:
AcceptCall(const QString &caller, const QString &displayName, QWidget *parent = nullptr); AcceptCall(
const QString &caller,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl,
QWidget *parent = nullptr);
signals: signals:
void accept(); void accept();

View file

@ -4,12 +4,18 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include "Config.h" #include "Config.h"
#include "Utils.h"
#include "dialogs/PlaceCall.h" #include "dialogs/PlaceCall.h"
#include "ui/Avatar.h"
namespace dialogs { namespace dialogs {
PlaceCall::PlaceCall(const QString &callee, const QString &displayName, QWidget *parent) PlaceCall::PlaceCall(
: QWidget(parent) const QString &callee,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl,
QWidget *parent) : QWidget(parent)
{ {
setAutoFillBackground(true); setAutoFillBackground(true);
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
@ -20,25 +26,31 @@ PlaceCall::PlaceCall(const QString &callee, const QString &displayName, QWidget
layout->setSpacing(conf::modals::WIDGET_SPACING); layout->setSpacing(conf::modals::WIDGET_SPACING);
layout->setMargin(conf::modals::WIDGET_MARGIN); layout->setMargin(conf::modals::WIDGET_MARGIN);
auto buttonLayout = new QHBoxLayout(); auto buttonLayout = new QHBoxLayout(this);
buttonLayout->setSpacing(15); buttonLayout->setSpacing(15);
buttonLayout->setMargin(0); buttonLayout->setMargin(0);
QFont f;
f.setPointSizeF(f.pointSizeF());
auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
if (!avatarUrl.isEmpty())
avatar->setImage(avatarUrl);
else
avatar->setLetter(utils::firstChar(roomName));
voiceBtn_ = new QPushButton(tr("Voice Call"), this); voiceBtn_ = new QPushButton(tr("Voice Call"), this);
voiceBtn_->setDefault(true); voiceBtn_->setDefault(true);
videoBtn_ = new QPushButton(tr("Video Call"), this); //videoBtn_ = new QPushButton(tr("Video Call"), this);
cancelBtn_ = new QPushButton(tr("Cancel"), this); cancelBtn_ = new QPushButton(tr("Cancel"), this);
buttonLayout->addStretch(1); buttonLayout->addStretch(1);
buttonLayout->addWidget(avatar);
buttonLayout->addWidget(voiceBtn_); buttonLayout->addWidget(voiceBtn_);
buttonLayout->addWidget(videoBtn_); //buttonLayout->addWidget(videoBtn_);
buttonLayout->addWidget(cancelBtn_); buttonLayout->addWidget(cancelBtn_);
QLabel *label; QString name = displayName.isEmpty() ? callee : displayName;
if (!displayName.isEmpty() && displayName != callee) QLabel *label = new QLabel("Place a call to " + name + "?", this);
label = new QLabel("Place a call to " + displayName + " (" + callee + ")?", this);
else
label = new QLabel("Place a call to " + callee + "?", this);
layout->addWidget(label); layout->addWidget(label);
layout->addLayout(buttonLayout); layout->addLayout(buttonLayout);
@ -47,10 +59,10 @@ PlaceCall::PlaceCall(const QString &callee, const QString &displayName, QWidget
emit voice(); emit voice();
emit close(); emit close();
}); });
connect(videoBtn_, &QPushButton::clicked, this, [this]() { /*connect(videoBtn_, &QPushButton::clicked, this, [this]() {
emit video(); emit video();
emit close(); emit close();
}); });*/
connect(cancelBtn_, &QPushButton::clicked, this, [this]() { connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
emit cancel(); emit cancel();
emit close(); emit close();

View file

@ -12,16 +12,21 @@ class PlaceCall : public QWidget
Q_OBJECT Q_OBJECT
public: public:
PlaceCall(const QString &callee, const QString &displayName, QWidget *parent = nullptr); PlaceCall(
const QString &callee,
const QString &displayName,
const QString &roomName,
const QString &avatarUrl,
QWidget *parent = nullptr);
signals: signals:
void voice(); void voice();
void video(); // void video();
void cancel(); void cancel();
private: private:
QPushButton *voiceBtn_; QPushButton *voiceBtn_;
QPushButton *videoBtn_; // QPushButton *videoBtn_;
QPushButton *cancelBtn_; QPushButton *cancelBtn_;
}; };