diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index 51ab8b03d..322407b3a 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -236,6 +236,11 @@ QList StreamPublishEngine::sources() const { return {}; #endif } +void StreamPublishEngine::refreshSources() { +#ifdef HAVE_STREAM_CAPTURE + m->capture->refresh(); +#endif +} /* Команда не обращается к Qt capture: создание/замена backend происходит в отдельном процессе. */ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) { diff --git a/tools/chatgui/src/stream_publish_engine.h b/tools/chatgui/src/stream_publish_engine.h index f5f3eb20c..14020adae 100644 --- a/tools/chatgui/src/stream_publish_engine.h +++ b/tools/chatgui/src/stream_publish_engine.h @@ -60,6 +60,7 @@ public: QVariant previewFrame() const; // Собственный CPU QImage; пустой QVariant без кадра, lifetime не связан с capture. QList codecs() const; // Кодеки LiveVideoEncoder, найденные при создании; пусто без поддержки capture. QList sources() const; // Последний асинхронный snapshot worker; до sourcesChanged список может быть пуст. + void refreshSources(); // Запросить новый snapshot через IPC; GUI не опрашивает аппаратный backend. static bool supported(); // Возможности сборки HAVE_STREAM_CAPTURE; не гарантия наличия устройства или рабочего encoder. static bool cursorSupported(const StreamVideoSource& source); // Наличие cursor backend для выбранного screen/платформы. void setMuted(bool muted); // Только running+audio: меняет флаг actor и обнуляет исходящий PCM; ошибку смены трактует как отказ публикации. diff --git a/tools/chatgui/src/stream_publish_window.cpp b/tools/chatgui/src/stream_publish_window.cpp index 4f8ea3190..70cb6646d 100644 --- a/tools/chatgui/src/stream_publish_window.cpp +++ b/tools/chatgui/src/stream_publish_window.cpp @@ -338,7 +338,7 @@ void StreamPublishWindow::refreshSources() { auto* group = m_source->findChild(); group->setExclusive(false); action->setChecked(false); group->setExclusive(true); } m_source->addSeparator(); - connect(m_source->addAction(text("Refresh")), &QAction::triggered, this, &StreamPublishWindow::refreshSources); + connect(m_source->addAction(text("Refresh")), &QAction::triggered, m_engine, &StreamPublishEngine::refreshSources); refreshModes(config, changed && selected >= 0 && m_sources[selected].kind == StreamVideoSource::Screen); synchronize(); } diff --git a/tools/chatgui/src/video_capture.cpp b/tools/chatgui/src/video_capture.cpp index a050a4896..2ccd83bf7 100644 --- a/tools/chatgui/src/video_capture.cpp +++ b/tools/chatgui/src/video_capture.cpp @@ -243,3 +243,8 @@ VideoCapture::~VideoCapture() { m->shutdown(); } void VideoCapture::configure(const VideoCaptureConfig& config) { m->configure(config); } void VideoCapture::stop() { m->stop(); } bool VideoCapture::closing() const { return m->stopping; } +void VideoCapture::refresh() { + if (m->exiting || m->stopping) return; + if (m->process.state() == QProcess::NotRunning) m->launch(); + else m->send(packet(Refresh, m->generation)); +} diff --git a/tools/chatgui/src/video_capture.h b/tools/chatgui/src/video_capture.h index 9e54602fc..0378eb71f 100644 --- a/tools/chatgui/src/video_capture.h +++ b/tools/chatgui/src/video_capture.h @@ -44,6 +44,7 @@ public: void configure(const VideoCaptureConfig& config); void stop(); // Запросить выход процесса; deadline 1.5 с не продлевается сообщениями worker. Повтор безопасен. bool closing() const; // true до подтверждённого выхода процесса после stop; GUI никогда не ждёт его синхронно. + void refresh(); // Асинхронный опрос источников в worker; без capture-процесса запускает его заново. signals: /* Полный список устройств/режимов после опроса и при изменениях backend, независимо от enabled. */ void devicesChanged(const VideoCaptureDevices& devices); diff --git a/tools/chatgui/src/video_capture_protocol.h b/tools/chatgui/src/video_capture_protocol.h index 4cfe82aae..8d7f8345c 100644 --- a/tools/chatgui/src/video_capture_protocol.h +++ b/tools/chatgui/src/video_capture_protocol.h @@ -12,7 +12,7 @@ namespace VideoCaptureProtocol { /* GUI -> worker: Configure(bool,QByteArray device,qint32 w/h/fps), Quit(), Ack(), Buffer(QString name). * Worker -> GUI: Devices(count,[name,id,modes(w,h,fps)]), Frame(qint64 pts/correction,qint32 w/h), Stage/Error(QString). * Buffer/Devices используют поколение 0; остальные — поколение конфигурации/подтверждаемого кадра. */ -enum Type : quint8 { Configure = 1, Quit, Ack, Devices, Frame, Stage, Error, Buffer, Heartbeat }; +enum Type : quint8 { Configure = 1, Quit, Ack, Devices, Frame, Stage, Error, Buffer, Heartbeat, Refresh }; constexpr quint32 FrameBytes = 1920 * 1080 * 4; /* Буфер Call; Stream согласует буфер до MaxFrameBytes. */ constexpr quint32 MaxFrameBytes = 4096u * 4096u * 4u; /* Проверяемый верхний предел RGB32. */ constexpr quint32 MaxPacket = 65536; /* Максимум тела IPC-пакета, без префикса длины. */ diff --git a/tools/chatgui/src/video_capture_worker.cpp b/tools/chatgui/src/video_capture_worker.cpp index 6ef095a13..cd5cf1958 100644 --- a/tools/chatgui/src/video_capture_worker.cpp +++ b/tools/chatgui/src/video_capture_worker.cpp @@ -75,7 +75,12 @@ class QtVideoCaptureSource : public VideoCaptureSource { public: QtVideoCaptureSource() { connect(&devicesMonitor, &QMediaDevices::videoInputsChanged, this, &VideoCaptureSource::devicesChanged); - connect(qGuiApp, &QGuiApplication::screenAdded, this, &VideoCaptureSource::devicesChanged); + auto watchScreen = [this](QScreen* item) { + connect(item, &QScreen::geometryChanged, this, &VideoCaptureSource::devicesChanged); + connect(item, &QScreen::physicalDotsPerInchChanged, this, &VideoCaptureSource::devicesChanged); + }; + for (auto* item : QGuiApplication::screens()) watchScreen(item); + connect(qGuiApp, &QGuiApplication::screenAdded, this, [this, watchScreen](QScreen* item) { watchScreen(item); emit devicesChanged(); }); connect(qGuiApp, &QGuiApplication::screenRemoved, this, [this](QScreen* removed) { if (selectedScreen == removed) fail(QStringLiteral("Selected screen was disconnected")); emit devicesChanged(); @@ -132,7 +137,7 @@ public: if (best.isNull()) { fail(QStringLiteral("Selected camera mode is unavailable")); return false; } camera->setCameraFormat(best); session->setCamera(camera.get()); connect(camera.get(), &QCamera::activeChanged, this, [this, id = config.device](bool active) { - if (active) rate.activate(id); else rate.restore(); + if (active) rate.activate(id); else { rate.restore(); fail(QStringLiteral("Camera capture stopped unexpectedly")); } }); connect(camera.get(), &QCamera::errorOccurred, this, [this](QCamera::Error, const QString& message) { fail(message); }); emit stage(QStringLiteral("camera.start")); camera->start(); @@ -172,6 +177,8 @@ public: screen = std::make_unique(); screen->setScreen(selectedScreen); session->setScreenCapture(screen.get()); connect(screen.get(), &QScreenCapture::errorOccurred, this, [this](QScreenCapture::Error, const QString& message) { fail(message); }); + connect(screen.get(), &QScreenCapture::activeChanged, this, + [this](bool active) { if (!active) fail(QStringLiteral("Screen capture stopped unexpectedly")); }); emit stage(QStringLiteral("screen.start")); screen->start(); } } else { @@ -182,6 +189,8 @@ public: session->setWindowCapture(window.get()); connect(window.get(), &QWindowCapture::errorOccurred, this, [this](QWindowCapture::Error, const QString& message) { fail(message); }); + connect(window.get(), &QWindowCapture::activeChanged, this, + [this](bool active) { if (!active) fail(QStringLiteral("Window capture stopped unexpectedly")); }); emit stage(QStringLiteral("window.start")); window->start(); } DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "capture started kind=%u source=%s size=%dx%d fps=%d cursor=%d", @@ -339,7 +348,8 @@ class VideoCaptureWorker : public QObject { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera frame mapping failed: %s", qPrintable(buffer.errorString())); return false; } pixels = buffer.data(); - } else if (type == Quit) { generation = revision; stop(); stage(QStringLiteral("process.exit")); QCoreApplication::quit(); } + } else if (type == Refresh) enumerate(); + else if (type == Quit) { generation = revision; stop(); stage(QStringLiteral("process.exit")); QCoreApplication::quit(); } else if (type == Ack) { if (sending && revision == inFlight) { sending = false; if (pending) convert(pending, generation); } } else if (type == Configure) {