diff --git a/src/video/video.c b/src/video/video.c index cca987d6..091ddf25 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -45,10 +45,23 @@ int video_transcode_is_active(void) { return vc_active_get(); } /* Пробы не меняют глобальный уровень: параллельные звонки сохраняют рабочие ошибки. */ #ifdef _MSC_VER static __declspec(thread) struct { unsigned depth, repeated; char last[1024]; } g_codec_probe; +static __declspec(thread) struct video_ffmpeg_errors* g_ffmpeg_errors; #else static __thread struct { unsigned depth, repeated; char last[1024]; } g_codec_probe; +static __thread struct video_ffmpeg_errors* g_ffmpeg_errors; #endif +/* Сборщик действует только в потоке вызывающего и не заменяет общий FFmpeg callback. */ +void video_ffmpeg_errors_begin(struct video_ffmpeg_errors* errors) { + video_ffmpeg_init(); + errors->text[0] = '\0'; errors->previous = g_ffmpeg_errors; g_ffmpeg_errors = errors; +} + +void video_ffmpeg_errors_end(struct video_ffmpeg_errors* errors) { + if (g_ffmpeg_errors != errors) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "FFmpeg error capture ended out of order"); return; } + g_ffmpeg_errors = errors->previous; +} + static void video_codec_probe_flush(void) { if (g_codec_probe.repeated) DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "ffmpeg probe: repeated=%u message=%s", g_codec_probe.repeated, g_codec_probe.last); @@ -125,6 +138,10 @@ static void video_av_log_cb(void* avcl, int level, const char* fmt, va_list vl) size_t len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0'; if (!len) return; + if (g_ffmpeg_errors && level <= AV_LOG_ERROR) { + size_t used = strlen(g_ffmpeg_errors->text); + snprintf(g_ffmpeg_errors->text + used, sizeof(g_ffmpeg_errors->text) - used, "%s%s", used ? "\n" : "", buf); + } if (g_codec_probe.depth && level <= AV_LOG_WARNING) { if (!strcmp(g_codec_probe.last, buf)) { ++g_codec_probe.repeated; return; } video_codec_probe_flush(); snprintf(g_codec_probe.last, sizeof(g_codec_probe.last), "%s", buf); diff --git a/src/video/video.h b/src/video/video.h index 6670f367..8a533f24 100644 --- a/src/video/video.h +++ b/src/video/video.h @@ -20,6 +20,17 @@ struct UASYNC; * trace категории debug включает AMF и заголовки HEVC. В сборке без FFmpeg ничего не делает. */ void video_ffmpeg_init(void); +/* Сбор ошибок FFmpeg текущего потока для отображения причины отказа codec. + * begin/end парные, допускают вложенность; сообщения получает только внутренний сборщик. + * Уровни и обычный вывод логов не меняются; без FFmpeg текст остаётся пустым. */ +struct video_ffmpeg_errors { + char text[4096]; /* Первые сообщения уровня error/fatal, разделённые переводами строк. */ + struct video_ffmpeg_errors* previous; /* Предыдущий сборщик текущего потока; управляется begin/end. */ +}; +/* errors обязателен и живёт до end; begin очищает text, end оставляет его доступным вызывающему. */ +void video_ffmpeg_errors_begin(struct video_ffmpeg_errors* errors); +void video_ffmpeg_errors_end(struct video_ffmpeg_errors* errors); + /* Проба доступности codec: begin/end парные, вложенные, только для текущего потока. * Ожидаемые отказы FFmpeg и сводка повторов выводятся на trace категории video; * рабочие потоки сохраняют error/warn. begin/end должны выполняться в одном потоке. */ diff --git a/tools/chatgui/src/live_video_encoder.cpp b/tools/chatgui/src/live_video_encoder.cpp index ede51708..88d9bc99 100644 --- a/tools/chatgui/src/live_video_encoder.cpp +++ b/tools/chatgui/src/live_video_encoder.cpp @@ -2,6 +2,7 @@ #include "video_codec_log.h" #include #include +#include #include #include #include @@ -151,11 +152,13 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { } #ifdef HAVE_FFMPEG video_ffmpeg_init(); + QStringList failures; for (const auto& name : encoders(config.codec)) { if (!matchesBackend(name, backend)) continue; if (!config.backend.isEmpty() && name != config.backend.toLatin1()) continue; const AVCodec* encoder = avcodec_find_encoder_by_name(name.constData()); if (!encoder) continue; + VideoCodecErrors diagnostics; // Проба не оставляет references/PTS в live-контексте: после двух KEY encoder открывается заново. for (int pass = 0; pass < 2; pass++) { close(); m->config = config; m->name = QString::fromLatin1(name); @@ -365,7 +368,14 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { c->thread_count, c->active_thread_type); return true; } + QString reason = m->error; + const QString details = QString::fromUtf8(diagnostics.text()).trimmed(); + if (!details.isEmpty() && details != reason) reason += '\n' + details; + failures.append(QString::fromLatin1(name) + ": " + reason); + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "desktop encoder attempt failed name=%s reason=%s", + name.constData(), qPrintable(reason)); } + if (!failures.isEmpty()) m->error = failures.join("\n\n"); if (m->error.isEmpty()) m->error = "No working encoder for " + codecName(config.codec); #else m->error = "FFmpeg is unavailable"; diff --git a/tools/chatgui/src/live_video_encoder.h b/tools/chatgui/src/live_video_encoder.h index fecdaa49..b282fca5 100644 --- a/tools/chatgui/src/live_video_encoder.h +++ b/tools/chatgui/src/live_video_encoder.h @@ -59,7 +59,8 @@ public: bool encode(const QImage& image, uint64_t pts, bool key, QList& output, bool finish = false); /* Число принятых, но ещё не выданных входов профиля звонка; Automatic не ведёт эту таблицу и возвращает 0. */ unsigned pendingFrames() const; - /* Последняя записанная ошибка; успешный encode/setMaxQp не обязан очищать её. */ + /* Последняя ошибка; при отказе open — причины всех попыток с именами encoder и сообщениями FFmpeg. + * Успешный encode/setMaxQp не обязан очищать её. */ QString error() const; /* Имя FFmpeg encoder текущей/последней пробы; после close пусто, само по себе не означает успешный open. */ QString name() const; diff --git a/tools/chatgui/src/stream_publish_window.cpp b/tools/chatgui/src/stream_publish_window.cpp index b10a5ed6..1de5515e 100644 --- a/tools/chatgui/src/stream_publish_window.cpp +++ b/tools/chatgui/src/stream_publish_window.cpp @@ -211,6 +211,8 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g "Use CPU encoding instead? This may cause high CPU usage and slow down the computer.").arg(codec), QMessageBox::Yes | QMessageBox::No, this); question->setObjectName(QStringLiteral("streamCpuConfirmation")); + question->setTextFormat(Qt::PlainText); + question->setInformativeText(details.section("\n\n", 0, 0)); question->setDefaultButton(QMessageBox::No); question->setEscapeButton(QMessageBox::No); question->setDetailedText(details); question->setAttribute(Qt::WA_DeleteOnClose); auto* engine = m_engine; diff --git a/tools/chatgui/src/video_codec_log.h b/tools/chatgui/src/video_codec_log.h index d8bb93a0..f9b0e79c 100644 --- a/tools/chatgui/src/video_codec_log.h +++ b/tools/chatgui/src/video_codec_log.h @@ -16,6 +16,19 @@ public: VideoCodecProbe& operator=(const VideoCodecProbe&) = delete; }; +/* Сохраняет сообщения FFmpeg error/fatal текущей попытки независимо от фильтров логирования. + * Создать/уничтожить в одном потоке; text заимствован до уничтожения объекта. */ +class VideoCodecErrors { +public: + VideoCodecErrors() { video_ffmpeg_errors_begin(&m_errors); } + ~VideoCodecErrors() { video_ffmpeg_errors_end(&m_errors); } + VideoCodecErrors(const VideoCodecErrors&) = delete; + VideoCodecErrors& operator=(const VideoCodecErrors&) = delete; + const char* text() const { return m_errors.text; } +private: + video_ffmpeg_errors m_errors; +}; + // level/category — DEBUG_LEVEL_*/DEBUG_CATEGORY_*, далее printf-формат и аргументы; // аргументы форматирования вычисляются только после фильтрации по итоговому уровню. #define CODEC_LOG(level, category, ...) do { \ diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index b8be4213..5e447723 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -91,7 +91,13 @@ extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* code } 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); } + if (hardware) { + hardwareOpens++; + if (failHardware) { + av_log(context, AV_LOG_ERROR, "Test driver failure for %s: required API 13.1, found 13.0\n", codec->name); + return AVERROR(ENODEV); + } + } else softwareOpens++; } return __real_avcodec_open2(context, codec, options); @@ -930,6 +936,27 @@ private slots: QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000); // Core продолжает FIN drain после удаления desktop engine. #else QSKIP("Capture test requires FFmpeg and null audio wrappers"); +#endif + } + void encoderFailuresPreserveDriverDetails() { +#if defined(TEST_NULL_AUDIO) && defined(HAVE_FFMPEG) + if (!avcodec_find_encoder_by_name("h264_nvenc") || !avcodec_find_encoder_by_name("h264_qsv")) + QSKIP("Test needs two registered hardware encoders"); + failHardware = true; hardwareOpens = 0; softwareOpens = 0; + auto reset = qScopeGuard([] { failHardware = false; }); + LiveVideoConfig config; config.width = 320; config.height = 180; + LiveVideoEncoder encoder; + QVERIFY(!encoder.open(config, LiveVideoEncoder::Backend::HardwareOnly)); + const QString details = encoder.error(); + QVERIFY2(details.contains("h264_nvenc:"), qPrintable(details)); + QVERIFY2(details.contains("h264_qsv:"), qPrintable(details)); + QVERIFY2(details.contains("Test driver failure for h264_nvenc: required API 13.1, found 13.0"), qPrintable(details)); + QVERIFY2(details.contains("Test driver failure for h264_qsv: required API 13.1, found 13.0"), qPrintable(details)); + QVERIFY(details.indexOf("h264_nvenc:") < details.indexOf("\n\nh264_qsv:")); + QVERIFY(hardwareOpens.load() >= 2); QCOMPARE(softwareOpens.load(), 0u); + QVERIFY(encoder.open(config, LiveVideoEncoder::Backend::SoftwareOnly)); QVERIFY(encoder.error().isEmpty()); +#else + QSKIP("Encoder instrumentation unavailable"); #endif } void cpuConsent_data() { @@ -952,6 +979,10 @@ private slots: QVERIFY(engine.start(group, config)); QTRY_COMPARE(requests.size(), 1); uint64_t generation = requests.front().front().toULongLong(); auto* question = controls.findChild("streamCpuConfirmation"); QVERIFY(question); + QCOMPARE(question->detailedText(), requests.front().at(2).toString()); + QCOMPARE(question->informativeText(), question->detailedText().section("\n\n", 0, 0)); + QVERIFY(question->informativeText().contains("required API 13.1, found 13.0")); + QCOMPARE(question->textFormat(), Qt::PlainText); QCOMPARE(question->defaultButton(), question->button(QMessageBox::No)); QTest::qWait(allow ? 8200 : 150); // Время ответа человека не является timeout первого кадра. QCOMPARE(softwareOpens.load(), 0u); QCOMPARE(audioEncodes.load(), 0u); QVERIFY(errors.isEmpty()); diff --git a/tools/chatgui/tests/test_video_diagnostics.cpp b/tools/chatgui/tests/test_video_diagnostics.cpp index 2715c9d1..b8459df3 100644 --- a/tools/chatgui/tests/test_video_diagnostics.cpp +++ b/tools/chatgui/tests/test_video_diagnostics.cpp @@ -29,6 +29,34 @@ private slots: messages.clear(); debug_set_log_hook(capture); video_ffmpeg_init(); } void cleanup() { debug_set_log_hook(nullptr); } + void capturesDriverErrorsWithLogsDisabled() { + debug_set_level(DEBUG_LEVEL_DISABLED); + VideoCodecErrors errors; + av_log(nullptr, AV_LOG_ERROR, "Driver does not support the required nvenc API version. Required: 13.1 Found: 13.0\n"); + av_log(nullptr, AV_LOG_WARNING, "unrelated warning\n"); + std::thread playback([] { av_log(nullptr, AV_LOG_ERROR, "unrelated playback error\n"); }); playback.join(); + { + VideoCodecProbe probe; + av_log(nullptr, AV_LOG_ERROR, "encoder probe failed\n"); + } + QCOMPARE(QString::fromUtf8(errors.text()), QStringLiteral( + "Driver does not support the required nvenc API version. Required: 13.1 Found: 13.0\nencoder probe failed")); + QVERIFY(messages.empty()); + } + void nestedErrorCaptureAndBoundedOutput() { + VideoCodecErrors outer; + av_log(nullptr, AV_LOG_ERROR, "outer failure\n"); + { + VideoCodecErrors inner; + av_log(nullptr, AV_LOG_FATAL, "inner failure\n"); + QCOMPARE(QByteArray(inner.text()), QByteArray("inner failure")); + } + av_log(nullptr, AV_LOG_ERROR, "second outer failure\n"); + QCOMPARE(QByteArray(outer.text()), QByteArray("outer failure\nsecond outer failure")); + for (int i = 0; i < 1000; i++) av_log(nullptr, AV_LOG_ERROR, "repeated driver failure %d\n", i); + const QByteArray details(outer.text()); + QVERIFY(details.startsWith("outer failure\nsecond outer failure\n")); QCOMPARE(details.size(), 4095); + } void probeLevelsRepeatsAndConcurrentErrors() { debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_TRACE); {