From fd73fae77f00ced74fcfa4a7513f87ddf784ead1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 01:30:06 +0200 Subject: [PATCH] Keep stream capture failures and cursor snapshots tied to their source --- tools/chatgui/src/stream_publish_engine.cpp | 28 +++++++++++++------ tools/chatgui/src/stream_screen_cursor.h | 10 +++++-- .../chatgui/src/stream_screen_cursor_win.cpp | 28 +++++++++---------- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index 03519728..06d3fb0d 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -502,6 +502,9 @@ void StreamPublishEngine::imageLoop() { #ifdef HAVE_STREAM_CAPTURE for (;;) { QVideoFrame frame; uint64_t generation; int64_t pts; bool drawCursor; +#ifdef UTUN_STREAM_WIN_CURSOR + std::shared_ptr cursor; +#endif m->imageProgress("wait-frame"); { std::unique_lock lock(m->lock); @@ -509,6 +512,10 @@ void StreamPublishEngine::imageLoop() { if (m->imageStop) return; frame = std::move(m->imageInput); generation = m->imageGeneration; pts = m->imagePts; drawCursor = m->imageDrawCursor; m->imagePending = false; + if (!m->running || generation != m->sourceGeneration) continue; +#ifdef UTUN_STREAM_WIN_CURSOR + cursor = m->cursor; +#endif } int64_t convertAt = monotonicUs(); m->imageProgress("convert-frame"); @@ -520,25 +527,30 @@ void StreamPublishEngine::imageLoop() { if (image.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed generation=%llu format=%d size=%dx%d", (unsigned long long)generation, frame.pixelFormat(), frame.width(), frame.height()); - QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Video frame conversion failed")); }); + QMetaObject::invokeMethod(this, [this, generation] { + if (generation == m->sourceGeneration) sourceFailed(m->generation, text("Video frame conversion failed")); + }); continue; } #ifdef UTUN_STREAM_WIN_CURSOR - std::shared_ptr cursor; - { std::lock_guard guard(m->lock); cursor = m->cursor; } - if (drawCursor && cursor && !cursor->draw(image)) { - QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Could not capture the mouse cursor")); }); + if (drawCursor && (!cursor || !cursor->draw(image))) { + if (!cursor) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor missing for captured frame generation=%llu", (unsigned long long)generation); + QMetaObject::invokeMethod(this, [this, generation] { + if (generation == m->sourceGeneration) sourceFailed(m->generation, text("Could not capture the mouse cursor")); + }); continue; } - QVideoFrame preview = previewFrameFromImage(image); - if (!preview.isValid()) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop preview frame unavailable generation=%llu", - (unsigned long long)generation); #endif #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) if (frame.rotation() != QtVideo::Rotation::None) image = image.transformed(QTransform().rotate(static_cast(frame.rotation()))); if (frame.mirrored()) image = image.transformed(QTransform().scale(-1, 1)); #else if (frame.rotationAngle()) image = image.transformed(QTransform().rotate(frame.rotationAngle())); +#endif +#ifdef UTUN_STREAM_WIN_CURSOR + QVideoFrame preview = previewFrameFromImage(image); + if (!preview.isValid()) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop preview frame unavailable generation=%llu", + (unsigned long long)generation); #endif m->imageProgress("publish-image"); { diff --git a/tools/chatgui/src/stream_screen_cursor.h b/tools/chatgui/src/stream_screen_cursor.h index 2031a953..f8d011ad 100644 --- a/tools/chatgui/src/stream_screen_cursor.h +++ b/tools/chatgui/src/stream_screen_cursor.h @@ -1,5 +1,6 @@ -/* Системный курсор Windows/X11 накладывается на QScreen::grabWindow до масштабирования. - * Только GUI-поток. Положение, hotspot и bitmap используются в физических пикселях выбранного монитора. */ +/* Системный курсор накладывается до масштабирования. Создаётся в GUI: X11 draw остаётся в GUI, + * Windows draw работает в image worker через сохранённый HMONITOR, без обращения к QScreen. + * Положение, hotspot и bitmap используют физические пиксели выбранного монитора. */ #pragma once #include #include @@ -14,8 +15,11 @@ public: ~StreamScreenCursor(); bool draw(QImage& frame); private: +#ifdef Q_OS_WIN + void* m_monitor = nullptr; + QString m_screenName; +#else QPointer m_screen; -#ifndef Q_OS_WIN QWindow m_origin; struct _XDisplay* m_display = nullptr; #endif diff --git a/tools/chatgui/src/stream_screen_cursor_win.cpp b/tools/chatgui/src/stream_screen_cursor_win.cpp index e9f8cfa6..ccb1d729 100644 --- a/tools/chatgui/src/stream_screen_cursor_win.cpp +++ b/tools/chatgui/src/stream_screen_cursor_win.cpp @@ -6,11 +6,16 @@ #include #include -StreamScreenCursor::StreamScreenCursor(QScreen* screen) : m_screen(screen) { +StreamScreenCursor::StreamScreenCursor(QScreen* screen) { if (!screen) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: Windows screen unavailable"); return; } + m_screenName = screen->name(); + auto* native = screen->nativeInterface(); + if (!native || !(m_monitor = native->handle())) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: native monitor unavailable screen=%s", qPrintable(m_screenName)); return; + } m_ready = true; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop cursor opened screen=%s scale=%.3f backend=Win32", - qPrintable(screen->name()), screen->devicePixelRatio()); + qPrintable(m_screenName), screen->devicePixelRatio()); } StreamScreenCursor::~StreamScreenCursor() { @@ -19,30 +24,25 @@ StreamScreenCursor::~StreamScreenCursor() { /* DrawIconEx работает с фоном кадра: это сохраняет и alpha, и AND/XOR системных курсоров. */ bool StreamScreenCursor::draw(QImage& frame) { - if (!m_ready || !m_screen || frame.isNull()) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor unavailable ready=%d screen=%d frame=%dx%d", - m_ready, !m_screen.isNull(), frame.width(), frame.height()); return false; + if (!m_ready || !m_monitor || frame.isNull()) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor unavailable ready=%d monitor=%p frame=%dx%d", + m_ready, m_monitor, frame.width(), frame.height()); return false; } CURSORINFO cursor{}; cursor.cbSize = sizeof(cursor); if (!GetCursorInfo(&cursor)) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: GetCursorInfo failed error=%lu", GetLastError()); return false; } if (!(cursor.flags & CURSOR_SHOWING)) return true; - auto* native = m_screen->nativeInterface(); - if (!native) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: native monitor unavailable screen=%s", qPrintable(m_screen->name())); - return false; - } MONITORINFOEXW monitor{}; monitor.cbSize = sizeof(monitor); - if (!GetMonitorInfoW(native->handle(), reinterpret_cast(&monitor))) { + if (!GetMonitorInfoW(static_cast(m_monitor), reinterpret_cast(&monitor))) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: GetMonitorInfo failed screen=%s error=%lu", - qPrintable(m_screen->name()), GetLastError()); return false; + qPrintable(m_screenName), GetLastError()); return false; } const QSize monitorSize(monitor.rcMonitor.right - monitor.rcMonitor.left, monitor.rcMonitor.bottom - monitor.rcMonitor.top); if (frame.size() != monitorSize) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor: captured frame does not match monitor screen=%s frame=%dx%d monitor=%dx%d", - qPrintable(m_screen->name()), frame.width(), frame.height(), monitorSize.width(), monitorSize.height()); + qPrintable(m_screenName), frame.width(), frame.height(), monitorSize.width(), monitorSize.height()); return false; } // RECT использует физические координаты общего виртуального desktop, включая отрицательные origins. @@ -105,7 +105,7 @@ bool StreamScreenCursor::draw(QImage& frame) { painter.drawImage(area.topLeft(), composed); if (!m_reported) { DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop cursor drawn screen=%s device=%ls origin=%d,%d position=%d,%d hotspot=%lu,%lu sprite=%dx%d frame=%dx%d", - qPrintable(m_screen->name()), monitor.szDevice, origin.x(), origin.y(), position.x(), position.y(), icon.xHotspot, + qPrintable(m_screenName), monitor.szDevice, origin.x(), origin.y(), position.x(), position.y(), icon.xHotspot, icon.yHotspot, size.width(), size.height(), frame.width(), frame.height()); m_reported = true; }