Replace desktop call settings with compact capture control menus

This commit is contained in:
evgeny committed 2026-10-28 14:06:40 +01:00
1 parent f76aacfba1
commit 2540909fda
2 files changed
+134 -78

No files matched your search

+123 -73
View File
@@ -1,5 +1,6 @@
#include <QCoreApplication>
#include "callwindow.h"
#include "capture_controls.h"
#include "../transport/gui_bridge.h"
#include "call_audio_engine.h"
#include "call_video_engine.h"
@@ -11,8 +12,9 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QSpinBox>
#include <QFormLayout>
#include <QWidgetAction>
#include <QTimer>
#include <QDateTime>
#include <QCloseEvent>
@@ -32,6 +34,7 @@ extern "C" {
enum class Glyph { Phone, PhoneDown, Mic, MicOff, Camera, CameraOff, Settings };
static QString callText(const char* text) { return QCoreApplication::translate("ChatGui", text); }
using namespace CaptureControls;
// Отдельное окно показа. Закрытие скрывает только изображение, не завершает звонок.
class CallVideoWindow : public QWidget {
@@ -317,11 +320,6 @@ CallWindow::CallWindow(QWidget* parent)
m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QCoreApplication::translate("ChatGui", "Mute"), this);
m_muteBtn->setCheckable(true);
m_deviceCombo = new QComboBox(this);
m_deviceCombo->setStyleSheet(
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 6px 10px; font-size: 13px; background: palette(base); }");
auto* root = new QVBoxLayout(this);
root->setContentsMargins(12, 12, 12, 12);
root->setSizeConstraint(QLayout::SetMinimumSize);
@@ -360,11 +358,28 @@ CallWindow::CallWindow(QWidget* parent)
m_videoStatus = new QLabel(this); 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);
m_videoMenuBar->setObjectName("callVideoControls");
auto* menuStyle = new MenuStyle; menuStyle->setParent(this); m_videoMenuBar->setStyle(menuStyle);
videoLayout->addWidget(m_videoMenuBar, 0, Qt::AlignHCenter); lay->insertWidget(0, m_videoControls);
auto makeMenu = [this](const char* heading, const char* name) {
auto* menu = m_videoMenuBar->addMenu(QString()); menu->setObjectName(QString::fromLatin1(name));
new QActionGroup(menu); menu->addSection(callText(heading)); return menu;
};
m_camera = makeMenu("Camera", "callVideoCamera");
m_codec = makeMenu("Video codec", "callVideoCodec");
m_bitrate = makeMenu("Video bitrate", "callVideoBitrate");
m_resolution = makeMenu("Video resolution", "callVideoResolution");
m_fps = makeMenu("Frame rate", "callVideoFps");
m_output = makeMenu("Audio output", "callAudioOutput");
addOption(m_output, callText("Default (auto)"), QString());
auto* actions = new QHBoxLayout();
m_videoEnabled = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::CameraOff, 96), callText("Turn video on"), this);
m_videoEnabled->setObjectName("callVideoEnabled");
m_videoEnabled->setCheckable(true); m_videoEnabled->setAccessibleName(callText("Turn video on"));
actions->addStretch(1);
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_showVideoBtn = new QPushButton(callText("Show video"), this); actions->addWidget(m_showVideoBtn);
lay->addLayout(actions);
@@ -378,54 +393,32 @@ CallWindow::CallWindow(QWidget* parent)
m_videoWindow->raise(); m_videoWindow->activateWindow();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video window reopened");
});
auto* modes = new QHBoxLayout();
m_camera = new QComboBox(this);
m_resolution = new QComboBox(this);
auto refreshModes = [this] {
QSize wanted = m_resolution->currentData().toSize();
const int previousFps = m_resolution->currentData(Qt::UserRole + 1).toInt();
const int wantedFps = previousFps ? previousFps : 30;
if (wanted.isEmpty()) wanted = QSize(640, 480);
m_resolution->clear();
for (const auto& value : m_camera->currentData(Qt::UserRole + 1).toList()) {
const auto mode = value.toList(); const QSize size = mode[0].toSize(); const int fps = mode[1].toInt();
m_resolution->addItem(QStringLiteral("%1×%2 · %3 fps").arg(size.width()).arg(size.height()).arg(fps), size);
m_resolution->setItemData(m_resolution->count() - 1, fps, Qt::UserRole + 1);
}
int index = -1;
for (int i = 0; i < m_resolution->count(); ++i)
if (m_resolution->itemData(i).toSize() == wanted && m_resolution->itemData(i, Qt::UserRole + 1).toInt() == wantedFps) index = i;
if (index < 0) index = m_resolution->findData(QSize(640, 480));
if (index < 0) index = m_resolution->count() - 1;
m_resolution->setCurrentIndex(index); m_resolution->setEnabled(index >= 0);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call camera resolutions device=%s count=%d requested=%dx%d selected=%dx%d fps=%d",
m_camera->currentData().toByteArray().constData(), m_resolution->count(), wanted.width(), wanted.height(),
m_resolution->currentData().toSize().width(), m_resolution->currentData().toSize().height(),
m_resolution->currentData(Qt::UserRole + 1).toInt());
};
connect(m_video, &CallVideoEngine::camerasChanged, this, [this, refreshModes](const CallCameraDevices& cameras) {
const auto selected = m_camera->currentData();
m_camera->clear();
connect(m_video, &CallVideoEngine::camerasChanged, this, [this](const CallCameraDevices& cameras) {
const auto previous = value(m_camera); m_cameras = cameras;
clearOptions(m_camera, "Camera");
for (const auto& camera : cameras) {
QVariantList sizes;
for (const auto& mode : camera.modes) sizes.append(QVariant(QVariantList{mode.size, mode.fps}));
m_camera->addItem(camera.name, camera.id);
m_camera->setItemData(m_camera->count() - 1, sizes, Qt::UserRole + 1);
auto* action = addOption(m_camera, camera.name, camera.id); action->setEnabled(!camera.modes.isEmpty());
}
int index = m_camera->findData(selected);
if (index < 0) for (int i = 0; i < m_camera->count(); ++i) {
if (!m_camera->itemData(i, Qt::UserRole + 1).toList().isEmpty()) { index = i; break; }
auto* previousAction = option(m_camera, previous);
if (previousAction && previousAction->isEnabled()) previousAction->setChecked(true);
else for (auto* action : m_camera->actions()) {
if (action->isCheckable() && action->isEnabled()) { action->setChecked(true); break; }
}
if (index >= 0) m_camera->setCurrentIndex(index);
refreshModes();
refreshVideoModes();
if (m_videoEnabled->isChecked()) applyVideo();
});
m_codec = new QComboBox(this); m_codec->addItem(callText("Auto (H.265 → H.264)"), 0); m_codec->addItem("H.265", 2); m_codec->addItem("H.264", 1);
modes->addWidget(m_camera, 1); modes->addWidget(m_resolution); modes->addWidget(m_codec); videoLayout->addLayout(modes);
auto* rates = new QHBoxLayout(); rates->addWidget(new QLabel(callText("Bitrate range, kbit/s:"), this));
m_videoMin = new QSpinBox(this); m_videoMax = new QSpinBox(this);
for (auto* spin : {m_videoMin, m_videoMax}) { spin->setRange(300, 4000); spin->setSingleStep(100); rates->addWidget(spin); }
m_videoMin->setValue(300); m_videoMax->setValue(4000); videoLayout->addLayout(rates); lay->addWidget(m_videoControls);
addOption(m_codec, callText("Auto (H.265 → H.264)"), 0); addOption(m_codec, "H.265", 2); addOption(m_codec, "H.264", 1);
auto* ranges = new QWidget(m_bitrate); auto* rates = new QFormLayout(ranges);
auto* rangeAction = new QWidgetAction(m_bitrate); rangeAction->setDefaultWidget(ranges); m_bitrate->addAction(rangeAction);
m_videoMin = new QSpinBox(ranges); m_videoMax = new QSpinBox(ranges);
m_videoMin->setObjectName("callVideoMinimumBitrate"); m_videoMax->setObjectName("callVideoMaximumBitrate");
for (auto* spin : {m_videoMin, m_videoMax}) {
spin->setRange(300, 4000); spin->setSingleStep(100); spin->setSuffix(callText(" kbit/s"));
}
rates->addRow(callText("Call video: minimum bitrate"), m_videoMin);
rates->addRow(callText("Call video: maximum bitrate"), m_videoMax);
m_videoMin->setValue(300); m_videoMax->setValue(4000);
ranges->setToolTip(callText("Video bitrate adapts to the connection within this range. Default: 300–4000 kbit/s."));
connect(m_videoEnabled, &QPushButton::toggled, this, [this](bool checked) {
m_videoEnabled->setIcon(makeRoundIcon(checked ? QColor("#2980b9") : QColor("#7f8c8d"), checked ? Glyph::Camera : Glyph::CameraOff, 96));
const QString action = callText(checked ? "Turn video off" : "Turn video on");
@@ -433,13 +426,28 @@ CallWindow::CallWindow(QWidget* parent)
DEBUG_INFO(DEBUG_CATEGORY_CALL, "desktop video button id=%016llx enabled=%d", (unsigned long long)m_callId, checked);
applyVideo();
});
connect(m_camera, QOverload<int>::of(&QComboBox::activated), this, [this, refreshModes] { refreshModes(); applyVideo(); });
for (auto* combo : {m_resolution, m_codec})
connect(combo, QOverload<int>::of(&QComboBox::activated), this, [this] { applyVideo(); });
connect(m_camera, &QMenu::triggered, this, [this] { refreshVideoModes(); applyVideo(); });
connect(m_resolution, &QMenu::triggered, this, [this] {
populateFrameRates(m_fps, m_modes, value(m_resolution).toSize(), value(m_fps).toInt()); applyVideo();
});
for (auto* menu : {m_fps, m_codec}) connect(menu, &QMenu::triggered, this, &CallWindow::applyVideo);
auto updateRange = [this] {
if (m_videoMin->value() > m_videoMax->value()) m_videoMax->setValue(m_videoMin->value());
if (m_videoCall) call_video_port_range(m_videoCall, m_videoMin->value() * 1000, m_videoMax->value() * 1000);
if (m_videoCall && call_video_port_range(m_videoCall, m_videoMin->value() * 1000, m_videoMax->value() * 1000) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call bitrate range refused id=%llu min_kbps=%d max_kbps=%d",
(unsigned long long)m_videoCall, m_videoMin->value(), m_videoMax->value());
m_videoStatus->setText(callText("Could not save the call video bitrate range.")); m_videoStatus->show();
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call bitrate range selected id=%llu min_kbps=%d max_kbps=%d",
(unsigned long long)m_videoCall, m_videoMin->value(), m_videoMax->value());
updateVideoMenuButtons();
};
connect(m_videoMin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int minimum) {
if (minimum > m_videoMax->value()) m_videoMax->setValue(minimum); updateVideoMenuButtons();
});
connect(m_videoMax, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int maximum) {
if (maximum < m_videoMin->value()) m_videoMin->setValue(maximum); updateVideoMenuButtons();
});
connect(m_videoMin, &QSpinBox::editingFinished, this, updateRange);
connect(m_videoMax, &QSpinBox::editingFinished, this, updateRange);
connect(m_video, &CallVideoEngine::statusChanged, this, [this](const QString& text) {
@@ -492,19 +500,18 @@ CallWindow::CallWindow(QWidget* parent)
btn->addWidget(m_settingsCell);
btn->addStretch(1);
lay->addLayout(btn);
videoLayout->addWidget(m_deviceCombo);
connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept);
connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline);
connect(m_hangupBtn, &QPushButton::clicked, this, &CallWindow::onHangup);
connect(m_muteBtn, &QPushButton::toggled, this, &CallWindow::onMuteToggled);
connect(m_deviceCombo, QOverload<int>::of(&QComboBox::activated), this, &CallWindow::onDeviceChanged);
connect(m_output, &QMenu::triggered, this, &CallWindow::onDeviceChanged);
m_durationTimer = new QTimer(this);
m_durationTimer->setInterval(1000);
connect(m_durationTimer, &QTimer::timeout, this, &CallWindow::updateDuration);
setState(Outgoing);
updateVideoMenuButtons(); setState(Outgoing);
}
CallWindow::~CallWindow() {
@@ -556,7 +563,7 @@ void CallWindow::setState(State s) {
m_durationLabel->show();
m_startMs = QDateTime::currentMSecsSinceEpoch();
m_durationTimer->start();
populateDeviceCombo();
populateOutputDevices();
m_muteBtn->setChecked(false);
break;
case Ended:
@@ -569,21 +576,19 @@ void CallWindow::setState(State s) {
updateButtons();
}
void CallWindow::populateDeviceCombo() {
m_deviceCombo->blockSignals(true);
m_deviceCombo->clear();
m_deviceCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString());
void CallWindow::populateOutputDevices() {
clearOptions(m_output, "Audio output");
addOption(m_output, callText("Default (auto)"), QString());
const auto devs = SoundManager::enumeratePlaybackDevices();
for (int i = 0; i < devs.size(); i++) {
QString label = devs[i].name;
if (devs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)");
m_deviceCombo->addItem(label, devs[i].id);
addOption(m_output, label, devs[i].id);
}
QString cur = CallAudioEngine::instance()->playbackDevice();
for (int i = 0; i < m_deviceCombo->count(); i++) {
if (m_deviceCombo->itemData(i).toString() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
}
m_deviceCombo->blockSignals(false);
if (!select(m_output, cur))
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call audio output unavailable device=%s; using default selection", qPrintable(cur));
updateVideoMenuButtons();
}
void CallWindow::updateButtons() {
@@ -616,16 +621,59 @@ void CallWindow::updateVideoPresentation() {
}
void CallWindow::applyVideo() {
updateVideoMenuButtons();
if (!m_videoCall) return;
QSize size = m_resolution->currentData().toSize();
const bool enabled = m_videoEnabled->isChecked() && !size.isEmpty();
QSize size = value(m_resolution).toSize();
const bool enabled = m_videoEnabled->isChecked() && !size.isEmpty() && selected(m_fps);
if (m_videoEnabled->isChecked() && !enabled) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera has no selected mode device=%s", m_camera->currentData().toByteArray().constData());
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera has no selected mode device=%s", value(m_camera).toByteArray().constData());
m_videoStatus->setText(callText("No supported video modes")); m_videoStatus->show();
}
if (size.isEmpty()) size = QSize(640, 480);
const int fps = m_resolution->currentData(Qt::UserRole + 1).toInt();
m_video->setVideo(enabled, m_camera->currentData().toByteArray(), size.width(), size.height(), m_codec->currentData().toInt(), fps ? fps : 30);
const int fps = value(m_fps).toInt();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video selection id=%llu enabled=%d device=%s mode=%dx%d/%d codec=%d",
(unsigned long long)m_videoCall, enabled, value(m_camera).toByteArray().constData(), size.width(), size.height(), fps, value(m_codec).toInt());
m_video->setVideo(enabled, value(m_camera).toByteArray(), size.width(), size.height(), value(m_codec).toInt(), fps ? fps : 30);
}
void CallWindow::refreshVideoModes() {
QSize wanted = value(m_resolution).toSize(); if (wanted.isEmpty()) wanted = QSize(640, 480);
const int previousFps = selected(m_fps) ? value(m_fps).toInt() : 30;
clearOptions(m_resolution, "Video resolution"); m_modes.clear();
for (const auto& camera : m_cameras) if (camera.id == value(m_camera).toByteArray()) { m_modes = camera.modes; break; }
for (const auto& mode : m_modes) if (!option(m_resolution, mode.size))
addOption(m_resolution, QStringLiteral("%1 × %2").arg(mode.size.width()).arg(mode.size.height()), mode.size);
if (!select(m_resolution, wanted) && !select(m_resolution, QSize(640, 480)) && !m_modes.isEmpty())
select(m_resolution, m_modes.back().size);
populateFrameRates(m_fps, m_modes, value(m_resolution).toSize(), previousFps);
if (m_modes.isEmpty()) DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera has no supported modes device=%s", value(m_camera).toByteArray().constData());
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call camera modes device=%s count=%lld requested=%dx%d selected=%dx%d/%d",
value(m_camera).toByteArray().constData(), (long long)m_modes.size(), wanted.width(), wanted.height(),
value(m_resolution).toSize().width(), value(m_resolution).toSize().height(), value(m_fps).toInt());
updateVideoMenuButtons();
}
void CallWindow::updateVideoMenuButtons() {
auto update = [this](QMenu* menu, const char* heading, const QString& shape, const QString& label, const QString& detail = {}) {
menu->menuAction()->setIcon(controlIcon(palette(), shape, label, detail));
QString description = callText(heading);
if (auto* action = selected(menu)) description += QStringLiteral(": ") + action->text();
menu->menuAction()->setToolTip(description); menu->setAccessibleName(description);
};
auto* camera = selected(m_camera);
update(m_camera, "Camera", "camera", camera ? camera->text() : "—");
int codec = value(m_codec).toInt();
update(m_codec, "Video codec", "chip", codec ? QStringLiteral("H%1").arg(codec == 2 ? 265 : 264) : "Auto");
auto shortRate = [](int kbps) { return kbps >= 1000 ? QString::number(kbps / 1000.0, 'g', 2) + 'M' : QString::number(kbps) + 'k'; };
update(m_bitrate, "Video bitrate", "gauge", shortRate(m_videoMax->value()), shortRate(m_videoMin->value()));
QString range = callText("Bitrate range, kbit/s:") + QStringLiteral(" %1–%2").arg(m_videoMin->value()).arg(m_videoMax->value());
m_bitrate->menuAction()->setToolTip(range); m_bitrate->setAccessibleName(range);
QSize size = value(m_resolution).toSize();
update(m_resolution, "Video resolution", "screen", size.isEmpty() ? "—" : QString::number(std::min(size.width(), size.height())) + 'p');
update(m_fps, "Frame rate", "fps", selected(m_fps) ? value(m_fps).toString() : "—", "FPS");
auto* output = selected(m_output); update(m_output, "Audio output", "output", output ? output->text() : "—");
m_resolution->menuAction()->setEnabled(!m_modes.isEmpty()); m_fps->menuAction()->setEnabled(!m_modes.isEmpty());
m_videoMenuBar->setFixedWidth(m_videoMenuBar->sizeHint().width());
}
void CallWindow::onAccept() {
@@ -654,9 +702,11 @@ void CallWindow::onMuteToggled(bool checked) {
m_muteCaption->setText(checked ? QCoreApplication::translate("ChatGui", "Unmute") : QCoreApplication::translate("ChatGui", "Mute"));
}
void CallWindow::onDeviceChanged(int index) {
QString id = m_deviceCombo->itemData(index).toString();
void CallWindow::onDeviceChanged() {
QString id = value(m_output).toString();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call audio output selected device=%s", qPrintable(id));
CallAudioEngine::instance()->setPlaybackDevice(id);
updateVideoMenuButtons();
}
/* Закрытие непринятого вызова равнозначно Decline; локальный Ended исключает повторную команду. */
+11 -5
View File
@@ -6,10 +6,12 @@
#include <QWidget>
#include <QImage>
#include <cstdint>
#include "call_camera_capture.h"
class QLabel;
class QPushButton;
class QComboBox;
class QMenu;
class QMenuBar;
class QTimer;
class QSpinBox;
class CallVideoEngine;
@@ -53,16 +55,21 @@ private slots:
void onHangup(); // Ставит Hangup; при отказе сохраняет окно и показывает ошибку.
void updateDuration(); // Только Active: прошедшее wall-clock время в минуты:секунды.
void onMuteToggled(bool checked); // Меняет mute общего аудиодвижка, иконку и подпись.
void onDeviceChanged(int index); // Применяет backend ID выбранного выхода m_deviceCombo.
void onDeviceChanged(); // Применяет backend ID выбранного аудиовыхода m_output.
private:
void updateButtons(); // Видимость действий по State, затем общая раскладка видео.
void populateDeviceCombo(); // Выходы SoundManager: пустой ID — default, текущий выбор из CallAudioEngine.
void populateOutputDevices(); // Выходы SoundManager: пустой ID — default, текущий выбор из CallAudioEngine.
void refreshVideoModes(); // Уникальные размеры выбранной камеры, затем доступные FPS; сохраняет текущий допустимый режим.
void updateVideoMenuButtons(); // Текущие camera/codec/resolution/FPS/bitrate/output внутри кнопок, полные названия в подсказках.
void applyVideo(); // Применяет выбранную камеру/режим; без доступного режима отправку видео отключает.
void updateVideoPresentation(); // Отдельное remote-видео, локальный preview, настройки и диагностические подписи.
CallVideoEngine* m_video; // Дочерний TX/RX/показ; start по таймеру 100мс только при Active и совпадающем активном аудиозвонке.
QPushButton* m_videoEnabled; // Круглая checkable-кнопка отправки видео; наличие camera mode проверяется отдельно.
QComboBox *m_camera, *m_resolution, *m_codec; // ID камеры, размер/FPS и codec: 0=Auto, 2=H.265, 1=H.264.
QMenuBar* m_videoMenuBar; // Общий стиль трансляции: кнопки 48x48, значки 40x36, переключение раскрытых меню одним кликом.
QMenu *m_camera, *m_resolution, *m_fps, *m_codec, *m_bitrate, *m_output; // checked QAction/data хранит выбранный вариант.
CallCameraDevices m_cameras; // Последний полный снимок worker; режимы не переносятся между разными ID камер.
QList<StreamVideoMode> m_modes; // Допустимые пары resolution/FPS текущей камеры.
QSpinBox *m_videoMin, *m_videoMax; // Диапазон 300..4000 кбит/с; native port получает бит/с.
QLabel *m_localPreview, *m_videoStatus; // Малый локальный preview и статус/ошибка видеодвижка.
QWidget *m_videoControls, *m_panel; // Сворачиваемые настройки и центральная панель шириной до 640.
@@ -89,7 +96,6 @@ private:
QPushButton* m_declineBtn; // Decline для Incoming; результат закрывает окно через MainWindow.
QPushButton* m_hangupBtn; // Завершение остальных живых состояний.
QPushButton* m_muteBtn; // Checkable mute общего CallAudioEngine, виден при Active.
QComboBox* m_deviceCombo; // Выбор общего выходного устройства звонка; находится внутри настроек.
QTimer* m_durationTimer; // GUI 1с; запускается в Active, останавливается в Ended/close.
QWidget* m_acceptCell; // Кнопка Accept и её подпись, показываются вместе.