Browse Source

Test stream startup consent and nonblocking microphone failure

master
evgeny 2 days ago
parent
commit
21f4de492c
  1. 5
      tools/chatgui/CMakeLists.txt
  2. 116
      tools/chatgui/tests/test_stream_publish.cpp

5
tools/chatgui/CMakeLists.txt

@ -638,7 +638,10 @@ endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(test_stream_publish PRIVATE TEST_NULL_AUDIO)
target_link_options(test_stream_publish PRIVATE "-Wl,--wrap=ma_context_init" "-Wl,--wrap=ma_device_init"
"-Wl,--wrap=opus_codec_encode")
"-Wl,--wrap=ma_device_uninit" "-Wl,--wrap=opus_codec_encode")
if(FFMPEG_FOUND)
target_link_options(test_stream_publish PRIVATE "-Wl,--wrap=avcodec_open2")
endif()
endif()
add_test(NAME test_stream_publish COMMAND test_stream_publish)
set_tests_properties(test_stream_publish PROPERTIES TIMEOUT 60 ENVIRONMENT "QT_QPA_PLATFORM=offscreen")

116
tools/chatgui/tests/test_stream_publish.cpp

@ -7,6 +7,7 @@
#include <QCheckBox>
#include <QButtonGroup>
#include <QPushButton>
#include <QMessageBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScreen>
@ -53,8 +54,25 @@ extern "C" {
#ifdef TEST_NULL_AUDIO
static ma_device_data_proc captureCallback;
static std::atomic<bool> failAudioEncoding{false};
static std::atomic<bool> suppressMicrophone{false}, silentMicrophone{false}, slowCaptureClose{false};
static std::atomic<unsigned> captureOpens{0}, audioEncodes{0};
#ifdef HAVE_FFMPEG
static std::atomic<unsigned> softwareOpens{0}, hardwareOpens{0};
static std::atomic<bool> failHardware{false};
extern "C" int __real_avcodec_open2(AVCodecContext*, const AVCodec*, AVDictionary**);
extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* codec, AVDictionary** options) {
if (av_codec_is_encoder(codec)) {
QByteArray name(codec->name);
bool hardware = name.endsWith("_nvenc") || name.endsWith("_qsv") || name.endsWith("_amf") || name.endsWith("_vaapi");
if (hardware) { hardwareOpens++; if (failHardware) return AVERROR(ENODEV); }
else softwareOpens++;
}
return __real_avcodec_open2(context, codec, options);
}
#endif
extern "C" int __real_opus_codec_encode(opus_codec_encoder_t*, const int16_t*, int, uint8_t*, int);
extern "C" int __wrap_opus_codec_encode(opus_codec_encoder_t* encoder, const int16_t* pcm, int frames, uint8_t* data, int capacity) {
audioEncodes++;
return failAudioEncoding.exchange(false) ? -1 : __real_opus_codec_encode(encoder, pcm, frames, data, capacity);
}
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
@ -63,21 +81,29 @@ extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const
ma_backend backend = ma_backend_null; return __real_ma_context_init(&backend, 1, nullptr, context);
}
static void injectMicrophone(ma_device* device, void* out, const void*, ma_uint32 frames) {
if (suppressMicrophone) return;
static uint64_t position = 0;
std::vector<int16_t> pcm(frames * device->capture.channels);
for (unsigned i = 0; i < frames; i++, position++) {
pcm[i * device->capture.channels] = (int16_t)(1200 * std::sin(2 * 3.141592653589793 * 440 * position / 48000));
if (device->capture.channels == 2) pcm[i * 2 + 1] = (int16_t)(3600 * std::sin(2 * 3.141592653589793 * 880 * position / 48000));
}
if (silentMicrophone) std::fill(pcm.begin(), pcm.end(), 0);
captureCallback(device, out, pcm.data(), frames);
}
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) {
auto copy = *config;
if (copy.deviceType == ma_device_type_capture && copy.capture.format == ma_format_s16) {
captureOpens++;
captureCallback = copy.dataCallback; copy.dataCallback = injectMicrophone;
}
return __real_ma_device_init(context, &copy, device);
}
extern "C" void __real_ma_device_uninit(ma_device*);
extern "C" void __wrap_ma_device_uninit(ma_device* device) {
if (device->type == ma_device_type_capture && slowCaptureClose) std::this_thread::sleep_for(std::chrono::milliseconds(700));
__real_ma_device_uninit(device);
}
#endif
class StreamPublishTest : public QObject {
@ -256,6 +282,8 @@ private slots:
#ifdef HAVE_STREAM_CAPTURE
if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run with X11 screen capture enabled");
StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000);
connect(&engine, &StreamPublishEngine::softwareEncodingRequested, &engine,
[&engine](uint64_t generation) { engine.answerSoftwareEncoding(generation, true); });
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
StreamPublishConfig config; config.audio = false; config.nativeSize = true; config.automaticBitrate = true;
config.video.width = 640; config.video.height = 360; config.video.fps = 15;
@ -466,6 +494,92 @@ 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 cpuConsent_data() {
QTest::addColumn<bool>("allow");
QTest::newRow("decline") << false;
QTest::newRow("allow") << true;
}
void cpuConsent() {
#if defined(HAVE_STREAM_CAPTURE) && defined(TEST_NULL_AUDIO) && defined(HAVE_FFMPEG)
if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run under isolated Xvfb");
QFETCH(bool, allow);
failHardware = true; softwareOpens = 0; audioEncodes = 0;
auto reset = qScopeGuard([] { failHardware = false; });
StreamPublishEngine engine; StreamPublishWindow controls(&engine, group); controls.show();
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
QSignalSpy requests(&engine, &StreamPublishEngine::softwareEncodingRequested);
StreamPublishConfig config; config.audio = false; config.video.width = 320; config.video.height = 180; config.video.fps = 15;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen) { config.source = source; break; }
QVERIFY(!config.source.id.isEmpty()); QCOMPARE(softwareOpens.load(), 0u);
QVERIFY(engine.start(group, config)); QTRY_COMPARE(requests.size(), 1);
uint64_t generation = requests.front().front().toULongLong();
auto* question = controls.findChild<QMessageBox*>("streamCpuConfirmation"); QVERIFY(question);
QCOMPARE(question->defaultButton(), question->button(QMessageBox::No));
QTest::qWait(allow ? 8200 : 150); // Время ответа человека не является timeout первого кадра.
QCOMPARE(softwareOpens.load(), 0u); QCOMPARE(audioEncodes.load(), 0u); QVERIFY(errors.isEmpty());
engine.answerSoftwareEncoding(generation + 1, true); QCOMPARE(engine.softwareEncodingRequest(), generation);
question->done(allow ? QMessageBox::Yes : QMessageBox::No);
if (allow) {
QTRY_VERIFY(softwareOpens.load() > 0); QTRY_VERIFY(engine.previewFrame().isValid());
QVERIFY(errors.isEmpty()); engine.stop();
} else {
QTRY_VERIFY(!engine.running()); QCOMPARE(errors.size(), 1); QCOMPARE(softwareOpens.load(), 0u);
}
QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000);
// Согласие не сохраняется между трансляциями.
unsigned before = softwareOpens.load();
QVERIFY(engine.start(group, config)); QTRY_COMPARE(requests.size(), 2);
QCOMPARE(softwareOpens.load(), before); engine.stop();
engine.answerSoftwareEncoding(generation, true); QCOMPARE(softwareOpens.load(), before);
QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000);
#else
QSKIP("Capture and encoder instrumentation unavailable");
#endif
}
void microphoneFailure_data() {
QTest::addColumn<bool>("initialFailure");
QTest::newRow("no-first-frame") << true;
QTest::newRow("stops-after-silence") << false;
}
void microphoneFailure() {
#if defined(HAVE_STREAM_CAPTURE) && defined(TEST_NULL_AUDIO) && defined(HAVE_FFMPEG)
if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run under isolated Xvfb");
QFETCH(bool, initialFailure);
auto* sound = SoundManager::instance(); QVERIFY(sound->init());
failHardware = true; suppressMicrophone = initialFailure; silentMicrophone = true; slowCaptureClose = true;
softwareOpens = 0; hardwareOpens = 0; audioEncodes = 0; captureOpens = 0;
auto reset = qScopeGuard([sound] {
failHardware = suppressMicrophone = silentMicrophone = slowCaptureClose = false; sound->shutdown();
});
StreamPublishEngine engine; StreamPublishWindow controls(&engine, group);
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
QSignalSpy requests(&engine, &StreamPublishEngine::softwareEncodingRequested);
StreamPublishConfig config; config.video.width = 320; config.video.height = 180; config.video.fps = 15;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen) { config.source = source; break; }
QVERIFY(engine.start(group, config));
if (!initialFailure) {
QTRY_COMPARE(requests.size(), 1); QCOMPARE(softwareOpens.load(), 0u); QCOMPARE(audioEncodes.load(), 0u);
engine.answerSoftwareEncoding(requests.front().front().toULongLong(), true);
QTRY_VERIFY(audioEncodes.load() > 0); QVERIFY(errors.isEmpty()); suppressMicrophone = true;
}
QElapsedTimer clock; clock.start(); qint64 previous = clock.elapsed(), maxGap = 0;
QTimer heartbeat; heartbeat.setInterval(10);
connect(&heartbeat, &QTimer::timeout, this, [&] {
auto now = clock.elapsed(); maxGap = std::max(maxGap, now - previous); previous = now;
});
heartbeat.start(); QTRY_VERIFY_WITH_TIMEOUT(!engine.running(), 2500);
QCOMPARE(errors.size(), 1); QVERIFY(errors.front().front().toString().contains("microphone"));
QVERIFY(engine.active()); // Фоновое закрытие ещё идёт, GUI продолжает обрабатывать события.
QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 2500); heartbeat.stop(); QVERIFY2(maxGap < 400, qPrintable(QString::number(maxGap)));
QCOMPARE(captureOpens.load(), 1u);
bool shown = false;
for (auto* label : controls.findChildren<QLabel*>()) shown |= label->text().contains("microphone is not delivering audio");
QVERIFY(shown);
if (initialFailure) { QVERIFY(requests.isEmpty()); QCOMPARE(hardwareOpens.load(), 0u); QCOMPARE(softwareOpens.load(), 0u); QCOMPARE(audioEncodes.load(), 0u); }
#else
QSKIP("Capture and encoder instrumentation unavailable");
#endif
}
void screenAndWindowCapture() {
#if defined(HAVE_STREAM_CAPTURE) && defined(TEST_NULL_AUDIO)
if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Real screen/window capture runs separately under Xvfb");
@ -473,6 +587,8 @@ private slots:
QLabel pattern("Desktop capture test"); pattern.setWindowTitle("UTUN capture fixture"); pattern.resize(320, 180);
pattern.setStyleSheet("background: #c04020; color: white; font-size: 20px;"); pattern.show(); QTest::qWait(100);
StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000);
connect(&engine, &StreamPublishEngine::softwareEncodingRequested, &engine,
[&engine](uint64_t generation) { engine.answerSoftwareEncoding(generation, true); });
StreamPublishConfig config; config.video.width = 640; config.video.height = 360; config.video.fps = 15;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen) { config.source = source; break; }
QVERIFY(!config.source.id.isEmpty());

Loading…
Cancel
Save