From 9bacde3bc1337325c80263b1f0f5d57daaf8afd3 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 13:53:33 +0200 Subject: [PATCH] Select proportional screen and advertised camera stream modes --- tools/chatgui/src/stream_publish_window.cpp | 184 +++++++++++++++++--- tools/chatgui/tests/test_stream_publish.cpp | 120 +++++++++++++ 2 files changed, 279 insertions(+), 25 deletions(-) diff --git a/tools/chatgui/src/stream_publish_window.cpp b/tools/chatgui/src/stream_publish_window.cpp index cbcd7a33..1d269619 100644 --- a/tools/chatgui/src/stream_publish_window.cpp +++ b/tools/chatgui/src/stream_publish_window.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #ifdef HAVE_STREAM_CAPTURE #include #include @@ -23,8 +25,14 @@ namespace { QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); } QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; } } -StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent) +StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent, const QString& saved) : QWidget(parent, Qt::Window), m_engine(engine), m_group(group) { + if (!saved.isEmpty()) { + QJsonParseError error; auto document = QJsonDocument::fromJson(saved.toUtf8(), &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) + 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); auto* layout = new QVBoxLayout(this); #ifdef HAVE_STREAM_CAPTURE @@ -49,10 +57,8 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g connect(refresh, &QPushButton::clicked, this, &StreamPublishWindow::refreshSources); form->addRow(text("Video source"), sourceRow); connect(m_source, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { m_source->setToolTip(m_source->currentText()); }); m_mode = new QComboBox(settings); - for (auto mode : {QSize(640, 360), QSize(1280, 720), QSize(1920, 1080)}) - for (int fps : {15, 30, 60}) m_mode->addItem(QStringLiteral("%1 × %2 · %3 fps").arg(mode.width()).arg(mode.height()).arg(fps), - QVariantList{mode.width(), mode.height(), fps}); - m_mode->setCurrentIndex(4); form->addRow(text("Video mode"), m_mode); + 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); for (int bitrate : {250000, 500000, 1000000, 2000000, 4000000, 8000000, 16000000}) @@ -84,14 +90,23 @@ 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(); }); - for (auto* box : {m_source, m_mode, m_codec, m_bitrate}) connect(box, QOverload::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply); + connect(m_source, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + 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::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply); + connect(m_microphone, QOverload::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply); + connect(m_audio, &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::of(&QButtonGroup::buttonClicked), this, &StreamPublishWindow::apply); #endif connect(engine, &StreamPublishEngine::codecsChanged, this, &StreamPublishWindow::refreshCodecs); - connect(engine, &StreamPublishEngine::configurationChanged, this, &StreamPublishWindow::synchronize); + connect(engine, &StreamPublishEngine::sourcesChanged, this, &StreamPublishWindow::refreshSources); + connect(engine, &StreamPublishEngine::configurationChanged, this, [this] { synchronize(); saveSettings(); }); connect(engine, &StreamPublishEngine::stateChanged, this, [this](const QString& value) { auto error = m_status->property("streamError").toString(); m_status->setText(error.isEmpty() ? value : value + "\n" + error); synchronize(); @@ -104,53 +119,173 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g m_stats->setText(text("Video queue: %1 · Inflight: %2 · Send buffer: %3\nACK, all connections, last second: %4") .arg(bytes(stats.video_bytes), bytes(stats.inflight_bytes), bytes(stats.total_bytes), bytes(stats.acked_1s))); }); - refreshSources(); refreshCodecs(); synchronize(); + refreshSources(); refreshCodecs(); restoreSettings(); synchronize(); +} + +/* В БД храним пользовательский выбор, а не runtime handles источника и рассчитанный auto-битрейт. */ +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())); + }; + int sourceIndex = -1; + for (int i = 0; i < m_sources.size(); i++) { + const auto& source = m_sources[i]; + if (source.kind != m_settings.value("source_kind").toInt()) continue; + bool matches = source.kind == StreamVideoSource::Window ? source.name == m_settings.value("source_name").toString() + : source.id == QByteArray::fromBase64(m_settings.value("source_id").toString().toLatin1()); + if (matches) { sourceIndex = i; break; } + } + if (sourceIndex >= 0) m_source->setCurrentIndex(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); + restored.video.fps = m_settings.value("fps").toInt(30); restored.nativeSize = m_settings.value("native_size").toBool(); + refreshModes(restored, sourceIndex < 0 && restored.source.kind == StreamVideoSource::Screen); + int bitrate = m_settings.value("bitrate").toInt(); + 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 (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)); + select(m_microphone, m_settings.value("microphone").toString(), "microphone"); + m_updating = false; + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings restored: %s", QJsonDocument(m_settings).toJson(QJsonDocument::Compact).constData()); +} + +void StreamPublishWindow::saveSettings() { + if (m_updating) return; + auto config = selection(); auto saved = m_settings; + if (m_source->currentIndex() >= 0) { + 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) { + 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; + saved["audio"] = config.audio; saved["microphone"] = config.microphone; + if (saved == m_settings) return; + m_settings = saved; + auto json = QJsonDocument(saved).toJson(QJsonDocument::Compact); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings selected: %s", json.constData()); + emit settingsChanged(QString::fromUtf8(json)); } void StreamPublishWindow::refreshSources() { - m_updating = true; auto config = m_engine->active() ? m_engine->config() : selection(); + QScopedValueRollback updating(m_updating, true); + auto config = m_engine->active() ? m_engine->config() : selection(); m_sources = StreamPublishEngine::sources(); m_source->clear(); - int selected = 0; + int selected = -1; for (int i = 0; i < m_sources.size(); i++) { - const auto& source = m_sources[i]; + auto& source = m_sources[i]; + if (source.kind == config.source.kind && source.id == config.source.id && source.window == config.source.window) { + selected = i; + if (m_engine->active() && !config.source.size.isEmpty()) source.size = config.source.size; + } QString kind = source.kind == StreamVideoSource::Camera ? text("Camera") : source.kind == StreamVideoSource::Screen ? text("Screen") : text("Window"); - m_source->addItem(kind + ": " + source.name); - if (source.kind == config.source.kind && source.id == config.source.id && source.window == config.source.window) selected = i; + 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); } - m_source->setCurrentIndex(m_sources.isEmpty() ? -1 : selected); m_updating = false; + bool changed = selected < 0; + if (changed && !m_engine->active() && !m_sources.isEmpty()) selected = 0; + m_source->setCurrentIndex(selected); + refreshModes(config, changed && selected >= 0 && m_sources[selected].kind == StreamVideoSource::Screen); + synchronize(); +} + +/* Перестраиваем весь список под блокировкой, чтобы в engine не ушёл промежуточный режим другого источника. */ +void StreamPublishWindow::refreshModes(const StreamPublishConfig& previous, bool maximum) { + QScopedValueRollback updating(m_updating, true); + m_mode->clear(); m_modes.clear(); + int sourceIndex = m_source->currentIndex(); + 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 (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); + } + 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; + } + const auto& mode = m_modes[selected]; + 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_DEBUG(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()); } void StreamPublishWindow::refreshCodecs() { - m_updating = true; int previous = m_codec->currentData().toInt(); m_codec->clear(); + 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(); } 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) { config.video.width = mode[0].toInt(); config.video.height = mode[1].toInt(); config.video.fps = mode[2].toInt(); } + 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(); - if (!config.video.bitrate) config.video.bitrate = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000); + 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); config.microphone = m_microphone->currentData().toString(); config.audio = m_audio->isChecked(); return config; } -void StreamPublishWindow::apply() { if (!m_updating && m_engine->active()) m_engine->reconfigure(selection()); } +void StreamPublishWindow::apply() { + if (m_updating || m_mode->currentIndex() < 0) return; + if (m_engine->active()) m_engine->reconfigure(selection()); + saveSettings(); +} void StreamPublishWindow::synchronize() { - m_updating = true; bool active = m_engine->active(); + QScopedValueRollback updating(m_updating, true); bool active = m_engine->active(); m_start->setText(active ? text("Stop stream") : text("Start stream")); - m_start->setEnabled((active && m_engine->running()) || (!active && StreamPublishEngine::supported() && m_source->count() && m_codec->count())); + 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()); } if (active) { auto config = m_engine->config(); for (int i = 0; i < m_sources.size(); i++) if (m_sources[i].kind == config.source.kind - && m_sources[i].id == config.source.id && m_sources[i].window == config.source.window) m_source->setCurrentIndex(i); - for (int i = 0; i < m_mode->count(); i++) if (m_mode->itemData(i).toList() == QVariantList{config.video.width, config.video.height, config.video.fps}) m_mode->setCurrentIndex(i); + && m_sources[i].id == config.source.id && m_sources[i].window == config.source.window) { + m_source->setCurrentIndex(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 automatic = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000); - int bitrateIndex = config.video.bitrate == automatic && m_bitrate->currentData().toInt() == 0 ? 0 : m_bitrate->findData(config.video.bitrate); + 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; @@ -159,5 +294,4 @@ void StreamPublishWindow::synchronize() { 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)); } - m_updating = false; } diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index ffa75810..9d4826ca 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -3,6 +3,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -117,6 +123,120 @@ private slots: sqlite3_finalize(statement); QVERIFY(group); QVERIFY(gui_stream_attach(inst)); uasync_mark_running(ua); connect(&poll, &QTimer::timeout, this, [this] { uasync_poll(ua, 0); }); poll.start(1); } + void captureSettingsSurviveReopening() { + QString saved; + QVariantList selectedMode; + { + StreamPublishEngine engine; + StreamPublishWindow controls(&engine, group); + connect(&controls, &StreamPublishWindow::settingsChanged, this, [&saved](const QString& value) { saved = value; }); + auto boxes = controls.findChildren(); QCOMPARE(boxes.size(), 5); + if (StreamPublishEngine::supported()) QTRY_VERIFY_WITH_TIMEOUT(boxes[2]->count() > 0, 15000); + auto sources = StreamPublishEngine::sources(); + int screen = -1; + for (int i = 0; i < sources.size(); i++) if (sources[i].kind == StreamVideoSource::Screen && !sources[i].size.isEmpty()) { screen = i; break; } + if (screen < 0) QSKIP("No enumerated screen for capture settings"); + boxes[0]->setCurrentIndex(screen); + QSize maximum = stream_video_screen_size(sources[screen].size); + QCOMPARE(boxes[1]->currentData().toList(), (QVariantList{maximum.width(), maximum.height(), 30})); + QVERIFY(boxes[1]->count() >= 6); + boxes[1]->setCurrentIndex(boxes[1]->count() - 1); // Меньший размер сохраняется при повторном открытии. + selectedMode = boxes[1]->currentData().toList(); + boxes[3]->setCurrentIndex(boxes[3]->findData(4000000)); + controls.findChild()->button(270)->click(); + controls.findChildren().first()->setChecked(false); + if (boxes[2]->count() > 1) boxes[2]->setCurrentIndex(boxes[2]->count() - 1); + QVERIFY(!saved.isEmpty()); + } + const auto stored = QJsonDocument::fromJson(saved.toUtf8()).object(); + QCOMPARE(stored.value("width").toInt(), selectedMode[0].toInt()); QCOMPARE(stored.value("fps").toInt(), 60); + QVERIFY(!stored.value("native_size").toBool()); + QCOMPARE(stored.value("bitrate").toInt(), 4000000); QCOMPARE(stored.value("rotation").toInt(), 270); + QVERIFY(!stored.value("audio").toBool()); + StreamPublishEngine reopenedEngine; + StreamPublishWindow reopened(&reopenedEngine, group, nullptr, saved); + QSignalSpy changes(&reopened, &StreamPublishWindow::settingsChanged); + if (StreamPublishEngine::supported()) QTRY_VERIFY_WITH_TIMEOUT(!reopenedEngine.codecs().isEmpty(), 15000); + auto boxes = reopened.findChildren(); + QCOMPARE(boxes[1]->currentData().toList(), selectedMode); + QCOMPARE(boxes[3]->currentData().toInt(), 4000000); + if (StreamPublishEngine::supported()) QTRY_COMPARE(boxes[2]->currentData().toInt(), stored.value("codec").toInt()); + QCOMPARE(reopened.findChild()->checkedId(), 270); + QVERIFY(!reopened.findChildren().first()->isChecked()); + QVERIFY(changes.isEmpty()); // Асинхронный список кодеков не перезаписывает сохранённый выбор. + boxes[3]->setCurrentIndex(0); + QCOMPARE(changes.size(), 1); + auto automatic = changes.takeFirst().front().toString(); + QCOMPARE(QJsonDocument::fromJson(automatic.toUtf8()).object().value("bitrate").toInt(-1), 0); + StreamPublishWindow restoredAuto(&reopenedEngine, group, nullptr, automatic); + QCOMPARE(restoredAuto.findChildren()[3]->currentIndex(), 0); + } + void proportionalScreenModes_data() { + QTest::addColumn("source"); QTest::addColumn("maximum"); QTest::addColumn("reduced"); + QTest::newRow("full-hd") << QSize(1920, 1080) << QSize(1920, 1080) << QSize(1280, 720); + QTest::newRow("16-10") << QSize(1920, 1200) << QSize(1920, 1200) << QSize(1152, 720); + QTest::newRow("ultrawide") << QSize(3440, 1440) << QSize(3440, 1440) << QSize(1720, 720); + QTest::newRow("limited") << QSize(5120, 1440) << QSize(4096, 1152) << QSize(2560, 720); + QTest::newRow("retina") << QSize(5120, 2880) << QSize(4096, 2304) << QSize(1280, 720); + QTest::newRow("portrait") << QSize(1440, 5120) << QSize(1152, 4096) << QSize(720, 2560); + QTest::newRow("odd") << QSize(1365, 767) << QSize(1364, 766) << QSize(1280, 720); + } + void proportionalScreenModes() { + QFETCH(QSize, source); QFETCH(QSize, maximum); QFETCH(QSize, reduced); + auto modes = stream_video_screen_modes(source); QVERIFY(!modes.isEmpty()); QCOMPARE(modes.front().size, maximum); + QVERIFY(modes.contains({reduced, 30})); + QList seen; + for (const auto& mode : modes) { + QVERIFY(mode.size.width() <= 4096 && mode.size.height() <= 4096); + QVERIFY(mode.size.width() <= source.width() && mode.size.height() <= source.height()); + QVERIFY(!(mode.size.width() % 2) && !(mode.size.height() % 2)); + double scale = std::min(double(mode.size.width()) / source.width(), double(mode.size.height()) / source.height()); + QVERIFY(std::abs(mode.size.width() - source.width() * scale) < 8); + QVERIFY(std::abs(mode.size.height() - source.height() * scale) < 8); + QVERIFY(!seen.contains(mode)); seen.append(mode); + } + int selected = stream_video_select_mode(modes, {reduced, 60}, true); + QCOMPARE(modes[selected].size, maximum); QCOMPARE(modes[selected].fps, 60); + selected = stream_video_select_mode(modes, {reduced, 60}, false); + QCOMPARE(modes[selected].size, reduced); QCOMPARE(modes[selected].fps, 60); + } + void cameraAdvertisedModes() { + QList modes; + stream_video_add_camera_modes(modes, {1920, 1080}, 29.97, 29.97); + QCOMPARE(modes.size(), 1); QCOMPARE(modes.front().fps, 30); + stream_video_add_camera_modes(modes, {1920, 1080}, 29.97, 29.97); QCOMPARE(modes.size(), 1); + stream_video_add_camera_modes(modes, {1280, 720}, 15, 60); + QVERIFY(modes.contains({{1280, 720}, 15})); QVERIFY(modes.contains({{1280, 720}, 60})); + QVERIFY(!modes.contains({{1920, 1080}, 60})); QVERIFY(!modes.contains({{1920, 1080}, 15})); + stream_video_add_camera_modes(modes, {640, 480}, 7, 7); QVERIFY(modes.contains({{640, 480}, 7})); + int count = modes.size(); + stream_video_add_camera_modes(modes, {5120, 2880}, 30, 30); + stream_video_add_camera_modes(modes, {1281, 720}, 30, 30); + stream_video_add_camera_modes(modes, {1280, 720}, 120, 120); + stream_video_add_camera_modes(modes, {1280, 720}, 60, 30); + QCOMPARE(modes.size(), count); + QVERIFY(!stream_video_camera_supports({640, 480}, 30, 30, {{1280, 720}, 30})); + } + void screenSelectionAndRefresh() { + StreamPublishEngine engine; StreamPublishWindow controls(&engine, group); + auto* sourceBox = controls.findChild("streamVideoSource"); + auto* modeBox = controls.findChild("streamVideoMode"); + auto sources = StreamPublishEngine::sources(); + QSignalSpy changes(&controls, &StreamPublishWindow::settingsChanged); + unsigned screens = 0; + for (int i = 0; i < sources.size(); i++) { + if (sources[i].kind != StreamVideoSource::Screen || sources[i].size.isEmpty()) continue; + screens++; sourceBox->setCurrentIndex(-1); changes.clear(); sourceBox->setCurrentIndex(i); + QSize size = stream_video_screen_size(sources[i].size); + auto mode = modeBox->currentData().toList(); + QCOMPARE(mode[0].toInt(), size.width()); QCOMPARE(mode[1].toInt(), size.height()); + QVERIFY(modeBox->currentData(Qt::UserRole + 1).toBool()); QCOMPARE(changes.size(), 1); + QVERIFY(sourceBox->currentText().contains(sources[i].name)); + modeBox->setCurrentIndex(modeBox->count() - 1); mode = modeBox->currentData().toList(); changes.clear(); + emit engine.sourcesChanged(); QCOMPARE(modeBox->currentData().toList(), mode); QVERIFY(changes.isEmpty()); + } + if (!screens) QSKIP("No enumerated screens"); + } void broadcastAllowsRadioMicrophone() { AudioCaptureLease call, radio(AudioCaptureLease::Mode::Radio), recorder, publisher(AudioCaptureLease::Mode::Exclusive); QVERIFY(call.acquire()); QVERIFY(radio.acquire()); QVERIFY(recorder.acquire()); QVERIFY(!publisher.acquire());