Route desktop codec diagnostics through the shared log
Quiet capability probes, preserve runtime errors, and register the desktop portal application. Add logging and desktop launcher regression coverage.
This commit is contained in:
1 parent
9ba78c084b
commit
7c814f5cb9
17 files changed
+349
-44
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} "$<TARGET_FILE_DIR:vibechat>/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()
|
||||
|
||||
@@ -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 <QTransform>
|
||||
@@ -43,16 +43,17 @@ struct CallVideoDecoder::Impl {
|
||||
static AVPixelFormat getFormat(AVCodecContext* codec, const AVPixelFormat* formats) {
|
||||
auto* self = static_cast<Impl*>(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<bool>* 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<std::mutex> 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<bool>* 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<bool>* 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;
|
||||
}
|
||||
@@ -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 <QElapsedTimer>
|
||||
@@ -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<LiveVideoEncoder>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<bool>* 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<bool>* 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<bool>* running)
|
||||
limit.fps = std::max<unsigned>(limit.fps, fps);
|
||||
limit.pixels_per_second = std::max<unsigned>(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<bool>* 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);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "desktop_integration.h"
|
||||
#include "debug_config.h"
|
||||
#include <QDir>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Linux: описание приложения для desktop portal при запуске переносимой/devel-сборки.
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
// Создаёт отсутствующий org.utun.vibechat.desktop в XDG data applications.
|
||||
// executable — абсолютный путь к GUI; готовые системные/user записи не заменяет.
|
||||
// Возвращает false с предупреждением в логе при ошибке записи; вне Linux возвращает true.
|
||||
bool registerDesktopApplication(const QString& executable);
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "gui_log.h"
|
||||
#include "debug_config.h"
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <cstring>
|
||||
|
||||
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); }
|
||||
@@ -0,0 +1,6 @@
|
||||
// Диагностика Qt через общий лог ядра: уровни/категории и debug_file применяются как к остальным сообщениям.
|
||||
#pragma once
|
||||
#include <QtGlobal>
|
||||
|
||||
// Вызывать до создания QApplication; возвращает прежний Qt handler (для восстановления в тестах).
|
||||
QtMessageHandler installGuiLogHandler();
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "live_video_encoder.h"
|
||||
#include "debug_config.h"
|
||||
#include "video_codec_log.h"
|
||||
#include <QElapsedTimer>
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
@@ -96,7 +96,7 @@ bool verifiedProfile(const LiveVideoConfig& config, const QList<LiveEncodedFrame
|
||||
uint16_t(config.rotation), uint8_t(config.codec), uint8_t(config.level), uint32_t(config.bitrate),
|
||||
uint8_t(config.codec == STREAM_MEDIA_H264 ? CALL_VIDEO_CONSTRAINED_BASELINE : CALL_VIDEO_MAIN)};
|
||||
if (call_video_check_key(&format, reinterpret_cast<const uint8_t*>(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<LiveEncodedFrame
|
||||
bool valid = result >= 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<LiveEncodedFrame> 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<LiveEncodedFrame>& 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<uint64_t>(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<int> 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;
|
||||
|
||||
@@ -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 <cstdio>
|
||||
#include <cstring>
|
||||
@@ -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()),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<debug_level_t>(video_codec_log_level(level)); \
|
||||
if (debug_should_output(codecLevel, category)) \
|
||||
debug_output(codecLevel, category, __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__); \
|
||||
} while (0)
|
||||
@@ -0,0 +1,44 @@
|
||||
// Настоящий desktop parser/launcher: путь с пробелами, кавычками и спецсимволами сохраняется без shell-подстановок.
|
||||
#include <QtTest>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryDir>
|
||||
#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"
|
||||
@@ -0,0 +1,87 @@
|
||||
// Пробы codec не скрывают рабочие ошибки в соседнем потоке и восстанавливают уровни после завершения.
|
||||
#include <QtTest>
|
||||
#include <QFile>
|
||||
#include <QLoggingCategory>
|
||||
#include <QTemporaryDir>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "../src/video_codec_log.h"
|
||||
#include "../src/gui_log.h"
|
||||
extern "C" {
|
||||
#include <libavutil/log.h>
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct Message { int level; QString text; };
|
||||
std::mutex lock;
|
||||
std::vector<Message> messages;
|
||||
void capture(int level, const char*, const char* text) {
|
||||
std::lock_guard<std::mutex> 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"
|
||||
Reference in new issue
Block a user