From a852a9738b71eab26bcf323f1d5129258757ace6 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Oct 2026 00:03:01 +0200 Subject: [PATCH] Select camera capture rate and verify 30/60 fps pacing --- tools/chatgui/src/call_camera_source.h | 2 +- tools/chatgui/src/call_camera_worker.cpp | 12 +++--- .../tests/test_call_camera_capture.cpp | 37 +++++++++++++++---- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/tools/chatgui/src/call_camera_source.h b/tools/chatgui/src/call_camera_source.h index c55fe666..6d381cf3 100644 --- a/tools/chatgui/src/call_camera_source.h +++ b/tools/chatgui/src/call_camera_source.h @@ -10,7 +10,7 @@ class CallCameraSource : public QObject { Q_OBJECT public: virtual CallCameraDevices devices() = 0; - virtual bool start(const QByteArray& device, int width, int height) = 0; + virtual bool start(const QByteArray& device, int width, int height, int fps) = 0; virtual void stop() = 0; signals: void devicesChanged(); diff --git a/tools/chatgui/src/call_camera_worker.cpp b/tools/chatgui/src/call_camera_worker.cpp index 5cd60b76..68feae87 100644 --- a/tools/chatgui/src/call_camera_worker.cpp +++ b/tools/chatgui/src/call_camera_worker.cpp @@ -49,7 +49,7 @@ public: } return result; } - bool start(const QByteArray& device, int width, int height) override { + bool start(const QByteArray& device, int width, int height, int fps) override { QCameraDevice selected; for (const auto& item : QMediaDevices::videoInputs()) if (item.id() == device) selected = item; if (selected.isNull()) { emit error(QStringLiteral("Camera is unavailable")); return false; } @@ -59,18 +59,18 @@ public: QCameraFormat best; for (const auto& format : selected.videoFormats()) { if (!stream_video_camera_supports(format.resolution(), format.minFrameRate(), format.maxFrameRate(), - {QSize(width, height), 1})) continue; + {QSize(width, height), fps})) continue; if (best.isNull() || format.maxFrameRate() < best.maxFrameRate()) best = format; } if (best.isNull()) { - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera mode unavailable device=%s size=%dx%d fps=30", device.constData(), width, height); + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera mode unavailable device=%s size=%dx%d output_fps=%d", device.constData(), width, height, fps); emit error(QStringLiteral("Selected camera mode is unavailable")); return false; } camera->setCameraFormat(best); const auto format = camera->cameraFormat(); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera selected device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", + DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera selected device=%s size=%dx%d pixel=%d capture_fps=%.3f..%.3f output_fps=%d", device.constData(), format.resolution().width(), format.resolution().height(), format.pixelFormat(), - format.minFrameRate(), format.maxFrameRate()); + format.minFrameRate(), format.maxFrameRate(), fps); connect(camera.get(), &QCamera::activeChanged, this, [this, device](bool active) { if (active) rate.activate(device); else rate.restore(); }); @@ -211,7 +211,7 @@ class CameraWorker : public QObject { post = !slot->posted; slot->posted = true; } if (post) QMetaObject::invokeMethod(this, [this, slot, revision] { convert(slot, revision); }, Qt::QueuedConnection); }, Qt::DirectConnection); - if (source->start(device, width, height)) stage(QStringLiteral("running")); + if (source->start(device, width, height, fps)) stage(QStringLiteral("running")); } else return false; return true; } diff --git a/tools/chatgui/tests/test_call_camera_capture.cpp b/tools/chatgui/tests/test_call_camera_capture.cpp index b55c22b0..10f5ae84 100644 --- a/tools/chatgui/tests/test_call_camera_capture.cpp +++ b/tools/chatgui/tests/test_call_camera_capture.cpp @@ -1,5 +1,6 @@ #include "../src/call_camera_source.h" #include "../src/call_camera_buffer.h" +#include "../src/call_video_cadence.h" #include "../../../src/call/call_video_playout.h" #include "debug_config.h" #include @@ -73,12 +74,13 @@ public: }); } CallCameraDevices devices() override { - return {{QStringLiteral("Test camera"), "red", {{QSize(1920, 1080), 30}, {QSize(1280, 720), 30}, {QSize(640, 480), 30}}}}; + return {{QStringLiteral("Test camera"), "red", {{QSize(1920, 1080), 30}, {QSize(1280, 720), 30}, + {QSize(640, 480), 30}, {QSize(640, 480), 60}}}}; } - bool start(const QByteArray& id, int w, int h) override { + bool start(const QByteArray& id, int w, int h, int fps) override { device = id; width = w; height = h; record("start:" + id); if (id == "start-stall") QThread::sleep(60); - timer.setInterval(id == "fast" ? 5 : 33); + timer.setInterval(id == "fast" ? 3 : 1000 / fps); timer.start(); return true; } void stop() override { @@ -115,21 +117,40 @@ private slots: path = directory.filePath(QString::number(QDateTime::currentMSecsSinceEpoch())); qputenv("UTUN_CAMERA_TEST_LOG", path.toUtf8()); } - void fastCameraKeepsThirtyFps() { + void captureCadence() { + for (int input : {50, 100, 330}) for (int output : {30, 50, 60}) { + if (output > input) continue; + CallVideoCadence cadence(output); unsigned accepted = 0; + for (int i = 0; i < input * 3; ++i) if (cadence.accept(1000000 + i * 1000000ll / input)) ++accepted; + qInfo("capture cadence input=%d output=%d accepted=%u/3s", input, output, accepted); + QVERIFY(std::abs(int(accepted) - output * 3) <= 1); + QVERIFY(!cadence.accept(1000000)); + QVERIFY(cadence.accept(5000000)); QVERIFY(!cadence.accept(5000001)); + } + CallVideoCadence jitter(30); unsigned accepted = 0; + for (int i = 0; i < 90; ++i) if (jitter.accept(1000000 + i * 1000000ll / 30 + (i % 2 ? 700 : -700))) ++accepted; + QCOMPARE(accepted, 90u); + } + void fastCameraKeepsSelectedFps_data() { + QTest::addColumn("fps"); QTest::newRow("30") << 30; QTest::newRow("60") << 60; + } + void fastCameraKeepsSelectedFps() { + QFETCH(int, fps); auto capture = std::make_unique(); CallCameraDevices devices; connect(capture.get(), &CallCameraCapture::devicesChanged, this, [&](const CallCameraDevices& value) { devices = value; }); unsigned frames = 0; connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage&, qint64, qint64) { ++frames; }); QSignalSpy errors(capture.get(), &CallCameraCapture::failed); - capture->configure(true, "fast", 640, 480); + capture->configure(true, "fast", 640, 480, fps); QTRY_VERIFY_WITH_TIMEOUT(frames >= 5, 3000); QCOMPARE(devices.size(), 1); QCOMPARE(devices.front().id, QByteArray("red")); QVERIFY(devices.front().modes.contains({QSize(1920, 1080), 30})); + QVERIFY(devices.front().modes.contains({QSize(640, 480), 60})); const unsigned before = frames; QElapsedTimer timer; timer.start(); QTest::qWait(2000); - const double fps = (frames - before) * 1000.0 / timer.elapsed(); - qInfo("fast camera converted output fps=%.2f frames=%u", fps, frames - before); - QVERIFY(fps >= 25.0); QVERIFY(fps <= 31.0); QCOMPARE(errors.size(), 0); + const double actual = (frames - before) * 1000.0 / timer.elapsed(); + qInfo("fast camera converted target=%d output_fps=%.2f frames=%u", fps, actual, frames - before); + QVERIFY(actual >= fps * 0.85); QVERIFY(actual <= fps + 1); QCOMPARE(errors.size(), 0); const qint64 child = pid(); capture.reset(); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); } void latestFrameAndGeneration_data() {