Pass only immutable source identifiers into the publisher

This commit is contained in:
evgeny committed 2026-11-01 20:15:42 +01:00
1 parent 61663e261d
commit 14bdd2a58f
3 files changed
+23 -32

No files matched your search

@@ -6,15 +6,8 @@
#include "../transport/stream_bridge.h"
#include "debug_config.h"
#include <QGuiApplication>
#include <QThread>
#include <QScreen>
#include <QRandomGenerator>
#include <QTransform>
#include <QPixmap>
#include <QJsonArray>
#include <QJsonDocument>
#include <QEventLoop>
#include <QScopeGuard>
#include <thread>
#include <mutex>
#include <condition_variable>
@@ -32,20 +25,7 @@ extern "C" {
namespace {
int64_t monotonicUs() { return call_video_playout_now_us(); }
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
#ifdef HAVE_STREAM_CAPTURE
/* Имя QScreen не уникально (например, несколько X11 screens); номер различает одинаковые дескрипторы. */
QByteArray screenId(QScreen* screen) {
int duplicate = 0;
for (auto* other : QGuiApplication::screens()) {
if (other == screen) break;
if (other->name() == screen->name() && other->manufacturer() == screen->manufacturer()
&& other->model() == screen->model() && other->serialNumber() == screen->serialNumber()) duplicate++;
}
return QJsonDocument(QJsonArray{screen->name(), screen->manufacturer(), screen->model(), screen->serialNumber(), duplicate})
.toJson(QJsonDocument::Compact);
}
#endif
}
struct StreamPublishEngine::Impl {
struct Audio { int64_t pts = 0, created = 0; uint64_t sequence = 0; unsigned frames = 0; int16_t pcm[1920]{}; };
@@ -106,8 +86,6 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s
source.kind = static_cast<StreamVideoSource::Kind>(device.kind);
source.id = device.id; source.name = device.id == "portal" ? text("Screen (system selection)") : device.name;
source.size = device.size; source.modes = device.modes;
if (source.kind == StreamVideoSource::Screen)
for (auto* screen : QGuiApplication::screens()) if (screenId(screen) == source.id) source.screen = screen;
m->sources.append(source);
}
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop capture source snapshot count=%lld", (long long)m->sources.size());
+1 -4
View File
@@ -12,8 +12,6 @@
#include <QVariant>
#include <QImage>
#include <QList>
#include <QPointer>
#include <QScreen>
#include <memory>
#include "live_video_encoder.h"
#include "stream_video_modes.h"
@@ -28,9 +26,8 @@ struct StreamVideoSource {
QByteArray id; // Camera device-id, дескриптор screen либо "portal" для системного выбора Wayland.
QSize size; // Физический размер screen; может быть неизвестен до первого кадра portal.
QList<StreamVideoMode> modes; // Режимы камеры из объявленных возможностей; screen-режимы строятся отдельно.
QPointer<QScreen> screen; // Живой монитор; удаление устройства не должно переключать захват на его одноимённого соседа.
bool sameSource(const StreamVideoSource& other) const { // Сравнивает backend/handles, без имени, разрешения и списка режимов.
return kind == other.kind && id == other.id && screen == other.screen;
return kind == other.kind && id == other.id;
}
};
struct StreamPublishConfig {
+22 -6
View File
@@ -156,6 +156,22 @@ static void captureLifecycleLog(int, const char*, const char* message) {
captureDiagnostics.append(QString::fromUtf8(message));
}
/* Только native-capture тесты сопоставляют внешний snapshot с экранами своего Qt application. */
static QScreen* testScreen(const StreamVideoSource& source) {
for (auto* screen : QGuiApplication::screens()) {
int duplicate = 0;
for (auto* other : QGuiApplication::screens()) {
if (other == screen) break;
if (other->name() == screen->name() && other->manufacturer() == screen->manufacturer()
&& other->model() == screen->model() && other->serialNumber() == screen->serialNumber()) ++duplicate;
}
const auto id = QJsonDocument(QJsonArray{screen->name(), screen->manufacturer(), screen->model(),
screen->serialNumber(), duplicate}).toJson(QJsonDocument::Compact);
if (id == source.id) return screen;
}
return nullptr;
}
class StreamPublishTest : public QObject {
Q_OBJECT
UASYNC* ua = nullptr;
@@ -581,10 +597,10 @@ private slots:
StreamPublishEngine engine;
if (StreamPublishEngine::supported()) QTRY_VERIFY_WITH_TIMEOUT(!engine.sources().isEmpty(), 5000); StreamPublishConfig config; config.startMuted = true;
config.video.width = 320; config.video.height = 180; config.video.fps = 15;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen && source.screen) {
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen && testScreen(source)) {
config.source = source; break;
}
QVERIFY(config.source.screen);
QVERIFY(testScreen(config.source));
connect(&engine, &StreamPublishEngine::softwareEncodingRequested, &engine,
[&engine](uint64_t generation) { engine.answerSoftwareEncoding(generation, true); });
QVERIFY(engine.start(group, config)); QVERIFY(engine.muted());
@@ -621,7 +637,7 @@ private slots:
for (const auto& source : engine.sources()) {
if (source.kind != StreamVideoSource::Screen || source.size.isEmpty()) continue;
// При дробном DPI size*dpr округляется; сверяемся с физическими пикселями снимка.
QSize capturedSize = source.screen->grabWindow(0).size(); QVERIFY(!capturedSize.isEmpty());
QSize capturedSize = testScreen(source)->grabWindow(0).size(); QVERIFY(!capturedSize.isEmpty());
QSize expected = stream_video_screen_size(capturedSize);
config.source = source; config.source.size = {}; // Как portal: размер станет известен только из первого кадра.
if (!engine.active()) {
@@ -691,8 +707,8 @@ private slots:
StreamPublishEngine engine;
if (StreamPublishEngine::supported()) QTRY_VERIFY_WITH_TIMEOUT(!engine.sources().isEmpty(), 5000); QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000);
StreamPublishConfig config; config.audio = false;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen && source.screen == screen) config.source = source;
QVERIFY(config.source.screen); QVERIFY(config.showCursor);
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen && testScreen(source) == screen) config.source = source;
QVERIFY(testScreen(config.source)); QVERIFY(config.showCursor);
config.video.width = 320; config.video.height = 180;
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
QVERIFY(engine.start(group, config));
@@ -762,7 +778,7 @@ private slots:
StreamPublishEngine engine;
if (StreamPublishEngine::supported()) QTRY_VERIFY_WITH_TIMEOUT(!engine.sources().isEmpty(), 5000);
for (const auto& source : engine.sources()) {
if (source.kind != StreamVideoSource::Screen || source.screen != screen) continue;
if (source.kind != StreamVideoSource::Screen || testScreen(source) != screen) continue;
QVERIFY(StreamPublishEngine::cursorSupported(source));
StreamPublishWindow controls(&engine, group);
auto* sources = menu(controls, "streamVideoSource"); QVERIFY(sources);