Browse Source

Select proportional screen and advertised camera stream modes

master
evgeny 2 days ago
parent
commit
9bacde3bc1
  1. 184
      tools/chatgui/src/stream_publish_window.cpp
  2. 120
      tools/chatgui/tests/test_stream_publish.cpp

184
tools/chatgui/src/stream_publish_window.cpp

@ -13,6 +13,8 @@
#include <QPainter>
#include <QScrollArea>
#include <QSignalBlocker>
#include <QJsonDocument>
#include <QScopedValueRollback>
#ifdef HAVE_STREAM_CAPTURE
#include <QVideoWidget>
#include <QVideoSink>
@ -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<int>::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<int>::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply);
connect(m_source, QOverload<int>::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<int>::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply);
connect(m_microphone, QOverload<int>::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<int>::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<bool> 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<bool> 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<bool> 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;
}

120
tools/chatgui/tests/test_stream_publish.cpp

@ -3,6 +3,12 @@
#include <QtTest>
#include <QTimer>
#include <QLabel>
#include <QComboBox>
#include <QCheckBox>
#include <QButtonGroup>
#include <QPushButton>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScreen>
#include <QLoggingCategory>
#include <atomic>
@ -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<QComboBox*>(); 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<QButtonGroup*>()->button(270)->click();
controls.findChildren<QCheckBox*>().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<QComboBox*>();
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<QButtonGroup*>()->checkedId(), 270);
QVERIFY(!reopened.findChildren<QCheckBox*>().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<QComboBox*>()[3]->currentIndex(), 0);
}
void proportionalScreenModes_data() {
QTest::addColumn<QSize>("source"); QTest::addColumn<QSize>("maximum"); QTest::addColumn<QSize>("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<StreamVideoMode> 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<StreamVideoMode> 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<QComboBox*>("streamVideoSource");
auto* modeBox = controls.findChild<QComboBox*>("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());

Loading…
Cancel
Save