Pin call actions to the bottom and add a separate diagnostics toggle

This commit is contained in:
evgeny committed 2026-10-28 14:19:14 +01:00
1 parent 264a13705f
commit b82e9cc98e
3 files changed
+45 -16

No files matched your search

+5 -4
View File
@@ -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());
+36 -11
View File
@@ -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();
}
+4 -1
View File
@@ -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.
};