Show FFmpeg driver errors when hardware encoding fails

This commit is contained in:
evgeny committed 2026-11-01 08:24:43 +01:00
1 parent d8db018166
commit c889928e52
8 files changed
+115 -2

No files matched your search

+17
View File
@@ -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);
+11
View File
@@ -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 должны выполняться в одном потоке. */
+10
View File
@@ -2,6 +2,7 @@
#include "video_codec_log.h"
#include <QElapsedTimer>
#include <QPainter>
#include <QStringList>
#include <algorithm>
#include <cstdlib>
#include <cstring>
@@ -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";
+2 -1
View File
@@ -59,7 +59,8 @@ public:
bool encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& 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;
@@ -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;
+13
View File
@@ -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 { \
+32 -1
View File
@@ -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<QMessageBox*>("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());
@@ -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);
{