Browse Source

Resolve desktop capture modes from source capabilities

master
evgeny 2 days ago
parent
commit
6228873ce2
  1. 80
      tools/chatgui/src/stream_publish_engine.cpp
  2. 10
      tools/chatgui/src/stream_publish_window.h

80
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<StreamVideoSource> StreamPublishEngine::sources() {
QList<StreamVideoSource> 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<QCamera>(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<LiveVideoEncoder> 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<std::mutex> 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<std::mutex> 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
}

10
tools/chatgui/src/stream_publish_window.h

@ -1,6 +1,7 @@
/* Управление publisher: окно можно закрыть и открыть повторно, engine живёт у MainWindow. */
#pragma once
#include <QWidget>
#include <QJsonObject>
#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<StreamVideoSource> m_sources;
QList<StreamVideoMode> 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();
};

Loading…
Cancel
Save