Move desktop broadcast settings into compact top menus with separate resolution and FPS
This commit is contained in:
1 parent
7d3f0852f7
commit
35875a2c43
3 files changed
+254
-138
No files matched your search
@@ -5,15 +5,17 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QMenuBar>
|
||||
#include <QMenu>
|
||||
#include <QActionGroup>
|
||||
#include <QWidgetAction>
|
||||
#include <QToolButton>
|
||||
#include <QProxyStyle>
|
||||
#include <QPainter>
|
||||
#include <QScrollArea>
|
||||
#include <QSignalBlocker>
|
||||
#include <QJsonDocument>
|
||||
#include <QScopedValueRollback>
|
||||
@@ -28,8 +30,64 @@
|
||||
|
||||
namespace {
|
||||
constexpr int MaximumManualBitrate = 12000000;
|
||||
class StreamMenuStyle : public QProxyStyle {
|
||||
public:
|
||||
int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const override {
|
||||
return metric == PM_SmallIconSize && qobject_cast<const QMenuBar*>(widget) ? 36 : QProxyStyle::pixelMetric(metric, option, widget);
|
||||
}
|
||||
};
|
||||
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
||||
QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; }
|
||||
|
||||
QAction* selected(QMenu* menu) { return menu->findChild<QActionGroup*>()->checkedAction(); }
|
||||
QVariant value(QMenu* menu) { auto* action = selected(menu); return action ? action->data() : QVariant(); }
|
||||
QAction* option(QMenu* menu, const QVariant& data) {
|
||||
for (auto* action : menu->actions()) if (action->isCheckable() && action->data() == data) return action;
|
||||
return nullptr;
|
||||
}
|
||||
bool select(QMenu* menu, const QVariant& data) {
|
||||
auto* action = option(menu, data);
|
||||
if (action) action->setChecked(true);
|
||||
return action;
|
||||
}
|
||||
QAction* addOption(QMenu* menu, const QString& label, const QVariant& data) {
|
||||
auto* group = menu->findChild<QActionGroup*>();
|
||||
auto* action = menu->addAction(label); action->setCheckable(true); action->setData(data); group->addAction(action);
|
||||
if (!group->checkedAction()) action->setChecked(true);
|
||||
return action;
|
||||
}
|
||||
void clearOptions(QMenu* menu, const char* heading) { menu->clear(); menu->addSection(text(heading)); }
|
||||
|
||||
/* Компактные векторные значки с текущим значением, читаемые в обеих темах. */
|
||||
QIcon controlIcon(const QPalette& palette, const QString& shape, const QString& label = {}) {
|
||||
QPixmap pixmap(40, 36); pixmap.fill(Qt::transparent); QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QPen(palette.windowText(), 1.6));
|
||||
if (shape == "camera") {
|
||||
painter.drawRoundedRect(QRectF(5, 9, 30, 21), 3, 3); painter.drawEllipse(QPointF(20, 19), 6, 6);
|
||||
painter.drawLine(12, 8, 15, 5); painter.drawLine(15, 5, 25, 5); painter.drawLine(25, 5, 28, 8);
|
||||
} else if (shape == "screen") {
|
||||
painter.drawRoundedRect(QRectF(3, 4, 34, 25), 2, 2); painter.drawLine(20, 29, 20, 33); painter.drawLine(12, 33, 28, 33);
|
||||
} else if (shape == "settings") {
|
||||
painter.translate(20, 18);
|
||||
for (int i = 0; i < 8; i++) { painter.drawLine(0, -10, 0, -15); painter.rotate(45); }
|
||||
painter.drawEllipse(QPointF(), 10, 10); painter.drawEllipse(QPointF(), 4, 4);
|
||||
} else if (shape == "microphone") {
|
||||
painter.drawRoundedRect(QRectF(15, 3, 10, 19), 5, 5); painter.drawArc(QRectF(10, 12, 20, 16), 180 * 16, 180 * 16);
|
||||
painter.drawLine(20, 28, 20, 33); painter.drawLine(14, 33, 26, 33);
|
||||
} else if (shape == "sound" || shape == "silent") {
|
||||
QPolygon speaker; speaker << QPoint(6, 14) << QPoint(13, 14) << QPoint(22, 7) << QPoint(22, 29)
|
||||
<< QPoint(13, 22) << QPoint(6, 22); painter.drawPolygon(speaker);
|
||||
if (shape == "sound") painter.drawArc(QRectF(18, 6, 17, 24), -70 * 16, 140 * 16);
|
||||
else { painter.drawLine(27, 13, 35, 23); painter.drawLine(27, 23, 35, 13); }
|
||||
} else {
|
||||
painter.drawRoundedRect(QRectF(3, 5, 34, 26), 4, 4);
|
||||
}
|
||||
if (!label.isEmpty()) {
|
||||
QFont font = painter.font(); font.setPixelSize(label.size() > 4 ? 10 : 12); font.setBold(true); painter.setFont(font);
|
||||
painter.drawText(QRectF(3, 5, 34, 26), Qt::AlignCenter, label);
|
||||
}
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
/* Горизонталь — сохранённое окно источника, одна линия объединяет субпотоки получателя. */
|
||||
@@ -55,8 +113,8 @@ protected:
|
||||
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);
|
||||
QColor background = palette().window().color();
|
||||
painter.setPen(Qt::NoPen); painter.setBrush(background.lightness() < 128 ? background.lighter(135) : background.darker(106));
|
||||
painter.drawRoundedRect(rect(), 10, 10);
|
||||
QRectF area = QRectF(rect()).adjusted(6, 6, -6, -6);
|
||||
painter.drawRoundedRect(rect(), 5, 5);
|
||||
QRectF area = QRectF(rect()).adjusted(3, 3, -3, -3);
|
||||
if (!m_positions.count) {
|
||||
QColor color = palette().windowText().color(); color.setAlphaF(0.3);
|
||||
painter.setPen(QPen(color, 2)); painter.drawLine(QPointF(area.left(), area.center().y()), QPointF(area.right(), area.center().y()));
|
||||
@@ -93,13 +151,36 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture settings invalid: %s", qPrintable(error.errorString()));
|
||||
else m_settings = document.object();
|
||||
}
|
||||
setWindowTitle(text("Broadcast stream")); resize(720, 720); setMinimumSize(460, 480);
|
||||
setWindowTitle(text("Broadcast stream")); resize(900, 620); setMinimumSize(780, 360);
|
||||
auto* escape = new QShortcut(QKeySequence(Qt::Key_Escape), this); escape->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
connect(escape, &QShortcut::activated, this, &QWidget::close);
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
auto* controls = new QHBoxLayout; controls->addStretch();
|
||||
auto* controls = new QHBoxLayout;
|
||||
m_menuBar = new QMenuBar(this); m_menuBar->setNativeMenuBar(false); m_menuBar->setMouseTracking(true);
|
||||
auto* menuStyle = new StreamMenuStyle; menuStyle->setParent(m_menuBar); m_menuBar->setStyle(menuStyle);
|
||||
m_menuBar->setObjectName(QStringLiteral("streamControls"));
|
||||
m_menuBar->setStyleSheet("QMenuBar { background: transparent; } QMenuBar::item { padding: 3px; }");
|
||||
auto makeMenu = [this](const char* heading, const char* name) {
|
||||
auto* menu = m_menuBar->addMenu(QString()); menu->setObjectName(QString::fromLatin1(name));
|
||||
menu->menuAction()->setToolTip(text(heading)); menu->setAccessibleName(text(heading));
|
||||
new QActionGroup(menu); menu->addSection(text(heading)); return menu;
|
||||
};
|
||||
m_source = makeMenu("Video source", "streamVideoSource");
|
||||
m_codec = makeMenu("Video codec", "streamVideoCodec");
|
||||
m_bitrate = makeMenu("Video bitrate", "streamVideoBitrate");
|
||||
m_resolution = makeMenu("Video resolution", "streamVideoResolution");
|
||||
m_fps = makeMenu("Frame rate", "streamVideoFps");
|
||||
m_rotation = makeMenu("Rotation", "streamVideoRotation");
|
||||
m_maxQp = makeMenu("Maximum QP", "streamMaxQp");
|
||||
m_microphone = makeMenu("Audio source", "streamAudioSource");
|
||||
auto* settingsMenu = makeMenu("Settings", "streamExtraSettings");
|
||||
settingsMenu->menuAction()->setIcon(controlIcon(palette(), "settings"));
|
||||
controls->addWidget(m_menuBar); controls->addStretch();
|
||||
m_sound = new QToolButton(this); m_sound->setCheckable(true); m_sound->setIconSize(QSize(40, 36));
|
||||
m_sound->setObjectName(QStringLiteral("streamSound")); m_sound->setAutoRaise(true); controls->addWidget(m_sound);
|
||||
m_start = new QPushButton(text("Start stream"), this); m_start->setMinimumHeight(40);
|
||||
m_buffer = new StreamBufferIndicator(this); m_buffer->setFixedSize(std::max(160, m_start->sizeHint().width() * 2), 40);
|
||||
m_start->setObjectName(QStringLiteral("streamStart"));
|
||||
m_buffer = new StreamBufferIndicator(this); m_buffer->setFixedSize(std::max(80, m_start->sizeHint().width()), 20);
|
||||
controls->addWidget(m_buffer); controls->addWidget(m_start); layout->addLayout(controls);
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
auto* preview = new QVideoWidget(this); m_preview = preview;
|
||||
@@ -113,56 +194,38 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
m_preview = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), this);
|
||||
#endif
|
||||
layout->addWidget(m_preview, 1);
|
||||
auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true);
|
||||
auto* settings = new QWidget(scroll); auto* form = new QFormLayout(settings);
|
||||
auto* settings = new QWidget(settingsMenu); auto* form = new QFormLayout(settings);
|
||||
form->setRowWrapPolicy(QFormLayout::WrapLongRows);
|
||||
scroll->setWidget(settings); scroll->setMinimumHeight(220); layout->addWidget(scroll);
|
||||
m_source = new QComboBox(settings); m_source->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
|
||||
m_source->setMinimumContentsLength(16); m_source->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
auto* sourceRow = new QWidget(settings); auto* sourceLayout = new QHBoxLayout(sourceRow); sourceLayout->setContentsMargins(0, 0, 0, 0);
|
||||
sourceLayout->addWidget(m_source, 1); auto* refresh = new QPushButton(text("Refresh"), settings); sourceLayout->addWidget(refresh);
|
||||
connect(refresh, &QPushButton::clicked, this, &StreamPublishWindow::refreshSources); form->addRow(text("Video source"), sourceRow);
|
||||
connect(m_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] { m_source->setToolTip(m_source->currentText()); });
|
||||
m_mode = new QComboBox(settings);
|
||||
m_mode->setObjectName(QStringLiteral("streamVideoMode")); m_source->setObjectName(QStringLiteral("streamVideoSource"));
|
||||
form->addRow(text("Video mode"), m_mode);
|
||||
m_codec = new QComboBox(settings); form->addRow(text("Video codec"), m_codec);
|
||||
m_bitrate = new QComboBox(settings); m_bitrate->addItem(text("Automatic"), 0);
|
||||
auto* settingsAction = new QWidgetAction(settingsMenu); settingsAction->setDefaultWidget(settings); settingsMenu->addAction(settingsAction);
|
||||
addOption(m_bitrate, text("Automatic"), 0);
|
||||
for (int bitrate : {250000, 500000, 1000000, 2000000, 4000000, 8000000, MaximumManualBitrate})
|
||||
m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||||
addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop manual stream bitrate selection maximum_bps=%d", MaximumManualBitrate);
|
||||
form->addRow(text("Video bitrate"), m_bitrate);
|
||||
m_rotation = new QButtonGroup(this); auto* rotationRow = new QWidget(settings); auto* angles = new QHBoxLayout(rotationRow);
|
||||
angles->setContentsMargins(0, 0, 0, 0);
|
||||
for (int angle : {0, 90, 270}) {
|
||||
auto* button = new QPushButton(QStringLiteral("%1°").arg(angle), settings); button->setCheckable(true); button->setFixedSize(66, 34);
|
||||
auto* action = addOption(m_rotation, QStringLiteral("%1°").arg(angle), angle);
|
||||
QPixmap icon(20, 20); icon.fill(Qt::transparent); QPainter painter(&icon);
|
||||
painter.translate(10, 10); painter.rotate(-angle); painter.setPen(QPen(QColor("#e53935"), 3)); painter.drawLine(-6, -6, 6, -6);
|
||||
painter.setPen(QPen(palette().color(QPalette::Text), 1)); painter.drawLine(-6, -4, -6, 6); painter.drawLine(6, -4, 6, 6); painter.drawLine(-6, 6, 6, 6);
|
||||
painter.end(); button->setIcon(QIcon(icon)); button->setToolTip(text("The red edge marks the top for viewers"));
|
||||
button->setStyleSheet("QPushButton:checked { background: #1976d2; color: white; }");
|
||||
m_rotation->addButton(button, angle); angles->addWidget(button);
|
||||
painter.end(); action->setIcon(QIcon(icon)); action->setToolTip(text("The red edge marks the top for viewers"));
|
||||
}
|
||||
m_rotation->button(0)->setChecked(true); angles->addStretch(); form->addRow(text("Rotation"), rotationRow);
|
||||
m_audio = new QCheckBox(text("Microphone enabled"), settings); m_audio->setChecked(true); form->addRow(m_audio);
|
||||
m_cursor = new QCheckBox(text("Show mouse cursor"), settings); m_cursor->setChecked(true);
|
||||
m_cursor->setObjectName(QStringLiteral("streamShowCursor")); form->insertRow(2, m_cursor);
|
||||
m_microphone = new QComboBox(settings); m_microphone->addItem(text("Default microphone"), QString());
|
||||
for (const auto& device : SoundManager::enumerateCaptureDevices()) m_microphone->addItem(device.name, device.id);
|
||||
form->addRow(text("Microphone"), m_microphone);
|
||||
m_cursor->setObjectName(QStringLiteral("streamShowCursor")); form->addRow(m_cursor);
|
||||
m_noAudio = addOption(m_microphone, text("No audio"), QVariant());
|
||||
addOption(m_microphone, text("Default microphone"), QString()); select(m_microphone, QString());
|
||||
for (const auto& device : SoundManager::enumerateCaptureDevices()) addOption(m_microphone, device.name, device.id);
|
||||
m_inputChannels = new QLabel(settings); form->addRow(text("Audio input"), m_inputChannels);
|
||||
m_mute = new QCheckBox(text("Mute stream audio"), settings); form->addRow(m_mute);
|
||||
m_maxQp = new QSpinBox(settings); m_maxQp->setObjectName(QStringLiteral("streamMaxQp"));
|
||||
m_maxQp->setRange(0, 51); m_maxQp->setSpecialValueText(text("Automatic")); m_maxQp->setValue(28);
|
||||
for (int qp : {0, 18, 23, 28, 35, 40}) addOption(m_maxQp, qp ? QString::number(qp) : text("Automatic"), qp);
|
||||
select(m_maxQp, 28);
|
||||
m_maxQp->setToolTip(text("Lower QP preserves more detail and may require a higher bitrate. Applies to H.264 and H.265."));
|
||||
m_adaptiveQp = new QCheckBox(text("Adapt QP to the target bitrate"), settings);
|
||||
m_adaptiveQp->setObjectName(QStringLiteral("streamAdaptiveQp"));
|
||||
m_adaptiveQp->setToolTip(text(
|
||||
"Gradually raise the QP ceiling above the selected value when bitrate is too high, then return it. Requires live QP adjustment."));
|
||||
int bitrateRow = 0; QFormLayout::ItemRole bitrateRole;
|
||||
form->getWidgetPosition(m_bitrate, &bitrateRow, &bitrateRole);
|
||||
form->insertRow(bitrateRow + 1, text("Maximum QP"), m_maxQp); form->insertRow(bitrateRow + 2, m_adaptiveQp);
|
||||
connect(m_mute, &QCheckBox::toggled, engine, &StreamPublishEngine::setMuted);
|
||||
form->addRow(m_adaptiveQp);
|
||||
connect(m_sound, &QToolButton::toggled, this, [this](bool enabled) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio enabled=%d", enabled);
|
||||
m_engine->setMuted(!enabled);
|
||||
});
|
||||
m_status = new QLabel(this); m_status->setWordWrap(true); layout->addWidget(m_status);
|
||||
m_stats = new QLabel(this); m_stats->setObjectName(QStringLiteral("streamPublishStats"));
|
||||
m_stats->setWordWrap(true); m_stats->setMinimumHeight(66); layout->addWidget(m_stats);
|
||||
@@ -170,24 +233,19 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
if (!m_engine->active()) m_status->setProperty("streamError", QString());
|
||||
if (m_engine->active()) m_engine->stop(); else m_engine->start(m_group, selection()); synchronize();
|
||||
});
|
||||
connect(m_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
|
||||
connect(m_source, &QMenu::triggered, this, [this](QAction* action) {
|
||||
if (!action->isCheckable()) return;
|
||||
if (m_updating) return;
|
||||
auto previous = selection();
|
||||
refreshModes(previous, previous.source.kind == StreamVideoSource::Screen); apply();
|
||||
if (!m_engine->active()) synchronize();
|
||||
});
|
||||
for (auto* box : {m_mode, m_codec, m_bitrate}) connect(box, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply);
|
||||
connect(m_codec, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] { if (!m_updating && !m_engine->active()) synchronize(); });
|
||||
connect(m_microphone, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply);
|
||||
connect(m_audio, &QCheckBox::toggled, this, &StreamPublishWindow::apply);
|
||||
connect(m_resolution, &QMenu::triggered, this, [this] { refreshFrameRates(value(m_fps).toInt()); apply(); });
|
||||
for (auto* menu : {m_fps, m_codec, m_bitrate, m_maxQp, m_rotation, m_microphone})
|
||||
connect(menu, &QMenu::triggered, this, &StreamPublishWindow::apply);
|
||||
for (auto* menu : {m_codec, m_maxQp}) connect(menu, &QMenu::triggered, this, [this] { if (!m_updating) synchronize(); });
|
||||
connect(m_cursor, &QCheckBox::toggled, this, &StreamPublishWindow::apply);
|
||||
connect(m_maxQp, QOverload<int>::of(&QSpinBox::valueChanged), this, [this] { apply(); if (!m_engine->active()) synchronize(); });
|
||||
connect(m_adaptiveQp, &QCheckBox::toggled, this, &StreamPublishWindow::apply);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
connect(m_rotation, &QButtonGroup::idClicked, this, &StreamPublishWindow::apply);
|
||||
#else
|
||||
connect(m_rotation, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &StreamPublishWindow::apply);
|
||||
#endif
|
||||
connect(engine, &StreamPublishEngine::codecsChanged, this, &StreamPublishWindow::refreshCodecs);
|
||||
connect(engine, &StreamPublishEngine::sourcesChanged, this, &StreamPublishWindow::refreshSources);
|
||||
connect(engine, &StreamPublishEngine::configurationChanged, this, [this] { synchronize(); saveSettings(); });
|
||||
@@ -242,10 +300,9 @@ void StreamPublishWindow::closeEvent(QCloseEvent* event) {
|
||||
void StreamPublishWindow::restoreSettings() {
|
||||
if (m_settings.isEmpty() || m_engine->active()) return;
|
||||
m_updating = true;
|
||||
auto select = [](QComboBox* box, const QVariant& value, const char* field) {
|
||||
int index = box->findData(value);
|
||||
if (index >= 0) box->setCurrentIndex(index);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture setting unavailable field=%s value=%s", field, qPrintable(value.toString()));
|
||||
auto restore = [](QMenu* menu, const QVariant& data, const char* field) {
|
||||
if (!select(menu, data))
|
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture setting unavailable field=%s value=%s", field, qPrintable(data.toString()));
|
||||
};
|
||||
int sourceIndex = -1;
|
||||
for (int i = 0; i < m_sources.size(); i++) {
|
||||
@@ -255,7 +312,7 @@ void StreamPublishWindow::restoreSettings() {
|
||||
: source.id == QByteArray::fromBase64(m_settings.value("source_id").toString().toLatin1());
|
||||
if (matches) { sourceIndex = i; break; }
|
||||
}
|
||||
if (sourceIndex >= 0) m_source->setCurrentIndex(sourceIndex);
|
||||
if (sourceIndex >= 0) select(m_source, sourceIndex);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved capture source unavailable; using current source");
|
||||
auto restored = selection();
|
||||
restored.video.width = m_settings.value("width").toInt(1280); restored.video.height = m_settings.value("height").toInt(720);
|
||||
@@ -266,17 +323,16 @@ void StreamPublishWindow::restoreSettings() {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved stream bitrate adjusted old_bps=%d maximum_bps=%d", bitrate, MaximumManualBitrate);
|
||||
bitrate = MaximumManualBitrate;
|
||||
}
|
||||
if (bitrate > 0 && m_bitrate->findData(bitrate) < 0)
|
||||
m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||||
select(m_bitrate, bitrate, "bitrate");
|
||||
if (bitrate > 0 && !option(m_bitrate, bitrate))
|
||||
addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||||
restore(m_bitrate, bitrate, "bitrate");
|
||||
int maxQp = m_settings.value("max_qp").toInt(28);
|
||||
if (maxQp < 0 || maxQp > 51) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved QP ceiling invalid value=%d", maxQp);
|
||||
m_maxQp->setValue(std::clamp(maxQp, 0, 51)); m_adaptiveQp->setChecked(m_settings.value("adaptive_qp").toBool());
|
||||
if (auto* button = m_rotation->button(m_settings.value("rotation").toInt())) button->setChecked(true);
|
||||
else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved capture rotation invalid");
|
||||
m_audio->setChecked(m_settings.value("audio").toBool(true));
|
||||
restore(m_maxQp, maxQp, "max_qp"); m_adaptiveQp->setChecked(m_settings.value("adaptive_qp").toBool());
|
||||
restore(m_rotation, m_settings.value("rotation").toInt(), "rotation");
|
||||
m_cursor->setChecked(m_settings.value("show_cursor").toBool(true));
|
||||
select(m_microphone, m_settings.value("microphone").toString(), "microphone");
|
||||
if (m_settings.value("audio").toBool(true)) restore(m_microphone, m_settings.value("microphone").toString(), "microphone");
|
||||
else m_noAudio->setChecked(true);
|
||||
m_updating = false;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings restored: %s", QJsonDocument(m_settings).toJson(QJsonDocument::Compact).constData());
|
||||
}
|
||||
@@ -284,20 +340,20 @@ void StreamPublishWindow::restoreSettings() {
|
||||
void StreamPublishWindow::saveSettings() {
|
||||
if (m_updating) return;
|
||||
auto config = selection(); auto saved = m_settings;
|
||||
if (m_source->currentIndex() >= 0) {
|
||||
if (selected(m_source)) {
|
||||
saved["source_kind"] = config.source.kind;
|
||||
saved["source_id"] = QString::fromLatin1(config.source.id.toBase64());
|
||||
saved["source_name"] = config.source.kind == StreamVideoSource::Window ? config.source.name : QString();
|
||||
}
|
||||
if (m_mode->currentIndex() >= 0) {
|
||||
if (selected(m_resolution) && selected(m_fps)) {
|
||||
saved["width"] = config.video.width; saved["height"] = config.video.height; saved["fps"] = config.video.fps;
|
||||
saved["native_size"] = config.nativeSize;
|
||||
}
|
||||
if (m_codec->currentIndex() >= 0) saved["codec"] = config.video.codec;
|
||||
saved["bitrate"] = m_bitrate->currentData().toInt(); saved["rotation"] = config.video.rotation;
|
||||
if (selected(m_codec)) saved["codec"] = config.video.codec;
|
||||
saved["bitrate"] = value(m_bitrate).toInt(); saved["rotation"] = config.video.rotation;
|
||||
saved["audio"] = config.audio; saved["microphone"] = config.microphone;
|
||||
saved["show_cursor"] = config.showCursor;
|
||||
saved["max_qp"] = m_maxQp->value(); saved["adaptive_qp"] = m_adaptiveQp->isChecked();
|
||||
saved["max_qp"] = value(m_maxQp).toInt(); saved["adaptive_qp"] = m_adaptiveQp->isChecked();
|
||||
if (saved == m_settings) return;
|
||||
m_settings = saved;
|
||||
auto json = QJsonDocument(saved).toJson(QJsonDocument::Compact);
|
||||
@@ -308,7 +364,7 @@ void StreamPublishWindow::saveSettings() {
|
||||
void StreamPublishWindow::refreshSources() {
|
||||
QScopedValueRollback<bool> updating(m_updating, true);
|
||||
auto config = m_engine->active() ? m_engine->config() : selection();
|
||||
m_sources = StreamPublishEngine::sources(); m_source->clear();
|
||||
m_sources = StreamPublishEngine::sources(); clearOptions(m_source, "Video source");
|
||||
int selected = -1;
|
||||
for (int i = 0; i < m_sources.size(); i++) {
|
||||
auto& source = m_sources[i];
|
||||
@@ -320,11 +376,16 @@ void StreamPublishWindow::refreshSources() {
|
||||
QString label = kind + ": " + source.name;
|
||||
if (source.kind == StreamVideoSource::Screen && !source.size.isEmpty())
|
||||
label += QStringLiteral(" (%1 × %2)").arg(source.size.width()).arg(source.size.height());
|
||||
m_source->addItem(label);
|
||||
addOption(m_source, label, i);
|
||||
}
|
||||
bool changed = selected < 0;
|
||||
if (changed && !m_engine->active() && !m_sources.isEmpty()) selected = 0;
|
||||
m_source->setCurrentIndex(selected);
|
||||
if (selected >= 0) select(m_source, selected);
|
||||
else if (auto* action = ::selected(m_source)) {
|
||||
auto* group = m_source->findChild<QActionGroup*>(); group->setExclusive(false); action->setChecked(false); group->setExclusive(true);
|
||||
}
|
||||
m_source->addSeparator();
|
||||
connect(m_source->addAction(text("Refresh")), &QAction::triggered, this, &StreamPublishWindow::refreshSources);
|
||||
refreshModes(config, changed && selected >= 0 && m_sources[selected].kind == StreamVideoSource::Screen);
|
||||
synchronize();
|
||||
}
|
||||
@@ -332,72 +393,98 @@ void StreamPublishWindow::refreshSources() {
|
||||
/* Перестраиваем весь список под блокировкой, чтобы в engine не ушёл промежуточный режим другого источника. */
|
||||
void StreamPublishWindow::refreshModes(const StreamPublishConfig& previous, bool maximum) {
|
||||
QScopedValueRollback<bool> updating(m_updating, true);
|
||||
m_mode->clear(); m_modes.clear();
|
||||
int sourceIndex = m_source->currentIndex();
|
||||
clearOptions(m_resolution, "Video resolution"); clearOptions(m_fps, "Frame rate"); m_modes.clear();
|
||||
int sourceIndex = selected(m_source) ? value(m_source).toInt() : -1;
|
||||
if (sourceIndex < 0 || sourceIndex >= m_sources.size()) return;
|
||||
const auto& source = m_sources[sourceIndex];
|
||||
if (source.kind == StreamVideoSource::Camera) m_modes = source.modes;
|
||||
else if (source.kind == StreamVideoSource::Screen) m_modes = stream_video_screen_modes(source.size);
|
||||
else {
|
||||
for (QSize size : {QSize(1920, 1080), QSize(1280, 720), QSize(640, 360)})
|
||||
for (int fps : {15, 30, 60}) m_modes.append({size, fps});
|
||||
for (int fps : {15, 24, 25, 30, 50, 60}) m_modes.append({size, fps});
|
||||
}
|
||||
for (const auto& mode : m_modes) {
|
||||
QString label = mode.size.isEmpty() ? text("Original resolution · %1 fps").arg(mode.fps)
|
||||
: QStringLiteral("%1 × %2 · %3 fps").arg(mode.size.width()).arg(mode.size.height()).arg(mode.fps);
|
||||
m_mode->addItem(label, QVariantList{mode.size.width(), mode.size.height(), mode.fps});
|
||||
m_mode->setItemData(m_mode->count() - 1, source.kind == StreamVideoSource::Screen && mode.size == m_modes.front().size, Qt::UserRole + 1);
|
||||
if (option(m_resolution, mode.size)) continue;
|
||||
QString label = mode.size.isEmpty() ? text("Original resolution")
|
||||
: QStringLiteral("%1 × %2").arg(mode.size.width()).arg(mode.size.height());
|
||||
addOption(m_resolution, label, mode.size);
|
||||
}
|
||||
maximum |= source.kind == StreamVideoSource::Screen && previous.nativeSize;
|
||||
StreamVideoMode wanted{QSize(previous.video.width, previous.video.height), previous.video.fps};
|
||||
int selected = stream_video_select_mode(m_modes, wanted, maximum);
|
||||
m_mode->setCurrentIndex(selected);
|
||||
if (selected < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop source has no selectable modes source=%s", qPrintable(source.name));
|
||||
m_mode->setToolTip(text("No supported video modes")); return;
|
||||
m_resolution->menuAction()->setToolTip(text("No supported video modes")); return;
|
||||
}
|
||||
const auto& mode = m_modes[selected];
|
||||
select(m_resolution, mode.size); refreshFrameRates(mode.fps);
|
||||
if (!maximum && wanted != mode) DEBUG_WARN(DEBUG_CATEGORY_VIDEO,
|
||||
"desktop video mode adjusted source=%s requested=%dx%d/%d selected=%dx%d/%d",
|
||||
qPrintable(source.name), wanted.size.width(), wanted.size.height(), wanted.fps, mode.size.width(), mode.size.height(), mode.fps);
|
||||
m_mode->setToolTip(source.kind == StreamVideoSource::Camera ? text("Frame rate is limited by the camera and the selected stream mode") : QString());
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "desktop video modes source=%s count=%lld selected=%dx%d/%d native=%d",
|
||||
qPrintable(source.name), (long long)m_modes.size(), mode.size.width(), mode.size.height(), mode.fps,
|
||||
m_mode->currentData(Qt::UserRole + 1).toBool());
|
||||
source.kind == StreamVideoSource::Screen && mode.size == m_modes.front().size);
|
||||
}
|
||||
|
||||
void StreamPublishWindow::refreshFrameRates(int previous) {
|
||||
clearOptions(m_fps, "Frame rate");
|
||||
QList<int> rates{15, 24, 25, 30, 50, 60};
|
||||
for (const auto& mode : m_modes) if (!rates.contains(mode.fps)) rates.append(mode.fps);
|
||||
std::sort(rates.begin(), rates.end());
|
||||
QSize size = value(m_resolution).toSize();
|
||||
int fallback = -1;
|
||||
for (int fps : rates) {
|
||||
auto* action = addOption(m_fps, text("%1 fps").arg(fps), fps);
|
||||
bool available = selected(m_resolution) && m_modes.contains({size, fps}); action->setEnabled(available);
|
||||
if (available && (fallback < 0 || fps == 30)) fallback = fps;
|
||||
}
|
||||
auto* requested = option(m_fps, previous);
|
||||
if (requested && requested->isEnabled()) select(m_fps, previous);
|
||||
else if (fallback >= 0) {
|
||||
select(m_fps, fallback);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop stream FPS adjusted resolution=%dx%d requested=%d selected=%d",
|
||||
size.width(), size.height(), previous, fallback);
|
||||
} else {
|
||||
auto* group = m_fps->findChild<QActionGroup*>(); group->setExclusive(false);
|
||||
if (auto* action = selected(m_fps)) action->setChecked(false);
|
||||
group->setExclusive(true);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop stream resolution has no available FPS width=%d height=%d", size.width(), size.height());
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop stream FPS choices width=%d height=%d choices=%lld selected=%d",
|
||||
size.width(), size.height(), (long long)rates.size(), value(m_fps).toInt());
|
||||
}
|
||||
void StreamPublishWindow::refreshCodecs() {
|
||||
m_updating = true;
|
||||
int previous = m_codec->currentIndex() >= 0 ? m_codec->currentData().toInt() : m_settings.value("codec").toInt();
|
||||
m_codec->clear();
|
||||
for (int codec : m_engine->codecs()) m_codec->addItem(LiveVideoEncoder::codecName(codec), codec);
|
||||
int index = m_codec->findData(previous); if (index >= 0) m_codec->setCurrentIndex(index);
|
||||
else if (previous && m_codec->count()) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved codec=%d unavailable; using codec=%d",
|
||||
previous, m_codec->currentData().toInt());
|
||||
m_updating = false; synchronize();
|
||||
QScopedValueRollback<bool> updating(m_updating, true);
|
||||
int previous = selected(m_codec) ? value(m_codec).toInt() : m_settings.value("codec").toInt();
|
||||
clearOptions(m_codec, "Video codec");
|
||||
for (int codec : m_engine->codecs()) addOption(m_codec, LiveVideoEncoder::codecName(codec), codec);
|
||||
if (!select(m_codec, previous) && previous && selected(m_codec))
|
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved codec=%d unavailable; using codec=%d", previous, value(m_codec).toInt());
|
||||
synchronize();
|
||||
}
|
||||
StreamPublishConfig StreamPublishWindow::selection() const {
|
||||
StreamPublishConfig config;
|
||||
if (m_source->currentIndex() >= 0 && m_source->currentIndex() < m_sources.size()) config.source = m_sources[m_source->currentIndex()];
|
||||
auto mode = m_mode->currentData().toList();
|
||||
if (mode.size() == 3) {
|
||||
if (mode[0].toInt() > 0 && mode[1].toInt() > 0) { config.video.width = mode[0].toInt(); config.video.height = mode[1].toInt(); }
|
||||
config.video.fps = mode[2].toInt();
|
||||
}
|
||||
config.nativeSize = config.source.kind == StreamVideoSource::Screen && m_mode->currentData(Qt::UserRole + 1).toBool();
|
||||
config.video.codec = m_codec->currentData().toInt(); config.video.rotation = m_rotation->checkedId();
|
||||
config.video.bitrate = m_bitrate->currentData().toInt();
|
||||
int sourceIndex = selected(m_source) ? value(m_source).toInt() : -1;
|
||||
if (sourceIndex >= 0 && sourceIndex < m_sources.size()) config.source = m_sources[sourceIndex];
|
||||
QSize size = value(m_resolution).toSize();
|
||||
if (!size.isEmpty()) { config.video.width = size.width(); config.video.height = size.height(); }
|
||||
if (selected(m_fps)) config.video.fps = value(m_fps).toInt();
|
||||
config.nativeSize = config.source.kind == StreamVideoSource::Screen && !m_modes.isEmpty() && size == m_modes.front().size;
|
||||
config.video.codec = value(m_codec).toInt(); config.video.rotation = value(m_rotation).toInt();
|
||||
config.video.bitrate = value(m_bitrate).toInt();
|
||||
config.automaticBitrate = !config.video.bitrate;
|
||||
if (config.automaticBitrate) config.video.bitrate = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000);
|
||||
if (config.video.codec == 1 || config.video.codec == 2) {
|
||||
config.video.maxQp = m_maxQp->value() ? m_maxQp->value() : -1;
|
||||
config.video.maxQp = value(m_maxQp).toInt() ? value(m_maxQp).toInt() : -1;
|
||||
config.adaptiveQp = config.video.maxQp >= 0 && m_adaptiveQp->isChecked();
|
||||
}
|
||||
config.showCursor = m_cursor->isChecked();
|
||||
config.microphone = m_microphone->currentData().toString(); config.audio = m_audio->isChecked(); return config;
|
||||
config.microphone = value(m_microphone).toString(); config.audio = selected(m_microphone) != m_noAudio; return config;
|
||||
}
|
||||
void StreamPublishWindow::apply() {
|
||||
if (m_updating || m_mode->currentIndex() < 0) return;
|
||||
if (m_updating || !selected(m_resolution) || !selected(m_fps)) return;
|
||||
if (m_engine->active()) m_engine->reconfigure(selection());
|
||||
if (!m_engine->active()) synchronize(); else updateMenuButtons();
|
||||
saveSettings();
|
||||
}
|
||||
void StreamPublishWindow::synchronize() {
|
||||
@@ -405,35 +492,59 @@ void StreamPublishWindow::synchronize() {
|
||||
if (!active) m_buffer->setPositions({});
|
||||
m_start->setText(active ? text("Stop stream") : text("Start stream"));
|
||||
m_start->setEnabled((active && m_engine->running()) || (!active && StreamPublishEngine::supported()
|
||||
&& m_source->currentIndex() >= 0 && m_mode->currentIndex() >= 0 && m_codec->count()));
|
||||
for (auto* box : {m_source, m_mode, m_codec, m_bitrate}) box->setEnabled(!active || m_engine->running());
|
||||
m_audio->setEnabled(!active); m_microphone->setEnabled(!active); m_mute->setEnabled(m_engine->running() && m_engine->config().audio);
|
||||
{ QSignalBlocker blocked(m_mute); m_mute->setChecked(m_engine->muted()); }
|
||||
&& selected(m_source) && selected(m_resolution) && selected(m_fps) && selected(m_codec)));
|
||||
for (auto* menu : {m_source, m_resolution, m_fps, m_codec, m_bitrate, m_rotation})
|
||||
menu->menuAction()->setEnabled(!active || m_engine->running());
|
||||
m_microphone->menuAction()->setEnabled(!active);
|
||||
m_sound->setEnabled(m_engine->running() && m_engine->config().audio);
|
||||
{ QSignalBlocker blocked(m_sound); m_sound->setChecked(active ? !m_engine->muted() && m_engine->config().audio : selection().audio); }
|
||||
if (active) {
|
||||
auto config = m_engine->config();
|
||||
for (int i = 0; i < m_sources.size(); i++) if (m_sources[i].sameSource(config.source)) {
|
||||
m_source->setCurrentIndex(i); if (!config.source.size.isEmpty()) m_sources[i].size = config.source.size;
|
||||
select(m_source, i); if (!config.source.size.isEmpty()) m_sources[i].size = config.source.size;
|
||||
}
|
||||
refreshModes(config);
|
||||
m_codec->setCurrentIndex(m_codec->findData(config.video.codec));
|
||||
int bitrateIndex = config.automaticBitrate ? 0 : m_bitrate->findData(config.video.bitrate);
|
||||
if (bitrateIndex < 0) {
|
||||
m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(config.video.bitrate / 1000000.0, 0, 'f', 2), config.video.bitrate);
|
||||
bitrateIndex = m_bitrate->count() - 1;
|
||||
}
|
||||
m_bitrate->setCurrentIndex(bitrateIndex);
|
||||
select(m_codec, config.video.codec);
|
||||
int bitrate = config.automaticBitrate ? 0 : config.video.bitrate;
|
||||
if (!option(m_bitrate, bitrate)) addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||||
select(m_bitrate, bitrate);
|
||||
if (config.video.codec == 1 || config.video.codec == 2) {
|
||||
m_maxQp->setValue(config.video.maxQp < 0 ? 0 : config.video.maxQp); m_adaptiveQp->setChecked(config.adaptiveQp);
|
||||
int qp = config.video.maxQp < 0 ? 0 : config.video.maxQp;
|
||||
if (!select(m_maxQp, qp)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop active QP ceiling unavailable value=%d", qp);
|
||||
m_adaptiveQp->setChecked(config.adaptiveQp);
|
||||
}
|
||||
if (auto* button = m_rotation->button(config.video.rotation)) button->setChecked(true);
|
||||
m_audio->setChecked(config.audio); m_microphone->setCurrentIndex(m_microphone->findData(config.microphone));
|
||||
select(m_rotation, config.video.rotation);
|
||||
if (config.audio) select(m_microphone, config.microphone); else m_noAudio->setChecked(true);
|
||||
m_cursor->setChecked(config.showCursor);
|
||||
}
|
||||
m_cursor->setVisible(StreamPublishEngine::cursorSupported(selection().source));
|
||||
bool qpSupported = m_codec->currentData().toInt() == 1 || m_codec->currentData().toInt() == 2;
|
||||
m_maxQp->setEnabled(qpSupported && (!active || m_engine->running()));
|
||||
m_adaptiveQp->setEnabled(m_maxQp->isEnabled() && m_maxQp->value() > 0);
|
||||
bool qpSupported = value(m_codec).toInt() == 1 || value(m_codec).toInt() == 2;
|
||||
m_maxQp->menuAction()->setEnabled(qpSupported && (!active || m_engine->running()));
|
||||
m_adaptiveQp->setEnabled(m_maxQp->menuAction()->isEnabled() && value(m_maxQp).toInt() > 0);
|
||||
bool portal = selection().source.id == "portal";
|
||||
m_cursor->setEnabled(!active || (m_engine->running() && !portal));
|
||||
m_cursor->setToolTip(portal ? text("Change cursor visibility before starting screen sharing") : QString());
|
||||
updateMenuButtons();
|
||||
}
|
||||
|
||||
void StreamPublishWindow::updateMenuButtons() {
|
||||
auto update = [this](QMenu* menu, const char* heading, const QString& shape, const QString& label = {}) {
|
||||
menu->menuAction()->setIcon(controlIcon(palette(), shape, label));
|
||||
QString description = text(heading);
|
||||
if (auto* action = selected(menu)) description += QStringLiteral(": ") + action->text();
|
||||
menu->menuAction()->setToolTip(description); menu->setAccessibleName(description);
|
||||
};
|
||||
auto config = selection();
|
||||
update(m_source, "Video source", config.source.kind == StreamVideoSource::Camera ? "camera" : "screen");
|
||||
update(m_codec, "Video codec", "chip", selected(m_codec) ? LiveVideoEncoder::codecName(config.video.codec).remove('.') : "—");
|
||||
update(m_bitrate, "Video bitrate", "gauge", config.automaticBitrate ? "Auto" : QString::number(config.video.bitrate / 1000000.0, 'g', 2));
|
||||
QSize size = value(m_resolution).toSize();
|
||||
update(m_resolution, "Video resolution", "screen", size.isEmpty() ? "Auto" : QString::number(std::min(size.width(), size.height())) + 'p');
|
||||
update(m_fps, "Frame rate", "fps", selected(m_fps) ? QString::number(config.video.fps) : "—");
|
||||
update(m_rotation, "Rotation", "rotation", QStringLiteral("%1°").arg(config.video.rotation));
|
||||
update(m_maxQp, "Maximum QP", "qp", value(m_maxQp).toInt() ? value(m_maxQp).toString() : "Auto");
|
||||
update(m_microphone, "Audio source", config.audio ? "microphone" : "silent");
|
||||
m_sound->setIcon(controlIcon(palette(), m_sound->isChecked() ? "sound" : "silent"));
|
||||
m_sound->setToolTip(text(m_sound->isChecked() ? "Turn stream audio off" : "Turn stream audio on"));
|
||||
m_sound->setAccessibleName(m_sound->toolTip());
|
||||
}
|
||||
@@ -7,12 +7,13 @@
|
||||
#include <QWidget>
|
||||
#include <QJsonObject>
|
||||
#include "stream_publish_engine.h"
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QButtonGroup;
|
||||
class QSpinBox;
|
||||
class QMenu;
|
||||
class QMenuBar;
|
||||
class QToolButton;
|
||||
class QAction;
|
||||
class QCloseEvent;
|
||||
class StreamBufferIndicator;
|
||||
class StreamPublishWindow : public QWidget {
|
||||
@@ -28,16 +29,18 @@ protected:
|
||||
private:
|
||||
StreamPublishEngine* m_engine; // Заимствован; QWidget-дерево владеет только дочерними controls.
|
||||
uint64_t m_group; // Группа для следующего запуска из этого окна.
|
||||
QList<StreamVideoSource> m_sources; // Текущий список camera/screen, индексы совпадают с m_source.
|
||||
QList<StreamVideoMode> m_modes; // Режимы выбранного источника, индексы совпадают с m_mode.
|
||||
QComboBox *m_source, *m_microphone, *m_mode, *m_codec, *m_bitrate; // Выбор source/device/mode/codec и ручного либо auto bitrate.
|
||||
QCheckBox *m_audio, *m_mute, *m_cursor; // Audio/device фиксируются на start; mute меняется отдельно, cursor portal — только до start.
|
||||
QSpinBox* m_maxQp; // UI 0 означает Automatic → config.maxQp=-1; явные значения 1..51 только H.264/H.265.
|
||||
QList<StreamVideoSource> m_sources; // Текущие camera/screen; data пунктов m_source содержит индекс источника.
|
||||
QList<StreamVideoMode> m_modes; // Допустимые пары resolution/FPS выбранного источника.
|
||||
QMenuBar* m_menuBar; // Верхний ряд: Qt переключает раскрытые меню одним кликом, повторный клик закрывает меню.
|
||||
QMenu *m_source, *m_resolution, *m_fps, *m_codec, *m_bitrate, *m_rotation; // Выбор хранится в checked QAction/data.
|
||||
QMenu *m_microphone, *m_maxQp; // Аудиоисточник (включая «Без звука») и шесть вариантов QP: auto/18/23/28/35/40.
|
||||
QAction* m_noAudio; // Пункт без захвата микрофона; остальные пункты m_microphone содержат device id, пустой — default.
|
||||
QToolButton* m_sound; // Checked означает передачу звука; mute меняется без перезапуска активной трансляции.
|
||||
QCheckBox* m_cursor; // Cursor portal можно менять только до start.
|
||||
QCheckBox* m_adaptiveQp; // Доступен при явном maxQp; engine проверяет поддержку live QP у encoder.
|
||||
QLabel *m_status, *m_stats, *m_inputChannels; // Статус/ошибка, bitrate/QP/очереди/ACK и фактические входные каналы.
|
||||
QPushButton* m_start; // Start/Stop; отключена во время завершения actor либо при отсутствии доступных source/mode/codec.
|
||||
StreamBufferIndicator* m_buffer; // Диапазоны получателей по истории source: oldest → live; tooltip содержит peer/subflows.
|
||||
QButtonGroup* m_rotation; // Варианты 0/90/270°, задают метаданные rotation для зрителя.
|
||||
QWidget* m_preview; // QVideoWidget при HAVE_STREAM_CAPTURE, иначе текст о недоступности capture.
|
||||
bool m_updating = false; // Защита от применения/сохранения промежуточного состояния при перестроении controls.
|
||||
QJsonObject m_settings; // Последний сохранённый выбор; source id в base64, окно по имени, режим/codec/audio/cursor/QP отдельно.
|
||||
@@ -45,6 +48,8 @@ private:
|
||||
void refreshSources(); // Сохраняет выбранный handle; пропавший active-source не подменяется другим, до start возможен первый доступный.
|
||||
// Режимы camera/screen; выбирает близкий previous либо native screen.
|
||||
void refreshModes(const StreamPublishConfig& previous, bool maximum = false);
|
||||
void refreshFrameRates(int previous); // Все стандартные и аппаратные FPS; недоступные для выбранного размера видимы и отключены.
|
||||
void updateMenuButtons(); // Обновляет иконки со значениями и подсказки верхнего ряда после выбора/синхронизации.
|
||||
void refreshCodecs(); // Перечитывает engine.codecs(), сохраняет текущий/saved codec, недоступный заменяет первым.
|
||||
void synchronize(); // Читает engine config/state без apply; настраивает controls и ограничения изменения audio/cursor/QP.
|
||||
void apply(); // Для выбранного режима: reconfigure только active-engine, затем сохранение выбора; не во время m_updating.
|
||||
|
||||
@@ -26,12 +26,12 @@ inline QSize stream_video_screen_size(QSize source, int shortSide = 4096) {
|
||||
return size.width() >= 16 && size.height() >= 16 ? size : QSize();
|
||||
}
|
||||
|
||||
/* Строит максимум и ступени короткой стороны 2160/1440/1080/720/480/360, убирая дубли размеров, с FPS 15/30/60.
|
||||
* Первый размер максимальный; для пустого source возвращает три режима с пустым размером, для слишком малого — пустой список. */
|
||||
/* Строит максимум и ступени короткой стороны 2160/1440/1080/720/480/360, убирая дубли размеров, с FPS 15/24/25/30/50/60.
|
||||
* Первый размер максимальный; для пустого source возвращает шесть режимов с пустым размером, для слишком малого — пустой список. */
|
||||
inline QList<StreamVideoMode> stream_video_screen_modes(QSize source) {
|
||||
QList<StreamVideoMode> modes;
|
||||
if (source.isEmpty()) {
|
||||
for (int fps : {15, 30, 60}) modes.append({QSize(), fps});
|
||||
for (int fps : {15, 24, 25, 30, 50, 60}) modes.append({QSize(), fps});
|
||||
return modes;
|
||||
}
|
||||
QSize maximum = stream_video_screen_size(source);
|
||||
@@ -41,7 +41,7 @@ inline QList<StreamVideoMode> stream_video_screen_modes(QSize source) {
|
||||
QSize size = stream_video_screen_size(source, side);
|
||||
if (!size.isEmpty() && !sizes.contains(size)) sizes.append(size);
|
||||
}
|
||||
for (QSize size : sizes) for (int fps : {15, 30, 60}) modes.append({size, fps});
|
||||
for (QSize size : sizes) for (int fps : {15, 24, 25, 30, 50, 60}) modes.append({size, fps});
|
||||
return modes;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user