Feed publisher CPU frames and decouple FIN from capture teardown
This commit is contained in:
1 parent
5256dfa8c5
commit
eed07cb358
5 files changed
+106
-447
No files matched your search
@@ -2,6 +2,7 @@
|
||||
#include "sound_manager.h"
|
||||
#include "audio_event_queue.h"
|
||||
#include "stream_qp_control.h"
|
||||
#include "video_capture.h"
|
||||
#include "../transport/stream_bridge.h"
|
||||
#include "debug_config.h"
|
||||
#include <QGuiApplication>
|
||||
@@ -25,26 +26,11 @@
|
||||
extern "C" {
|
||||
#include "opus_codec.h"
|
||||
#include "audio_compressor.h"
|
||||
#include "../../../src/call/call_video_playout.h"
|
||||
}
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
#include <QCamera>
|
||||
#include <QMediaDevices>
|
||||
#include <QMediaCaptureSession>
|
||||
#include <QVideoSink>
|
||||
#include <QScreenCapture>
|
||||
#include <QWindowCapture>
|
||||
#include <QVideoFrame>
|
||||
#endif
|
||||
#if defined(UTUN_STREAM_X11_CURSOR) || defined(UTUN_STREAM_WIN_CURSOR)
|
||||
#include "stream_screen_cursor.h"
|
||||
#endif
|
||||
#ifdef UTUN_STREAM_PORTAL
|
||||
#include "stream_portal_capture.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
int64_t monotonicUs() { return std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count(); }
|
||||
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); номер различает одинаковые дескрипторы. */
|
||||
@@ -58,24 +44,7 @@ QByteArray screenId(QScreen* screen) {
|
||||
return QJsonDocument(QJsonArray{screen->name(), screen->manufacturer(), screen->model(), screen->serialNumber(), duplicate})
|
||||
.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
QVideoFrame previewFrameFromImage(const QImage& source) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
return QVideoFrame(source);
|
||||
#else
|
||||
QImage image = source.convertToFormat(QImage::Format_RGBA8888);
|
||||
QVideoFrame frame(QVideoFrameFormat(image.size(), QVideoFrameFormat::Format_RGBA8888));
|
||||
if (!frame.map(QVideoFrame::WriteOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop preview frame map failed size=%dx%d", image.width(), image.height());
|
||||
return {};
|
||||
}
|
||||
for (int y = 0; y < image.height(); y++)
|
||||
std::memcpy(frame.bits(0) + y * frame.bytesPerLine(0), image.constScanLine(y), image.width() * 4);
|
||||
frame.unmap();
|
||||
return frame;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
struct StreamPublishEngine::Impl {
|
||||
@@ -83,8 +52,7 @@ struct StreamPublishEngine::Impl {
|
||||
std::mutex lock;
|
||||
std::condition_variable wake;
|
||||
std::thread videoThread, audioThread, joinThread;
|
||||
std::unique_ptr<QThread> imageThread;
|
||||
std::atomic<bool> workersJoined{false}, videoExited{false}, audioExited{false}, imageJoined{false};
|
||||
std::atomic<bool> workersJoined{false}, videoExited{false}, audioExited{false};
|
||||
bool stopping = false, finSent = false, destroying = false;
|
||||
int64_t stopStarted = 0, stopReport = 0;
|
||||
std::atomic<bool> running{false}, muted{false};
|
||||
@@ -102,9 +70,6 @@ struct StreamPublishEngine::Impl {
|
||||
uint64_t captureReportRaw = 0, captureReportConverted = 0, captureReportEncoded = 0, captureReportDelivered = 0;
|
||||
std::atomic<const char*> videoStage{"stopped"};
|
||||
std::atomic<int64_t> videoStageAt{0};
|
||||
std::atomic<const char*> imageStage{"starting"};
|
||||
std::atomic<int64_t> imageStageAt{monotonicUs()};
|
||||
std::atomic<uint64_t> imageFrames{0};
|
||||
std::atomic<uint64_t> convertedFrames{0}, encodedFrames{0}, deliveredFrames{0}, repeatedFrames{0};
|
||||
std::atomic<uint64_t> encodedBytes{0}, lastKeyBytes{0}; // Видео до сетевой отправки, без повторов транспорта.
|
||||
std::atomic<int> videoMaxQp{-1};
|
||||
@@ -113,9 +78,6 @@ struct StreamPublishEngine::Impl {
|
||||
void videoProgress(const char* stage) {
|
||||
videoStageAt.store(monotonicUs()); videoStage.store(stage);
|
||||
}
|
||||
void imageProgress(const char* stage) {
|
||||
imageStageAt.store(monotonicUs()); imageStage.store(stage);
|
||||
}
|
||||
bool hasGood = false, rollingBack = false, videoReady = false;
|
||||
uint64_t softwareRequest = 0;
|
||||
int softwareCodec = 0;
|
||||
@@ -124,28 +86,9 @@ struct StreamPublishEngine::Impl {
|
||||
QList<int> codecs;
|
||||
stream_publisher_status stats{};
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
// Очередь преобразования принадлежит capture backend; без Qt Multimedia нет ни QVideoFrame, ни её worker.
|
||||
std::condition_variable imageWake;
|
||||
QVideoFrame imageInput;
|
||||
uint64_t imageGeneration = 0;
|
||||
int64_t imagePts = -1;
|
||||
bool imagePending = false, imageStop = false, imageDrawCursor = false;
|
||||
std::unique_ptr<QMediaCaptureSession> session;
|
||||
std::unique_ptr<QVideoSink> sink;
|
||||
std::unique_ptr<QCamera> camera;
|
||||
std::unique_ptr<QScreenCapture> screen;
|
||||
#ifdef UTUN_STREAM_PORTAL
|
||||
std::unique_ptr<StreamPortalCapture> portal;
|
||||
#endif
|
||||
std::unique_ptr<QTimer> screenGrabTimer;
|
||||
#if defined(UTUN_STREAM_X11_CURSOR) || defined(UTUN_STREAM_WIN_CURSOR)
|
||||
std::shared_ptr<StreamScreenCursor> cursor;
|
||||
#endif
|
||||
std::unique_ptr<QWindowCapture> window;
|
||||
QImage frame;
|
||||
QImage latestImage;
|
||||
QVideoFrame previewFrame;
|
||||
QVideoFrame presentedPreview;
|
||||
std::unique_ptr<VideoCapture> capture;
|
||||
QList<StreamVideoSource> sources;
|
||||
QImage frame, latestImage, previewFrame, presentedPreview;
|
||||
int64_t lastImageCaptureAt = 0;
|
||||
int64_t previewTickAt = 0, previewReportAt = 0, previewGapMax = 0, previewDispatchMax = 0;
|
||||
unsigned previewUpdates = 0; // Диагностика GUI-предпросмотра; только GUI-поток.
|
||||
@@ -156,15 +99,39 @@ 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->capture = std::make_unique<VideoCapture>(nullptr, VideoCapture::Usage::Stream);
|
||||
connect(m->capture.get(), &VideoCapture::devicesChanged, this, [this](const VideoCaptureDevices& devices) {
|
||||
m->sources.clear();
|
||||
for (const auto& device : devices) {
|
||||
StreamVideoSource source;
|
||||
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());
|
||||
emit sourcesChanged();
|
||||
});
|
||||
connect(m->capture.get(), &VideoCapture::failed, this, [this](const QString& error) {
|
||||
if (m->running) sourceFailed(m->generation, error, config().source.kind == StreamVideoSource::Camera);
|
||||
});
|
||||
connect(m->capture.get(), &VideoCapture::frameReady, this, [this](const QImage& image, qint64 pts, qint64) {
|
||||
if (!m->running) return;
|
||||
const int64_t now = monotonicUs();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
if (m->framePts < 0 && m->wanted.source.id == "portal") m->started = now;
|
||||
m->frame = m->latestImage = m->previewFrame = image;
|
||||
m->framePts = std::max<int64_t>(1, pts - m->origin); m->frameGeneration = m->generation;
|
||||
++m->capturedFrames;
|
||||
if (!m->convertedCaptureFrames++) DEBUG_INFO(DEBUG_CATEGORY_VIDEO,
|
||||
"desktop first CPU capture frame id=%llu source_generation=%llu size=%dx%d age_us=%lld",
|
||||
(unsigned long long)m->id, (unsigned long long)m->sourceGeneration, image.width(), image.height(), (long long)(now - pts));
|
||||
}
|
||||
m->wake.notify_all();
|
||||
});
|
||||
m_previewTimer.setTimerType(Qt::PreciseTimer); m_previewTimer.setInterval(33);
|
||||
connect(&m_previewTimer, &QTimer::timeout, this, [this] {
|
||||
if (!m->running) {
|
||||
@@ -175,10 +142,10 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s
|
||||
if (m->previewTickAt) m->previewGapMax = std::max(m->previewGapMax, tickAt - m->previewTickAt);
|
||||
m->previewTickAt = tickAt;
|
||||
if (!m->previewReportAt) m->previewReportAt = tickAt;
|
||||
QVideoFrame frame;
|
||||
QImage frame;
|
||||
{ std::lock_guard<std::mutex> guard(m->lock);
|
||||
if (m->previewFrame != m->presentedPreview) frame = m->presentedPreview = m->previewFrame; }
|
||||
if (frame.isValid()) {
|
||||
if (m->previewFrame.cacheKey() != m->presentedPreview.cacheKey()) frame = m->presentedPreview = m->previewFrame; }
|
||||
if (!frame.isNull()) {
|
||||
int64_t dispatchAt = monotonicUs();
|
||||
emit previewVideoChanged(QVariant::fromValue(frame));
|
||||
m->previewDispatchMax = std::max(m->previewDispatchMax, monotonicUs() - dispatchAt); m->previewUpdates++;
|
||||
@@ -199,57 +166,23 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s
|
||||
if (m->running && m->wanted.audio) publicationFailed(m->id, text("The audio device was disconnected. Select a microphone and start again."));
|
||||
m_capture.close(); // Контекст нельзя освободить до завершения фонового закрытия устройства.
|
||||
});
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
m->imageThread.reset(QThread::create([this] {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop image worker started tid=%llu", (unsigned long long)(quintptr)QThread::currentThreadId());
|
||||
imageLoop();
|
||||
m->imageProgress("release-thread-resources");
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop image loop returned; releasing thread resources frames=%llu tid=%llu",
|
||||
(unsigned long long)m->imageFrames.load(), (unsigned long long)(quintptr)QThread::currentThreadId());
|
||||
}));
|
||||
m->imageThread->start();
|
||||
#else
|
||||
m->imageJoined = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream capture unavailable in this build; image worker disabled");
|
||||
#endif
|
||||
|
||||
}
|
||||
StreamPublishEngine::~StreamPublishEngine() {
|
||||
const int64_t destroyAt = monotonicUs();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher destruction begin id=%llu running=%d stopping=%d image_stage=%s image_frames=%llu",
|
||||
(unsigned long long)m->id, m->running.load(), m->stopping, m->imageStage.load(), (unsigned long long)m->imageFrames.load());
|
||||
m->destroying = true; blockSignals(true);
|
||||
stop(); m_timer.stop(); m_previewTimer.stop();
|
||||
closeSource();
|
||||
stop(); m_timer.stop(); m_previewTimer.stop(); closeSource();
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
{ std::lock_guard<std::mutex> guard(m->lock); m->imageStop = true; m->imagePending = false; m->imageInput = {}; }
|
||||
m->imageWake.notify_all();
|
||||
// Supervisor переживает GUI-владельца и завершает capture без участия engine/ядра.
|
||||
m->capture.reset();
|
||||
#endif
|
||||
if (!finishStop() || !finishImageStop()) {
|
||||
// Qt освобождает thread-local графику через GUI. Даже при выходе приложения нельзя блокировать его join().
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop publisher destruction waits for workers stopping=%d image_joined=%d image_stage=%s",
|
||||
m->stopping, m->imageJoined.load(), m->imageStage.load());
|
||||
if (!finishStop()) {
|
||||
QEventLoop loop; QTimer timer;
|
||||
int64_t reportAt = destroyAt;
|
||||
connect(&timer, &QTimer::timeout, &loop, [this, &loop, destroyAt, &reportAt] {
|
||||
const bool imageDone = finishImageStop();
|
||||
if (finishStop() && imageDone) { loop.quit(); return; }
|
||||
const int64_t now = monotonicUs();
|
||||
if (now - reportAt < 5000000) return;
|
||||
reportAt = now;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publisher destruction waiting elapsed_ms=%lld stopping=%d workers_joined=%d "
|
||||
"video_stage=%s image_joined=%d image_stage=%s image_stage_age_ms=%lld image_frames=%llu microphone_closing=%d",
|
||||
(long long)((now - destroyAt) / 1000), m->stopping, m->workersJoined.load(), m->videoStage.load(),
|
||||
m->imageJoined.load(), m->imageStage.load(), (long long)((now - m->imageStageAt.load()) / 1000),
|
||||
(unsigned long long)m->imageFrames.load(), m_capture.closing());
|
||||
});
|
||||
timer.start(10);
|
||||
const int result = loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher destruction GUI loop exited result=%d stopping=%d image_joined=%d",
|
||||
result, m->stopping, m->imageJoined.load());
|
||||
if (m->stopping || !m->imageJoined)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop publisher destruction GUI loop exited before worker cleanup completed");
|
||||
connect(&timer, &QTimer::timeout, &loop, [this, &loop] { if (finishStop()) loop.quit(); });
|
||||
timer.start(10); loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher destruction complete elapsed_ms=%lld", (long long)((monotonicUs() - destroyAt) / 1000));
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher destruction complete elapsed_ms=%lld",
|
||||
(long long)((monotonicUs() - destroyAt) / 1000));
|
||||
}
|
||||
bool StreamPublishEngine::supported() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
@@ -266,7 +199,7 @@ uint64_t StreamPublishEngine::videoBitrate() const { return active() ? m->videoB
|
||||
int StreamPublishEngine::videoMaxQp() const { return active() ? m->videoMaxQp.load() : -1; }
|
||||
QVariant StreamPublishEngine::previewFrame() const {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
std::lock_guard<std::mutex> guard(m->lock); return QVariant::fromValue(m->previewFrame);
|
||||
std::lock_guard<std::mutex> guard(m->lock); return m->previewFrame.isNull() ? QVariant() : QVariant::fromValue(m->previewFrame);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
@@ -277,9 +210,9 @@ bool StreamPublishEngine::cursorSupported(const StreamVideoSource& source) {
|
||||
if (source.kind == StreamVideoSource::Screen && source.id == "portal") return true;
|
||||
#endif
|
||||
#ifdef UTUN_STREAM_X11_CURSOR
|
||||
return source.kind == StreamVideoSource::Screen && source.screen && source.id != "portal" && QGuiApplication::platformName() == "xcb";
|
||||
return source.kind == StreamVideoSource::Screen && source.id != "portal" && QGuiApplication::platformName() == "xcb";
|
||||
#elif defined(UTUN_STREAM_WIN_CURSOR)
|
||||
return source.kind == StreamVideoSource::Screen && source.screen && QGuiApplication::platformName() == "windows";
|
||||
return source.kind == StreamVideoSource::Screen && QGuiApplication::platformName() == "windows";
|
||||
#else
|
||||
Q_UNUSED(source); return false;
|
||||
#endif
|
||||
@@ -297,303 +230,41 @@ void StreamPublishEngine::setMuted(bool muted) {
|
||||
emit configurationChanged();
|
||||
}
|
||||
|
||||
QList<StreamVideoSource> StreamPublishEngine::sources() {
|
||||
QList<StreamVideoSource> sources;
|
||||
QList<StreamVideoSource> StreamPublishEngine::sources() const {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
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_TRACE(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 {
|
||||
int number = 0;
|
||||
for (auto* screen : QGuiApplication::screens()) {
|
||||
QSize size = screen->size() * screen->devicePixelRatio();
|
||||
sources.push_back({StreamVideoSource::Screen, QStringLiteral("%1: %2").arg(++number).arg(screen->name()),
|
||||
screenId(screen), {}, size, {}, screen});
|
||||
DEBUG_TRACE(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());
|
||||
}
|
||||
}
|
||||
return m->sources;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
return sources;
|
||||
}
|
||||
|
||||
/* Source создаётся в GUI; callbacks старого поколения не меняют новый capture. */
|
||||
/* Команда не обращается к Qt capture: создание/замена backend происходит в отдельном процессе. */
|
||||
bool StreamPublishEngine::openSource(const StreamPublishConfig& config) {
|
||||
closeSource();
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
uint64_t generation;
|
||||
{ std::lock_guard<std::mutex> guard(m->lock); generation = ++m->sourceGeneration;
|
||||
m->backendOrigin = m->backendAnchor = -1; m->capturedFrames = m->convertedCaptureFrames = 0; m->lastImageCaptureAt = 0; }
|
||||
m->session = std::make_unique<QMediaCaptureSession>(); m->sink = std::make_unique<QVideoSink>(this);
|
||||
m->session->setVideoSink(m->sink.get());
|
||||
connect(m->sink.get(), &QVideoSink::videoFrameChanged, this, [this, generation](const QVideoFrame& frame) {
|
||||
if (!frame.isValid()) { DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "desktop capture sink invalid frame generation=%llu", (unsigned long long)generation); return; }
|
||||
if (!m->running) return;
|
||||
int64_t callbackAt = monotonicUs(), capture = frame.startTime(), framePts = 0;
|
||||
bool convert = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
if (generation != m->sourceGeneration) return;
|
||||
if (!m->capturedFrames++) DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop capture first raw frame kind=%d size=%dx%d pixel=%d PTS=%lld",
|
||||
m->wanted.source.kind, frame.width(), frame.height(), frame.pixelFormat(), (long long)capture);
|
||||
if (capture >= 0) {
|
||||
if (m->backendOrigin < 0) {
|
||||
m->backendOrigin = capture; m->backendAnchor = callbackAt;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop capture timing generation=%llu source_PTS=%lld anchor=%lld calibration=arrival-estimate",
|
||||
(unsigned long long)generation, (long long)capture, (long long)callbackAt);
|
||||
}
|
||||
callbackAt = m->backendAnchor + capture - m->backendOrigin;
|
||||
} else if (m->backendAnchor < 0) {
|
||||
m->backendAnchor = callbackAt;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop capture has no source PTS; using monotonic callback estimate");
|
||||
}
|
||||
int fps = std::clamp(m->wanted.video.fps, 1, 60);
|
||||
convert = !m->lastImageCaptureAt || callbackAt - m->lastImageCaptureAt >= 1000000 / fps;
|
||||
#ifndef UTUN_STREAM_WIN_CURSOR
|
||||
m->previewFrame = frame;
|
||||
#endif
|
||||
if (!convert) { m->wake.notify_all(); return; }
|
||||
m->lastImageCaptureAt = callbackAt;
|
||||
if (m->framePts < 0 && m->wanted.source.id == "portal") m->started = monotonicUs();
|
||||
framePts = std::max<int64_t>(1, callbackAt - m->origin);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
if (!m->running || generation != m->sourceGeneration) return;
|
||||
m->imageInput = frame; m->imageGeneration = generation; m->imagePts = framePts;
|
||||
m->imageDrawCursor = m->wanted.showCursor && cursorSupported(m->wanted.source); m->imagePending = true;
|
||||
}
|
||||
m->imageWake.notify_one();
|
||||
}, Qt::DirectConnection);
|
||||
auto failed = [this, generation, camera = config.source.kind == StreamVideoSource::Camera](const QString& error) {
|
||||
if (generation == m->sourceGeneration) sourceFailed(m->generation, error, camera);
|
||||
};
|
||||
if (config.source.kind == StreamVideoSource::Camera) {
|
||||
QCameraDevice selected;
|
||||
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;
|
||||
for (const auto& format : selected.videoFormats()) {
|
||||
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;
|
||||
}
|
||||
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"), true);
|
||||
}, Qt::QueuedConnection);
|
||||
connect(m->camera.get(), &QCamera::errorOccurred, this, [failed](QCamera::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
|
||||
m->camera->start();
|
||||
} else if (config.source.kind == StreamVideoSource::Screen) {
|
||||
QScreen* selected = nullptr;
|
||||
if (config.source.id != "portal") {
|
||||
for (auto* screen : QGuiApplication::screens())
|
||||
if (screen == config.source.screen && screenId(screen) == config.source.id) { selected = screen; break; }
|
||||
if (!selected) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop screen disappeared"); return false; }
|
||||
}
|
||||
#ifdef UTUN_STREAM_PORTAL
|
||||
if (config.source.id == "portal") {
|
||||
m->portal = std::make_unique<StreamPortalCapture>();
|
||||
connect(m->portal.get(), &StreamPortalCapture::frameReady, m->sink.get(), &QVideoSink::setVideoFrame);
|
||||
connect(m->portal.get(), &StreamPortalCapture::errorOccurred, this, failed, Qt::QueuedConnection);
|
||||
m->portal->start(config.showCursor);
|
||||
} else
|
||||
#endif
|
||||
// QScreen даёт выбранный монитор без курсора; накладываем его один раз до preview и кодирования.
|
||||
// Для X11 это также исключает путаницу Qt Multimedia с одноимёнными root.
|
||||
if (selected && QGuiApplication::platformName() == "xcb") {
|
||||
#if defined(UTUN_STREAM_X11_CURSOR)
|
||||
{ std::lock_guard<std::mutex> guard(m->lock); m->cursor = std::make_shared<StreamScreenCursor>(selected); }
|
||||
#endif
|
||||
m->screenGrabTimer = std::make_unique<QTimer>(); m->screenGrabTimer->setTimerType(Qt::PreciseTimer);
|
||||
connect(m->screenGrabTimer.get(), &QTimer::timeout, this, [this, generation, screen = QPointer<QScreen>(selected), failed] {
|
||||
if (generation != m->sourceGeneration || !m->running) return;
|
||||
int64_t captured = monotonicUs();
|
||||
QImage image = screen ? screen->grabWindow(0).toImage() : QImage();
|
||||
if (image.isNull()) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop native screen grab failed present=%d", !screen.isNull());
|
||||
m->screenGrabTimer->stop();
|
||||
QMetaObject::invokeMethod(this, [failed] { failed(text("Video capture stopped")); }); return;
|
||||
}
|
||||
#if defined(UTUN_STREAM_X11_CURSOR) || defined(UTUN_STREAM_WIN_CURSOR)
|
||||
if (this->config().showCursor && !m->cursor->draw(image)) {
|
||||
m->screenGrabTimer->stop();
|
||||
QMetaObject::invokeMethod(this, [failed] { failed(text("Could not capture the mouse cursor")); }); return;
|
||||
}
|
||||
#endif
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
QVideoFrame frame(image);
|
||||
#else
|
||||
image = image.convertToFormat(QImage::Format_RGBA8888);
|
||||
QVideoFrame frame(QVideoFrameFormat(image.size(), QVideoFrameFormat::Format_RGBA8888));
|
||||
if (frame.map(QVideoFrame::WriteOnly)) {
|
||||
for (int y = 0; y < image.height(); y++) std::memcpy(frame.bits(0) + y * frame.bytesPerLine(0), image.constScanLine(y), image.width() * 4);
|
||||
frame.unmap();
|
||||
} else frame = {};
|
||||
#endif
|
||||
if (!frame.isValid()) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop native video frame allocation failed size=%dx%d", image.width(), image.height());
|
||||
m->screenGrabTimer->stop();
|
||||
QMetaObject::invokeMethod(this, [failed] { failed(text("Video frame conversion failed")); }); return;
|
||||
}
|
||||
frame.setStartTime(captured); m->sink->setVideoFrame(frame);
|
||||
});
|
||||
m->screenGrabTimer->start(std::max(1, 1000 / std::clamp(config.video.fps, 1, 60)));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop native screen grab started platform=%s source=%s interval_ms=%d cursor=%d supported=%d",
|
||||
qPrintable(QGuiApplication::platformName()), qPrintable(config.source.name), m->screenGrabTimer->interval(),
|
||||
config.showCursor, cursorSupported(config.source));
|
||||
} else {
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
if (selected) { std::lock_guard<std::mutex> guard(m->lock); m->cursor = std::make_shared<StreamScreenCursor>(selected); }
|
||||
#endif
|
||||
m->screen = std::make_unique<QScreenCapture>();
|
||||
m->screen->setScreen(selected);
|
||||
m->session->setScreenCapture(m->screen.get());
|
||||
connect(m->screen.get(), &QScreenCapture::activeChanged, this, [this, generation](bool active) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen capture active=%d generation=%llu", active, (unsigned long long)generation);
|
||||
if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped"));
|
||||
}, Qt::QueuedConnection);
|
||||
connect(m->screen.get(), &QScreenCapture::errorOccurred, this, [failed](QScreenCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
|
||||
m->screen->start();
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop Windows screen capture backend=QtMultimedia cursor_overlay=frame_worker source=%s",
|
||||
qPrintable(config.source.name));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
auto window = config.source.window.value<QCapturableWindow>();
|
||||
if (!window.isValid()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop window is unavailable"); return false; }
|
||||
m->window = std::make_unique<QWindowCapture>(); m->window->setWindow(window); m->session->setWindowCapture(m->window.get());
|
||||
connect(m->window.get(), &QWindowCapture::activeChanged, this, [this, generation](bool active) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop window capture active=%d generation=%llu", active, (unsigned long long)generation);
|
||||
if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped"));
|
||||
}, Qt::QueuedConnection);
|
||||
connect(m->window.get(), &QWindowCapture::errorOccurred, this, [failed](QWindowCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
|
||||
m->window->start();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
++m->sourceGeneration; m->frame = {}; m->latestImage = {}; m->previewFrame = {}; m->presentedPreview = {};
|
||||
m->framePts = -1; m->capturedFrames = m->convertedCaptureFrames = 0;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop capture started generation=%llu kind=%d source=%s target=%dx%d/%dfps",
|
||||
(unsigned long long)generation, config.source.kind, qPrintable(config.source.name), config.video.width, config.video.height, config.video.fps);
|
||||
m->capture->configure({true, config.source.id, config.video.width, config.video.height, config.video.fps,
|
||||
static_cast<VideoCaptureKind>(config.source.kind), config.showCursor && cursorSupported(config.source)});
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop capture requested id=%llu source_generation=%llu kind=%d source=%s",
|
||||
(unsigned long long)m->id, (unsigned long long)m->sourceGeneration, config.source.kind, qPrintable(config.source.name));
|
||||
return true;
|
||||
#else
|
||||
Q_UNUSED(config); return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Только CPU-данные GUI. Аппаратное закрытие имеет независимое владение и deadline у supervisor. */
|
||||
void StreamPublishEngine::closeSource() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
++m->sourceGeneration; m->imagePending = false; m->imageInput = {};
|
||||
m->frame = {}; m->latestImage = {}; m->previewFrame = {}; m->presentedPreview = {}; m->framePts = -1;
|
||||
#if defined(UTUN_STREAM_X11_CURSOR) || defined(UTUN_STREAM_WIN_CURSOR)
|
||||
m->cursor.reset();
|
||||
#endif
|
||||
++m->sourceGeneration; m->frame = {}; m->latestImage = {}; m->previewFrame = {}; m->presentedPreview = {}; m->framePts = -1;
|
||||
}
|
||||
m->screenGrabTimer.reset();
|
||||
#ifdef UTUN_STREAM_PORTAL
|
||||
m->portal.reset();
|
||||
#endif
|
||||
if (m->capture) m->capture->stop();
|
||||
emit previewVideoChanged(QVariant());
|
||||
if (m->sink) disconnect(m->sink.get(), nullptr, this, nullptr);
|
||||
if (m->camera) { disconnect(m->camera.get(), nullptr, this, nullptr); m->camera->stop(); }
|
||||
if (m->screen) { disconnect(m->screen.get(), nullptr, this, nullptr); m->screen->stop(); }
|
||||
if (m->window) { disconnect(m->window.get(), nullptr, this, nullptr); m->window->stop(); }
|
||||
m->session.reset(); m->camera.reset(); m->screen.reset(); m->window.reset(); m->sink.reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Долгоживущий worker выполняет Qt Multimedia преобразование; video worker его TLS не создаёт. */
|
||||
void StreamPublishEngine::imageLoop() {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
for (;;) {
|
||||
QVideoFrame frame; uint64_t generation; int64_t pts; bool drawCursor;
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
std::shared_ptr<StreamScreenCursor> cursor;
|
||||
#endif
|
||||
m->imageProgress("wait-frame");
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m->lock);
|
||||
m->imageWake.wait(lock, [this] { return m->imageStop || m->imagePending; });
|
||||
if (m->imageStop) return;
|
||||
frame = std::move(m->imageInput); generation = m->imageGeneration; pts = m->imagePts;
|
||||
drawCursor = m->imageDrawCursor; m->imagePending = false;
|
||||
if (!m->running || generation != m->sourceGeneration) continue;
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
cursor = m->cursor;
|
||||
#endif
|
||||
}
|
||||
int64_t convertAt = monotonicUs();
|
||||
m->imageProgress("convert-frame");
|
||||
QImage image = frame.toImage();
|
||||
// Отметка ставится перед деструкторами image/frame, включая return/continue внутри итерации.
|
||||
auto releaseFrame = qScopeGuard([this] { m->imageProgress("release-frame"); });
|
||||
m->imageFrames++;
|
||||
m->imageProgress("transform-image");
|
||||
if (image.isNull()) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed generation=%llu format=%d size=%dx%d",
|
||||
(unsigned long long)generation, frame.pixelFormat(), frame.width(), frame.height());
|
||||
QMetaObject::invokeMethod(this, [this, generation] {
|
||||
if (generation == m->sourceGeneration) sourceFailed(m->generation, text("Video frame conversion failed"));
|
||||
});
|
||||
continue;
|
||||
}
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
if (drawCursor && (!cursor || !cursor->draw(image))) {
|
||||
if (!cursor) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop cursor missing for captured frame generation=%llu", (unsigned long long)generation);
|
||||
QMetaObject::invokeMethod(this, [this, generation] {
|
||||
if (generation == m->sourceGeneration) sourceFailed(m->generation, text("Could not capture the mouse cursor"));
|
||||
});
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
if (frame.rotation() != QtVideo::Rotation::None) image = image.transformed(QTransform().rotate(static_cast<int>(frame.rotation())));
|
||||
if (frame.mirrored()) image = image.transformed(QTransform().scale(-1, 1));
|
||||
#else
|
||||
if (frame.rotationAngle()) image = image.transformed(QTransform().rotate(frame.rotationAngle()));
|
||||
#endif
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
QVideoFrame preview = previewFrameFromImage(image);
|
||||
if (!preview.isValid()) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop preview frame unavailable generation=%llu",
|
||||
(unsigned long long)generation);
|
||||
#endif
|
||||
m->imageProgress("publish-image");
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m->lock);
|
||||
if (m->imageStop) return;
|
||||
if (!m->running || generation != m->sourceGeneration) continue;
|
||||
#ifdef UTUN_STREAM_WIN_CURSOR
|
||||
if (preview.isValid()) m->previewFrame = std::move(preview);
|
||||
#endif
|
||||
if (m->framePts < 0 && m->wanted.source.id == "portal") m->started = monotonicUs();
|
||||
m->latestImage = image; m->frame = std::move(image); m->framePts = pts; m->frameGeneration = m->generation;
|
||||
if (!m->convertedCaptureFrames++) DEBUG_INFO(DEBUG_CATEGORY_VIDEO,
|
||||
"desktop first image conversion complete generation=%llu size=%dx%d callback_tid=%llu gui_thread=%d convert_us=%lld",
|
||||
(unsigned long long)generation, m->latestImage.width(), m->latestImage.height(),
|
||||
(unsigned long long)(quintptr)QThread::currentThreadId(), QThread::currentThread() == qApp->thread(),
|
||||
(long long)(monotonicUs() - convertAt));
|
||||
}
|
||||
m->wake.notify_all();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -694,9 +365,6 @@ bool StreamPublishEngine::reconfigure(const StreamPublishConfig& config) {
|
||||
sourceFailed(m->generation, text("Could not open the selected video source"), config.source.kind == StreamVideoSource::Camera);
|
||||
return false;
|
||||
}
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
if (m->screenGrabTimer) m->screenGrabTimer->setInterval(std::max(1, 1000 / std::clamp(config.video.fps, 1, 60)));
|
||||
#endif
|
||||
m->wake.notify_all(); emit stateChanged(text("Applying video settings…")); return true;
|
||||
}
|
||||
|
||||
@@ -732,8 +400,11 @@ void StreamPublishEngine::stop() {
|
||||
{ std::lock_guard<std::mutex> guard(m->lock); m->softwareRequest = 0; }
|
||||
m->wake.notify_all();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop closing video source");
|
||||
gui_stream_publish_stop(m->id); m->finSent = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop publisher admission closed id=%llu; accepted data drains independently of capture",
|
||||
(unsigned long long)m->id);
|
||||
closeSource();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop video source closed elapsed_ms=%lld", (long long)((monotonicUs() - started) / 1000));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop video source close requested elapsed_ms=%lld", (long long)((monotonicUs() - started) / 1000));
|
||||
m_capture.closeAsync();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop waiting asynchronously for workers stage=%s", m->videoStage.load());
|
||||
emit stateChanged(text("Finishing stream…"));
|
||||
@@ -758,25 +429,13 @@ bool StreamPublishEngine::finishStop() {
|
||||
(unsigned long long)m->id, (long long)((monotonicUs() - m->stopStarted) / 1000), m_capture.closing());
|
||||
}
|
||||
if (m_capture.closing()) return false;
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
if (m->capture && m->capture->closing()) return false;
|
||||
#endif
|
||||
m_microphone.release(); m->stopping = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Проверка QThread с нулевым таймаутом сохраняет GUI event loop доступным для native cleanup. */
|
||||
bool StreamPublishEngine::finishImageStop() {
|
||||
if (m->imageJoined) return true;
|
||||
if (!m->imageThread) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop image worker missing before native thread completion");
|
||||
return false;
|
||||
}
|
||||
if (!m->imageThread->wait(0)) return false;
|
||||
m->imageThread.reset();
|
||||
m->imageJoined = true;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop image native thread cleanup completed frames=%llu",
|
||||
(unsigned long long)m->imageFrames.load());
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t StreamPublishEngine::softwareEncodingRequest() const {
|
||||
std::lock_guard<std::mutex> guard(m->lock); return m->softwareRequest;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* Desktop publisher принадлежит MainWindow и переживает закрытие окна управления.
|
||||
* Публичные методы — GUI-поток: start открывает actor/capture, но первый KEY и Live приходят асинхронно.
|
||||
* reconfigure публикует желаемую конфигурацию; ошибка видео откатывает к последней рабочей, повторная ошибка останавливает.
|
||||
* Audio callback только копирует PCM; Opus/video кодируются в отдельных workers, QVideoFrame преобразует долгоживущий image worker.
|
||||
* Capture/preview хранят последний кадр, данные уходят через общий C publisher actor.
|
||||
* stop закрывает capture, оставляя GUI доступным; отдельный join ждёт завершения workers/TLS, затем finishStop отправляет FIN.
|
||||
* active остаётся true до окончания actor; новый start разрешён после active=false. Деструктор ждёт также image worker, обрабатывая Qt events. */
|
||||
* Захват/конвертация изолированы в video capture процессе; GUI и encoder получают только собственные CPU QImage.
|
||||
* Audio callback копирует PCM; Opus/video кодируются в отдельных workers, данные уходят через общий C publisher actor.
|
||||
* stop сразу закрывает admission publisher и асинхронно запрашивает выход capture, FIN дренируется независимо.
|
||||
* active остаётся true до окончания actor, workers и capture; только после этого разрешён следующий start.
|
||||
* Деструктор передаёт закрытие capture независимому supervisor, ожидая лишь encoder/audio с обработкой Qt events. */
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
@@ -21,11 +22,11 @@
|
||||
#include "../../../src/stream/stream_publisher.h"
|
||||
|
||||
struct StreamVideoSource {
|
||||
enum Kind { Camera, Screen, Window }; // Window поддерживается openSource, но sources() перечисляет только camera/screen.
|
||||
enum Kind { Camera, Screen, Window }; // Соответствует VideoCaptureKind; native handles остаются в worker.
|
||||
Kind kind = Camera; // Backend захвата.
|
||||
QString name; // Отображаемое имя; не часть идентичности источника.
|
||||
QByteArray id; // Camera device-id, дескриптор screen либо "portal" для системного выбора Wayland.
|
||||
QVariant window; // QCapturableWindow для Kind::Window; пустой у camera/screen.
|
||||
QVariant window; // Зарезервировано пустым; окна выбираются по id из snapshot worker.
|
||||
QSize size; // Физический размер screen; может быть неизвестен до первого кадра portal.
|
||||
QList<StreamVideoMode> modes; // Режимы камеры из объявленных возможностей; screen-режимы строятся отдельно.
|
||||
QPointer<QScreen> screen; // Живой монитор; удаление устройства не должно переключать захват на его одноимённого соседа.
|
||||
@@ -57,9 +58,9 @@ public:
|
||||
StreamPublishConfig config() const; // Копия wanted под lock; может ещё ожидать применения/отката.
|
||||
uint64_t videoBitrate() const; // Фактический выход video encoder за последний интервал около секунды, бит/с; читать в GUI-потоке.
|
||||
int videoMaxQp() const; // Текущий потолок QP с учётом адаптации; -1 — настройки encoder.
|
||||
QVariant previewFrame() const; // Shared QVideoFrame последнего capture, пустой/невалидный без кадра; сетевое кодирование не требуется.
|
||||
QVariant previewFrame() const; // Собственный CPU QImage; пустой QVariant без кадра, lifetime не связан с capture.
|
||||
QList<int> codecs() const; // Кодеки LiveVideoEncoder, найденные при создании; пусто без поддержки capture.
|
||||
static QList<StreamVideoSource> sources(); // Текущие камеры и screen: Wayland даёт один portal, остальные платформы — QScreen handles.
|
||||
QList<StreamVideoSource> sources() const; // Последний асинхронный snapshot worker; до sourcesChanged список может быть пуст.
|
||||
static bool supported(); // Возможности сборки HAVE_STREAM_CAPTURE; не гарантия наличия устройства или рабочего encoder.
|
||||
static bool cursorSupported(const StreamVideoSource& source); // Наличие cursor backend для выбранного screen/платформы.
|
||||
void setMuted(bool muted); // Только running+audio: меняет флаг actor и обнуляет исходящий PCM; ошибку смены трактует как отказ публикации.
|
||||
@@ -72,7 +73,7 @@ signals:
|
||||
void sourcesChanged(); // Камеры/мониторы изменились либо уточнён размер захваченного screen; GUI перечитывает sources/config.
|
||||
void stateChanged(const QString& state); // Пользовательский текст запуска, Live, применения, остановки или ожидания CPU/portal.
|
||||
void configurationChanged(); // Настройки подтверждены либо изменилось состояние start/stop/mute; перечитать config/active/running.
|
||||
void previewVideoChanged(const QVariant& frame); // GUI публикует новый QVideoFrame до ~30fps; пустой QVariant очищает preview при closeSource.
|
||||
void previewVideoChanged(const QVariant& frame); // CPU QImage до ~30fps; пустой QVariant очищает preview при closeSource.
|
||||
void telemetryChanged(const stream_publisher_status& status); // Снимок actor, примерно каждые 100мс вне ожидания workers.
|
||||
void errorOccurred(const QString& error); // Ошибка может привести к rollback либо stop; сама не означает active=false.
|
||||
void audioChannelsChanged(int channels); // Фактический mono/stereo вход; mono дублируется в stereo-транспорт.
|
||||
@@ -85,16 +86,14 @@ private:
|
||||
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Exclusive}; // Удерживается до закрытия устройства и завершения workers.
|
||||
QTimer m_timer; // GUI 100мс: finishStop, состояние actor, telemetry, bitrate и таймауты capture.
|
||||
QTimer m_previewTimer; // GUI 33мс: отправляет только изменившийся previewFrame.
|
||||
bool openSource(const StreamPublishConfig& config); // Пересоздаёт capture backend в GUI, true — принят запуск; ошибки возможны позже.
|
||||
bool openSource(const StreamPublishConfig& config); // Команда отдельному capture-процессу; ошибки приходят асинхронно.
|
||||
void closeSource(); // Меняет sourceGeneration, очищает кадры, отключает callbacks и закрывает backend.
|
||||
bool openMicrophone(); // true — открыт вход; нормализует каналы и подключает captureAudio.
|
||||
// Только текущее поколение: rollback к good либо stop; cameraError добавляет пояснение об ошибке подключения камеры.
|
||||
void sourceFailed(uint64_t generation, const QString& error, bool cameraError = false);
|
||||
void publicationFailed(uint64_t id, const QString& error); // Ошибка всей текущей публикации, без video rollback: errorOccurred и stop.
|
||||
void tick(); // GUI-проверка состояния actor/остановки, capture/audio timeout и статистики.
|
||||
bool finishStop(); // true — workers join и закрытие микрофона завершены, FIN уже отправлен; actor ещё может дренироваться.
|
||||
bool finishImageStop(); // Неблокирующий wait(0) image QThread при уничтожении; true — native TLS уже освобождён.
|
||||
void imageLoop(); // Долгоживущий QThread: последний QVideoFrame → QImage, rotation/mirror и cursor Windows.
|
||||
bool finishStop(); // true — workers, микрофон и capture завершились; FIN уже дренируется независимо в actor.
|
||||
void videoLoop(StreamPublishConfig initial); // Video worker: encoder/revision, HW→CPU consent, KEY, bitrate/QP и feed actor.
|
||||
void audioLoop(); // Audio worker: 20мс stereo, compressor/Opus, пропуск устаревших blocks, feed actor.
|
||||
void captureAudio(const AudioCapture::Frame& frame); // Hardware callback: до 960 frames на элемент ограниченной очереди, без allocations/codec.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "stream_publish_window.h"
|
||||
#include "videoplayer_window.h"
|
||||
#include "capture_controls.h"
|
||||
#include "sound_manager.h"
|
||||
#include "debug_config.h"
|
||||
@@ -130,12 +131,12 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g
|
||||
m_buffer = new StreamBufferIndicator(this); m_buffer->setFixedSize(std::max(80, m_start->sizeHint().width()), 20);
|
||||
controls->addWidget(m_buffer); controls->addWidget(m_start); layout->addLayout(controls);
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
auto* preview = new QVideoWidget(this); m_preview = preview;
|
||||
preview->setMinimumHeight(160); preview->setAspectRatioMode(Qt::KeepAspectRatio);
|
||||
preview->videoSink()->setVideoFrame(engine->previewFrame().value<QVideoFrame>());
|
||||
auto* preview = new VideoRenderWidget(this); m_preview = preview;
|
||||
preview->setMinimumHeight(160);
|
||||
preview->setFrame(engine->previewFrame().value<QImage>());
|
||||
connect(engine, &StreamPublishEngine::previewVideoChanged, this, [this, preview](const QVariant& frame) {
|
||||
auto video = frame.value<QVideoFrame>();
|
||||
if (!video.isValid() || isVisible()) preview->videoSink()->setVideoFrame(video);
|
||||
auto image = frame.value<QImage>();
|
||||
if (image.isNull() || isVisible()) preview->setFrame(image);
|
||||
});
|
||||
#else
|
||||
m_preview = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), this);
|
||||
@@ -316,7 +317,7 @@ void StreamPublishWindow::saveSettings() {
|
||||
void StreamPublishWindow::refreshSources() {
|
||||
QScopedValueRollback<bool> updating(m_updating, true);
|
||||
auto config = m_engine->active() ? m_engine->config() : selection();
|
||||
m_sources = StreamPublishEngine::sources(); clearOptions(m_source, "Video source");
|
||||
m_sources = m_engine->sources(); clearOptions(m_source, "Video source");
|
||||
int selected = -1;
|
||||
for (int i = 0; i < m_sources.size(); i++) {
|
||||
auto& source = m_sources[i];
|
||||
|
||||
@@ -62,7 +62,7 @@ void StreamGridWindow::createTile(Tile& tile) {
|
||||
tile.title->setToolTip(tile.name); layout->addWidget(tile.title);
|
||||
tile.status = new QLabel(text("Connecting…"), tile.widget); layout->addWidget(tile.status);
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
auto* preview = new QVideoWidget(tile.widget); preview->setAspectRatioMode(Qt::KeepAspectRatio);
|
||||
auto* preview = new VideoRenderWidget(tile.widget);
|
||||
tile.video = preview;
|
||||
#else
|
||||
tile.video = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), tile.widget);
|
||||
@@ -82,7 +82,7 @@ void StreamGridWindow::createTile(Tile& tile) {
|
||||
for (auto* child : tile.video->findChildren<QWidget*>()) child->installEventFilter(this);
|
||||
}
|
||||
|
||||
/* QVariant хранит shared QVideoFrame; оба окна читают последний захваченный кадр без перекодирования. */
|
||||
/* QVariant хранит CPU QImage; очистка локального preview не затрагивает capture-процесс. */
|
||||
void StreamGridWindow::setLocalPreview(uint64_t group, const QVariant& frame) {
|
||||
if (m_closed || group != m_group) return;
|
||||
m_preview = frame;
|
||||
@@ -91,13 +91,13 @@ void StreamGridWindow::setLocalPreview(uint64_t group, const QVariant& frame) {
|
||||
|
||||
void StreamGridWindow::updatePreview(Tile& tile) {
|
||||
#ifdef HAVE_STREAM_CAPTURE
|
||||
auto frame = m_preview.value<QVideoFrame>();
|
||||
static_cast<QVideoWidget*>(tile.video)->videoSink()->setVideoFrame(frame);
|
||||
tile.status->setText(text(frame.isValid() ? "Live" : "Connecting…"));
|
||||
if (frame.isValid() && !tile.previewPresented)
|
||||
auto frame = m_preview.value<QImage>();
|
||||
static_cast<VideoRenderWidget*>(tile.video)->setFrame(frame);
|
||||
tile.status->setText(text(!frame.isNull() ? "Live" : "Connecting…"));
|
||||
if (!frame.isNull() && !tile.previewPresented)
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview first local frame source=%llx size=%dx%d decoder=0 audio=0",
|
||||
(unsigned long long)tile.source, frame.width(), frame.height());
|
||||
tile.previewPresented = frame.isValid();
|
||||
tile.previewPresented = !frame.isNull();
|
||||
#else
|
||||
Q_UNUSED(tile);
|
||||
#endif
|
||||
|
||||
File renamed without changes.
Reference in new issue
Block a user