From 2caa9504c9126876e5394b5c4e6d39e34bd459ca Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 14 Oct 2026 12:11:36 +0200 Subject: [PATCH] Report capture termination and preserve explicit microphone selection --- tools/chatgui/src/messagelist.cpp | 1 + tools/chatgui/src/stream_publish_engine.cpp | 38 ++++++++++++++------- tools/chatgui/transport/stream_bridge.h | 5 +-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index c7de2d3a..73a5a728 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index d1f14e3c..7756ba1b 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -40,7 +40,7 @@ struct StreamPublishEngine::Impl { std::condition_variable wake; std::thread videoThread, audioThread, probeThread; std::atomic running{false}, muted{false}; - std::atomic audioDropped{0}; + std::atomic audioDropped{0}, audioMissing{0}; AudioEventQueue audio; uint64_t id = 0, group = 0, generation = 0, sourceGeneration = 0, confirmedGeneration = 0, frameGeneration = 0, audioSequence = 0; int64_t origin = 0, started = 0, framePts = -1, backendOrigin = -1, backendAnchor = -1; @@ -175,9 +175,10 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) { } if (best.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera lacks requested fps=%d", config.video.fps); return false; } m->camera->setCameraFormat(best); m->session->setCamera(m->camera.get()); - connect(m->camera.get(), &QCamera::activeChanged, this, [generation](bool active) { + connect(m->camera.get(), &QCamera::activeChanged, this, [this, generation](bool active) { DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera active=%d generation=%llu", active, (unsigned long long)generation); - }); + if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped")); + }, Qt::QueuedConnection); connect(m->camera.get(), &QCamera::errorOccurred, this, [failed](QCamera::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); m->camera->start(); } else if (config.source.kind == StreamVideoSource::Screen) { @@ -189,18 +190,20 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) { m->screen->setScreen(selected); } m->session->setScreenCapture(m->screen.get()); - connect(m->screen.get(), &QScreenCapture::activeChanged, this, [generation](bool active) { + connect(m->screen.get(), &QScreenCapture::activeChanged, this, [this, generation](bool active) { DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen capture active=%d generation=%llu", active, (unsigned long long)generation); - }); + if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped")); + }, Qt::QueuedConnection); connect(m->screen.get(), &QScreenCapture::errorOccurred, this, [failed](QScreenCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); m->screen->start(); } else { auto window = config.source.window.value(); if (!window.isValid()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop window is unavailable"); return false; } m->window = std::make_unique(); m->window->setWindow(window); m->session->setWindowCapture(m->window.get()); - connect(m->window.get(), &QWindowCapture::activeChanged, this, [generation](bool active) { + connect(m->window.get(), &QWindowCapture::activeChanged, this, [this, generation](bool active) { DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop window capture active=%d generation=%llu", active, (unsigned long long)generation); - }); + if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped")); + }, Qt::QueuedConnection); connect(m->window.get(), &QWindowCapture::errorOccurred, this, [failed](QWindowCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); m->window->start(); } @@ -224,8 +227,11 @@ void StreamPublishEngine::closeSource() { } bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& config) { - if (active()) { emit errorOccurred(text("A stream is already running")); return false; } - if (!supported() || !codecs().contains(config.video.codec)) { emit errorOccurred(text("Video capture or encoder is unavailable")); return false; } + if (active()) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publication already active"); emit errorOccurred(text("A stream is already running")); return false; } + if (!supported() || !codecs().contains(config.video.codec)) { + DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop capture/encoder unavailable codec=%d", config.video.codec); + emit errorOccurred(text("Video capture or encoder is unavailable")); return false; + } if (config.audio && !m_microphone.acquire()) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publication blocked: microphone owned by another operation"); emit errorOccurred(text("The microphone is used by another operation")); return false; @@ -248,7 +254,7 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi if (!openSource(config) || (config.audio && !openMicrophone())) { sourceFailed(m->generation, text("Could not open the selected capture device")); return false; } - emit stateChanged(config.source.id == "portal" ? text("Select a screen or window in the system dialog") : text("Starting stream…")); + emit stateChanged(config.source.id == "portal" ? text("Select a screen in the system dialog") : text("Starting stream…")); emit configurationChanged(); return true; } @@ -256,6 +262,7 @@ bool StreamPublishEngine::reconfigure(const StreamPublishConfig& config) { if (!m->running) return false; StreamPublishConfig previous = this->config(); if (config.audio != previous.audio || config.microphone != previous.microphone) { + DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop microphone selection changed during live publication"); emit errorOccurred(text("Change the microphone before starting the stream")); return false; } bool sourceChange = config.source.kind != previous.source.kind || config.source.id != previous.source.id @@ -298,6 +305,7 @@ bool StreamPublishEngine::openMicrophone() { auto config = this->config(); auto* context = SoundManager::instance()->context(); if (!context) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream audio context unavailable"); return false; } ma_device_id device; bool selected = SoundManager::deviceId(config.microphone, &device); + if (!config.microphone.isEmpty() && !selected) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop selected microphone ID is unavailable"); return false; } for (int channels : {2, 1}) { ma_device_config capture = ma_device_config_init(ma_device_type_capture); capture.capture.format = ma_format_s16; capture.capture.channels = channels; capture.sampleRate = 48000; @@ -323,7 +331,7 @@ void StreamPublishEngine::captureAudio(const void* input, unsigned frames) { m->audioAnchor = first; m->audioFrames = 0; } first = m->audioAnchor + (int64_t)m->audioFrames * 1000000 / 48000; m->audioFrames += frames; - if (!pcm) m->audioDropped++; + if (!pcm) m->audioMissing++; for (unsigned offset = 0; offset < frames;) { Impl::Audio record; record.frames = std::min(960u, frames - offset); record.pts = std::max(0, first + (int64_t)offset * 1000000 / 48000); record.created = monotonicUs(); @@ -424,7 +432,11 @@ void StreamPublishEngine::videoLoop() { if (frame.isValid()) { if (capture <= lastPts || monotonicUs() - m->origin - capture > 250000) { skipped++; continue; } lastImage = frame.toImage(); - if (lastImage.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed"); continue; } + if (lastImage.isNull()) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed format=%d", frame.pixelFormat()); + QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Video frame conversion failed")); }); + continue; + } #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) if (frame.rotation() != QtVideo::Rotation::None) lastImage = lastImage.transformed(QTransform().rotate(static_cast(frame.rotation()))); if (frame.mirrored()) lastImage = lastImage.transformed(QTransform().scale(-1, 1)); @@ -508,4 +520,6 @@ void StreamPublishEngine::tick() { } unsigned dropped = m->audioDropped.exchange(0); if (dropped) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop audio capture discarded blocks=%u", dropped); + unsigned missing = m->audioMissing.exchange(0); + if (missing) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop microphone callbacks lack PCM count=%u; sent silence", missing); } diff --git a/tools/chatgui/transport/stream_bridge.h b/tools/chatgui/transport/stream_bridge.h index 5bec4d30..c1805a8a 100644 --- a/tools/chatgui/transport/stream_bridge.h +++ b/tools/chatgui/transport/stream_bridge.h @@ -1,5 +1,6 @@ -/* Desktop live viewer: attach/detach в uasync до destroy ядра; остальные вызовы потокобезопасны. - * Один viewer, команды содержат IDs; подготовлено не более двух media records, общий предел RX 4MiB/16 записей. +/* Desktop live media: attach/detach в uasync до destroy ядра; остальные вызовы потокобезопасны. + * Один общий publisher и один viewer; команды содержат IDs, не borrowed pointers ядра. + * Подготовлено не более двух media records, общий предел RX 4MiB/16 записей. * Watch до attach запоминает выбранную группу. Каталог идёт через CHAT_EVT_STREAM_SOURCES, * media pull читает ближайшие кадры из общего RS-кольца; копия нужна только для передачи между потоками. */ #pragma once