diff --git a/src/video/video.c b/src/video/video.c index 26f5175a..5ddf95f2 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -41,6 +41,8 @@ int video_transcode_is_active(void) { return vc_active_get(); } /* ─── сборка без ffmpeg: стабы ─── */ +void video_ffmpeg_init(void) {} + int video_probe(const char* path) { (void)path; return 0; } int video_get_info(const char* path, struct video_info* out) { (void)path; (void)out; return -1; } int video_needs_transcode(const struct video_info* info) { (void)info; return 1; } @@ -64,25 +66,36 @@ int video_extract_thumbnail(const char* src, const char* jpg_out, int max_w) { static pthread_once_t g_ffmpeg_once = PTHREAD_ONCE_INIT; -/* Перехват логов FFmpeg: все сообщения идут в наш debug-лог с категорией video, - * иначе ошибки ffmpeg теряются в stderr и не видно причину сбоя транскода. */ +/* AMF передаёт свой внутренний трейс через AV_LOG_DEBUG. Остальные подробные логи FFmpeg отсекаем. */ static void video_av_log_cb(void* avcl, int level, const char* fmt, va_list vl) { - (void)avcl; - if (level > AV_LOG_WARNING) return; /* отсекаем INFO/VERBOSE/DEBUG/TRACE — демуксер спамит */ + const char *encoder = NULL; + if (level > AV_LOG_WARNING) { + if (!debug_should_output(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DEBUG)) return; + if (!avcl || *(const AVClass **)avcl != avcodec_get_class()) return; + const AVCodecContext *codec = avcl; + if (!codec->codec || !codec->codec->name) return; + const char *name = codec->codec->name; + size_t length = strlen(name); + if (length < 4 || strcmp(name + length - 4, "_amf")) return; + encoder = name; + } char buf[1024]; vsnprintf(buf, sizeof(buf), fmt, vl); size_t len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0'; - if (level <= AV_LOG_ERROR) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "ffmpeg: %s", buf); + if (encoder) DEBUG_TRACE(DEBUG_CATEGORY_DEBUG, "ffmpeg AMF encoder=%s: %s", encoder, buf); + else if (level <= AV_LOG_ERROR) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "ffmpeg: %s", buf); else DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "ffmpeg: %s", buf); } static void ffmpeg_init_once(void) { av_log_set_callback(video_av_log_cb); - av_log_set_level(AV_LOG_WARNING); + av_log_set_level(AV_LOG_DEBUG); avformat_network_init(); } +void video_ffmpeg_init(void) { pthread_once(&g_ffmpeg_once, ffmpeg_init_once); } + /* строковое описание кода ошибки ffmpeg (используется только в worker-потоке) */ static const char* vc_errstr(int err) { static char buf[64]; @@ -94,7 +107,7 @@ static const char* vc_errstr(int err) { int video_probe(const char* path) { if (!path || !path[0]) return 0; - pthread_once(&g_ffmpeg_once, ffmpeg_init_once); + video_ffmpeg_init(); AVFormatContext* fmt = NULL; if (avformat_open_input(&fmt, path, NULL, NULL) != 0) return 0; @@ -117,7 +130,7 @@ int video_get_info(const char* path, struct video_info* out) { if (!out) return -1; memset(out, 0, sizeof(*out)); if (!path || !path[0]) return -1; - pthread_once(&g_ffmpeg_once, ffmpeg_init_once); + video_ffmpeg_init(); AVFormatContext* fmt = NULL; if (avformat_open_input(&fmt, path, NULL, NULL) != 0) { @@ -445,7 +458,7 @@ static void vc_flush(struct vc_ctx* ctx, struct vc_stream* st) { } static int vc_run_transcode(struct vc_ctx* ctx) { - pthread_once(&g_ffmpeg_once, ffmpeg_init_once); + video_ffmpeg_init(); int ret = avformat_open_input(&ctx->ifmt, ctx->src, NULL, NULL); if (ret != 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_open_input failed: %s", vc_errstr(ret)); return -1; } @@ -607,7 +620,7 @@ int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const ch int video_extract_thumbnail(const char* src, const char* jpg_out, int max_w) { if (!src || !src[0] || !jpg_out || !jpg_out[0]) return -1; if (max_w <= 0) max_w = 320; - pthread_once(&g_ffmpeg_once, ffmpeg_init_once); + video_ffmpeg_init(); AVFormatContext* fmt = NULL; if (avformat_open_input(&fmt, src, NULL, NULL) != 0) { diff --git a/src/video/video.h b/src/video/video.h index 909c5329..c3c8241c 100644 --- a/src/video/video.h +++ b/src/video/video.h @@ -10,6 +10,10 @@ extern "C" { struct UASYNC; +/* Один раз подключает FFmpeg к общему логу; trace категории debug включает внутренние сообщения AMF. + * Можно вызывать из любого потока. В сборке без FFmpeg ничего не делает. */ +void video_ffmpeg_init(void); + /* ── целевой профиль: H.264 в MP4, не более 720p / 2 Mbit ── */ #define VIDEO_TARGET_MAX_WIDTH 1280 diff --git a/tools/chatgui/src/live_video_encoder.cpp b/tools/chatgui/src/live_video_encoder.cpp index f281e608..ed1a7ce1 100644 --- a/tools/chatgui/src/live_video_encoder.cpp +++ b/tools/chatgui/src/live_video_encoder.cpp @@ -8,6 +8,7 @@ extern "C" { #include "../../../src/stream/stream_media.h" #include "../../../src/call/call_video_bitstream.h" +#include "../../../src/video/video.h" #ifdef HAVE_FFMPEG #include #include @@ -143,6 +144,7 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { m->error = "Invalid video configuration"; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encoder invalid configuration"); return false; } #ifdef HAVE_FFMPEG + video_ffmpeg_init(); for (const auto& name : encoders(config.codec)) { if (!matchesBackend(name, backend)) continue; if (!config.backend.isEmpty() && name != config.backend.toLatin1()) continue; @@ -333,6 +335,21 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { } continue; } + if (name.endsWith("_amf")) { + QByteArray settings; + for (const char* option : {"usage", "rc", "quality", "enforce_hrd", "filler_data", "skip_frame", + "min_qp_i", "max_qp_i", "min_qp_p", "max_qp_p"}) { + int64_t value = 0; + int result = av_opt_get_int(c->priv_data, option, 0, &value); + if (!settings.isEmpty()) settings += ' '; + settings += QByteArray(option) + '=' + (result >= 0 ? QByteArray::number(value) : QByteArray("unavailable")); + } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, + "AMF effective settings encoder=%s %s target_bps=%lld max_bps=%lld vbv_bits=%d qmin=%d qmax=%d " + "time_base=%d/%d framerate=%d/%d; driver trace requires debug=trace", + name.constData(), settings.constData(), (long long)c->bit_rate, (long long)c->rc_max_rate, c->rc_buffer_size, + c->qmin, c->qmax, c->time_base.num, c->time_base.den, c->framerate.num, c->framerate.den); + } DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop encoder ready name=%s codec=%d size=%dx%d fps=%d bitrate=%d level=%d refs=%d vbv=%d profile_verified=%d forced_keys=%d", name.constData(), config.codec, config.width, config.height, config.fps, config.bitrate, @@ -378,9 +395,6 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList qPrintable(m->name), m->timestamps.size(), (unsigned long long)pts); return false; } bool forceKey = key && (m->forcedKeys || m->submittedFrames == 0); - if (key && !forceKey) DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, - "desktop AMF ignores forced key request; periodic GOP IDR remains active name=%s submitted=%lld", - qPrintable(m->name), (long long)m->submittedFrames); m->frame->pts = av_rescale_q(static_cast(pts), AVRational{1, 1000000}, m->codec->time_base); m->frame->pict_type = forceKey ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_NONE; if (forceKey) m->frame->flags |= AV_FRAME_FLAG_KEY; else m->frame->flags &= ~AV_FRAME_FLAG_KEY; @@ -420,7 +434,8 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList } encoded.pts = m->timestamps.front().second; m->timestamps.pop_front(); } - if (key) DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop requested KEY name=%s input_PTS=%llu output_PTS=%llu key=%d flags=%x bytes=%d", + if (key && (encoded.key || m->forcedKeys)) DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, + "desktop requested KEY name=%s input_PTS=%llu output_PTS=%llu key=%d flags=%x bytes=%d", qPrintable(m->name), (unsigned long long)pts, (unsigned long long)encoded.pts, encoded.key, packet->flags, packet->size); if (encoded.key) { const uint8_t* config = m->filter ? m->filter->par_out->extradata : m->codec->extradata; diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index 60150862..30796c5c 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -36,6 +36,7 @@ extern "C" { #include "mem.h" #include "sqlite3.h" #include "opus_codec.h" +#include "video/video.h" #ifdef HAVE_FFMPEG #include #include @@ -128,6 +129,13 @@ extern "C" void __wrap_ma_device_uninit(ma_device* device) { } #endif +#ifdef HAVE_FFMPEG +static QStringList ffmpegDiagnosticMessages; +static void captureFfmpegDiagnostic(int, const char*, const char* message) { + if (strstr(message, "UTUN_AMF_LOG_TEST")) ffmpegDiagnosticMessages.append(QString::fromUtf8(message)); +} +#endif + class StreamPublishTest : public QObject { Q_OBJECT UASYNC* ua = nullptr; @@ -183,6 +191,38 @@ private slots: sqlite3_finalize(statement); QVERIFY(group); QVERIFY(gui_stream_attach(inst)); uasync_mark_running(ua); connect(&poll, &QTimer::timeout, this, [this] { uasync_poll(ua, 0); }); poll.start(1); } + void amfDriverDiagnosticsRespectCategoryFilter() { +#ifdef HAVE_FFMPEG + video_ffmpeg_init(); + auto previousDebug = g_debug_config.category_levels[DEBUG_CATEGORY_DEBUG]; + auto previousVideo = g_debug_config.category_levels[DEBUG_CATEGORY_VIDEO]; + AVCodecContext* context = avcodec_alloc_context3(nullptr); QVERIFY(context); + auto cleanup = qScopeGuard([&] { + context->codec = nullptr; avcodec_free_context(&context); debug_set_log_hook(nullptr); + debug_set_category_level(DEBUG_CATEGORY_DEBUG, previousDebug); + debug_set_category_level(DEBUG_CATEGORY_VIDEO, previousVideo); ffmpegDiagnosticMessages.clear(); + }); + AVCodec codec{}; codec.name = "hevc_amf"; context->codec = &codec; + debug_set_log_hook(captureFfmpegDiagnostic); + debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_TRACE); + debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_WARN); + av_log(context, AV_LOG_DEBUG, "UTUN_AMF_LOG_TEST driver value=%d\n", 17); + QCOMPARE(ffmpegDiagnosticMessages.size(), 1); + QVERIFY(ffmpegDiagnosticMessages.front().contains("encoder=hevc_amf: UTUN_AMF_LOG_TEST driver value=17")); + codec.name = "libx264"; + av_log(context, AV_LOG_DEBUG, "UTUN_AMF_LOG_TEST unrelated encoder\n"); + av_log(nullptr, AV_LOG_DEBUG, "UTUN_AMF_LOG_TEST unrelated context\n"); + QCOMPARE(ffmpegDiagnosticMessages.size(), 1); + codec.name = "hevc_amf"; debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); + av_log(context, AV_LOG_DEBUG, "UTUN_AMF_LOG_TEST driver disabled\n"); + QCOMPARE(ffmpegDiagnosticMessages.size(), 1); + av_log(nullptr, AV_LOG_ERROR, "UTUN_AMF_LOG_TEST normal error\n"); + QCOMPARE(ffmpegDiagnosticMessages.size(), 2); + QVERIFY(ffmpegDiagnosticMessages.back().contains("normal error")); +#else + QSKIP("FFmpeg unavailable"); +#endif + } void captureSettingsSurviveReopening() { QString saved; QVariantList selectedMode;