#include "../src/call_camera_source.h" #include "../src/call_camera_buffer.h" #include "../../../src/call/call_video_playout.h" #include "debug_config.h" #include #include #include #include #include #include #ifdef TEST_CALL_VIDEO_ENGINE #include "../src/call_video_engine.h" #include "../../../src/call/call_video_port.h" #include "../../../src/call/call_audio.h" extern "C" int __wrap_call_audio_get_clock(uint64_t, int64_t, call_clock_snapshot* out) { *out = {}; return 0; } extern "C" int __wrap_call_video_port_open(uint64_t, const call_video_caps*) { return 0; } extern "C" void __wrap_call_video_port_close(uint64_t) {} extern "C" int __wrap_call_video_port_status(uint64_t, struct call_video_port_status* out) { *out = {}; return 0; } extern "C" int __wrap_call_video_port_pull(uint64_t, call_video_frame*) { return 0; } extern "C" void __wrap_call_video_port_request_key(uint64_t) {} extern "C" int __wrap_call_video_port_decoded(uint64_t, uint64_t, int) { return 0; } #endif #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) #include #endif #ifdef Q_OS_WIN #include #else #include #endif static void record(const QByteArray& event) { QFile file(qEnvironmentVariable("UTUN_CAMERA_TEST_LOG")); if (file.open(QIODevice::WriteOnly | QIODevice::Append)) file.write(QByteArray::number(QCoreApplication::applicationPid()) + ' ' + event + '\n'); } static bool alive(qint64 pid) { #ifdef Q_OS_WIN HANDLE handle = OpenProcess(SYNCHRONIZE, FALSE, DWORD(pid)); if (!handle) return false; bool running = WaitForSingleObject(handle, 0) == WAIT_TIMEOUT; CloseHandle(handle); return running; #else return kill(pid, 0) == 0; #endif } #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) class StuckBuffer : public QAbstractVideoBuffer { public: MapData map(QVideoFrame::MapMode) override { record("convert-blocked"); QThread::sleep(60); return {}; } QVideoFrameFormat format() const override { return {QSize(16, 16), QVideoFrameFormat::Format_BGRX8888}; } }; #endif class TestSource : public CallCameraSource { QTimer timer; QByteArray device; int width = 16, height = 16; public: TestSource() { record("created"); timer.setInterval(33); timer.setTimerType(Qt::PreciseTimer); connect(&timer, &QTimer::timeout, this, [this] { #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) if (device == "convert-stall") { QVideoFrame raw(std::make_unique()); raw.setStartTime(call_video_playout_now_us()); emit frame(raw); return; } #endif QVideoFrame raw(QVideoFrameFormat(QSize(width, height), QVideoFrameFormat::Format_BGRX8888)); if (!raw.map(QVideoFrame::WriteOnly)) qFatal("test frame map failed"); QImage image(raw.bits(0), width, height, raw.bytesPerLine(0), QImage::Format_RGB32); image.fill(device == "blue" ? Qt::blue : Qt::red); raw.unmap(); raw.setStartTime(call_video_playout_now_us()); emit frame(raw); }); } CallCameraDevices devices() override { return {{QStringLiteral("Test camera"), "red"}}; } 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); timer.start(); return true; } void stop() override { timer.stop(); if (!device.isEmpty()) record("stop:" + device); if (device == "stop-stall") QThread::sleep(60); device.clear(); } }; class Heartbeat { QTimer timer; QElapsedTimer clock; qint64 previous = 0; public: qint64 maximum = 0; unsigned ticks = 0; Heartbeat() { clock.start(); timer.setInterval(5); timer.setTimerType(Qt::PreciseTimer); QObject::connect(&timer, &QTimer::timeout, [&] { const qint64 now = clock.elapsed(); maximum = std::max(maximum, now - previous); previous = now; ++ticks; }); timer.start(); } }; class CameraTest : public QObject { Q_OBJECT QTemporaryDir directory; QString path; QByteArray log() { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return {}; return file.readAll(); } qint64 pid() { return log().split(' ').value(0).toLongLong(); } private slots: void init() { path = directory.filePath(QString::number(QDateTime::currentMSecsSinceEpoch())); qputenv("UTUN_CAMERA_TEST_LOG", path.toUtf8()); } void latestFrameAndGeneration() { 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) { ++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); { 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); 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); QTRY_VERIFY_WITH_TIMEOUT(frames > before && color == QColor(Qt::blue), 2000); QVERIFY(!wrongGeneration); const unsigned stopped = frames; capture->configure(false, "blue", 1280, 720); 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); } void bufferLifetime() { CallCameraBuffer creator, peer; QVERIFY(creator.create()); const QString name = creator.name(); QVERIFY(peer.attach(name)); creator.data()[0] = 17; QCOMPARE(peer.data()[0], uchar(17)); creator.close(); peer.data()[0] = 23; QCOMPARE(peer.data()[0], uchar(23)); peer.close(); QVERIFY(!peer.attach(name)); } void blockedOperation_data() { QTest::addColumn("device"); QTest::newRow("start") << QByteArray("start-stall"); #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) QTest::newRow("conversion") << QByteArray("convert-stall"); #endif } void blockedOperation() { QFETCH(QByteArray, device); auto capture = std::make_unique(); QSignalSpy errors(capture.get(), &CallCameraCapture::failed); Heartbeat heartbeat; capture->configure(true, device); QTRY_COMPARE_WITH_TIMEOUT(errors.size(), 1, 7500); QVERIFY(heartbeat.ticks > 100); QVERIFY(heartbeat.maximum < 150); qInfo("blocked %s: GUI gap max=%lldms ticks=%u", device.constData(), (long long)heartbeat.maximum, heartbeat.ticks); if (device == "convert-stall") QVERIFY(log().contains("convert-blocked")); const qint64 child = pid(); QVERIFY(child > 0); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); // Тот же владелец допускает явный повтор после отказа, без зависшего старого процесса. unsigned frames = 0; connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage&, qint64, qint64) { ++frames; }); capture->configure(true, "blue"); QTRY_VERIFY_WITH_TIMEOUT(frames > 0, 3000); capture.reset(); } void destroyWhileStopping() { auto capture = std::make_unique(); unsigned frames = 0; connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage&, qint64, qint64) { ++frames; }); capture->configure(true, "stop-stall"); QTRY_VERIFY_WITH_TIMEOUT(frames > 0, 3000); const qint64 child = pid(); QVERIFY(child > 0); Heartbeat heartbeat; QElapsedTimer elapsed; elapsed.start(); capture.reset(); QVERIFY(elapsed.elapsed() < 50); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3500); QVERIFY(log().contains("stop:stop-stall")); QVERIFY(heartbeat.maximum < 150); } void applicationExit() { QProcess probe; probe.start(QCoreApplication::applicationFilePath(), {QStringLiteral("--exit-probe")}); QTRY_COMPARE_WITH_TIMEOUT(probe.state(), QProcess::NotRunning, 3000); QCOMPARE(probe.exitStatus(), QProcess::NormalExit); QCOMPARE(probe.exitCode(), 0); QVERIFY(!probe.readAllStandardError().contains("Destroyed while process")); } #ifdef TEST_CALL_VIDEO_ENGINE void engineKeepsCameraAcrossCodecChange() { auto engine = std::make_unique(); unsigned frames = 0; connect(engine.get(), &CallVideoEngine::localImageChanged, this, [&](const QImage& image) { if (!image.isNull()) ++frames; }); engine->start(7); engine->setVideo(true, "red", 640, 480, 1); QTRY_VERIFY_WITH_TIMEOUT(frames >= 5, 4000); QCOMPARE(log().count("start:red"), 1); unsigned before = frames; engine->setVideo(true, "red", 640, 480, 2); QTRY_VERIFY_WITH_TIMEOUT(frames >= before + 5, 2000); QCOMPARE(log().count("start:red"), 1); QVERIFY(!log().contains("stop:red")); engine->setVideo(true, "stop-stall", 640, 480, 2); QTRY_VERIFY_WITH_TIMEOUT(log().contains("start:stop-stall"), 2000); const qint64 child = pid(); Heartbeat heartbeat; QElapsedTimer elapsed; elapsed.start(); engine.reset(); QVERIFY(elapsed.elapsed() < 150); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3500); QVERIFY(heartbeat.maximum < 150); } #endif }; int main(int argc, char** argv) { QGuiApplication app(argc, argv); debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); if (app.arguments().value(1) == "--call-camera-worker") return call_camera_worker_run(app.arguments().value(2), std::make_unique()); if (app.arguments().value(1) == "--exit-probe") { auto capture = std::make_unique(); capture->configure(true, "stop-stall"); QTimer::singleShot(500, &app, &QCoreApplication::quit); const int result = app.exec(); capture.reset(); return result; } CameraTest test; return QTest::qExec(&test, argc, argv); } #include "test_call_camera_capture.moc"