diff --git a/resources/icons/ui/end-call.png b/resources/icons/ui/end-call.png new file mode 100644 index 0000000000000000000000000000000000000000..6cbb983e8d289503098a46f627d8b0cefcd03ea8 --- /dev/null +++ b/resources/icons/ui/end-call.png @@ -0,0 +1,5 @@ +‰PNG + +��� IHDR���@���@���ªiqÞ��� pHYs�����šœ��5IDATxœíØÍ‹Ma�ðß¹(ã£$ÅJ)ÑH²aa=ÅÚBÄÆÒìåcïÛ_@YÐÌÊRBÙ‰L$_#¢‘ŒÅyOÍæÎœ{ç½Ç<¿zsë>_çÜ;羄B!„B!„ÿKÑ`± ++±"|Oñ “øˆ?M4Õ«¬Á^ìÂÖC¨ùþx‰)á!¾dï4“E8€Kxª¼zÓ™ãOÊ})ÕZÔÈd³(”Wø2ÞË?ð\ñ>ÕÞ¥Ù²%Á³CäŠg©§Å=œÛ�Nb¢n©Ç¥u‡ªsë8‚‹ØT7qòñ¯ðoÓë?RÀ²«±±;°k;¬ûgpW¹˜®mǘúW` +÷pBùÍŸã³Y`3Žã~vÐÏXš¡c-œVÔÉ-8ÞM±wØÓTš¥U·Àz<è°H?/ ŠûX7Wò}øÜe~_À4>a¸]âcø5äaÓÊÎLX`tžIÒª8‡b1Φ?þ7£Ê»Á¤<]hwÀ4&[~Žî3E2&ü1W5ÎS> \—ç–:œ±¹veêõŠGó]ÂW寱¦Œ¤šY†¯t»„[j<aõÀ nvØkÛá+-\«™è9ö䞪»Õ?›˜uøJWçHt[ýó½&,Å †¯´[ÂoœÒŸÿ:åÏðßæ9|¥¥<©’¼ÃþLÍöÒ°òÐ¥êû‚.†Ÿi*ÏòŠåÊ‹5ô¯ !„B!„B!„¾ò÷x³%Ú®����IEND®B`‚ \ No newline at end of file diff --git a/resources/icons/ui/microphone-mute.png b/resources/icons/ui/microphone-mute.png new file mode 100644 index 0000000000000000000000000000000000000000..0042fbe2babc3815f79c30098e687d321f0b3da6 --- /dev/null +++ b/resources/icons/ui/microphone-mute.png @@ -0,0 +1,8 @@ +‰PNG + +��� IHDR���@���@���ªiqÞ���sBIT|dˆ��� pHYs�����šœ��#IDATxœí›É‹I‡¿n—qéÅ}pZO" +.ƒ6¸‹Ìa^æ0"2Œ0 +"êMýô ¢ øxP¦]Û E<ˆ(*82.ÓJƒÚ®m·¶‡xe™U•YYå{öâPõ¢¢ò‹Z2+2«‰Àà,ðø|Ú€SÀV`LÁm*DÀ!¶'ź€}ÀˆRZêA󀧤ƒGí0»„öªªx‡;|`o……·ZIÓ€²Ãö˜\pÛs«p•üð]�ê +%È©5èÁ¶ÚGCë}þôs‰…OpXàáüÖjDÿ꟥œ· [/ÏfE&'-§\øpRJÖS>|8 óÕÈ,µ)¦1EÃvHÌR Ђo¦ª‘Y*oN¡ÿ³–½ò$ êá!{j²% fàÁ=5n 8IÁƒ}jìP³ðž€ª‡¯~†Äüž”�_ðCIP4\ª4¢Xfð‰K€/øUÀëÊoH2¼i[¤1ÿ|L hÁßmÿ$â³Ùȵ"4:²=Ö☓H9ëm‚OphH‰µ¸Ù7*²í4±â«$Hd&IU> ïE¾Pðà'—)>w·××Ñ¿Û°¯?ð!´}%%Fø÷‘í>].]}Çç½ò#Û# >/]º&à¡aŸmÍM㶿ٞdð‰&)Q® ¸eØ7Ýâ8 økÈ4{X³~wsœ#U€Nô‹6ö»!öñˆO708+œZÐÛÛØM _$vòÂû]ÈEf©?ÐÛÛØ{`¦!þNƒïßÙ±ì5”âà?¿â7 =RØ·‹¯¿J‘ükâ×6øôã*-øâ—Ŭ3ø@Š4¥Êþ °éª¢¿=�ö#«Êâ´ˆ/{¡ÀviøÊ><hL¨X\y-¬ÅÀCÜH÷\šlàÿ'û,mð_wy=È‹pJŽ¶ç– |;²d.‹¦ÿÄÄíD‰ÒäòÂ;/û[ÄG9AºÁ¸±Á + ˆ¬ßÓYÇö¯€sÈGÍàyeÿp¤¾8ø…ä/ÌvàW +õ™¤ÕÕe±V`œÄxÙ–®7!‹µÀÛ ø/â&ʵn?ØÜI9&Éþ¶�?ú†KSž¹º:`°©&\Dêÿs)ùŠÒž¨¬Öb¬áË×,íJCœ¥:MvSZÆ·“ÜÕ=EþЮª¥%`/òÌšTõð>/ЊŒàŽ!oõ@qð}‘JmZbM“ªcI2îcžŸðªfd¦'é™ù3WÛnSÒ_ëæ#ã¾êà>°}ZP.Scç+§á9Ûâ¢"ÏeEHiÊ÷ÕïDñ?�Ú‹Š¦"‰è£7P72sÏSü^õªWß™>ÖÐëݼ©C����IEND®B`‚ \ No newline at end of file diff --git a/resources/icons/ui/microphone-unmute.png b/resources/icons/ui/microphone-unmute.png new file mode 100644 index 0000000000000000000000000000000000000000..27999c70500ed3564c15e3131b3ed7157a79ab78 --- /dev/null +++ b/resources/icons/ui/microphone-unmute.png @@ -0,0 +1,5 @@ +‰PNG + +��� IHDR���@���@���ªiqÞ���sBIT|dˆ��� pHYs�����šœ��çIDATxœíšIkA€¿$j4F—ãQƒàÅ7Œõ`@^\Pô ŠGу?ÀƒDE/âÁC@‘Ä=ˆxAÜ—`$!·hÆ,㡦u¬©žéš®ê2¦?x‡êyýê½7ݯkƒh™œ�Z€.`0%]@3p˜±O‘P \†€d©N<µÀJ “ÜËÒ,wà¯QÖ?ÐÞ“ïÀšÈ½6D ð…üƒ÷ä07bßCSÜ'|ðž´‘F’í˜Þ“-‘F’{˜O@K”„¡óÁ'ŸÐɦ-4mXjÁ&ˆ°Ø´Q ¨²`Óc¦iƒ60΂MÓm$`X'Àµ®‰àÚ×Ä pí€kâ¸vÀ5qò¸§�±BSjØ—0L�ªÉcÑD7eÀ]àbÑr½n‡Ø|�^wÉ°Æ1þž£¿SèÂÎz@دè¯CÒ9¬î0]jÛœúešÔž¡ss\];àš8l¸\¯Ý·n×FKíDž¾¡Oj+túuê&à³âš¼TÝ¡iS‡÷R{ŠBGå£/º P'ïÛ=Ô´©ƒl»Z¡#')+º xª¸¶Dj·4íáb›=e +½gúþM™‡n(ôö`~¸KÑO“¤3�Œeä]ß™ïb1âŸ0üc2‹m¢à¥ëµé“ÏgðºÔì–®%€hVd@ƒÂÖ^`T߬P…8Ç“žù¨+r=ÁÎùÉ °Ma·qp"]·ŸÌy5®’éìyÝ@¯B?—|6ùؼ¬Ð¿.$=jGv¢>‹~³BßOnó|l5(ôâàÍ…#}Àjý‚ÔoÀ[Žo€sˆSe~Ô"j‚|ïÉP‘äI ðDáÌ×”£¹(f§$ÈòZêWé0VËsƒ,D¬z$÷af¢T�$ó“—DÂùúE"`Õ»ÜD8k€›>¶{Ò"aêw3‰(–—ïvqG!¢V\!ós›^k6˜pÜä\~-p ˜”E§hE¼·íü™¹•#Æ‹'C+³Øè6“Ǩ/ +fc爜'X8'dš"à�bth*ðnÄÐwX-á•GQóƒÊkà01bßR¬N#^lÃâ^ÄÎÓ)`ÃìJ!°ƒÌà·ã `z×{R¿EÊùˆé0â ¯¨˜°WMîĪ6U«Cßl¯PïO8gf×ýä š;ÀQшýà=9kÊi“5 Ü ©¯ÀÔâ?-6) ÄÀʦwv QdØ®Ç�pxnÉ~LLÌã5!TGï™äÐ����IEND®B`‚ \ No newline at end of file diff --git a/resources/icons/ui/place-call.png b/resources/icons/ui/place-call.png new file mode 100644 index 0000000000000000000000000000000000000000..a820cf3fceafebf30ed5915c48c5f785a931734b --- /dev/null +++ b/resources/icons/ui/place-call.png @@ -0,0 +1,5 @@ +‰PNG + +��� IHDR���@���@���ªiqÞ��� pHYs�����šœ��©IDATxœíš=kTA†Ÿ«ñ‚š¤qêEüDe;+í,$ä/(ùÚè/AÐb! +QKcaa¡‰ZDü*Äb± lt-&4ÎÜ;w7óÀévξçÝÙ»gÎ,D"‘H$‰˜™ºÀ0°ˆ0$b« ‹]ácFBlôq3྄آl±xMß1÷”㺠ØðÓ1wËq]Pª4 î�jðK`cÀÇÜÛ€]ŽkƒacÀ»ùO•X–Kä?]bm6½6ìà zˆÁ{&¾ánB'¼d{l¾àYÕB¤°1�Ü x¼r\;R øÖ´%ÄVA,a_ü[`·ˆÒ +¹Î&.`øM~ñKÀ)!XÀ\ü2 /à"úâ?{ucøÀÿ¬uå2ú]ð˜œÿ}ŸÐ›pAPWP®¢7à0)¨+[—èM¸%¨+(ô}Á/ଠ® ÜA¿zÀ1A]Á˜Þ£7a…†ò8êt&,;䤅cs‹üû¹CmI€»˜Mx�l/™ÿäzŒìŒ1Euƒ&q{&´Q“¥,OxÜ�Î1b7Pã¨BM&¬�Gäk£&KÃf}Ôàö5ðu5?ƒ1-òE÷€3yl‹Ï‹9O5¦…¹S þns˜0¬÷Q|f¶ãÀ¨s„óü»U}Ÿ…()ù¿YÌ£&Ͼ‹7�Ô§;‹¹YÊbø8ä5µ4 ã8êªÝwµ1�ÔÙá6çË5 £C~¿Ðx@®`¯5Þ€Œ¸„ùXÝx2ÆP÷ø}FÔ’iàÅ.du±Z¸o`?ê.âð•btmÞ N$¨Ýq8¼‡PÿGœø+Ràê!û]Di$‰D"‘‘ç9~]üÈ����IEND®B`‚ \ No newline at end of file diff --git a/resources/res.qrc b/resources/res.qrc index 3fd3fc9658a071e2c06c5633f9d75d0b4c908d1e..b245f48fabe9abffbc69a39675245c4bc558693d 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -70,6 +70,11 @@ <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@2x.png</file> <file>icons/emoji-categories/nature.png</file> diff --git a/src/ActiveCallBar.cpp b/src/ActiveCallBar.cpp index a5ef754df7bda2b66b9a64fc6d716952f4851379..5703c1edf117677d1d117f4f98dfb072536a306e 100644 --- a/src/ActiveCallBar.cpp +++ b/src/ActiveCallBar.cpp @@ -1,10 +1,17 @@ +#include <cstdio> + +#include <QDateTime> #include <QHBoxLayout> #include <QIcon> #include <QLabel> #include <QString> +#include <QTimer> #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::setMuteIcon(bool muted) +{ + 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) +ActiveCallBar::setCallParty( + const QString &userid, + const QString &displayName, + const QString &roomName, + const QString &avatarUrl) { - if (!displayName.isEmpty() && displayName != userid) - callPartyLabel_->setText("Active Call: " + displayName + " (" + userid + ")"); + callPartyLabel_->setText( + (displayName.isEmpty() ? userid : displayName) + " -"); + + if (!avatarUrl.isEmpty()) + avatar_->setImage(avatarUrl); else - callPartyLabel_->setText("Active Call: " + userid); + 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 dd01e2ad749841a1fbc7e5f456eae78b26ca6bf6..8440d7f3e5817392835abb486d0730ecbf14c777 100644 --- a/src/ActiveCallBar.h +++ b/src/ActiveCallBar.h @@ -2,9 +2,12 @@ #include <QWidget> +#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 92af3b2f8c85081cd80592edbd9fc549ccef9b63..b5c59e081847cad6385046175940db3c09c56f34 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -68,9 +68,9 @@ CallManager::CallManager(QSharedPointer<UserSettings> 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<RoomMember> 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<RoomMember> 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<CallInvite> &callInviteEvent) if (callInviteEvent.content.call_id.empty()) return; - std::vector<RoomMember> 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<CallInvite> &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); - - auto dialog = new dialogs::AcceptCall(caller.user_id, caller.display_name, MainWindow::instance()); + std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); + const RoomMember &caller = + members.front().user_id == utils::localUser() ? members.back() : members.front(); + emit newCallParty(caller.user_id, caller.display_name, + QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url)); + + auto dialog = new dialogs::AcceptCall( + caller.user_id, + caller.display_name, + QString::fromStdString(roomInfo.name), + QString::fromStdString(roomInfo.avatar_url), + MainWindow::instance()); 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<CallAnswer> &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<CallAnswer> &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 8a93241fe49f84508614a9a42f6f77c62a507d03..df83a87a085dc4dfdd3ad3f1a843b2076034ebcf 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 15b7c545364c9ee7ef783b0c68b344213bcb70d7..5b8ea4752de79838b4d49d4bc098074f4d2f487f 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -138,13 +138,13 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> 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> 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<RoomMember> 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 2be0b404100f5f1f4b1fc99e7a677437be7ebb76..d49fc746cf43db7b785cfe586ad1e18d12164565 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 ae58f4e3fa594a2c6a6101386b0e58a09f4435d5..27dff57f7d00a51f3474a63fc823751b72da0196 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -26,6 +26,7 @@ #include <QTextEdit> #include <QWidget> +#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 4ef7a818807997589f783c352a2bc6260fcf79e8..5baed72e8b09da375a0d2781a62fa2e600ecac1b 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<WebRTCSession::State>(); + 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<mtx::events::msg::CallCandidates::Candidate>& candidates) +WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &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 fffefb254c993e189a8de3e0f03d23a439d31b96..42db204d00d788e88fea7fa8fb89323c9e3244a3 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<mtx::events::msg::CallCandidates::Candidate>&); - 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<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: - 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 f04a613a33d1e7e2a245225a5a81e71e00bc379e..6b5e2e60d81848eb4ebf043a85b3eb7ff5b2f875 100644 --- a/src/dialogs/AcceptCall.cpp +++ b/src/dialogs/AcceptCall.cpp @@ -1,43 +1,83 @@ #include <QLabel> #include <QPushButton> +#include <QString> #include <QVBoxLayout> #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 a410d6b79b7e31d4bb21f32390b3988d1fa606ab..8e3ed3b2b74dd9c6dbb0205b03f101151ce0cf0c 100644 --- a/src/dialogs/AcceptCall.h +++ b/src/dialogs/AcceptCall.h @@ -1,9 +1,9 @@ #pragma once -#include <QString> #include <QWidget> 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 8b37ff6af923b2f134aa179288b6680cafbf1e6d..c5c78f94059ece043cf1ea87c080d290d9467471 100644 --- a/src/dialogs/PlaceCall.cpp +++ b/src/dialogs/PlaceCall.cpp @@ -4,12 +4,18 @@ #include <QVBoxLayout> #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 b4de1428c9ca18aab6c058e2e71bd475d15b67cd..1c157b7b4d13eb244e55252703164efc04b3b0ad 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_; };