From 88cfa3a8fa7554ab545f6779f2dda9709f72fbbb Mon Sep 17 00:00:00 2001 From: trilene Date: Wed, 22 Jul 2020 21:15:45 -0400 Subject: [PATCH] Polish voice call UI --- resources/icons/ui/end-call.png | Bin 0 -> 643 bytes resources/icons/ui/microphone-mute.png | Bin 0 -> 1153 bytes resources/icons/ui/microphone-unmute.png | Bin 0 -> 1093 bytes resources/icons/ui/place-call.png | Bin 0 -> 759 bytes resources/res.qrc | 5 + src/ActiveCallBar.cpp | 144 ++++++++++++++++++----- src/ActiveCallBar.h | 23 +++- src/CallManager.cpp | 43 ++++--- src/CallManager.h | 6 +- src/ChatPage.cpp | 30 +++-- src/TextInputWidget.cpp | 20 ++-- src/TextInputWidget.h | 3 +- src/WebRTCSession.cpp | 76 ++++++------ src/WebRTCSession.h | 20 +++- src/dialogs/AcceptCall.cpp | 68 ++++++++--- src/dialogs/AcceptCall.h | 9 +- src/dialogs/PlaceCall.cpp | 36 ++++-- src/dialogs/PlaceCall.h | 11 +- 18 files changed, 348 insertions(+), 146 deletions(-) create mode 100644 resources/icons/ui/end-call.png create mode 100644 resources/icons/ui/microphone-mute.png create mode 100644 resources/icons/ui/microphone-unmute.png create mode 100644 resources/icons/ui/place-call.png diff --git a/resources/icons/ui/end-call.png b/resources/icons/ui/end-call.png new file mode 100644 index 0000000000000000000000000000000000000000..6cbb983e8d289503098a46f627d8b0cefcd03ea8 GIT binary patch literal 643 zcmV-}0(||6P)%6%!-TGYs7vnZ5T_%hzhKk!`fKGoi(*^ja%7{Rj) zL*$BQsi(y~oZA3SVxkWQZWj}&$E9_Ls&nfUID(7VR5x+*1&{FrukZ$M)9XJ{z_Jvp zu>l*g1v{`4Ydfs_8fS1~{Gmhg9es<8$Sel_@Eu2;k z)X6P4-c7L%jd$47RGOt#x4?Ql=&(pXFQAJ1ST|SlefZpEfqq}WG(KUtUE*W-HIL*A zVFA<18IR6PSYULXNf*)rsyK&YG0vtx{x_q_yTPP98(nC3z%(XX8h(!MGerv*GA8_x zF;^2c&Qm@hT$ksTI-Ie&b`E9g^@N@)sTV8+fxN+O7Zq002ovPDHLkV1gWlD);~Z literal 0 HcmV?d00001 diff --git a/resources/icons/ui/microphone-mute.png b/resources/icons/ui/microphone-mute.png new file mode 100644 index 0000000000000000000000000000000000000000..0042fbe2babc3815f79c30098e687d321f0b3da6 GIT binary patch literal 1153 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 literal 0 HcmV?d00001 diff --git a/resources/res.qrc b/resources/res.qrc index 3fd3fc96..b245f48f 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -70,6 +70,11 @@ icons/ui/mail-reply.png + icons/ui/place-call.png + icons/ui/end-call.png + icons/ui/microphone-mute.png + icons/ui/microphone-unmute.png + icons/emoji-categories/people.png icons/emoji-categories/people@2x.png icons/emoji-categories/nature.png diff --git a/src/ActiveCallBar.cpp b/src/ActiveCallBar.cpp index a5ef754d..5703c1ed 100644 --- a/src/ActiveCallBar.cpp +++ b/src/ActiveCallBar.cpp @@ -1,10 +1,17 @@ +#include + +#include #include #include #include #include +#include #include "ActiveCallBar.h" +#include "ChatPage.h" +#include "Utils.h" #include "WebRTCSession.h" +#include "ui/Avatar.h" #include "ui/FlatButton.h" ActiveCallBar::ActiveCallBar(QWidget *parent) @@ -12,7 +19,7 @@ ActiveCallBar::ActiveCallBar(QWidget *parent) { setAutoFillBackground(true); auto p = palette(); - p.setColor(backgroundRole(), Qt::green); + p.setColor(backgroundRole(), QColorConstants::Svg::limegreen); setPalette(p); QFont f; @@ -24,51 +31,126 @@ ActiveCallBar::ActiveCallBar(QWidget *parent) setFixedHeight(contentHeight + widgetMargin); - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(widgetMargin); - topLayout_->setContentsMargins( + layout_ = new QHBoxLayout(this); + layout_->setSpacing(widgetMargin); + layout_->setContentsMargins( 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - topLayout_->setSizeConstraint(QLayout::SetMinimumSize); QFont labelFont; - labelFont.setPointSizeF(labelFont.pointSizeF() * 1.2); + labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1); labelFont.setWeight(QFont::Medium); + avatar_ = new Avatar(this, QFontMetrics(f).height() * 2.5); + callPartyLabel_ = new QLabel(this); 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); - QIcon muteIcon; - 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")); + setMuteIcon(false); muteBtn_->setFixedSize(buttonSize_, buttonSize_); muteBtn_->setCornerRadius(buttonSize_ / 2); - connect(muteBtn_, &FlatButton::clicked, this, [this]() { - if (WebRTCSession::instance().toggleMuteAudioSrc(muted_)) { - QIcon icon; - 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); - } + connect(muteBtn_, &FlatButton::clicked, this, [this](){ + if (WebRTCSession::instance().toggleMuteAudioSrc(muted_)) + setMuteIcon(muted_); }); - topLayout_->addWidget(callPartyLabel_, 0, Qt::AlignLeft); - topLayout_->addWidget(muteBtn_, 0, Qt::AlignRight); + layout_->addWidget(avatar_, 0, Qt::AlignLeft); + 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 -ActiveCallBar::setCallParty(const QString &userid, const QString &displayName) +ActiveCallBar::setMuteIcon(bool muted) { - if (!displayName.isEmpty() && displayName != userid) - callPartyLabel_->setText("Active Call: " + displayName + " (" + userid + ")"); - else - callPartyLabel_->setText("Active Call: " + userid); + QIcon icon; + if (muted) { + muteBtn_->setToolTip("Unmute Mic"); + 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; + } } diff --git a/src/ActiveCallBar.h b/src/ActiveCallBar.h index dd01e2ad..8440d7f3 100644 --- a/src/ActiveCallBar.h +++ b/src/ActiveCallBar.h @@ -2,9 +2,12 @@ #include +#include "WebRTCSession.h" + class QHBoxLayout; class QLabel; -class QString; +class QTimer; +class Avatar; class FlatButton; class ActiveCallBar : public QWidget @@ -15,12 +18,24 @@ public: ActiveCallBar(QWidget *parent = nullptr); 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: - QHBoxLayout *topLayout_ = nullptr; + QHBoxLayout *layout_ = nullptr; + Avatar *avatar_ = nullptr; QLabel *callPartyLabel_ = nullptr; + QLabel *stateLabel_ = nullptr; + QLabel *durationLabel_ = nullptr; FlatButton *muteBtn_ = nullptr; - int buttonSize_ = 32; + int buttonSize_ = 22; bool muted_ = false; + qint64 callStartTime_ = 0; + QTimer *timer_ = nullptr; + + void setMuteIcon(bool muted); }; diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 92af3b2f..b5c59e08 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -68,9 +68,9 @@ CallManager::CallManager(QSharedPointer userSettings) turnServerTimer_.setInterval(res.ttl * 1000 * 0.9); }); - connect(&session_, &WebRTCSession::pipelineChanged, this, - [this](bool started) { - if (!started) + connect(&session_, &WebRTCSession::stateChanged, this, + [this](WebRTCSession::State state) { + if (state == WebRTCSession::State::DISCONNECTED) playRingtone("qrc:/media/media/callend.ogg", false); }); @@ -87,9 +87,9 @@ CallManager::sendInvite(const QString &roomid) if (onActiveCall()) return; - std::vector members(cache::getMembers(roomid.toStdString())); - if (members.size() != 2) { - emit ChatPage::instance()->showNotification("Voice/Video calls are limited to 1:1 rooms"); + auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); + if (roomInfo.member_count != 2) { + emit ChatPage::instance()->showNotification("Voice calls are limited to 1:1 rooms."); return; } @@ -105,11 +105,13 @@ CallManager::sendInvite(const QString &roomid) // TODO Add invite timeout generateCallID(); + std::vector members(cache::getMembers(roomid.toStdString())); const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); - emit newCallParty(callee.user_id, callee.display_name); + emit newCallParty(callee.user_id, callee.display_name, + QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url)); playRingtone("qrc:/media/media/ringback.ogg", true); if (!session_.createOffer()) { - emit ChatPage::instance()->showNotification("Problem setting up call"); + emit ChatPage::instance()->showNotification("Problem setting up call."); endCall(); } } @@ -127,7 +129,7 @@ CallManager::hangUp() bool CallManager::onActiveCall() { - return session_.isActive(); + return session_.state() != WebRTCSession::State::DISCONNECTED; } void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) @@ -156,8 +158,8 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent) if (callInviteEvent.content.call_id.empty()) return; - std::vector members(cache::getMembers(callInviteEvent.room_id)); - if (onActiveCall() || members.size() != 2) { + auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); + if (onActiveCall() || roomInfo.member_count != 2) { emit newMessage(QString::fromStdString(callInviteEvent.room_id), CallHangUp{callInviteEvent.content.call_id, 0, CallHangUp::Reason::InviteTimeOut}); return; @@ -168,10 +170,18 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent) callid_ = callInviteEvent.content.call_id; remoteICECandidates_.clear(); - const RoomMember &caller = members.front().user_id == utils::localUser() ? members.back() : members.front(); - emit newCallParty(caller.user_id, caller.display_name); + std::vector members(cache::getMembers(callInviteEvent.room_id)); + const RoomMember &caller = + members.front().user_id == utils::localUser() ? members.back() : members.front(); + emit newCallParty(caller.user_id, caller.display_name, + QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url)); - auto dialog = new dialogs::AcceptCall(caller.user_id, caller.display_name, 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, [this, callInviteEvent](){ MainWindow::instance()->hideOverlay(); @@ -198,7 +208,7 @@ CallManager::answerInvite(const CallInvite &invite) session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); if (!session_.acceptOffer(invite.sdp)) { - emit ChatPage::instance()->showNotification("Problem setting up call"); + emit ChatPage::instance()->showNotification("Problem setting up call."); hangUp(); return; } @@ -232,6 +242,7 @@ CallManager::handleEvent(const RoomEvent &callAnswerEvent) if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() && callid_ == callAnswerEvent.content.call_id) { + emit ChatPage::instance()->showNotification("Call answered on another device."); stopRingtone(); MainWindow::instance()->hideOverlay(); return; @@ -240,7 +251,7 @@ CallManager::handleEvent(const RoomEvent &callAnswerEvent) if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) { stopRingtone(); if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { - emit ChatPage::instance()->showNotification("Problem setting up call"); + emit ChatPage::instance()->showNotification("Problem setting up call."); hangUp(); } } diff --git a/src/CallManager.h b/src/CallManager.h index 8a93241f..df83a87a 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -36,7 +36,11 @@ signals: void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer&); void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp&); 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: void retrieveTurnServer(); diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 15b7c545..5b8ea475 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -138,13 +138,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect( &callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty); connect(&WebRTCSession::instance(), - &WebRTCSession::pipelineChanged, + &WebRTCSession::stateChanged, this, - [this](bool callStarted) { - if (callStarted) - activeCallBar_->show(); - else + [this](WebRTCSession::State state) { + if (state == WebRTCSession::State::DISCONNECTED) activeCallBar_->hide(); + else + activeCallBar_->show(); }); // Splitter @@ -469,22 +469,28 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) if (callManager_.onActiveCall()) { callManager_.hangUp(); } else { - if (cache::singleRoomInfo(current_room_.toStdString()).member_count != 2) { - showNotification("Voice/Video calls are limited to 1:1 rooms"); + if (auto roomInfo = + cache::singleRoomInfo(current_room_.toStdString()); + roomInfo.member_count != 2) { + showNotification("Voice calls are limited to 1:1 rooms."); } else { std::vector members( cache::getMembers(current_room_.toStdString())); const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); - auto dialog = - new dialogs::PlaceCall(callee.user_id, callee.display_name, MainWindow::instance()); + auto dialog = new dialogs::PlaceCall( + callee.user_id, + callee.display_name, + QString::fromStdString(roomInfo.name), + QString::fromStdString(roomInfo.avatar_url), + MainWindow::instance()); connect(dialog, &dialogs::PlaceCall::voice, this, [this]() { callManager_.sendInvite(current_room_); }); - connect(dialog, &dialogs::PlaceCall::video, this, [this]() { - showNotification("Video calls not yet implemented"); - }); + /*connect(dialog, &dialogs::PlaceCall::video, this, [this]() { + showNotification("Video calls not yet implemented."); + });*/ utils::centerWidget(dialog, MainWindow::instance()); dialog->show(); } diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 2be0b404..d49fc746 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -31,7 +31,6 @@ #include "Logging.h" #include "TextInputWidget.h" #include "Utils.h" -#include "WebRTCSession.h" #include "ui/FlatButton.h" #include "ui/LoadingIndicator.h" @@ -455,9 +454,9 @@ TextInputWidget::TextInputWidget(QWidget *parent) topLayout_->setContentsMargins(13, 1, 13, 0); callBtn_ = new FlatButton(this); - changeCallButtonState(false); + changeCallButtonState(WebRTCSession::State::DISCONNECTED); connect(&WebRTCSession::instance(), - &WebRTCSession::pipelineChanged, + &WebRTCSession::stateChanged, this, &TextInputWidget::changeCallButtonState); @@ -664,17 +663,16 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::changeCallButtonState(bool callStarted) +TextInputWidget::changeCallButtonState(WebRTCSession::State state) { - // TODO Telephone and HangUp icons - co-opt the ones below for now QIcon icon; - if (callStarted) { - callBtn_->setToolTip(tr("Hang up")); - icon.addFile(":/icons/icons/ui/remove-symbol.png"); - } else { + if (state == WebRTCSession::State::DISCONNECTED) { 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_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + callBtn_->setIconSize(QSize(ButtonHeight * 1.1, ButtonHeight * 1.1)); } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index ae58f4e3..27dff57f 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -26,6 +26,7 @@ #include #include +#include "WebRTCSession.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" #include "popups/SuggestionsPopup.h" @@ -149,7 +150,7 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void changeCallButtonState(bool callStarted); + void changeCallButtonState(WebRTCSession::State); private slots: void addSelectedEmoji(const QString &emoji); diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index 4ef7a818..5baed72e 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -11,6 +11,8 @@ extern "C" { #include "gst/webrtc/webrtc.h" } +Q_DECLARE_METATYPE(WebRTCSession::State) + namespace { bool gisoffer; 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); } +WebRTCSession::WebRTCSession() : QObject() +{ + qRegisterMetaType(); + connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); +} + bool WebRTCSession::init(std::string *errorMessage) { @@ -54,14 +62,14 @@ WebRTCSession::init(std::string *errorMessage) nhlog::ui()->info("Initialised " + gstVersion); // GStreamer Plugins: - // Base: audioconvert, audioresample, opus, playback, videoconvert, volume + // Base: audioconvert, audioresample, opus, playback, volume // Good: autodetect, rtpmanager, vpx // Bad: dtls, srtp, webrtc // libnice [GLib]: nice initialised_ = true; std::string strError = gstVersion + ": Missing plugins: "; 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(); for (guint i = 0; i < g_strv_length((gchar**)needed); i++) { GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); @@ -91,17 +99,19 @@ WebRTCSession::createOffer() } bool -WebRTCSession::acceptOffer(const std::string& sdp) +WebRTCSession::acceptOffer(const std::string &sdp) { nhlog::ui()->debug("Received offer:\n{}", sdp); + if (state_ != State::DISCONNECTED) + return false; + gisoffer = false; glocalsdp.clear(); gcandidates.clear(); int opusPayloadType = getPayloadType(sdp, "opus"); - if (opusPayloadType == -1) { + if (opusPayloadType == -1) return false; - } GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); if (!offer) @@ -120,9 +130,11 @@ WebRTCSession::acceptOffer(const std::string& sdp) bool WebRTCSession::startPipeline(int opusPayloadType) { - if (isActive()) + if (state_ != State::DISCONNECTED) return false; + emit stateChanged(State::INITIATING); + if (!createPipeline(opusPayloadType)) return false; @@ -132,7 +144,12 @@ WebRTCSession::startPipeline(int opusPayloadType) nhlog::ui()->info("WebRTC: Setting STUN server: {}", stunServer_); 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 if (gisoffer) @@ -152,16 +169,14 @@ WebRTCSession::startPipeline(int opusPayloadType) GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { nhlog::ui()->error("WebRTC: unable to start pipeline"); - gst_object_unref(pipe_); - pipe_ = nullptr; - webrtc_ = nullptr; + end(); return false; } GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); gst_bus_add_watch(bus, newBusMessage, this); gst_object_unref(bus); - emit pipelineChanged(true); + emit stateChanged(State::INITIATED); return true; } @@ -180,10 +195,7 @@ WebRTCSession::createPipeline(int opusPayloadType) if (error) { nhlog::ui()->error("WebRTC: Failed to parse pipeline: {}", error->message); g_error_free(error); - if (pipe_) { - gst_object_unref(pipe_); - pipe_ = nullptr; - } + end(); return false; } return true; @@ -193,7 +205,7 @@ bool WebRTCSession::acceptAnswer(const std::string &sdp) { nhlog::ui()->debug("WebRTC: Received sdp:\n{}", sdp); - if (!isActive()) + if (state_ != State::OFFERSENT) return false; GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); @@ -206,18 +218,20 @@ WebRTCSession::acceptAnswer(const std::string &sdp) } void -WebRTCSession::acceptICECandidates(const std::vector& candidates) +WebRTCSession::acceptICECandidates(const std::vector &candidates) { - if (isActive()) { - for (const auto& c : candidates) + if (state_ >= State::INITIATED) { + for (const auto &c : candidates) g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str()); } + if (state_ < State::CONNECTED) + emit stateChanged(State::CONNECTING); } bool WebRTCSession::toggleMuteAudioSrc(bool &isMuted) { - if (!isActive()) + if (state_ < State::INITIATED) return false; GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); @@ -241,20 +255,7 @@ WebRTCSession::end() pipe_ = nullptr; } webrtc_ = nullptr; - emit pipelineChanged(false); -} - -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)); - } + emit stateChanged(State::DISCONNECTED); } namespace { @@ -373,8 +374,10 @@ gboolean onICEGatheringCompletion(gpointer timerid) { *(guint*)(timerid) = 0; - if (gisoffer) + if (gisoffer) { emit WebRTCSession::instance().offerCreated(glocalsdp, gcandidates); + emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT); + } else emit WebRTCSession::instance().answerCreated(glocalsdp, gcandidates); @@ -445,6 +448,9 @@ linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe 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); } } diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index fffefb25..42db204d 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -14,6 +14,15 @@ class WebRTCSession : public QObject Q_OBJECT public: + enum class State { + DISCONNECTED, + INITIATING, + INITIATED, + OFFERSENT, + CONNECTING, + CONNECTED + }; + static WebRTCSession& instance() { static WebRTCSession instance; @@ -27,7 +36,7 @@ public: bool acceptAnswer(const std::string &sdp); void acceptICECandidates(const std::vector&); - bool isActive() { return pipe_ != nullptr; } + State state() const {return state_;} bool toggleMuteAudioSrc(bool &isMuted); void end(); @@ -37,12 +46,16 @@ public: signals: void offerCreated(const std::string &sdp, const std::vector&); void answerCreated(const std::string &sdp, const std::vector&); - void pipelineChanged(bool started); + void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt + +private slots: + void setState(State state) {state_ = state;} private: - WebRTCSession() : QObject() {} + WebRTCSession(); bool initialised_ = false; + State state_ = State::DISCONNECTED; GstElement *pipe_ = nullptr; GstElement *webrtc_ = nullptr; std::string stunServer_; @@ -50,7 +63,6 @@ private: bool startPipeline(int opusPayloadType); bool createPipeline(int opusPayloadType); - void addTurnServers(); public: WebRTCSession(WebRTCSession const&) = delete; diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp index f04a613a..6b5e2e60 100644 --- a/src/dialogs/AcceptCall.cpp +++ b/src/dialogs/AcceptCall.cpp @@ -1,43 +1,83 @@ #include #include +#include #include #include "Config.h" +#include "Utils.h" #include "dialogs/AcceptCall.h" +#include "ui/Avatar.h" namespace dialogs { -AcceptCall::AcceptCall(const QString &caller, const QString &displayName, QWidget *parent) - : QWidget(parent) +AcceptCall::AcceptCall( + const QString &caller, + const QString &displayName, + const QString &roomName, + const QString &avatarUrl, + QWidget *parent) : QWidget(parent) { setAutoFillBackground(true); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); setWindowModality(Qt::WindowModal); setAttribute(Qt::WA_DeleteOnClose, true); + setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + auto layout = new QVBoxLayout(this); layout->setSpacing(conf::modals::WIDGET_SPACING); layout->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->setMargin(0); + QFont f; + f.setPointSizeF(f.pointSizeF()); + 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_->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(rejectBtn_); - QLabel *label; - if (!displayName.isEmpty() && displayName != caller) - label = new QLabel("Accept call from " + displayName + " (" + caller + ")?", this); - else - label = new QLabel("Accept call from " + caller + "?", this); - - layout->addWidget(label); + if (displayNameLabel) + layout->addWidget(displayNameLabel, 0, Qt::AlignCenter); + layout->addWidget(callerLabel, 0, Qt::AlignCenter); + layout->addWidget(voiceCallLabel, 0, Qt::AlignCenter); + layout->addWidget(avatar, 0, Qt::AlignCenter); layout->addLayout(buttonLayout); connect(acceptBtn_, &QPushButton::clicked, this, [this]() { diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h index a410d6b7..8e3ed3b2 100644 --- a/src/dialogs/AcceptCall.h +++ b/src/dialogs/AcceptCall.h @@ -1,9 +1,9 @@ #pragma once -#include #include class QPushButton; +class QString; namespace dialogs { @@ -12,7 +12,12 @@ class AcceptCall : public QWidget Q_OBJECT 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: void accept(); diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp index 8b37ff6a..c5c78f94 100644 --- a/src/dialogs/PlaceCall.cpp +++ b/src/dialogs/PlaceCall.cpp @@ -4,12 +4,18 @@ #include #include "Config.h" +#include "Utils.h" #include "dialogs/PlaceCall.h" +#include "ui/Avatar.h" namespace dialogs { -PlaceCall::PlaceCall(const QString &callee, const QString &displayName, QWidget *parent) - : QWidget(parent) +PlaceCall::PlaceCall( + const QString &callee, + const QString &displayName, + const QString &roomName, + const QString &avatarUrl, + QWidget *parent) : QWidget(parent) { setAutoFillBackground(true); 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->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); + auto buttonLayout = new QHBoxLayout(this); buttonLayout->setSpacing(15); 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_->setDefault(true); - videoBtn_ = new QPushButton(tr("Video Call"), this); + //videoBtn_ = new QPushButton(tr("Video Call"), this); cancelBtn_ = new QPushButton(tr("Cancel"), this); buttonLayout->addStretch(1); + buttonLayout->addWidget(avatar); buttonLayout->addWidget(voiceBtn_); - buttonLayout->addWidget(videoBtn_); + //buttonLayout->addWidget(videoBtn_); buttonLayout->addWidget(cancelBtn_); - QLabel *label; - if (!displayName.isEmpty() && displayName != callee) - label = new QLabel("Place a call to " + displayName + " (" + callee + ")?", this); - else - label = new QLabel("Place a call to " + callee + "?", this); + QString name = displayName.isEmpty() ? callee : displayName; + QLabel *label = new QLabel("Place a call to " + name + "?", this); layout->addWidget(label); layout->addLayout(buttonLayout); @@ -47,10 +59,10 @@ PlaceCall::PlaceCall(const QString &callee, const QString &displayName, QWidget emit voice(); emit close(); }); - connect(videoBtn_, &QPushButton::clicked, this, [this]() { + /*connect(videoBtn_, &QPushButton::clicked, this, [this]() { emit video(); emit close(); - }); + });*/ connect(cancelBtn_, &QPushButton::clicked, this, [this]() { emit cancel(); emit close(); diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h index b4de1428..1c157b7b 100644 --- a/src/dialogs/PlaceCall.h +++ b/src/dialogs/PlaceCall.h @@ -12,16 +12,21 @@ class PlaceCall : public QWidget Q_OBJECT 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: void voice(); - void video(); +// void video(); void cancel(); private: QPushButton *voiceBtn_; - QPushButton *videoBtn_; +// QPushButton *videoBtn_; QPushButton *cancelBtn_; };