diff --git a/src/core/coreav.cpp b/src/core/coreav.cpp index 169a6f8d5..549fe6f85 100644 --- a/src/core/coreav.cpp +++ b/src/core/coreav.cpp @@ -63,6 +63,7 @@ CoreAV::~CoreAV() { for (const ToxFriendCall& call : calls) cancelCall(call.callId); + stop(); toxav_kill(toxav); } @@ -98,56 +99,82 @@ bool CoreAV::anyActiveCalls() return !calls.isEmpty(); } -void CoreAV::answerCall(uint32_t friendNum) +bool CoreAV::isCallVideoEnabled(uint32_t friendNum) { + assert(calls.contains(friendNum)); + return calls[friendNum].videoEnabled; +} + +bool CoreAV::answerCall(uint32_t friendNum) +{ + if (QThread::currentThread() != coreavThread.get()) + { + bool ret; + QMetaObject::invokeMethod(this, "answerCall", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ret), Q_ARG(uint32_t, friendNum)); + return ret; + } + qDebug() << QString("answering call %1").arg(friendNum); assert(calls.contains(friendNum)); TOXAV_ERR_ANSWER err; if (toxav_answer(toxav, friendNum, AUDIO_DEFAULT_BITRATE, VIDEO_DEFAULT_BITRATE, &err)) { calls[friendNum].inactive = false; - emit avStart(friendNum, calls[friendNum].videoEnabled); + return true; } else { qWarning() << "Failed to answer call with error"<(_self); + + // Run this slow path callback asynchronously on the AV thread to avoid deadlocks + if (QThread::currentThread() != self->coreavThread.get()) + { + return (void)QMetaObject::invokeMethod(self, "callCallback", Qt::QueuedConnection, + Q_ARG(ToxAV*, toxav), Q_ARG(uint32_t, friendNum), + Q_ARG(bool, audio), Q_ARG(bool, video), Q_ARG(void*, _self)); + } + if (self->calls.contains(friendNum)) { qWarning() << QString("Rejecting call invite from %1, we're already in that call!").arg(friendNum); toxav_call_control(toxav, friendNum, TOXAV_CALL_CONTROL_CANCEL, nullptr); return; } + qDebug() << QString("Received call invite from %1").arg(friendNum); const auto& callIt = self->calls.insert({friendNum, video, *self}); // We don't get a state callback when answering, so fill the state ourselves in advance @@ -376,26 +413,34 @@ void CoreAV::callCallback(ToxAV* toxav, uint32_t friendNum, bool audio, bool vid emit reinterpret_cast(self)->avInvite(friendNum, video); } -void CoreAV::stateCallback(ToxAV *, uint32_t friendNum, uint32_t state, void *_self) +void CoreAV::stateCallback(ToxAV* toxav, uint32_t friendNum, uint32_t state, void *_self) { - // This callback needs to run in the CoreAV thread. - // Otherwise, there's a deadlock between the Core thread holding an internal - // toxav lock and trying to lock the CameraSource to stop it, and the - // av thread holding the Camera - CoreAV* self = static_cast(_self); - assert(self->calls.contains(friendNum)); + // Run this slow path callback asynchronously on the AV thread to avoid deadlocks + if (QThread::currentThread() != self->coreavThread.get()) + { + return (void)QMetaObject::invokeMethod(self, "stateCallback", Qt::QueuedConnection, + Q_ARG(ToxAV*, toxav), Q_ARG(uint32_t, friendNum), + Q_ARG(uint32_t, state), Q_ARG(void*, _self)); + } + + if(!self->calls.contains(friendNum)) + { + qWarning() << QString("stateCallback called, but call %1 is already dead").arg(friendNum); + return; + } ToxFriendCall& call = self->calls[friendNum]; if (state & TOXAV_FRIEND_CALL_STATE_ERROR) { qWarning() << "Call with friend"<avCallFailed(friendNum); + emit self->avEnd(friendNum); } else if (state & TOXAV_FRIEND_CALL_STATE_FINISHED) { + qDebug() << "Call with friend"<avEnd(friendNum); } @@ -412,13 +457,33 @@ void CoreAV::stateCallback(ToxAV *, uint32_t friendNum, uint32_t state, void *_s } } -void CoreAV::audioBitrateCallback(ToxAV*, uint32_t friendNum, bool stable, uint32_t rate, void *) +void CoreAV::audioBitrateCallback(ToxAV* toxav, uint32_t friendNum, bool stable, uint32_t rate, void *_self) { + CoreAV* self = static_cast(_self); + + // Run this slow path callback asynchronously on the AV thread to avoid deadlocks + if (QThread::currentThread() != self->coreavThread.get()) + { + return (void)QMetaObject::invokeMethod(self, "audioBitrateCallback", Qt::QueuedConnection, + Q_ARG(ToxAV*, toxav), Q_ARG(uint32_t, friendNum), + Q_ARG(bool, stable), Q_ARG(uint32_t, rate), Q_ARG(void*, _self)); + } + qDebug() << "Audio bitrate with"<(_self); + + // Run this slow path callback asynchronously on the AV thread to avoid deadlocks + if (QThread::currentThread() != self->coreavThread.get()) + { + return (void)QMetaObject::invokeMethod(self, "videoBitrateCallback", Qt::QueuedConnection, + Q_ARG(ToxAV*, toxav), Q_ARG(uint32_t, friendNum), + Q_ARG(bool, stable), Q_ARG(uint32_t, rate), Q_ARG(void*, _self)); + } + qDebug() << "Video bitrate with"< frame); - bool sendGroupCallAudio(int groupId); + bool isCallVideoEnabled(uint32_t friendNum); + bool sendCallAudio(uint32_t friendNum); ///< Returns false only on error, but not if there's nothing to send + void sendCallVideo(uint32_t friendNum, std::shared_ptr frame); + bool sendGroupCallAudio(int groupNum); VideoSource* getVideoSourceFromCall(int callNumber); ///< Get a call's video source void resetCallSources(); ///< Forces to regenerate each call's audio sources - void joinGroupCall(int groupId); ///< Starts a call in an existing AV groupchat. Call from the GUI thread. - void leaveGroupCall(int groupId); ///< Will not leave the group, just stop the call. Call from the GUI thread. - void disableGroupCallMic(int groupId); - void disableGroupCallVol(int groupId); - void enableGroupCallMic(int groupId); - void enableGroupCallVol(int groupId); - bool isGroupCallMicEnabled(int groupId) const; - bool isGroupCallVolEnabled(int groupId) const; - bool isGroupAvEnabled(int groupId) const; ///< True for AV groups, false for text-only groups + void joinGroupCall(int groupNum); ///< Starts a call in an existing AV groupchat. Call from the GUI thread. + void leaveGroupCall(int groupNum); ///< Will not leave the group, just stop the call. Call from the GUI thread. + void disableGroupCallMic(int groupNum); + void disableGroupCallVol(int groupNum); + void enableGroupCallMic(int groupNum); + void enableGroupCallVol(int groupNum); + bool isGroupCallMicEnabled(int groupNum) const; + bool isGroupCallVolEnabled(int groupNum) const; + bool isGroupAvEnabled(int groupNum) const; ///< True for AV groups, false for text-only groups - void startCall(uint32_t friendId, bool video=false); - void answerCall(uint32_t friendId); - void cancelCall(uint32_t friendId); - - void micMuteToggle(uint32_t friendId); - void volMuteToggle(uint32_t friendId); + void micMuteToggle(uint32_t friendNum); + void volMuteToggle(uint32_t friendNum); public slots: + bool startCall(uint32_t friendNum, bool video=false); + bool answerCall(uint32_t friendNum); + bool cancelCall(uint32_t friendNum); + /// Starts the CoreAV main loop that calls toxav's main loop void start(); + /// Stops the main loop void stop(); signals: - void avInvite(uint32_t friendId, bool video); - void avStart(uint32_t friendId, bool video); - void avCancel(uint32_t friendId); - void avEnd(uint32_t friendId); - void avRinging(uint32_t friendId, bool video); - void avCallFailed(uint32_t friendId); + void avInvite(uint32_t friendId, bool video); ///< Sent when a friend calls us + void avStart(uint32_t friendId, bool video); ///< Sent when a call we initiated has started + void avEnd(uint32_t friendId); ///< Sent when a call was ended by the peer - void videoFrameReceived(vpx_image* frame); +private slots: + static void callCallback(ToxAV *toxAV, uint32_t friendNum, bool audio, bool video, void* self); + static void stateCallback(ToxAV *, uint32_t friendNum, uint32_t state, void* self); + static void audioBitrateCallback(ToxAV *toxAV, uint32_t friendNum, bool stable, uint32_t rate, void* self); + static void videoBitrateCallback(ToxAV *toxAV, uint32_t friendNum, bool stable, uint32_t rate, void* self); private: void process(); - static void callCallback(ToxAV *toxAV, uint32_t friendNum, bool audio, bool video, void* self); - static void stateCallback(ToxAV *toxAV, uint32_t friendNum, uint32_t state, void* self); - static void audioBitrateCallback(ToxAV *toxAV, uint32_t friendNum, bool stable, uint32_t rate, void* self); - static void videoBitrateCallback(ToxAV *toxAV, uint32_t friendNum, bool stable, uint32_t rate, void* self); static void audioFrameCallback(ToxAV *toxAV, uint32_t friendNum, const int16_t *pcm, size_t sampleCount, uint8_t channels, uint32_t samplingRate, void* self); static void videoFrameCallback(ToxAV *toxAV, uint32_t friendNum, uint16_t w, uint16_t h, const uint8_t *y, const uint8_t *u, const uint8_t *v, int32_t ystride, int32_t ustride, int32_t vstride, void* self); - /// Intercepts a function call and moves it to another thread - /// Useful to move callbacks from the toxcore thread to our thread - template void asyncTransplantThunk(void(*fun)(Args...), Args... args); private: static constexpr uint32_t AUDIO_DEFAULT_BITRATE = 64; ///< In kb/s. More than enough for Opus. diff --git a/src/core/toxcall.cpp b/src/core/toxcall.cpp index 0460ded74..20ce00c2d 100644 --- a/src/core/toxcall.cpp +++ b/src/core/toxcall.cpp @@ -17,6 +17,8 @@ ToxCall::ToxCall(uint32_t CallId) : sendAudioTimer{new QTimer}, callId{CallId}, inactive{true}, muteMic{false}, muteVol{false}, alSource{0} { + qCritical() << "CREATED CALL "<setInterval(5); sendAudioTimer->setSingleShot(true); @@ -52,6 +54,9 @@ ToxCall::ToxCall(ToxCall&& other) ToxCall::~ToxCall() { + if (callId != (uint32_t)(int32_t)-1) + qCritical() << "DELETED CALL "< #endif +Q_DECLARE_OPAQUE_POINTER(ToxAV*) + static Nexus* nexus{nullptr}; Nexus::Nexus(QObject *parent) : @@ -90,6 +92,7 @@ void Nexus::start() qRegisterMetaType("int64_t"); qRegisterMetaType("QPixmap"); qRegisterMetaType("Profile*"); + qRegisterMetaType("ToxAV*"); qRegisterMetaType("ToxFile"); qRegisterMetaType("ToxFile::FileDirection"); qRegisterMetaType>("std::shared_ptr"); diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 6963a7240..388baa7a2 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -65,6 +65,9 @@ ChatForm::ChatForm(Friend* chatFriend) : f(chatFriend) , isTyping{false} { + Core* core = Core::getInstance(); + coreav = core->getAv(); + nameLabel->setText(f->getDisplayedName()); avatar->setPixmap(QPixmap(":/img/contact_dark.svg"), Qt::transparent); @@ -96,7 +99,7 @@ ChatForm::ChatForm(Friend* chatFriend) loadHistoryAction = menu.addAction(QString(), this, SLOT(onLoadHistory())); - connect(Core::getInstance(), &Core::fileSendStarted, this, &ChatForm::startFileSend); + connect(core, &Core::fileSendStarted, this, &ChatForm::startFileSend); connect(sendButton, &QPushButton::clicked, this, &ChatForm::onSendTriggered); connect(fileButton, &QPushButton::clicked, this, &ChatForm::onAttachClicked); connect(screenshotButton, &QPushButton::clicked, this, &ChatForm::onScreenshotClicked); @@ -104,7 +107,7 @@ ChatForm::ChatForm(Friend* chatFriend) connect(videoButton, &QPushButton::clicked, this, &ChatForm::onVideoCallTriggered); connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::onSendTriggered); connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged); - connect(Core::getInstance(), &Core::fileSendFailed, this, &ChatForm::onFileSendFailed); + connect(core, &Core::fileSendFailed, this, &ChatForm::onFileSendFailed); connect(this, &ChatForm::chatAreaCleared, getOfflineMsgEngine(), &OfflineMsgEngine::removeAllReciepts); connect(&typingTimer, &QTimer::timeout, this, [=]{ Core::getInstance()->sendTyping(f->getFriendID(), false); @@ -262,36 +265,25 @@ void ChatForm::onAvInvite(uint32_t FriendId, bool video) if (video) { callConfirm = new CallConfirmWidget(videoButton, *f); - if (f->getFriendWidget()->chatFormIsSet(false)) - callConfirm->show(); - - connect(callConfirm, &CallConfirmWidget::accepted, this, &ChatForm::onAnswerCallTriggered); - connect(callConfirm, &CallConfirmWidget::rejected, this, &ChatForm::onRejectCallTriggered); - - callButton->setObjectName("grey"); - callButton->setToolTip(""); videoButton->setObjectName("yellow"); videoButton->setToolTip(tr("Accept video call")); + videoButton->style()->polish(videoButton); connect(videoButton, &QPushButton::clicked, this, &ChatForm::onAnswerCallTriggered); } else { callConfirm = new CallConfirmWidget(callButton, *f); - if (f->getFriendWidget()->chatFormIsSet(false)) - callConfirm->show(); - - connect(callConfirm, &CallConfirmWidget::accepted, this, &ChatForm::onAnswerCallTriggered); - connect(callConfirm, &CallConfirmWidget::rejected, this, &ChatForm::onRejectCallTriggered); - callButton->setObjectName("yellow"); callButton->setToolTip(tr("Accept audio call")); - videoButton->setObjectName("grey"); - videoButton->setToolTip(""); + callButton->style()->polish(callButton); connect(callButton, &QPushButton::clicked, this, &ChatForm::onAnswerCallTriggered); } - callButton->style()->polish(callButton); - videoButton->style()->polish(videoButton); + if (f->getFriendWidget()->chatFormIsSet(false)) + callConfirm->show(); + + connect(callConfirm, &CallConfirmWidget::accepted, this, &ChatForm::onAnswerCallTriggered); + connect(callConfirm, &CallConfirmWidget::rejected, this, &ChatForm::onRejectCallTriggered); insertChatMessage(ChatMessage::createChatInfoMessage(tr("%1 calling").arg(f->getDisplayedName()), ChatMessage::INFO, @@ -346,26 +338,6 @@ void ChatForm::onAvStart(uint32_t FriendId, bool video) startCounter(); } -void ChatForm::onAvCancel(uint32_t FriendId) -{ - if (FriendId != f->getFriendID()) - return; - - qDebug() << "onAvCancel"; - - delete callConfirm; - callConfirm = nullptr; - - enableCallButtons(); - stopCounter(); - - hideNetcam(); - - addSystemInfoMessage(tr("%1 stopped calling").arg(f->getDisplayedName()), - ChatMessage::INFO, - QDateTime::currentDateTime()); -} - void ChatForm::onAvEnd(uint32_t FriendId) { if (FriendId != f->getFriendID()) @@ -381,12 +353,10 @@ void ChatForm::onAvEnd(uint32_t FriendId) hideNetcam(); } -void ChatForm::onAvRinging(uint32_t FriendId, bool video) +void ChatForm::showOutgoingCall(bool video) { - if (FriendId != f->getFriendID()) - return; - - qDebug() << "onAvRinging"; + audioInputFlag = true; + audioOutputFlag = true; disableCallButtons(); if (video) @@ -394,16 +364,16 @@ void ChatForm::onAvRinging(uint32_t FriendId, bool video) videoButton->setObjectName("yellow"); videoButton->style()->polish(videoButton); videoButton->setToolTip(tr("Cancel video call")); - connect(videoButton, SIGNAL(clicked()), - this, SLOT(onCancelCallTriggered())); + connect(videoButton, &QPushButton::clicked, + this, &ChatForm::onCancelCallTriggered); } else { callButton->setObjectName("yellow"); callButton->style()->polish(callButton); callButton->setToolTip(tr("Cancel audio call")); - connect(callButton, SIGNAL(clicked()), - this, SLOT(onCancelCallTriggered())); + connect(callButton, &QPushButton::clicked, + this, &ChatForm::onCancelCallTriggered); } addSystemInfoMessage(tr("Calling %1").arg(f->getDisplayedName()), @@ -425,9 +395,15 @@ void ChatForm::onAnswerCallTriggered() disableCallButtons(); - audioInputFlag = true; - audioOutputFlag = true; - emit answerCall(f->getFriendID()); + if (!coreav->answerCall(f->getFriendID())) + { + enableCallButtons(); + stopCounter(); + hideNetcam(); + return; + } + + onAvStart(f->getFriendID(), coreav->isCallVideoEnabled(f->getFriendID())); } void ChatForm::onHangupCallTriggered() @@ -440,10 +416,11 @@ void ChatForm::onHangupCallTriggered() audioInputFlag = false; audioOutputFlag = false; - emit hangupCall(f->getFriendID()); + coreav->cancelCall(f->getFriendID()); stopCounter(); enableCallButtons(); + hideNetcam(); } void ChatForm::onRejectCallTriggered() @@ -458,7 +435,7 @@ void ChatForm::onRejectCallTriggered() audioInputFlag = false; audioOutputFlag = false; - emit rejectCall(f->getFriendID()); + coreav->cancelCall(f->getFriendID()); enableCallButtons(); stopCounter(); @@ -468,45 +445,30 @@ void ChatForm::onCallTriggered() { qDebug() << "onCallTriggered"; - audioInputFlag = true; - audioOutputFlag = true; disableCallButtons(); - emit startCall(f->getFriendID(), false); + if (coreav->startCall(f->getFriendID(), false)) + showOutgoingCall(false); } void ChatForm::onVideoCallTriggered() { qDebug() << "onVideoCallTriggered"; - audioInputFlag = true; - audioOutputFlag = true; disableCallButtons(); - emit startCall(f->getFriendID(), true); -} - -void ChatForm::onAvCallFailed(uint32_t FriendId) -{ - if (FriendId != f->getFriendID()) - return; - - qDebug() << "onAvCallFailed"; - - delete callConfirm; - callConfirm = nullptr; - - enableCallButtons(); - stopCounter(); + if (coreav->startCall(f->getFriendID(), true)) + showOutgoingCall(false); } void ChatForm::onCancelCallTriggered() { qDebug() << "onCancelCallTriggered"; + if (!coreav->cancelCall(f->getFriendID())) + qWarning() << "Failed to cancel a call! Assuming we're not in call"; + enableCallButtons(); stopCounter(); - hideNetcam(); - emit cancelCall(f->getFriendID()); } void ChatForm::enableCallButtons() @@ -587,7 +549,7 @@ void ChatForm::onMicMuteToggle() { if (audioInputFlag == true) { - emit micMuteToggle(f->getFriendID()); + coreav->micMuteToggle(f->getFriendID()); if (micButton->objectName() == "red") { micButton->setObjectName("green"); @@ -607,7 +569,7 @@ void ChatForm::onVolMuteToggle() { if (audioOutputFlag == true) { - emit volMuteToggle(f->getFriendID()); + coreav->volMuteToggle(f->getFriendID()); if (volButton->objectName() == "red") { volButton->setObjectName("green"); diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index 286f8b387..46b87c49b 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -34,6 +34,7 @@ class CallConfirmWidget; class QHideEvent; class QMoveEvent; class OfflineMsgEngine; +class CoreAV; class ChatForm : public GenericChatForm { @@ -52,13 +53,6 @@ public: signals: void sendFile(uint32_t friendId, QString, QString, long long); - void startCall(uint32_t friendId, bool video); - void answerCall(uint32_t friendId); - void hangupCall(uint32_t friendId); - void cancelCall(uint32_t friendId); - void rejectCall(uint32_t friendId); - void micMuteToggle(uint32_t friendId); - void volMuteToggle(uint32_t friendId); void aliasChanged(const QString& alias); public slots: @@ -66,12 +60,7 @@ public slots: void onFileRecvRequest(ToxFile file); void onAvInvite(uint32_t FriendId, bool video); void onAvStart(uint32_t FriendId, bool video); - void onAvCancel(uint32_t FriendId); void onAvEnd(uint32_t FriendId); - void onAvRinging(uint32_t FriendId, bool video); - void onAvCallFailed(uint32_t FriendId); - void onMicMuteToggle(); - void onVolMuteToggle(); void onAvatarChange(uint32_t FriendId, const QPixmap& pic); void onAvatarRemoved(uint32_t FriendId); @@ -85,6 +74,8 @@ private slots: void onHangupCallTriggered(); void onCancelCallTriggered(); void onRejectCallTriggered(); + void onMicMuteToggle(); + void onVolMuteToggle(); void onFileSendFailed(uint32_t FriendId, const QString &fname); void onLoadHistory(); void onUpdateTime(); @@ -96,6 +87,7 @@ private slots: private: void retranslateUi(); + void showOutgoingCall(bool video); protected: virtual GenericNetCamView* createNetcam() final override; @@ -106,6 +98,7 @@ protected: virtual void showEvent(QShowEvent* event) final override; private: + CoreAV* coreav; Friend* f; CroppingLabel *statusMessageLabel; QLabel *callDuration; diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 73339e0d0..4c0d24c3e 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -892,21 +892,11 @@ void Widget::addFriend(int friendId, const QString &userId) connect(newfriend->getChatForm(), &GenericChatForm::sendMessage, core, &Core::sendMessage); connect(newfriend->getChatForm(), &GenericChatForm::sendAction, core, &Core::sendAction); connect(newfriend->getChatForm(), &ChatForm::sendFile, core, &Core::sendFile); - connect(newfriend->getChatForm(), &ChatForm::answerCall, core->getAv(), &CoreAV::answerCall); - connect(newfriend->getChatForm(), &ChatForm::hangupCall, core->getAv(), &CoreAV::cancelCall); - connect(newfriend->getChatForm(), &ChatForm::rejectCall, core->getAv(), &CoreAV::cancelCall); - connect(newfriend->getChatForm(), &ChatForm::startCall, core->getAv(), &CoreAV::startCall); - connect(newfriend->getChatForm(), &ChatForm::cancelCall, core->getAv(), &CoreAV::cancelCall); - connect(newfriend->getChatForm(), &ChatForm::micMuteToggle, core->getAv(), &CoreAV::micMuteToggle); - connect(newfriend->getChatForm(), &ChatForm::volMuteToggle, core->getAv(), &CoreAV::volMuteToggle); connect(newfriend->getChatForm(), &ChatForm::aliasChanged, newfriend->getFriendWidget(), &FriendWidget::setAlias); connect(core, &Core::fileReceiveRequested, newfriend->getChatForm(), &ChatForm::onFileRecvRequest); - connect(coreav, &CoreAV::avInvite, newfriend->getChatForm(), &ChatForm::onAvInvite); - connect(coreav, &CoreAV::avStart, newfriend->getChatForm(), &ChatForm::onAvStart); - connect(coreav, &CoreAV::avCancel, newfriend->getChatForm(), &ChatForm::onAvCancel); - connect(coreav, &CoreAV::avEnd, newfriend->getChatForm(), &ChatForm::onAvEnd); - connect(coreav, &CoreAV::avRinging, newfriend->getChatForm(), &ChatForm::onAvRinging); - connect(coreav, &CoreAV::avCallFailed, newfriend->getChatForm(), &ChatForm::onAvCallFailed); + connect(coreav, &CoreAV::avInvite, newfriend->getChatForm(), &ChatForm::onAvInvite, Qt::BlockingQueuedConnection); + connect(coreav, &CoreAV::avStart, newfriend->getChatForm(), &ChatForm::onAvStart, Qt::BlockingQueuedConnection); + connect(coreav, &CoreAV::avEnd, newfriend->getChatForm(), &ChatForm::onAvEnd, Qt::BlockingQueuedConnection); connect(core, &Core::friendAvatarChanged, newfriend->getChatForm(), &ChatForm::onAvatarChange); connect(core, &Core::friendAvatarChanged, newfriend->getFriendWidget(), &FriendWidget::onAvatarChange); connect(core, &Core::friendAvatarRemoved, newfriend->getChatForm(), &ChatForm::onAvatarRemoved);