From 3ba7601ba46084b01ad5297a1948cc8135fdfb0a Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 16:03:14 +0200 Subject: [PATCH] Close failed stream capture without blocking the GUI --- tools/chatgui/src/audio_capture.cpp | 33 ++++++++++++++++++++++++++--- tools/chatgui/src/audio_capture.h | 7 ++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/tools/chatgui/src/audio_capture.cpp b/tools/chatgui/src/audio_capture.cpp index fdfe7686..83063377 100644 --- a/tools/chatgui/src/audio_capture.cpp +++ b/tools/chatgui/src/audio_capture.cpp @@ -98,6 +98,7 @@ AudioCapture::~AudioCapture() { close(); } bool AudioCapture::open(const QString& input, Role role, int channels, uint64_t tag, Ready ready, std::function stopped, Callback callback) { + if (closing()) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "capture: previous device is still closing"); return false; } close(); static std::vector> sessions; // Только GUI; владение у подписчиков. QString key = input; @@ -129,13 +130,39 @@ bool AudioCapture::open(const QString& input, Role role, int channels, uint64_t return running(); } -void AudioCapture::close() { - if (!m_session) return; +std::shared_ptr AudioCapture::detach() { + if (!m_session) return {}; auto& slot = m_session->consumers[m_role == Role::Stream ? 0 : 1]; m_session->detach(slot); slot.client = nullptr; DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: detached role=%s remaining=%d", m_role == Role::Stream ? "stream" : "radio", int(m_session->consumers[0].client != nullptr) + int(m_session->consumers[1].client != nullptr)); - m_session.reset(); m_callback = {}; m_ready = {}; m_stopped = {}; + m_callback = {}; m_ready = {}; m_stopped = {}; + return std::move(m_session); +} + +void AudioCapture::close() { + if (m_closeThread.joinable()) m_closeThread.join(); + detach().reset(); +} + +/* Callback отсоединён в GUI; session и контекст должны жить до завершения ma_device_uninit. */ +void AudioCapture::closeAsync() { + if (closing()) return; + if (m_closeThread.joinable()) m_closeThread.join(); + auto session = detach(); + if (!session) return; + m_closeDone.store(false, std::memory_order_release); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: asynchronous close started shared=%d", session.use_count() > 1); + try { + m_closeThread = std::thread([this, session = std::move(session)]() mutable { + session.reset(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "capture: asynchronous close completed"); + m_closeDone.store(true, std::memory_order_release); + }); + } catch (const std::system_error& error) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "capture: close worker failed: %s", error.what()); + m_closeDone.store(true, std::memory_order_release); + } } void AudioCapture::recover() { if (m_session) m_session->recover(); } diff --git a/tools/chatgui/src/audio_capture.h b/tools/chatgui/src/audio_capture.h index 6f04e576..80e3a63c 100644 --- a/tools/chatgui/src/audio_capture.h +++ b/tools/chatgui/src/audio_capture.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include /* GUI владеет подпиской, сессия — устройством. Одинаковый выбранный вход открывается один раз. * Профиль трансляции имеет приоритет и сохраняется для рации после её остановки. @@ -28,12 +30,17 @@ public: bool open(const QString& input, Role role, int channels, uint64_t tag, Ready ready, std::function stopped, Callback callback); void close(); + void closeAsync(); // GUI отсоединяет подписчика; последний backend закрывается в worker. + bool closing() const { return !m_closeDone.load(std::memory_order_acquire); } void recover(); bool subscribed() const { return bool(m_session); } bool running() const; private: struct Session; + std::shared_ptr detach(); std::shared_ptr m_session; + std::thread m_closeThread; + std::atomic m_closeDone{true}; Ready m_ready; std::function m_stopped; Callback m_callback;