diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index 6bdd1000..31703bc7 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -69,6 +69,15 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s m_timer.setTimerType(Qt::PreciseTimer); m_timer.setInterval(100); connect(&m_timer, &QTimer::timeout, this, &StreamPublishEngine::tick); m_timer.start(); #ifdef HAVE_STREAM_CAPTURE + auto* devices = new QMediaDevices(this); + connect(devices, &QMediaDevices::videoInputsChanged, this, &StreamPublishEngine::sourcesChanged); + auto watchScreen = [this](QScreen* screen) { + connect(screen, &QScreen::geometryChanged, this, &StreamPublishEngine::sourcesChanged); + connect(screen, &QScreen::physicalDotsPerInchChanged, this, &StreamPublishEngine::sourcesChanged); + }; + for (auto* screen : QGuiApplication::screens()) watchScreen(screen); + connect(qGuiApp, &QGuiApplication::screenAdded, this, [this, watchScreen](QScreen* screen) { watchScreen(screen); emit sourcesChanged(); }); + connect(qGuiApp, &QGuiApplication::screenRemoved, this, &StreamPublishEngine::sourcesChanged); m_previewTimer.setTimerType(Qt::PreciseTimer); m_previewTimer.setInterval(33); connect(&m_previewTimer, &QTimer::timeout, this, [this] { if (!m->running) return; @@ -124,12 +133,27 @@ void StreamPublishEngine::setMuted(bool muted) { QList StreamPublishEngine::sources() { QList sources; #ifdef HAVE_STREAM_CAPTURE - for (const auto& camera : QMediaDevices::videoInputs()) sources.push_back({StreamVideoSource::Camera, camera.description(), camera.id(), {}}); + for (const auto& camera : QMediaDevices::videoInputs()) { + StreamVideoSource source{StreamVideoSource::Camera, camera.description(), camera.id(), {}}; + for (const auto& format : camera.videoFormats()) { + stream_video_add_camera_modes(source.modes, format.resolution(), format.minFrameRate(), format.maxFrameRate()); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop camera capability device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", + camera.id().constData(), format.resolution().width(), format.resolution().height(), format.pixelFormat(), + format.minFrameRate(), format.maxFrameRate()); + } + if (source.modes.isEmpty()) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop camera has no usable modes device=%s", camera.id().constData()); + sources.append(source); + } if (QGuiApplication::platformName().contains("wayland") || qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland" || qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { sources.push_back({StreamVideoSource::Screen, text("Screen (system selection)"), "portal", {}}); } else { - for (auto* screen : QGuiApplication::screens()) sources.push_back({StreamVideoSource::Screen, screen->name(), screen->name().toUtf8(), {}}); + for (auto* screen : QGuiApplication::screens()) { + QSize size = screen->size() * screen->devicePixelRatio(); + sources.push_back({StreamVideoSource::Screen, screen->name(), screen->name().toUtf8(), {}, size}); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop screen capability source=%s logical=%dx%d scale=%.3f pixels=%dx%d", + qPrintable(screen->name()), screen->size().width(), screen->size().height(), screen->devicePixelRatio(), size.width(), size.height()); + } for (const auto& window : QWindowCapture::capturableWindows()) sources.push_back({StreamVideoSource::Window, window.description(), {}, QVariant::fromValue(window)}); } @@ -177,15 +201,20 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) { for (const auto& camera : QMediaDevices::videoInputs()) if (camera.id() == config.source.id) selected = camera; if (selected.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera disappeared"); return false; } m->camera = std::make_unique(selected); - QCameraFormat best; int score = INT_MAX; + QCameraFormat best; for (const auto& format : selected.videoFormats()) { - if (format.maxFrameRate() + 0.1 < config.video.fps) continue; - int candidate = std::abs(format.resolution().width() - config.video.width) - + std::abs(format.resolution().height() - config.video.height); - if (candidate < score) { score = candidate; best = format; } + if (!stream_video_camera_supports(format.resolution(), format.minFrameRate(), format.maxFrameRate(), + {QSize(config.video.width, config.video.height), config.video.fps})) continue; + if (best.isNull() || format.maxFrameRate() < best.maxFrameRate()) best = format; + } + if (best.isNull()) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera mode unavailable device=%s size=%dx%d fps=%d", + config.source.id.constData(), config.video.width, config.video.height, config.video.fps); return false; } - 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()); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera selected device=%s size=%dx%d pixel=%d capture_fps=%.3f..%.3f output_fps=%d", + config.source.id.constData(), best.resolution().width(), best.resolution().height(), best.pixelFormat(), + best.minFrameRate(), best.maxFrameRate(), config.video.fps); 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")); @@ -259,7 +288,7 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi } m->running = true; try { - m->videoThread = std::thread(&StreamPublishEngine::videoLoop, this); + m->videoThread = std::thread(&StreamPublishEngine::videoLoop, this, config); if (config.audio) m->audioThread = std::thread(&StreamPublishEngine::audioLoop, this); } catch (const std::system_error& error) { publicationFailed(m->id, QString::fromUtf8(error.what())); return false; } if (!openSource(config) || (config.audio && !openMicrophone())) { @@ -409,7 +438,7 @@ void StreamPublishEngine::audioLoop() { opus_codec_encoder_destroy(encoder); audio_compressor_destroy(compressor); } -void StreamPublishEngine::videoLoop() { +void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { #ifdef HAVE_STREAM_CAPTURE std::unique_ptr encoder; StreamPublishConfig encoding; @@ -422,6 +451,31 @@ void StreamPublishEngine::videoLoop() { if (next > now) { m->wake.wait_for(lock, std::chrono::microseconds(next - now)); continue; } if (m->frameGeneration == generation) { frame = m->frame; capture = m->framePts; m->frame = {}; } else capture = -1; } + // Размер кадра точнее QScreen при HiDPI и единственный доступный после выбора Wayland portal. + if (config.source.kind == StreamVideoSource::Screen && frame.isValid() && config.source.size != frame.size()) { + QSize size = stream_video_screen_size(frame.size(), config.nativeSize ? 4096 : std::min(config.video.width, config.video.height)); + if (size.isEmpty()) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop screen invalid captured size=%dx%d", frame.width(), frame.height()); + QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Invalid stream video dimensions")); }); + continue; + } + std::lock_guard guard(m->lock); + if (m->generation != generation) continue; + config.source.size = frame.size(); + if (config.video.width != size.width() || config.video.height != size.height()) { + config.video.width = size.width(); config.video.height = size.height(); + if (config.automaticBitrate) config.video.bitrate = std::clamp(size.width() * size.height() * config.video.fps / 11, 250000, 16000000); + generation = ++m->generation; m->frameGeneration = generation; m->started = monotonicUs(); + } + m->wanted = config; + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen size resolved source=%s raw=%dx%d target=%dx%d native=%d generation=%llu", + qPrintable(config.source.name), frame.width(), frame.height(), size.width(), size.height(), config.nativeSize, + (unsigned long long)generation); + QMetaObject::invokeMethod(this, [this, generation] { if (generation == m->generation) emit sourcesChanged(); }); + } + if (config.source.kind == StreamVideoSource::Screen && config.source.size.isEmpty() && !frame.isValid()) { + std::unique_lock lock(m->lock); m->wake.wait_for(lock, std::chrono::milliseconds(5)); continue; + } if (generation != current) { auto comparable = config.video; comparable.bitrate = encoding.video.bitrate; bool onlyBitrate = encoder && comparable == encoding.video && config.source.kind == encoding.source.kind @@ -444,7 +498,9 @@ void StreamPublishEngine::videoLoop() { }); continue; } - encoder = std::move(candidate); current = generation; encoding = config; revision++; lastImage = {}; lastKey = -1000000; + // Начальный формат publisher уже зарегистрирован; уточнение размера требует новой revision даже до первого KEY. + revision = revision ? revision + 1 : config.video == initial.video ? 1 : 2; + encoder = std::move(candidate); current = generation; encoding = config; lastImage = {}; lastKey = -1000000; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop video prepared generation=%llu revision=%u encoder=%s", (unsigned long long)generation, revision, qPrintable(encoder->name())); } @@ -504,6 +560,8 @@ void StreamPublishEngine::videoLoop() { report = now; count = skipped = 0; } } +#else + Q_UNUSED(initial); #endif } diff --git a/tools/chatgui/src/stream_publish_window.h b/tools/chatgui/src/stream_publish_window.h index 9aa70b97..6f9e6aeb 100644 --- a/tools/chatgui/src/stream_publish_window.h +++ b/tools/chatgui/src/stream_publish_window.h @@ -1,6 +1,7 @@ /* Управление publisher: окно можно закрыть и открыть повторно, engine живёт у MainWindow. */ #pragma once #include +#include #include "stream_publish_engine.h" class QComboBox; class QLabel; @@ -10,11 +11,14 @@ class QButtonGroup; class StreamPublishWindow : public QWidget { Q_OBJECT public: - StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent = nullptr); + StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent = nullptr, const QString& settings = {}); +signals: + void settingsChanged(const QString& settings); private: StreamPublishEngine* m_engine; uint64_t m_group; QList m_sources; + QList m_modes; QComboBox *m_source, *m_microphone, *m_mode, *m_codec, *m_bitrate; QCheckBox *m_audio, *m_mute; QLabel *m_status, *m_stats, *m_inputChannels; @@ -22,9 +26,13 @@ private: QButtonGroup* m_rotation; QWidget* m_preview; bool m_updating = false; + QJsonObject m_settings; StreamPublishConfig selection() const; void refreshSources(); + void refreshModes(const StreamPublishConfig& previous, bool maximum = false); void refreshCodecs(); void synchronize(); void apply(); + void restoreSettings(); + void saveSettings(); };