diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index fc5f83ff..0101a64e 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -52,6 +52,13 @@ Autotools-сборка демона остаётся C-only (`.cpp` не ком присутствует и в демоне, аудиопроцессинг — только там, где есть GUI. Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`. +Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`). +Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug: +`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком; +рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию; +для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`. +Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal. + ## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd) diff --git a/src/video/video.c b/src/video/video.c index d226b39c..1411dab7 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -38,6 +38,34 @@ static void vc_active_set(int v) { __atomic_store_n(&g_transcode_active, v, __ int video_transcode_get_progress(void) { return vc_progress_get(); } 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; +#else +static __thread struct { unsigned depth, repeated; char last[1024]; } g_codec_probe; +#endif + +static void video_codec_probe_flush(void) { + if (g_codec_probe.repeated) + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "ffmpeg probe: repeated=%u message=%s", g_codec_probe.repeated, g_codec_probe.last); + g_codec_probe.repeated = 0; +} + +void video_codec_probe_begin(void) { + video_ffmpeg_init(); + ++g_codec_probe.depth; +} + +void video_codec_probe_end(void) { + if (!g_codec_probe.depth) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video codec probe end without begin"); return; } + if (--g_codec_probe.depth) return; + video_codec_probe_flush(); g_codec_probe.last[0] = '\0'; +} + +int video_codec_log_level(int level) { + return g_codec_probe.depth && level >= DEBUG_LEVEL_ERROR && level < DEBUG_LEVEL_DEBUG ? DEBUG_LEVEL_DEBUG : level; +} + #ifndef HAVE_FFMPEG /* ─── сборка без ffmpeg: стабы ─── */ @@ -92,6 +120,13 @@ static void video_av_log_cb(void* avcl, int level, const char* fmt, va_list vl) 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 (!len) return; + 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); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "ffmpeg probe source=%s level=%d: %s", avcl ? av_default_item_name(avcl) : "none", level, buf); + return; + } if (headers) { if (!strstr(buf, "Packet:") && !strstr(buf, "init_qp_minus26") && !strstr(buf, "slice_qp_delta") && !strstr(buf, "pic_width_in_luma_samples") && !strstr(buf, "pic_height_in_luma_samples") diff --git a/src/video/video.h b/src/video/video.h index ffcab504..999b8158 100644 --- a/src/video/video.h +++ b/src/video/video.h @@ -14,6 +14,13 @@ struct UASYNC; * trace категории debug включает AMF и заголовки HEVC. В сборке без FFmpeg ничего не делает. */ void video_ffmpeg_init(void); +/* Проба доступности codec: begin/end парные, вложенные, только для текущего потока. + * Ожидаемые отказы FFmpeg выводятся на debug с подсчётом повторов; рабочие потоки не затрагиваются. */ +void video_codec_probe_begin(void); +void video_codec_probe_end(void); +/* Возвращает debug вместо error/warn/info внутри пробы; level — DEBUG_LEVEL_* из debug_config.h. */ +int video_codec_log_level(int level); + /* ── целевой профиль: H.264 в MP4, не более 720p / 2 Mbit ── */ #define VIDEO_TARGET_MAX_WIDTH 1280 diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 00b60a4b..09b9ae24 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -144,6 +144,10 @@ target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS}) if(FFMPEG_FOUND) target_compile_definitions(chatgui_live_video PRIVATE HAVE_FFMPEG) + add_executable(test_video_diagnostics tests/test_video_diagnostics.cpp src/gui_log.cpp) + target_include_directories(test_video_diagnostics PRIVATE ${FFMPEG_INCLUDE_DIRS}) + target_link_libraries(test_video_diagnostics PRIVATE chatgui_live_video ${QT_TEST_LIB} ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) + add_test(NAME test_video_diagnostics COMMAND test_video_diagnostics) add_executable(test_video_frame_image tests/test_video_frame_image.cpp) target_compile_definitions(test_video_frame_image PRIVATE HAVE_FFMPEG) target_include_directories(test_video_frame_image PRIVATE ${FFMPEG_INCLUDE_DIRS}) @@ -266,6 +270,8 @@ set_target_properties(utun_voice PROPERTIES POSITION_INDEPENDENT_CODE ON) add_executable(vibechat WIN32 src/main.cpp + src/desktop_integration.cpp + src/gui_log.cpp src/single_instance.cpp src/mainwindow.cpp src/channellist.cpp @@ -377,6 +383,11 @@ add_custom_target(vibechat_licenses ALL COMMAND ${CMAKE_COMMAND} -E copy_if_different ${UTUN_LICENSE_FILES} "$/licenses" COMMENT "Copying uTun license documents") install(FILES ${UTUN_LICENSE_FILES} DESTINATION share/licenses/utun) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable(test_desktop_integration tests/test_desktop_integration.cpp src/desktop_integration.cpp) + target_link_libraries(test_desktop_integration PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun) + add_test(NAME test_desktop_integration COMMAND test_desktop_integration) +endif() if(FFMPEG_FOUND) target_compile_definitions(vibechat PRIVATE HAVE_FFMPEG) endif() diff --git a/tools/chatgui/src/call_video_decoder.cpp b/tools/chatgui/src/call_video_decoder.cpp index 09adcfa7..e619df46 100644 --- a/tools/chatgui/src/call_video_decoder.cpp +++ b/tools/chatgui/src/call_video_decoder.cpp @@ -1,6 +1,6 @@ #include "call_video_decoder.h" #include "video_frame_image.h" -#include "debug_config.h" +#include "video_codec_log.h" #include "../../../src/call/call_video_bitstream.h" #include "../../../src/call/call_video_playout.h" #include @@ -43,16 +43,17 @@ struct CallVideoDecoder::Impl { static AVPixelFormat getFormat(AVCodecContext* codec, const AVPixelFormat* formats) { auto* self = static_cast(codec->opaque); for (const auto* p = formats; *p != AV_PIX_FMT_NONE; ++p) if (*p == self->pixel) return *p; - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video GPU decoder pixel format unavailable expected=%d", self->pixel); + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, "video GPU decoder pixel format unavailable expected=%d", self->pixel); return AV_PIX_FMT_NONE; } int failed(const char* stage, int result) { error = QString("Video decoder %1: %2").arg(stage, ffError(result)); - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video decoder failed stage=%s backend=%s pending=%zu generation=%llu result=%d error=%s", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, "video decoder failed stage=%s backend=%s pending=%zu generation=%llu result=%d error=%s", stage, gpu ? av_hwdevice_get_type_name(DeviceType) : "cpu", pending.size(), (unsigned long long)format.generation, result, qPrintable(error)); return -1; } bool open(const call_video_format& f) { + video_ffmpeg_init(); format = f; const AVCodec* implementation = avcodec_find_decoder(f.codec == CALL_VIDEO_H265 ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264); codec = implementation ? avcodec_alloc_context3(implementation) : nullptr; @@ -84,7 +85,8 @@ struct CallVideoDecoder::Impl { int result = avcodec_open2(codec, implementation, nullptr); if (result < 0) { failed("open", result); return false; } error.clear(); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder opened codec=%u name=%s backend=%s threads=%d thread_type=%d level=%u generation=%llu", + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, + "video decoder opened codec=%u name=%s backend=%s threads=%d thread_type=%d level=%u generation=%llu", f.codec, implementation->name, gpu ? av_hwdevice_get_type_name(DeviceType) : "cpu", codec->thread_count, codec->active_thread_type, f.level, (unsigned long long)f.generation); return true; } @@ -97,7 +99,7 @@ unsigned CallVideoDecoder::pipelineFrames() const { return m->pipeline; } bool CallVideoDecoder::hardware() const { return m->gpu; } QString CallVideoDecoder::error() const { return m->error; } void CallVideoDecoder::useSoftware() { - DEBUG_WARN(DEBUG_CATEGORY_CALL, "video decoder switching to CPU after device failure pending=%zu; next input must be KEY", + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_CALL, "video decoder switching to CPU after device failure pending=%zu; next input must be KEY", m->pending.size()); close(); m->backend = Backend::SoftwareOnly; } @@ -155,7 +157,7 @@ int CallVideoDecoder::submit(const call_video_frame& f) { m->pending.push_back(f); m->pending.back().data = nullptr; return 1; #else - Q_UNUSED(f); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video decoder unavailable: no FFmpeg"); return -1; + Q_UNUSED(f); CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, "video decoder unavailable: no FFmpeg"); return -1; #endif } @@ -238,15 +240,16 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) static bool initialized = false; auto cancelled = [running] { if (!running || running->load()) return false; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capability probe cancelled by stop"); return true; + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "video decoder capability probe cancelled by stop"); return true; }; std::unique_lock guard(lock, std::defer_lock); while (!guard.try_lock()) { if (cancelled()) return {}; QThread::usleep(1000); } if (initialized) { - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capabilities cache hit wait_us=%lld", + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "video decoder capabilities cache hit wait_us=%lld", (long long)(call_video_playout_now_us() - started)); return caps; } + VideoCodecProbe diagnostics; call_video_caps probed{}; for (unsigned codec = 1; codec <= 2; ++codec) { call_video_limit limit{}; @@ -293,7 +296,7 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) if (valid && received < 8) QThread::usleep(1000); } valid = valid && received == 8 && outputBeforeEos; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder probe codec=%u profile=%u size=%ux%u fps=%u backend=%d " + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "video decoder probe codec=%u profile=%u size=%ux%u fps=%u backend=%d " "valid=%d inputs=%u outputs=%u pipeline=%u elapsed_us=%lld", codec, profile, w, h, fps, int(backend), valid, sent, received, decoder.pipelineFrames(), (long long)(call_video_playout_now_us() - start)); @@ -317,12 +320,18 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) } } } + unsigned gpu = 0; +#ifdef HAVE_FFMPEG + for (unsigned tier = 0; tier < 2; ++tier) for (unsigned rate = 0; rate < 3; ++rate) + gpu |= gpuProfiles[codec - 1][tier][rate].load(); +#endif + DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capabilities codec=%u available_profiles=%u gpu_profiles=%u", codec, limit.profiles, gpu); if (limit.profiles) { if (!call_video_limit_orientations(&limit, codec)) probed.decode[codec - 1] = limit; } } caps = probed; initialized = true; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capabilities verified elapsed_us=%lld", + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "video decoder capabilities verified elapsed_us=%lld", (long long)(call_video_playout_now_us() - started)); return caps; } diff --git a/tools/chatgui/src/call_video_encoder.cpp b/tools/chatgui/src/call_video_encoder.cpp index b849c616..55cd740b 100644 --- a/tools/chatgui/src/call_video_encoder.cpp +++ b/tools/chatgui/src/call_video_encoder.cpp @@ -1,5 +1,5 @@ #include "call_video_encoder.h" -#include "debug_config.h" +#include "video_codec_log.h" #include "../../../src/call/call_video_rate.h" #include "../../../src/call/call_video_bitstream.h" #include @@ -9,7 +9,7 @@ bool CallVideoEncoder::open(const call_video_format& format, uint32_t bitrate, c if (!call_video_format_valid(&format) || bitrate < CALL_VIDEO_RATE_MIN_BPS || bitrate > format.maximum_bps || (format.codec == CALL_VIDEO_H265 && format.profile != CALL_VIDEO_MAIN)) { m_error = "Invalid call video encoder configuration"; - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video open invalid generation=%llu codec=%u level=%u bitrate=%u cap=%u profile=%u", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, "call video open invalid generation=%llu codec=%u level=%u bitrate=%u cap=%u profile=%u", (unsigned long long)format.generation, format.codec, format.level, bitrate, format.maximum_bps, format.profile); return false; } LiveVideoConfig config; @@ -19,10 +19,11 @@ bool CallVideoEncoder::open(const call_video_format& format, uint32_t bitrate, c config.profile = format.codec == CALL_VIDEO_H264 ? LiveVideoProfile::H264ConstrainedBaseline : LiveVideoProfile::H265Main; auto candidate = std::make_unique(); if (!candidate->open(config, LiveVideoEncoder::Backend::Automatic)) { - m_error = candidate->error(); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call video open failed: %s", qPrintable(m_error)); return false; + m_error = candidate->error(); CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, + "call video open failed: %s", qPrintable(m_error)); return false; } close(); m_encoder = std::move(candidate); m_config = config; m_config.backend = m_encoder->name(); m_format = format; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video encoder opened generation=%llu codec=%d encoder=%s bitrate=%d level=%d", + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "call video encoder opened generation=%llu codec=%d encoder=%s bitrate=%d level=%d", (unsigned long long)format.generation, config.codec, qPrintable(m_config.backend), config.bitrate, config.level); return true; } diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index d2f570fc..c0a5ef83 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -3,6 +3,7 @@ #include "call_video_decoder.h" #include "call_video_wake.h" #include "call_video_queue.h" +#include "video_codec_log.h" #include "call_camera_capture.h" #include "../../../src/call/call_video_port.h" #include "../../../src/call/call_video_playout.h" @@ -270,6 +271,7 @@ call_video_caps CallVideoEngine::capabilities(const std::atomic* running) return verified; } DEBUG_INFO(DEBUG_CATEGORY_CALL, "video codec preparation begin; camera and network unused"); + VideoCodecProbe diagnostics; call_video_caps caps = CallVideoDecoder::capabilities(running); // Не связываем приём с наличием собственного encoder. for (unsigned codec = 1; codec <= 2; ++codec) { @@ -286,7 +288,7 @@ call_video_caps CallVideoEngine::capabilities(const std::atomic* running) probe.width = portrait ? size.height() : size.width(); probe.height = portrait ? size.width() : size.height(); CallVideoEncoder encoder; bothOrientations = encoder.open(probe, 1000000); - if (!bothOrientations) DEBUG_WARN(DEBUG_CATEGORY_CALL, "call codec probe unavailable codec=%u size=%ux%u: %s", + if (!bothOrientations) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call codec probe unavailable codec=%u size=%ux%u: %s", codec, probe.width, probe.height, qPrintable(encoder.error())); } if (bothOrientations) { @@ -295,7 +297,7 @@ call_video_caps CallVideoEngine::capabilities(const std::atomic* running) limit.fps = std::max(limit.fps, fps); limit.pixels_per_second = std::max(limit.pixels_per_second, size.width() * size.height() * fps); limit.maximum_bps = 4000000; limit.profiles = probe.profile; limit.level = std::max(limit.level, probe.level); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "call encoder probe codec=%u size=%dx%d fps=%u level=%u verified=1", + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call encoder probe codec=%u size=%dx%d fps=%u level=%u verified=1", codec, size.width(), size.height(), fps, probe.level); } } @@ -304,6 +306,10 @@ call_video_caps CallVideoEngine::capabilities(const std::atomic* running) } if (cancelled()) return {}; verified = caps; initialized = true; + if (!caps.encode[0].profiles && !caps.encode[1].profiles) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "video codec preparation: no usable encoder; details require call=debug,video=debug"); + if (!caps.decode[0].profiles && !caps.decode[1].profiles) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "video codec preparation: no usable decoder; details require call=debug,video=debug"); DEBUG_INFO(DEBUG_CATEGORY_CALL, "video codec preparation complete elapsed_us=%lld encode=%u/%u decode=%u/%u", (long long)(nowUs() - started), caps.encode[0].profiles, caps.encode[1].profiles, caps.decode[0].profiles, caps.decode[1].profiles); diff --git a/tools/chatgui/src/desktop_integration.cpp b/tools/chatgui/src/desktop_integration.cpp new file mode 100644 index 00000000..7fef8037 --- /dev/null +++ b/tools/chatgui/src/desktop_integration.cpp @@ -0,0 +1,38 @@ +#include "desktop_integration.h" +#include "debug_config.h" +#include +#include +#include + +bool registerDesktopApplication(const QString& executable) { +#ifdef Q_OS_LINUX + const QString relative = QStringLiteral("applications/org.utun.vibechat.desktop"); + if (!QStandardPaths::locate(QStandardPaths::GenericDataLocation, relative).isEmpty()) return true; + const QString root = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + const QString path = root + '/' + relative; + if (root.isEmpty() || !QDir().mkpath(root + "/applications")) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Desktop registration: cannot create applications directory in %s", qPrintable(root)); + return false; + } + // Exec сначала разбирается как desktop string, затем как командная строка; shell не используется. + QString command; + for (QChar c : executable) { + if (c == '%') command += "%%"; + else { if (c == '\\' || c == '"' || c == '$' || c == '`') command += '\\'; command += c; } + } + command = '"' + command + '"'; + command.replace("\\", "\\\\"); command.replace('\n', "\\n"); command.replace('\r', "\\r"); command.replace('\t', "\\t"); + // GLib проверяет executable до раскрытия %%; env оставляет путь аргументом и корректно обрабатывает '%' в имени. + const QByteArray content = QString("[Desktop Entry]\nType=Application\nName=VibeChat\nExec=/usr/bin/env %1\n" + "Icon=applications-internet\nTerminal=false\nCategories=Network;InstantMessaging;\n").arg(command).toUtf8(); + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(content) != content.size() || !file.commit()) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Desktop registration failed path=%s: %s", qPrintable(path), qPrintable(file.errorString())); + return false; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Desktop application registered path=%s", qPrintable(path)); +#else + Q_UNUSED(executable); +#endif + return true; +} diff --git a/tools/chatgui/src/desktop_integration.h b/tools/chatgui/src/desktop_integration.h new file mode 100644 index 00000000..c608d5d7 --- /dev/null +++ b/tools/chatgui/src/desktop_integration.h @@ -0,0 +1,8 @@ +// Linux: описание приложения для desktop portal при запуске переносимой/devel-сборки. +#pragma once +#include + +// Создаёт отсутствующий org.utun.vibechat.desktop в XDG data applications. +// executable — абсолютный путь к GUI; готовые системные/user записи не заменяет. +// Возвращает false с предупреждением в логе при ошибке записи; вне Linux возвращает true. +bool registerDesktopApplication(const QString& executable); diff --git a/tools/chatgui/src/gui_log.cpp b/tools/chatgui/src/gui_log.cpp new file mode 100644 index 00000000..3537e112 --- /dev/null +++ b/tools/chatgui/src/gui_log.cpp @@ -0,0 +1,17 @@ +#include "gui_log.h" +#include "debug_config.h" +#include +#include +#include + +static void guiLog(QtMsgType type, const QMessageLogContext& context, const QString& message) { + const debug_level_t level = type == QtDebugMsg ? DEBUG_LEVEL_DEBUG : type == QtInfoMsg ? DEBUG_LEVEL_INFO + : type == QtWarningMsg ? DEBUG_LEVEL_WARN : DEBUG_LEVEL_ERROR; + const char* name = context.category ? context.category : "default"; + const int category = !strncmp(name, "qt.multimedia", 13) ? DEBUG_CATEGORY_VIDEO : DEBUG_CATEGORY_GENERAL; + if (!debug_should_output(level, category)) return; + const auto text = message.toUtf8(); + debug_output(level, category, context.function, context.file, context.line, "Qt[%s]: %s", name, text.constData()); +} + +QtMessageHandler installGuiLogHandler() { return qInstallMessageHandler(guiLog); } diff --git a/tools/chatgui/src/gui_log.h b/tools/chatgui/src/gui_log.h new file mode 100644 index 00000000..a793617b --- /dev/null +++ b/tools/chatgui/src/gui_log.h @@ -0,0 +1,6 @@ +// Диагностика Qt через общий лог ядра: уровни/категории и debug_file применяются как к остальным сообщениям. +#pragma once +#include + +// Вызывать до создания QApplication; возвращает прежний Qt handler (для восстановления в тестах). +QtMessageHandler installGuiLogHandler(); diff --git a/tools/chatgui/src/live_video_encoder.cpp b/tools/chatgui/src/live_video_encoder.cpp index 8a170682..d2c222dd 100644 --- a/tools/chatgui/src/live_video_encoder.cpp +++ b/tools/chatgui/src/live_video_encoder.cpp @@ -1,5 +1,5 @@ #include "live_video_encoder.h" -#include "debug_config.h" +#include "video_codec_log.h" #include #include #include @@ -96,7 +96,7 @@ bool verifiedProfile(const LiveVideoConfig& config, const QList(data.constData()), data.size())) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "live call KEY verification failed codec=%d size=%dx%d level=%d bytes=%lld", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "live call KEY verification failed codec=%d size=%dx%d level=%d bytes=%lld", config.codec, config.width, config.height, config.level, (long long)data.size()); return false; } @@ -126,7 +126,7 @@ bool verifiedProfile(const LiveVideoConfig& config, const QList= 0 && idr && mainTier && decoder->profile == profile && decoder->level == config.level && !decoder->has_b_frames && decoded->format == AV_PIX_FMT_YUV420P && decoded->width == config.width && decoded->height == config.height; - if (!valid) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, + if (!valid) CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "live profile verification failed codec=%d result=%d idr=%d main_tier=%d profile=%d/%d level=%d/%d pixel=%d B=%d", config.codec, result, idr, mainTier, decoder ? decoder->profile : -1, profile, decoder ? decoder->level : -1, config.level, decoded ? decoded->format : -1, decoder ? decoder->has_b_frames : -1); @@ -146,7 +146,8 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { || (constrained && (config.level <= 0 || config.level > 255 || config.references != 1 || !config.vbvBits || !((config.codec == 1 && config.profile == LiveVideoProfile::H264ConstrainedBaseline) || (config.codec == 2 && config.profile == LiveVideoProfile::H265Main))))) { - m->error = "Invalid video configuration"; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encoder invalid configuration"); return false; + m->error = "Invalid video configuration"; CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, + "desktop encoder invalid configuration"); return false; } #ifdef HAVE_FFMPEG video_ffmpeg_init(); @@ -198,7 +199,7 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { const int workers = 4; QByteArray params = name == "libx264" ? "threads=4:sliced-threads=1:scenecut=0:open-gop=0:repeat-headers=0" : "bframes=0:rc-lookahead=0:open-gop=0:frame-threads=1:wpp=1:log-level=error:pools=" + QByteArray::number(workers); - DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "live CPU parallelism encoder=%s workers=%d mode=%s cpu_count=%d", + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_VIDEO, "live CPU parallelism encoder=%s workers=%d mode=%s cpu_count=%d", name.constData(), workers, name == "libx264" ? "sliced" : "wpp/frame-threads=1", av_cpu_count()); if (constrained) { av_dict_set(&options, "profile", name == "libx264" ? "baseline" : "main", 0); @@ -237,7 +238,7 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { av_dict_set(&options, "profile", config.codec == STREAM_MEDIA_H264 ? "constrained_baseline" : "main", 0); if (config.codec == STREAM_MEDIA_H265) av_dict_set(&options, "profile_tier", "main", 0); } - DEBUG_INFO(DEBUG_CATEGORY_VIDEO, + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_VIDEO, "desktop AMF low latency settings name=%s usage=%s quality=speed async_depth=%s forced_idr=%d", name.constData(), usage, asyncDepth ? "1" : "driver", m->forcedKeys); } else if (name == "libvpx-vp9" || name == "libaom-av1") { @@ -295,7 +296,7 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { if (frame.pts != pts) continue; if (!frame.key) { m->error = "Encoder did not honor the requested key frame"; - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "desktop encoder probe returned non-key name=%s pts=%llu attempt=%d received=%d", name.constData(), (unsigned long long)pts, attempt, received); return false; @@ -303,7 +304,7 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { verified = {frame}; if (constrained && !verifiedProfile(config, verified)) { m->error = "Encoder output violates the call bitstream contract"; - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "call GPU probe rejected KEY name=%s pts=%llu attempt=%d", + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "call GPU probe rejected KEY name=%s pts=%llu attempt=%d", name.constData(), (unsigned long long)pts, attempt); return false; } @@ -315,26 +316,26 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { output.clear(); } m->error = "Encoder did not produce a key frame within the startup probe limit"; - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "desktop encoder probe key delayed beyond limit name=%s pts=%llu max_delay_frames=%d received=%d", name.constData(), (unsigned long long)pts, maxDelayFrames, received); return false; }; QList output; if (!probeKey(0, output)) { - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop encoder initial KEY probe failed name=%s error=%s", + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "desktop encoder initial KEY probe failed name=%s error=%s", name.constData(), qPrintable(m->error)); break; } if (!m->forcedKeys) { - DEBUG_INFO(DEBUG_CATEGORY_VIDEO, + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_VIDEO, "desktop AMF uses periodic IDR name=%s gop_frames=%d forced_key_requests=disabled", name.constData(), c->gop_size); } else { output.clear(); const uint64_t forcedKeyPts = (1000000 / config.fps) * (maxDelayFrames + 2); if (!probeKey(forcedKeyPts, output)) { - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop encoder forced KEY probe failed name=%s error=%s", + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "desktop encoder forced KEY probe failed name=%s error=%s", name.constData(), qPrintable(m->error)); break; } @@ -350,13 +351,13 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { if (!settings.isEmpty()) settings += ' '; settings += QByteArray(option) + '=' + (result >= 0 ? QByteArray::number(value) : QByteArray("unavailable")); } - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, + CODEC_LOG(DEBUG_LEVEL_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, + CODEC_LOG(DEBUG_LEVEL_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 max_qp=%d dynamic_qp=%d threads=%d thread_type=%d", name.constData(), config.codec, config.width, config.height, config.fps, config.bitrate, @@ -369,14 +370,14 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) { #else m->error = "FFmpeg is unavailable"; #endif - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encoder open failed codec=%d backend=%d: %s", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "desktop encoder open failed codec=%d backend=%d: %s", config.codec, int(backend), qPrintable(m->error)); return false; } bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList& output, bool finish) { #ifdef HAVE_FFMPEG if (!m->codec || !m->frame || pts > INT64_MAX || (finish && !image.isNull())) { - m->error = "Invalid video frame"; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encode invalid frame/state"); return false; + m->error = "Invalid video frame"; CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "desktop encode invalid frame/state"); return false; } QElapsedTimer timer; timer.start(); if (!m->reportTimer.isValid()) m->reportTimer.start(); @@ -418,7 +419,7 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList } if (constrained && m->timestamps.size() >= 8) { m->error = "GPU encoder exceeded the bounded input pipeline"; - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "call encoder stalled name=%s pending=%zu pts=%llu", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "call encoder stalled name=%s pending=%zu pts=%llu", qPrintable(m->name), m->timestamps.size(), (unsigned long long)pts); return false; } bool forceKey = key && (m->forcedKeys || m->submittedFrames == 0); @@ -464,7 +465,7 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList m->headers->time_base_in = m->codec->time_base; diagnostic = av_bsf_init(m->headers); } - if (diagnostic >= 0) DEBUG_INFO(DEBUG_CATEGORY_DEBUG, + if (diagnostic >= 0) CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_DEBUG, "HEVC KEY header diagnostics enabled name=%s; slice_QP=26+init_qp_minus26+slice_qp_delta", qPrintable(m->name)); } @@ -475,7 +476,7 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList av_packet_free(©); } if (diagnostic < 0) { - DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "HEVC header diagnostics disabled name=%s pts=%lld error=%s", + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_VIDEO, "HEVC header diagnostics disabled name=%s pts=%lld error=%s", qPrintable(m->name), (long long)packet->pts, qPrintable(ffError(diagnostic))); av_bsf_free(&m->headers); } @@ -484,7 +485,7 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList encoded.pts = packet->pts >= 0 ? static_cast(packet->pts) : pts; if (constrained) { if (m->timestamps.empty() || packet->pts != m->timestamps.front().first) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "call GPU output PTS mismatch name=%s packet=%lld expected=%lld pending=%zu", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "call GPU output PTS mismatch name=%s packet=%lld expected=%lld pending=%zu", qPrintable(m->name), (long long)packet->pts, (long long)(m->timestamps.empty() ? -1 : m->timestamps.front().first), m->timestamps.size()); result = AVERROR_INVALIDDATA; break; @@ -519,7 +520,7 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList auto& frameMax = producedKey ? m->keyMax : m->deltaMax; frameMax = std::max(frameMax, totalUs); if (totalUs > 100000 && !m->slowReported) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_DEBUG, "video encoder slow frame name=%s size=%dx%d pts=%llu key=%d requested_key=%d " "total_us=%lld prepare_us=%lld submit_us=%lld drain_us=%lld", qPrintable(m->name), m->config.width, m->config.height, (unsigned long long)pts, producedKey, key, @@ -542,12 +543,12 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList qPrintable(m->name), (unsigned long long)pts); if (finish && !m->timestamps.empty()) { m->error = "Encoder did not drain all accepted inputs"; - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "call encoder drain incomplete name=%s pending=%zu", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "call encoder drain incomplete name=%s pending=%zu", qPrintable(m->name), m->timestamps.size()); return false; } return true; } - if (result < 0) { m->error = ffError(result); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encode failed name=%s PTS=%llu: %s", + if (result < 0) { m->error = ffError(result); CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "desktop encode failed name=%s PTS=%llu: %s", qPrintable(m->name), (unsigned long long)pts, qPrintable(m->error)); return false; } return true; #else @@ -571,13 +572,14 @@ QList LiveVideoEncoder::available(Backend backend) { } bool LiveVideoEncoder::setBitrate(int bitrate) { - if (bitrate < 64000 || bitrate > 16000000) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop live bitrate invalid value=%d", bitrate); return false; } + if (bitrate < 64000 || bitrate > 16000000) { CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, + "desktop live bitrate invalid value=%d", bitrate); return false; } #ifdef HAVE_FFMPEG // libx264 перечитывает эти три значения через reconfig_encoder при подаче следующего кадра. if (m->codec && m->name == "libx264" && m->config.profile == LiveVideoProfile::Automatic) { m->codec->bit_rate = m->codec->rc_max_rate = bitrate; m->codec->rc_buffer_size = m->config.vbvBits ? m->config.vbvBits : bitrate / 4; m->config.bitrate = bitrate; - DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop encoder bitrate updated in place name=libx264 bitrate=%d", bitrate); return true; + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_VIDEO, "desktop encoder bitrate updated in place name=libx264 bitrate=%d", bitrate); return true; } #endif DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop bitrate needs encoder replacement name=%s", qPrintable(m->name)); return false; @@ -596,13 +598,14 @@ bool LiveVideoEncoder::canSetMaxQp() const { bool LiveVideoEncoder::setMaxQp(int qp) { if (qp < 0 || qp > 51 || !canSetMaxQp()) { m->error = "Live QP adjustment is unavailable"; - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop live QP ceiling unavailable name=%s qp=%d", qPrintable(m->name), qp); return false; + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, + "desktop live QP ceiling unavailable name=%s qp=%d", qPrintable(m->name), qp); return false; } #ifdef HAVE_FFMPEG int result = m->name.endsWith("_amf") ? av_opt_set_int(m->codec->priv_data, "utun_qp_max", qp, 0) : 0; if (result < 0) { m->error = ffError(result); - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop live QP ceiling failed name=%s qp=%d error=%s", + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_VIDEO, "desktop live QP ceiling failed name=%s qp=%d error=%s", qPrintable(m->name), qp, qPrintable(m->error)); return false; } m->codec->qmin = 0; m->codec->qmax = qp; diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index f5859843..da40c6bf 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -21,6 +21,8 @@ #include "call_camera_capture.h" #include "call_video_engine.h" #include "single_instance.h" +#include "desktop_integration.h" +#include "gui_log.h" #include "config_parser.h" #include #include @@ -112,10 +114,13 @@ int main(int argc, char *argv[]) { debug_config_init(); /* До чтения конфига видны запуск и IPC; затем уровень заменяет настройка профиля. */ debug_set_level(DEBUG_LEVEL_INFO); + installGuiLogHandler(); #ifdef _WIN32 const bool consoleAttached = attach_parent_console(); #endif #ifdef Q_OS_LINUX + /* libva пишет INFO напрямую в stderr; ошибки сохраняем, явную настройку пользователя не заменяем. */ + if (!qEnvironmentVariableIsSet("LIBVA_MESSAGING_LEVEL")) qputenv("LIBVA_MESSAGING_LEVEL", "1"); /* На Wayland композитор владеет позицией окна (move() игнорируется) — * для полноценного ручного move/resize с пропорцией запускаемся через XWayland. */ if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) @@ -125,6 +130,7 @@ int main(int argc, char *argv[]) { app.setApplicationName("vibechat"); app.setApplicationVersion(QStringLiteral(VIBECHAT_VERSION)); app.setDesktopFileName("org.utun.vibechat"); + registerDesktopApplication(QCoreApplication::applicationFilePath()); app.setWindowIcon(QIcon(":/icons/app_icon.png")); app.setQuitOnLastWindowClosed(false); const bool minimized = app.arguments().contains(QStringLiteral("--minimized")); @@ -332,6 +338,7 @@ int main(int argc, char *argv[]) { QString lvl = cfg.debugLevel().trimmed(); if (lvl.isEmpty()) lvl = QStringLiteral("info"); debug_apply_global_level(lvl.toUtf8().constData()); + if (!cfg.debugCategories().trimmed().isEmpty()) debug_parse_config(cfg.debugCategories().toUtf8().constData()); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "vibechat: version=%s", qPrintable(app.applicationVersion())); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI dark palette: window=%s base=%s alternate=%s button=%s light=%s", qPrintable(p.color(QPalette::Window).name()), qPrintable(p.color(QPalette::Base).name()), diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 2b1b8a8d..12a81bad 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -509,7 +509,7 @@ void MainWindow::setupBridgeCallbacks() { gui_bridge_set_member_updated_cb(onMemberUpdatedCallback); gui_bridge_set_member_removed_cb(onMemberRemovedCallback); gui_bridge_set_my_node_id_cb([](uint64_t nid) { - qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid); + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid); if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid); if (AvatarManager::instance()) AvatarManager::instance()->initialize(); }); diff --git a/tools/chatgui/src/video_codec_log.h b/tools/chatgui/src/video_codec_log.h new file mode 100644 index 00000000..e1b04f7c --- /dev/null +++ b/tools/chatgui/src/video_codec_log.h @@ -0,0 +1,19 @@ +// Диагностика codec: scoped-проба понижает ожидаемые отказы до debug только в своём потоке. +// Создать VideoCodecProbe на время проверки; CODEC_LOG сохраняет обычный уровень вне неё. +#pragma once +#include "../../../src/video/video.h" +#include "debug_config.h" + +class VideoCodecProbe { +public: + VideoCodecProbe() { video_codec_probe_begin(); } + ~VideoCodecProbe() { video_codec_probe_end(); } + VideoCodecProbe(const VideoCodecProbe&) = delete; + VideoCodecProbe& operator=(const VideoCodecProbe&) = delete; +}; + +#define CODEC_LOG(level, category, ...) do { \ + const auto codecLevel = static_cast(video_codec_log_level(level)); \ + if (debug_should_output(codecLevel, category)) \ + debug_output(codecLevel, category, __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__); \ +} while (0) diff --git a/tools/chatgui/tests/test_desktop_integration.cpp b/tools/chatgui/tests/test_desktop_integration.cpp new file mode 100644 index 00000000..9fbc62bd --- /dev/null +++ b/tools/chatgui/tests/test_desktop_integration.cpp @@ -0,0 +1,44 @@ +// Настоящий desktop parser/launcher: путь с пробелами, кавычками и спецсимволами сохраняется без shell-подстановок. +#include +#include +#include +#include +#include +#include +#include "../src/desktop_integration.h" +#include "debug_config.h" + +class DesktopTest : public QObject { + Q_OBJECT +private slots: + void registrationAndLaunch() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + qputenv("XDG_DATA_HOME", directory.path().toUtf8()); + qputenv("XDG_DATA_DIRS", directory.filePath("system-data").toUtf8()); + const QString executable = directory.filePath("space quote\" dollar$ backtick` percent% slash\\-тест"); + QFile script(executable); QVERIFY(script.open(QIODevice::WriteOnly)); + script.write("#!/bin/sh\nprintf '%s' \"$0\" > \"$DESKTOP_RESULT\"\n"); script.close(); + QVERIFY(script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)); + QVERIFY(registerDesktopApplication(executable)); + const QString path = directory.filePath("applications/org.utun.vibechat.desktop"); + QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); const auto original = file.readAll(); file.close(); + QVERIFY(registerDesktopApplication("/must/not/replace/existing/entry")); + QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), original); file.close(); + const auto gio = QStandardPaths::findExecutable("gio"); + if (gio.isEmpty()) QSKIP("gio unavailable: desktop entry created; launcher validation needs GLib tools"); + const QString output = directory.filePath("result"); qputenv("DESKTOP_RESULT", output.toUtf8()); + QProcess process; process.start(gio, {"launch", path}); QVERIFY(process.waitForFinished(5000)); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); + QVERIFY2(process.exitCode() == 0, (process.readAllStandardError() + "\nentry:\n" + original).constData()); + QFile result(output); QTRY_VERIFY(result.exists()); QVERIFY(result.open(QIODevice::ReadOnly)); + QCOMPARE(QString::fromUtf8(result.readAll()), executable); + } + void registrationFailure() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + qputenv("XDG_DATA_HOME", directory.path().toUtf8()); + QFile obstacle(directory.filePath("applications")); QVERIFY(obstacle.open(QIODevice::WriteOnly)); obstacle.close(); + QVERIFY(!registerDesktopApplication("/example/vibechat")); + } +}; +QTEST_GUILESS_MAIN(DesktopTest) +#include "test_desktop_integration.moc" diff --git a/tools/chatgui/tests/test_video_diagnostics.cpp b/tools/chatgui/tests/test_video_diagnostics.cpp new file mode 100644 index 00000000..87220ae7 --- /dev/null +++ b/tools/chatgui/tests/test_video_diagnostics.cpp @@ -0,0 +1,87 @@ +// Пробы codec не скрывают рабочие ошибки в соседнем потоке и восстанавливают уровни после завершения. +#include +#include +#include +#include +#include +#include +#include +#include "../src/video_codec_log.h" +#include "../src/gui_log.h" +extern "C" { +#include +} + +namespace { +struct Message { int level; QString text; }; +std::mutex lock; +std::vector messages; +void capture(int level, const char*, const char* text) { + std::lock_guard guard(lock); messages.push_back({level, QString::fromUtf8(text)}); +} +} + +class DiagnosticsTest : public QObject { + Q_OBJECT +private slots: + void init() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); debug_enable_console(0); + messages.clear(); debug_set_log_hook(capture); video_ffmpeg_init(); + } + void cleanup() { debug_set_log_hook(nullptr); } + void probeLevelsRepeatsAndConcurrentErrors() { + { + VideoCodecProbe probe; + av_log(nullptr, AV_LOG_ERROR, "GPU unavailable\n"); + av_log(nullptr, AV_LOG_ERROR, "GPU unavailable\n"); + { + VideoCodecProbe nested; + av_log(nullptr, AV_LOG_ERROR, "GPU unavailable\n"); + CODEC_LOG(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_CALL, "expected probe failure"); + } + std::thread playback([] { av_log(nullptr, AV_LOG_ERROR, "live decoder failure\n"); }); playback.join(); + QCOMPARE(video_codec_log_level(DEBUG_LEVEL_WARN), int(DEBUG_LEVEL_DEBUG)); + } + av_log(nullptr, AV_LOG_ERROR, "runtime failure after probe\n"); + QCOMPARE(messages.size(), size_t(5)); + QCOMPARE(messages[0].level, int(DEBUG_LEVEL_DEBUG)); QVERIFY(messages[0].text.contains("GPU unavailable")); + QCOMPARE(messages[1].level, int(DEBUG_LEVEL_DEBUG)); QVERIFY(messages[1].text.contains("expected probe failure")); + QCOMPARE(messages[2].level, int(DEBUG_LEVEL_ERROR)); QVERIFY(messages[2].text.contains("live decoder failure")); + QCOMPARE(messages[3].level, int(DEBUG_LEVEL_DEBUG)); QVERIFY(messages[3].text.contains("repeated=2")); + QCOMPARE(messages[4].level, int(DEBUG_LEVEL_ERROR)); QVERIFY(messages[4].text.contains("runtime failure after probe")); + } + void infoLevelSkipsProbeDetails() { + debug_set_level(DEBUG_LEVEL_INFO); + { + VideoCodecProbe probe; + av_log(nullptr, AV_LOG_ERROR, "unsupported hardware mode\n"); + av_log(nullptr, AV_LOG_ERROR, "unsupported hardware mode\n"); + CODEC_LOG(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_CALL, "per-mode probe details"); + } + QVERIFY(messages.empty()); + CODEC_LOG(DEBUG_LEVEL_WARN, DEBUG_CATEGORY_CALL, "actual playback warning"); + QCOMPARE(messages.size(), size_t(1)); QCOMPARE(messages[0].level, int(DEBUG_LEVEL_WARN)); + } + void restoresFfmpegCallback() { + av_log_set_callback(av_log_default_callback); + video_ffmpeg_init(); + av_log(nullptr, AV_LOG_ERROR, "restored callback\n"); + QCOMPARE(messages.size(), size_t(1)); QVERIFY(messages[0].text.contains("restored callback")); + } + void qtAndFfmpegUseCommonFile() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const auto path = directory.filePath("chatgui.log"); debug_enable_file_output(path.toUtf8().constData(), 0); + const auto previous = installGuiLogHandler(); + const QLoggingCategory category("qt.multimedia.test"); + qWarning("portal diagnostic"); + qCWarning(category, "codec diagnostic"); + av_log(nullptr, AV_LOG_ERROR, "FFmpeg diagnostic\n"); + qInstallMessageHandler(previous); debug_disable_file_output(); + QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); const auto text = file.readAll(); + QVERIFY(text.contains("[WARN] [general]")); QVERIFY(text.contains("Qt[default]: portal diagnostic")); + QVERIFY(text.contains("[WARN] [video]")); QVERIFY(text.contains("Qt[qt.multimedia.test]: codec diagnostic")); + QVERIFY(text.contains("[ERROR] [video]")); QVERIFY(text.contains("ffmpeg: FFmpeg diagnostic")); + } +}; +QTEST_GUILESS_MAIN(DiagnosticsTest) +#include "test_video_diagnostics.moc"