diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index c0a5ef83..eae50296 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -404,8 +404,9 @@ void CallVideoEngine::transmit() { } format = next; appliedRevision = revision; forceKey = true; request = 0; if (call_video_port_format(m->id, &format, encoder.bitrate()) < 0) break; - m->report(QString("%1 · %2×%3 · %4 fps · %5 kbit/s").arg(format.codec == 2 ? "H.265" : "H.264") - .arg(width).arg(height).arg(format.fps).arg(encoder.bitrate() / 1000)); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video format applied codec=%u size=%ux%u fps=%u bitrate_bps=%u", + format.codec, format.width, format.height, format.fps, encoder.bitrate()); + m->report({}); } if (state.configured_generation != format.generation) { idle(); continue; } if (state.key_serial != keySerial) { keySerial = state.key_serial; forceKey = true; } @@ -428,8 +429,8 @@ void CallVideoEngine::transmit() { encodeMax = std::max(encodeMax, nowUs() - encodeStarted); if (completion.request) { call_video_port_applied(m->id, completion.generation, completion.request, completion.bitrate); - m->report(QString("%1 · %2×%3 · %4 fps · %5 kbit/s").arg(format.codec == 2 ? "H.265" : "H.264") - .arg(format.width).arg(format.height).arg(format.fps).arg(encoder.bitrate() / 1000)); + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call video bitrate applied generation=%u request=%llu bitrate_bps=%u", + completion.generation, (unsigned long long)completion.request, encoder.bitrate()); } if (!encoded) { m->report(encoder.error()); diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 19ef3eec..60651003 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -31,7 +31,7 @@ extern "C" { /* ── рисование иконок звонка (QPainter, без внешних ассетов) ── */ -enum class Glyph { Phone, PhoneDown, Mic, MicOff, Camera, CameraOff, Settings }; +enum class Glyph { Phone, PhoneDown, Mic, MicOff, Camera, CameraOff, Settings, Diagnostics }; static QString callText(const char* text) { return QCoreApplication::translate("ChatGui", text); } using namespace CaptureControls; @@ -239,6 +239,16 @@ static QIcon makeRoundIcon(const QColor& bg, Glyph glyph, int size) { case Glyph::Camera: drawCameraGlyph(&p, glyphRect, false); break; case Glyph::CameraOff: drawCameraGlyph(&p, glyphRect, true); break; case Glyph::Settings: drawSettingsGlyph(&p, glyphRect); break; + case Glyph::Diagnostics: { + p.setPen(QPen(Qt::white, glyphRect.width() * 0.07, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + p.drawLine(glyphRect.bottomLeft(), glyphRect.topLeft()); p.drawLine(glyphRect.bottomLeft(), glyphRect.bottomRight()); + QPainterPath graph; graph.moveTo(glyphRect.left() + glyphRect.width() * 0.1, glyphRect.center().y()); + graph.lineTo(glyphRect.left() + glyphRect.width() * 0.35, glyphRect.center().y()); + graph.lineTo(glyphRect.center().x(), glyphRect.top() + glyphRect.height() * 0.2); + graph.lineTo(glyphRect.left() + glyphRect.width() * 0.65, glyphRect.top() + glyphRect.height() * 0.8); + graph.lineTo(glyphRect.right() - glyphRect.width() * 0.1, glyphRect.top() + glyphRect.height() * 0.3); + p.setBrush(Qt::NoBrush); p.drawPath(graph); break; + } } p.end(); return QIcon(pm); @@ -278,6 +288,7 @@ CallWindow::CallWindow(QWidget* parent) setAttribute(Qt::WA_DeleteOnClose, false); m_avatarLabel = new QLabel(this); + m_avatarLabel->setObjectName("callAvatar"); m_avatarLabel->setFixedSize(80, 80); m_avatarLabel->setAlignment(Qt::AlignCenter); @@ -326,8 +337,7 @@ CallWindow::CallWindow(QWidget* parent) m_videoWindow = new CallVideoWindow(this); m_panel = new QWidget(this); m_panel->setObjectName("callPanel"); m_panel->setMaximumWidth(640); m_panel->setStyleSheet("QWidget#callPanel { background: palette(window); border-radius: 12px; }"); - // Вертикальное центрирование через отступы сохраняет минимальную высоту панели в расчёте размера окна. - root->addStretch(1); + // Содержимое сверху, отдельная строка действий всегда у нижнего края окна. root->addWidget(m_panel, 0, Qt::AlignHCenter); root->addStretch(1); QVBoxLayout* lay = new QVBoxLayout(m_panel); @@ -348,14 +358,15 @@ CallWindow::CallWindow(QWidget* parent) SoundManager::instance()->playLoop("call_connecting"); }); lay->addWidget(m_durationLabel); - lay->addWidget(m_connectionLabel); - lay->addWidget(m_bgpPathLabel); - lay->addWidget(m_statsLabel); + m_diagnostics = new QWidget(m_panel); m_diagnostics->setObjectName("callDiagnostics"); + auto* diagnosticLayout = new QVBoxLayout(m_diagnostics); diagnosticLayout->setContentsMargins(0, 0, 0, 0); + diagnosticLayout->addWidget(m_connectionLabel); diagnosticLayout->addWidget(m_bgpPathLabel); diagnosticLayout->addWidget(m_statsLabel); + lay->addWidget(m_diagnostics); m_video = new CallVideoEngine(this); m_video->setPresentationDriven(true); m_videoWindow->engine = m_video; m_localPreview = new QLabel(this); m_localPreview->setAlignment(Qt::AlignCenter); m_localPreview->hide(); lay->addWidget(m_localPreview, 0, Qt::AlignHCenter); - m_videoStatus = new QLabel(this); m_videoStatus->setWordWrap(true); lay->addWidget(m_videoStatus); + m_videoStatus = new QLabel(this); m_videoStatus->setObjectName("callVideoStatus"); m_videoStatus->setWordWrap(true); lay->addWidget(m_videoStatus); m_videoControls = new QWidget(this); auto* videoLayout = new QVBoxLayout(m_videoControls); videoLayout->setContentsMargins(0, 0, 0, 0); m_videoMenuBar = new QMenuBar(m_videoControls); m_videoMenuBar->setNativeMenuBar(false); m_videoMenuBar->setMouseTracking(true); @@ -381,6 +392,9 @@ CallWindow::CallWindow(QWidget* parent) m_settingsBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Settings, 96), callText("Video settings"), this); m_settingsBtn->setObjectName("callVideoSettings"); m_settingsBtn->setCheckable(true); m_settingsBtn->setAccessibleName(callText("Video settings")); + m_diagnosticsBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Diagnostics, 96), callText("Diagnostics"), this); + m_diagnosticsBtn->setObjectName("callDiagnosticsButton"); m_diagnosticsBtn->setCheckable(true); + m_diagnosticsBtn->setAccessibleName(callText("Diagnostics")); m_showVideoBtn = new QPushButton(callText("Show video"), this); actions->addWidget(m_showVideoBtn); lay->addLayout(actions); connect(m_settingsBtn, &QPushButton::toggled, this, [this](bool checked) { @@ -388,6 +402,11 @@ CallWindow::CallWindow(QWidget* parent) DEBUG_INFO(DEBUG_CATEGORY_CALL, "desktop video settings visible=%d", checked); updateVideoPresentation(); }); + connect(m_diagnosticsBtn, &QPushButton::toggled, this, [this](bool checked) { + m_diagnosticsBtn->setIcon(makeRoundIcon(checked ? QColor("#2980b9") : QColor("#7f8c8d"), Glyph::Diagnostics, 96)); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "desktop call diagnostics visible=%d id=%llu", checked, (unsigned long long)m_callId); + updateVideoPresentation(); + }); connect(m_showVideoBtn, &QPushButton::clicked, this, [this] { if (m_videoWindow->isMinimized()) m_videoWindow->showNormal(); else m_videoWindow->show(); m_videoWindow->raise(); m_videoWindow->activateWindow(); @@ -488,8 +507,10 @@ CallWindow::CallWindow(QWidget* parent) m_muteCell = wrapWithCaption(m_muteBtn, QCoreApplication::translate("ChatGui", "Mute"), &m_muteCaption); m_videoCell = wrapWithCaption(m_videoEnabled, callText("Camera")); m_settingsCell = wrapWithCaption(m_settingsBtn, callText("Video settings")); + m_diagnosticsCell = wrapWithCaption(m_diagnosticsBtn, callText("Diagnostics")); - QHBoxLayout* btn = new QHBoxLayout(); + auto* footer = new QWidget(this); footer->setObjectName("callActions"); + QHBoxLayout* btn = new QHBoxLayout(footer); btn->setContentsMargins(0, 0, 0, 0); btn->setSpacing(10); btn->addStretch(1); btn->addWidget(m_acceptCell); @@ -498,8 +519,9 @@ CallWindow::CallWindow(QWidget* parent) btn->addWidget(m_muteCell); btn->addWidget(m_videoCell); btn->addWidget(m_settingsCell); + btn->addWidget(m_diagnosticsCell); btn->addStretch(1); - lay->addLayout(btn); + root->addWidget(footer, 0, Qt::AlignHCenter); connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept); connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline); @@ -608,15 +630,18 @@ void CallWindow::updateVideoPresentation() { if (receiving) m_videoWindow->show(); else m_videoWindow->hide(); } bool active = m_state == Active, settings = active && m_settingsBtn->isChecked(); + m_avatarLabel->setVisible(!active || (!settings && !receiving && m_localImage.isNull())); m_localPreview->setVisible(active && !receiving && !m_localImage.isNull()); m_connectionLabel->setVisible(active); m_bgpPathLabel->setVisible(active && !m_bgpPathLabel->text().isEmpty()); m_statsLabel->setVisible(active && !m_statsLabel->text().isEmpty()); + m_diagnostics->setVisible(active && m_diagnosticsBtn->isChecked()); m_diagnosticsCell->setVisible(active); m_videoCell->setVisible(active); m_settingsCell->setVisible(active); m_videoControls->setVisible(settings); m_showVideoBtn->setVisible(receiving); m_videoStatus->setVisible(active && !m_videoStatus->text().isEmpty()); QSize panelMinimum = m_panel->minimumSizeHint(); - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call layout active=%d preview=%d settings=%d window=%dx%d panel_min=%dx%d", - active, m_localPreview->isVisible(), settings, width(), height(), panelMinimum.width(), panelMinimum.height()); + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call layout active=%d preview=%d settings=%d avatar=%d diagnostics=%d window=%dx%d panel_min=%dx%d", + active, m_localPreview->isVisible(), settings, !m_avatarLabel->isHidden(), !m_diagnostics->isHidden(), + width(), height(), panelMinimum.width(), panelMinimum.height()); update(); } diff --git a/tools/chatgui/src/callwindow.h b/tools/chatgui/src/callwindow.h index 41c6e265..b16cd0f9 100644 --- a/tools/chatgui/src/callwindow.h +++ b/tools/chatgui/src/callwindow.h @@ -36,7 +36,7 @@ public: quint64 callId() const { return m_callId; } // ID для команд моста и сигнала closed. void setPeerName(const QString& name); // Имя, буквенная аватарка и заголовок отдельного видеоокна. - void setBgpPath(const QString& path); // Текст пути; показывается только при Active и непустой строке. + void setBgpPath(const QString& path); // Текст пути; показывается в диагностике Active при непустой строке. void setConnectionType(uint8_t type, quint64 nextHop); // 0/1/2/3 — нет/direct/reverse/indirect; nextHop в tooltip только для 3. // rtt/buffer/minimum/maximum/reserve — мс; tempo=100 для 1x, dropped/underruns — счётчики; min/max за 10с. void setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve); @@ -75,6 +75,8 @@ private: QWidget *m_videoControls, *m_panel; // Сворачиваемые настройки и центральная панель шириной до 640. CallVideoWindow* m_videoWindow; // Дочернее Qt::Window; закрытие скрывает картинку, звонок продолжает работать. QPushButton *m_settingsBtn, *m_showVideoBtn; // Раскрытие настроек и повторное открытие remote-видео. + QPushButton* m_diagnosticsBtn; // Независимый checkable-переключатель блока метрик, по умолчанию выключен. + QWidget* m_diagnostics; // Тип соединения, BGP-путь, аудиобуфер/RTT/latency; скрытие не останавливает сбор метрик. QImage m_remoteImage, m_localImage; // Последние shared-кадры; видеодвижок очищает их при stop. bool m_receivingVideo = false; // Active + непустой remote-кадр; не признак видимости отдельного окна. quint64 m_videoCall = 0; // ID, с которым запущен видеодвижок; 0 после Ended/close. @@ -104,5 +106,6 @@ private: QWidget* m_muteCell; // Кнопка mute и подпись. QWidget* m_videoCell; // Кнопка камеры и подпись справа от mute в общей центрированной строке. QWidget* m_settingsCell; // Шестерёнка настроек видео и подпись справа от камеры. + QWidget* m_diagnosticsCell; // Кнопка диагностики и подпись справа от настроек в нижней строке действий. QLabel* m_muteCaption; // Меняется между Mute/Unmute. };