Show FFmpeg driver errors when hardware encoding fails
This commit is contained in:
1 parent
d8db018166
commit
c889928e52
8 files changed
+115
-2
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 должны выполняться в одном потоке. */
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 { \
|
||||
|
||||
@@ -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);
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user