232 lines
11 KiB
C++
232 lines
11 KiB
C++
#include "../src/call_camera_source.h"
|
|
#include "../src/call_camera_buffer.h"
|
|
#include "../../../src/call/call_video_playout.h"
|
|
#include "debug_config.h"
|
|
#include <QtTest>
|
|
#include <QGuiApplication>
|
|
#include <QFile>
|
|
#include <QTemporaryDir>
|
|
#include <QTimer>
|
|
#include <QProcess>
|
|
#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 <QAbstractVideoBuffer>
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
#include <qt_windows.h>
|
|
#else
|
|
#include <signal.h>
|
|
#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<StuckBuffer>()); 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.setInterval(id == "fast" ? 5 : 33);
|
|
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 fastCameraKeepsThirtyFps() {
|
|
auto capture = std::make_unique<CallCameraCapture>();
|
|
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);
|
|
QTRY_VERIFY_WITH_TIMEOUT(frames >= 5, 3000);
|
|
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 qint64 child = pid(); capture.reset(); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000);
|
|
}
|
|
void latestFrameAndGeneration() {
|
|
auto capture = std::make_unique<CallCameraCapture>();
|
|
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<QByteArray>("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<CallCameraCapture>();
|
|
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<CallCameraCapture>();
|
|
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<CallVideoEngine>();
|
|
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<TestSource>());
|
|
if (app.arguments().value(1) == "--exit-probe") {
|
|
auto capture = std::make_unique<CallCameraCapture>(); 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"
|