Exercise hung capture stop and destruction without blocking publisher GUI
This commit is contained in:
1 parent
4ab0be1882
commit
b34825cda0
3 files changed
+159
-2
No files matched your search
@@ -98,7 +98,7 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s
|
||||
m_timer.setTimerType(Qt::PreciseTimer); m_timer.setInterval(100);
|
||||
connect(&m_timer, &QTimer::timeout, this, &StreamPublishEngine::tick); m_timer.start();
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
m->capture = std::make_unique<VideoCapture>(nullptr, VideoCapture::Usage::Stream);
|
||||
m->capture = std::make_unique<VideoCapture>(this, VideoCapture::Usage::Stream);
|
||||
connect(m->capture.get(), &VideoCapture::devicesChanged, this, [this](const VideoCaptureDevices& devices) {
|
||||
m->sources.clear();
|
||||
for (const auto& device : devices) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* Источник только для тестовых worker-процессов: неподвижный экран и зависания на границах native lifecycle. */
|
||||
#pragma once
|
||||
#include "../src/video_capture_source.h"
|
||||
#include "../../../src/call/call_video_playout.h"
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QVideoFrameFormat>
|
||||
#include <cstring>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
#include <QAbstractVideoBuffer>
|
||||
class BlockedStreamFrame : public QAbstractVideoBuffer {
|
||||
public:
|
||||
MapData map(QVideoFrame::MapMode) override { QThread::sleep(60); return {}; }
|
||||
QVideoFrameFormat format() const override { return {QSize(16, 16), QVideoFrameFormat::Format_BGRX8888}; }
|
||||
};
|
||||
#endif
|
||||
|
||||
class StreamCaptureFixture : public VideoCaptureSource {
|
||||
QTimer timer;
|
||||
QByteArray mode;
|
||||
bool started = false;
|
||||
public:
|
||||
explicit StreamCaptureFixture(QByteArray testMode) : mode(std::move(testMode)) {
|
||||
timer.setSingleShot(true);
|
||||
connect(&timer, &QTimer::timeout, this, [this] {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
if (mode == "convert") {
|
||||
QVideoFrame raw(std::make_unique<BlockedStreamFrame>()); raw.setStartTime(call_video_playout_now_us());
|
||||
emit frame(raw); return;
|
||||
}
|
||||
#endif
|
||||
if (mode == "bad-frame") {
|
||||
QVideoFrame raw(QVideoFrameFormat(QSize(16, 16), QVideoFrameFormat::Format_Jpeg));
|
||||
if (raw.map(QVideoFrame::WriteOnly)) { std::memset(raw.bits(0), 0, raw.mappedBytes(0)); raw.unmap(); }
|
||||
emit frame(raw); return;
|
||||
}
|
||||
QImage image(640, 360, QImage::Format_RGB32); image.fill(Qt::red);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
QVideoFrame raw(image);
|
||||
#else
|
||||
QVideoFrame raw(QVideoFrameFormat(image.size(), QVideoFrameFormat::Format_BGRA8888));
|
||||
if (!raw.map(QVideoFrame::WriteOnly)) return;
|
||||
for (int row = 0; row < image.height(); ++row)
|
||||
std::memcpy(raw.bits(0) + row * raw.bytesPerLine(0), image.constScanLine(row), size_t(image.width()) * 4);
|
||||
raw.unmap();
|
||||
#endif
|
||||
raw.setStartTime(call_video_playout_now_us()); emit frame(raw);
|
||||
});
|
||||
}
|
||||
~StreamCaptureFixture() override { if (started && mode == "destroy") QThread::sleep(60); }
|
||||
VideoCaptureDevices devices(bool) override {
|
||||
return {{QStringLiteral("Isolated test screen"), "fixture", {}, VideoCaptureKind::Screen, QSize(640, 360)}};
|
||||
}
|
||||
bool start(const VideoCaptureConfig&) override { started = true; timer.start(1); return true; }
|
||||
void stop() override {
|
||||
timer.stop(); emit stage(QStringLiteral("screen.stop"));
|
||||
if (started && mode == "stop") QThread::sleep(60);
|
||||
}
|
||||
};
|
||||
@@ -53,6 +53,7 @@ extern "C" {
|
||||
#include <QVideoWidget>
|
||||
#include <QVideoSink>
|
||||
#include <QVideoFrame>
|
||||
#include "stream_capture_fixture.h"
|
||||
#endif
|
||||
#ifdef UTUN_STREAM_X11_CURSOR
|
||||
#include "../src/stream_screen_cursor.h"
|
||||
@@ -909,6 +910,94 @@ private slots:
|
||||
QCOMPARE(status.total_bytes, status.video_bytes + status.audio_bytes + status.stream_bytes + status.transport_bytes + status.inflight_bytes);
|
||||
gui_stream_publish_stop(id); QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000);
|
||||
}
|
||||
void isolatedCaptureStop_data() {
|
||||
QTest::addColumn<QByteArray>("mode"); QTest::addColumn<bool>("destroyEngine");
|
||||
QTest::newRow("screen-stop") << QByteArray("stop") << false;
|
||||
QTest::newRow("native-destructor") << QByteArray("destroy") << false;
|
||||
QTest::newRow("engine-destruction") << QByteArray("stop") << true;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
QTest::newRow("conversion") << QByteArray("convert") << false;
|
||||
#endif
|
||||
}
|
||||
void isolatedCaptureStop() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
QFETCH(QByteArray, mode); QFETCH(bool, destroyEngine);
|
||||
qputenv("UTUN_STREAM_CAPTURE_FIXTURE", mode);
|
||||
auto restore = qScopeGuard([] { qunsetenv("UTUN_STREAM_CAPTURE_FIXTURE"); });
|
||||
auto engine = std::make_unique<StreamPublishEngine>();
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!engine->sources().isEmpty(), 4000);
|
||||
StreamPublishConfig config; config.audio = false; config.showCursor = false;
|
||||
config.source = engine->sources().front(); config.video.width = 320; config.video.height = 180; config.video.fps = 15;
|
||||
connect(engine.get(), &StreamPublishEngine::softwareEncodingRequested, engine.get(),
|
||||
[&](uint64_t generation) { engine->answerSoftwareEncoding(generation, true); });
|
||||
uint64_t id = 0;
|
||||
connect(engine.get(), &StreamPublishEngine::telemetryChanged, this, [&](const stream_publisher_status& status) { id = status.id; });
|
||||
QSignalSpy errors(engine.get(), &StreamPublishEngine::errorOccurred);
|
||||
QVERIFY(engine->start(group, config)); QTRY_VERIFY_WITH_TIMEOUT(id, 3000);
|
||||
if (mode != "convert") QTRY_VERIFY_WITH_TIMEOUT(engine->previewFrame().isValid(), 3000);
|
||||
else QTest::qWait(200); // Worker уже вошёл в заведомо блокирующий map.
|
||||
unsigned ticks = 0; qint64 previous = 0, gap = 0; QElapsedTimer elapsed; elapsed.start(); QTimer heartbeat;
|
||||
connect(&heartbeat, &QTimer::timeout, this, [&] {
|
||||
const qint64 now = elapsed.elapsed(); gap = std::max(gap, now - previous); previous = now; ++ticks;
|
||||
}); heartbeat.start(10);
|
||||
QPointer<VideoCapture> capture = engine->findChild<VideoCapture*>(); QVERIFY(capture);
|
||||
if (destroyEngine) { engine.reset(); QVERIFY(elapsed.elapsed() < 500); }
|
||||
else {
|
||||
QSignalSpy stopped(capture, &VideoCapture::stopped);
|
||||
engine->stop(); QVERIFY(elapsed.elapsed() < 100); QVERIFY(capture->closing());
|
||||
QTRY_VERIFY_WITH_TIMEOUT(([&] {
|
||||
stream_publisher_status status{};
|
||||
return gui_stream_publish_status(id, status) && status.state == STREAM_PUBLISHER_DRAINING;
|
||||
})(), 1000);
|
||||
QVERIFY(capture->closing()); // FIN уже отправлен, аппаратное закрытие ещё висит.
|
||||
QTRY_COMPARE_WITH_TIMEOUT(stopped.count(), 1, 3000);
|
||||
QVERIFY(!capture->closing()); QTRY_VERIFY_WITH_TIMEOUT(!engine->active(), 4000);
|
||||
QVERIFY(errors.isEmpty());
|
||||
// Тот же engine открывает новую сессию после подтверждённого выхода прежнего worker.
|
||||
qputenv("UTUN_STREAM_CAPTURE_FIXTURE", "idle");
|
||||
QVERIFY(engine->start(group, config)); QTRY_VERIFY_WITH_TIMEOUT(engine->previewFrame().isValid(), 3000);
|
||||
engine->stop(); QTRY_VERIFY_WITH_TIMEOUT(!engine->active(), 4000);
|
||||
}
|
||||
QTRY_VERIFY_WITH_TIMEOUT(ended(id) || (engine && engine->active()), 4000);
|
||||
QVERIFY(ticks > 50); QVERIFY2(gap < 200, qPrintable(QString::number(gap)));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "[PASS] isolated publisher stop mode=%s destroy=%d GUI_gap_ms=%lld ticks=%u",
|
||||
mode.constData(), destroyEngine, (long long)gap, ticks);
|
||||
#else
|
||||
QSKIP("Capture unavailable");
|
||||
#endif
|
||||
}
|
||||
void isolatedStaticScreen() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
qputenv("UTUN_STREAM_CAPTURE_FIXTURE", "idle");
|
||||
auto restore = qScopeGuard([] { qunsetenv("UTUN_STREAM_CAPTURE_FIXTURE"); });
|
||||
StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.sources().isEmpty(), 4000);
|
||||
StreamPublishConfig config; config.audio = false; config.showCursor = false;
|
||||
config.source = engine.sources().front(); config.video.width = 320; config.video.height = 180; config.video.fps = 15;
|
||||
connect(&engine, &StreamPublishEngine::softwareEncodingRequested, &engine,
|
||||
[&](uint64_t generation) { engine.answerSoftwareEncoding(generation, true); });
|
||||
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
|
||||
QVERIFY(engine.start(group, config)); QTRY_VERIFY_WITH_TIMEOUT(engine.previewFrame().isValid(), 3000);
|
||||
QTest::qWait(5600); QVERIFY(engine.running()); QVERIFY(errors.isEmpty());
|
||||
engine.stop(); QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000);
|
||||
#else
|
||||
QSKIP("Capture unavailable");
|
||||
#endif
|
||||
}
|
||||
void captureConversionFailure() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
qputenv("UTUN_STREAM_CAPTURE_FIXTURE", "bad-frame");
|
||||
auto restore = qScopeGuard([] { qunsetenv("UTUN_STREAM_CAPTURE_FIXTURE"); });
|
||||
StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.sources().isEmpty(), 4000);
|
||||
StreamPublishConfig config; config.audio = false; config.showCursor = false; config.source = engine.sources().front();
|
||||
config.video.width = 320; config.video.height = 180;
|
||||
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
|
||||
QVERIFY(engine.start(group, config)); QTRY_VERIFY_WITH_TIMEOUT(!errors.isEmpty(), 3000);
|
||||
QVERIFY(errors.front().front().toString().contains("frame conversion failed"));
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000);
|
||||
#else
|
||||
QSKIP("Capture unavailable");
|
||||
#endif
|
||||
}
|
||||
void stopWithGuiThreadCleanup_data() {
|
||||
QTest::addColumn<bool>("destroy");
|
||||
QTest::newRow("stop") << false;
|
||||
@@ -1251,7 +1340,16 @@ private slots:
|
||||
}
|
||||
};
|
||||
int main(int argc, char** argv) {
|
||||
if (argc >= 2 && QByteArray(argv[1]) == "--video-capture-worker") return video_capture_worker_main(argc, argv);
|
||||
if (argc >= 2 && QByteArray(argv[1]) == "--video-capture-worker") {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
const auto fixture = qgetenv("UTUN_STREAM_CAPTURE_FIXTURE");
|
||||
if (!fixture.isEmpty()) {
|
||||
QGuiApplication app(argc, argv); debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
|
||||
return video_capture_worker_run(QString::fromLocal8Bit(argv[2]), std::make_unique<StreamCaptureFixture>(fixture), true);
|
||||
}
|
||||
#endif
|
||||
return video_capture_worker_main(argc, argv);
|
||||
}
|
||||
QApplication app(argc, argv); StreamPublishTest test; return QTest::qExec(&test, argc, argv);
|
||||
}
|
||||
#include "test_stream_publish.moc"
|
||||
Reference in new issue
Block a user