Resolve desktop capture modes from source capabilities
This commit is contained in:
1 parent
ea46c56bc1
commit
6228873ce2
2 files changed
+78
-12
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user