From 7a0bc9516cd90f0dab4b370ff1703d151879ff43 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 22:35:05 +0200 Subject: [PATCH] Populate call resolutions from camera modes and test Full HD IPC --- tools/chatgui/src/call_camera_capture.cpp | 13 ++++++- tools/chatgui/src/callwindow.cpp | 39 ++++++++++++++++--- .../tests/test_call_camera_capture.cpp | 20 +++++++--- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/tools/chatgui/src/call_camera_capture.cpp b/tools/chatgui/src/call_camera_capture.cpp index 2ba44221..a216f1a7 100644 --- a/tools/chatgui/src/call_camera_capture.cpp +++ b/tools/chatgui/src/call_camera_capture.cpp @@ -147,7 +147,16 @@ struct CallCameraCapture::Session : QObject { quint32 count = 0; in >> count; if (in.status() != QDataStream::Ok || count > 128) return false; CallCameraDevices devices; - for (quint32 i = 0; i < count; ++i) { QString name; QByteArray id; in >> name >> id; devices.append({name, id}); } + for (quint32 i = 0; i < count; ++i) { + CallCameraDevice device; quint32 modes = 0; in >> device.name >> device.id >> modes; + if (in.status() != QDataStream::Ok || modes > 256) return false; + for (quint32 j = 0; j < modes; ++j) { + qint32 w = 0, h = 0; in >> w >> h; + if (!validSize(w, h)) return false; + device.modes.append({QSize(w, h), 30}); + } + devices.append(device); + } if (owner) emit owner->devicesChanged(devices); } else if (type == Stage) { QString next; in >> next; @@ -158,7 +167,7 @@ struct CallCameraCapture::Session : QObject { if (revision == generation) fail(error); } else if (type == Frame) { qint64 pts = -1, correction = 0; qint32 w = 0, h = 0; in >> pts >> correction >> w >> h; - if (in.status() != QDataStream::Ok || pts < 0 || correction < 0 || w < 1 || h < 1 || w > 1280 || h > 720 || !pixels) return false; + if (in.status() != QDataStream::Ok || pts < 0 || correction < 0 || !validSize(w, h) || !pixels) return false; const qint64 now = call_video_playout_now_us(); const bool current = owner && enabled && revision == generation && !failureReported; if (current && pts <= now && now - pts <= CallCameraMaxFrameAgeUs) { diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 997e359a..bf3fbf66 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -340,15 +340,37 @@ CallWindow::CallWindow(QWidget* parent) }); auto* modes = new QHBoxLayout(); m_camera = new QComboBox(this); - connect(m_video, &CallVideoEngine::camerasChanged, this, [this](const CallCameraDevices& cameras) { + m_resolution = new QComboBox(this); + auto refreshModes = [this] { + QSize wanted = m_resolution->currentData().toSize(); + if (wanted.isEmpty()) wanted = QSize(640, 480); + m_resolution->clear(); + for (const auto& value : m_camera->currentData(Qt::UserRole + 1).toList()) { + const QSize size = value.toSize(); + m_resolution->addItem(QStringLiteral("%1×%2").arg(size.width()).arg(size.height()), size); + } + int index = m_resolution->findData(wanted); + if (index < 0) index = m_resolution->findData(QSize(640, 480)); + if (index < 0) index = m_resolution->count() - 1; + m_resolution->setCurrentIndex(index); m_resolution->setEnabled(index >= 0); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call camera resolutions device=%s count=%d requested=%dx%d selected=%dx%d fps=30", + m_camera->currentData().toByteArray().constData(), m_resolution->count(), wanted.width(), wanted.height(), + m_resolution->currentData().toSize().width(), m_resolution->currentData().toSize().height()); + }; + connect(m_video, &CallVideoEngine::camerasChanged, this, [this, refreshModes](const CallCameraDevices& cameras) { const auto selected = m_camera->currentData(); m_camera->clear(); - for (const auto& camera : cameras) m_camera->addItem(camera.first, camera.second); + for (const auto& camera : cameras) { + QVariantList sizes; + for (const auto& mode : camera.modes) sizes.append(mode.size); + m_camera->addItem(camera.name, camera.id); + m_camera->setItemData(m_camera->count() - 1, sizes, Qt::UserRole + 1); + } const int index = m_camera->findData(selected); if (index >= 0) m_camera->setCurrentIndex(index); + refreshModes(); if (m_videoEnabled->isChecked()) applyVideo(); }); - m_resolution = new QComboBox(this); m_resolution->addItem("640×480", QSize(640, 480)); m_resolution->addItem("1280×720", QSize(1280, 720)); m_codec = new QComboBox(this); m_codec->addItem(callText("Auto (H.265 → H.264)"), 0); m_codec->addItem("H.265", 2); m_codec->addItem("H.264", 1); modes->addWidget(m_camera, 1); modes->addWidget(m_resolution); modes->addWidget(m_codec); videoLayout->addLayout(modes); auto* rates = new QHBoxLayout(); rates->addWidget(new QLabel(callText("Bitrate range, kbit/s:"), this)); @@ -356,7 +378,8 @@ CallWindow::CallWindow(QWidget* parent) for (auto* spin : {m_videoMin, m_videoMax}) { spin->setRange(300, 4000); spin->setSingleStep(100); rates->addWidget(spin); } m_videoMin->setValue(300); m_videoMax->setValue(4000); videoLayout->addLayout(rates); lay->addWidget(m_videoControls); connect(m_videoEnabled, &QCheckBox::toggled, this, [this] { applyVideo(); }); - for (auto* combo : {m_camera, m_resolution, m_codec}) + connect(m_camera, QOverload::of(&QComboBox::activated), this, [this, refreshModes] { refreshModes(); applyVideo(); }); + for (auto* combo : {m_resolution, m_codec}) connect(combo, QOverload::of(&QComboBox::activated), this, [this] { applyVideo(); }); auto updateRange = [this] { if (m_videoMin->value() > m_videoMax->value()) m_videoMax->setValue(m_videoMin->value()); @@ -536,7 +559,13 @@ void CallWindow::updateVideoPresentation() { void CallWindow::applyVideo() { if (!m_videoCall) return; QSize size = m_resolution->currentData().toSize(); - m_video->setVideo(m_videoEnabled->isChecked(), m_camera->currentData().toByteArray(), size.width(), size.height(), m_codec->currentData().toInt()); + const bool enabled = m_videoEnabled->isChecked() && !size.isEmpty(); + if (m_videoEnabled->isChecked() && !enabled) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera has no selected mode device=%s", m_camera->currentData().toByteArray().constData()); + m_videoStatus->setText(callText("No supported video modes")); m_videoStatus->show(); + } + if (size.isEmpty()) size = QSize(640, 480); + m_video->setVideo(enabled, m_camera->currentData().toByteArray(), size.width(), size.height(), m_codec->currentData().toInt()); } void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) { diff --git a/tools/chatgui/tests/test_call_camera_capture.cpp b/tools/chatgui/tests/test_call_camera_capture.cpp index 4d29a7a1..8da2f279 100644 --- a/tools/chatgui/tests/test_call_camera_capture.cpp +++ b/tools/chatgui/tests/test_call_camera_capture.cpp @@ -71,7 +71,9 @@ public: raw.setStartTime(call_video_playout_now_us()); emit frame(raw); }); } - CallCameraDevices devices() override { return {{QStringLiteral("Test camera"), "red"}}; } + CallCameraDevices devices() override { + return {{QStringLiteral("Test camera"), "red", {{QSize(1920, 1080), 30}, {QSize(1280, 720), 30}, {QSize(640, 480), 30}}}}; + } bool start(const QByteArray& id, int w, int h) override { device = id; width = w; height = h; record("start:" + id); if (id == "start-stall") QThread::sleep(60); @@ -125,27 +127,35 @@ private slots: QVERIFY(fps >= 25.0); QVERIFY(fps <= 31.0); QCOMPARE(errors.size(), 0); const qint64 child = pid(); capture.reset(); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); } + void latestFrameAndGeneration_data() { + QTest::addColumn("size"); + QTest::newRow("720p") << QSize(1280, 720); + QTest::newRow("1080p") << QSize(1920, 1080); + } void latestFrameAndGeneration() { + QFETCH(QSize, size); auto capture = std::make_unique(); unsigned frames = 0; qint64 maxAge = 0; QColor color; bool expectBlue = false, wrongGeneration = false; connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage& image, qint64 pts, qint64) { + QCOMPARE(image.size(), size); + QCOMPARE(image.pixelColor(image.width() - 1, image.height() - 1), image.pixelColor(0, 0)); ++frames; maxAge = std::max(maxAge, call_video_playout_now_us() - pts); color = image.pixelColor(0, 0); if (expectBlue && color != QColor(Qt::blue)) wrongGeneration = true; }); QSignalSpy errors(capture.get(), &CallCameraCapture::failed); - capture->configure(true, "red", 1280, 720); + capture->configure(true, "red", size.width(), size.height()); { Heartbeat heartbeat; QTRY_VERIFY_WITH_TIMEOUT(frames >= 15, 4000); - qInfo("camera 720p latency max=%lldus GUI gap=%lldms", (long long)maxAge, (long long)heartbeat.maximum); + qInfo("camera %dx%d latency max=%lldus GUI gap=%lldms", size.width(), size.height(), (long long)maxAge, (long long)heartbeat.maximum); QVERIFY(maxAge < 100000); QVERIFY(heartbeat.maximum < 150); } QCOMPARE(color, QColor(Qt::red)); // Пока GUI занят, разрешён лишь один передаваемый кадр; после паузы берётся свежий. QThread::msleep(350); const unsigned before = frames; expectBlue = true; - capture->configure(true, "blue", 1280, 720); + capture->configure(true, "blue", size.width(), size.height()); QTRY_VERIFY_WITH_TIMEOUT(frames > before && color == QColor(Qt::blue), 2000); QVERIFY(!wrongGeneration); - const unsigned stopped = frames; capture->configure(false, "blue", 1280, 720); + const unsigned stopped = frames; capture->configure(false, "blue", size.width(), size.height()); QTest::qWait(150); QCOMPARE(frames, stopped); QCOMPARE(errors.size(), 0); const qint64 child = pid(); QVERIFY(child > 0); capture.reset(); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); }