Present desktop video sequentially through GUI paint acknowledgement

This commit is contained in:
evgeny committed 2026-10-19 17:23:58 +02:00
1 parent 0eb743d627
commit 4e9cce8a01
3 files changed
+47 -14

No files matched your search

+28 -11
View File
@@ -36,6 +36,8 @@ struct CallVideoEngine::Impl {
int64_t shownPts = -1;
uint64_t publicationEpoch = 0;
bool publicationPending = false;
bool presentationDriven = false;
uint64_t publicationSerial = 0;
unsigned publicationOverwritten = 0; // Только под lock: выбранные кадры, заменённые до GUI callback.
int64_t publicationQueuedAt = 0;
int64_t shownAt = 0;
@@ -88,19 +90,30 @@ CallVideoEngine::CallVideoEngine(QObject* parent) : QObject(parent), m(std::make
#endif
}
CallVideoEngine::~CallVideoEngine() { stop(); }
// В GUI одновременно не больше одного уведомления; при паузе UI остаётся последний выбранный кадр.
void CallVideoEngine::publishImage(const QImage& image, int64_t pts) {
uint64_t epoch;
void CallVideoEngine::setPresentationDriven(bool enabled) {
std::lock_guard<std::mutex> guard(m->lock); m->presentationDriven = enabled;
}
void CallVideoEngine::imagePresented(quint64 serial) {
{ std::lock_guard<std::mutex> guard(m->lock);
if (serial != m->publicationSerial) return;
m->publicationPending = false; }
m->wake.notify_all();
}
// Только один кадр до подтверждения рисования: GUI не заменяет ещё не показанное изображение.
void CallVideoEngine::publishImage(const QImage& image, int64_t pts) {
uint64_t epoch, serial;
{ std::unique_lock<std::mutex> guard(m->lock);
m->wake.wait(guard, [this] { return !m->running || !m->publicationPending; });
if (!m->running) return;
m->shown = image; m->shownPts = pts; m->shownAt = nowUs();
if (m->publicationPending) { ++m->publicationOverwritten; return; }
m->publicationPending = true; m->publicationQueuedAt = m->shownAt; epoch = m->publicationEpoch; }
QMetaObject::invokeMethod(this, [this, epoch] {
m->publicationPending = true; m->publicationQueuedAt = m->shownAt; epoch = m->publicationEpoch;
serial = ++m->publicationSerial; }
QMetaObject::invokeMethod(this, [this, epoch, serial] {
QImage image; int64_t pts, selected, queued; unsigned overwritten;
{ std::lock_guard<std::mutex> guard(m->lock);
if (epoch != m->publicationEpoch) return;
image = m->shown; pts = m->shownPts; selected = m->shownAt; queued = m->publicationQueuedAt;
overwritten = m->publicationOverwritten; m->publicationOverwritten = 0; m->publicationPending = false; }
overwritten = m->publicationOverwritten; m->publicationOverwritten = 0; }
const int64_t now = nowUs(), wait = now - queued;
++m->uiPublished; m->uiWaitMax = std::max(m->uiWaitMax, wait);
if (m->uiLastAt) m->uiGapMax = std::max(m->uiGapMax, now - m->uiLastAt);
@@ -110,7 +123,10 @@ void CallVideoEngine::publishImage(const QImage& image, int64_t pts) {
"selected_age_us=%lld pts=%lld mono_us=%lld", (unsigned long long)m->id, overwritten,
(long long)wait, (long long)(now - selected), (long long)pts, (long long)now);
m->uiOverwritten += overwritten;
emit imageChanged(image);
emit imageChanged(image, serial);
bool driven;
{ std::lock_guard<std::mutex> guard(m->lock); driven = m->presentationDriven; }
if (!driven || image.isNull()) imagePresented(serial);
if (!m->uiReportAt) m->uiReportAt = now;
if (now - m->uiReportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video GUI delivery call=%llu interval_us=%lld published=%u overwritten=%u "
@@ -142,12 +158,13 @@ void CallVideoEngine::closeCamera() {
}
void CallVideoEngine::stop() {
m->running = false; m_timer.stop(); closeCamera();
m->wake.notify_all();
if (m->tx.joinable()) m->tx.join();
if (m->rx.joinable()) m->rx.join();
if (m->id) call_video_port_close(m->id);
{ std::lock_guard<std::mutex> guard(m->lock);
++m->publicationEpoch; m->publicationPending = false; m->publicationOverwritten = 0; m->shown = {}; m->shownPts = -1; }
m->id = 0; m->enabled = false; emit imageChanged({});
m->id = 0; m->enabled = false; emit imageChanged({}, 0);
}
void CallVideoEngine::setVideo(bool enabled, const QByteArray& cameraId, int width, int height, int codec) {
const bool active = enabled && m->running;
@@ -341,7 +358,7 @@ void CallVideoEngine::receive() {
uint64_t reportedGeneration = 0, rejectedGeneration = 0;
while (m->running) {
if (requestedWake) wakeLateMax = std::max(wakeLateMax, nowUs() - requestedWake);
// Один кадр ждёт своего срока, второй позволяет пропустить устаревший показ после паузы CPU.
// Малый decoded lookahead; накопленный долг остаётся в encoded FIFO.
while (pictures.size() < 2 && m->running) {
call_video_frame packet{};
if (call_video_port_pull(m->id, &packet) == 1) {
@@ -380,7 +397,7 @@ void CallVideoEngine::receive() {
reportAt = now; decoded = 0; decodeMax = rxAgeMax = wakeLateMax = 0;
}
auto action = call_video_playout_poll(&playout, pictures.empty() ? nullptr : &pictures.front().timing,
pictures.size() > 1 ? &pictures[1].timing : nullptr, now, clock.valid, clock.pts_us);
pictures.size() > 1 ? &pictures[1].timing : nullptr, now, &clock);
if (action != CALL_VIDEO_PLAYOUT_WAIT) {
if (action == CALL_VIDEO_PLAYOUT_SHOW) {
const auto& f = pictures.front(); publishImage(f.image, f.timing.pts_us); lastPictureAt = now;
+3 -1
View File
@@ -14,9 +14,11 @@ public:
void start(quint64 id);
void stop();
void setVideo(bool enabled, const QByteArray& camera, int width, int height, int codec);
void setPresentationDriven(bool enabled); // Окно подтверждает рисование; без окна достаточно доставки signal.
void imagePresented(quint64 serial);
signals:
void camerasChanged(const CallCameraDevices& devices);
void imageChanged(const QImage& image);
void imageChanged(const QImage& image, quint64 serial);
void localImageChanged(const QImage& image);
void statusChanged(const QString& status);
private:
+16 -2
View File
@@ -44,7 +44,14 @@ public:
resize(qMin(1100, available.width() * 9 / 10), qMin(800, available.height() * 9 / 10));
}
QImage remote, local;
CallVideoEngine* engine = nullptr;
quint64 serial = 0;
protected:
void hideEvent(QHideEvent* event) override { presented(); QWidget::hideEvent(event); }
void changeEvent(QEvent* event) override {
QWidget::changeEvent(event);
if (isMinimized()) presented();
}
void closeEvent(QCloseEvent* event) override {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video window closed; call continues"); event->accept();
}
@@ -69,8 +76,12 @@ protected:
QRect target(QPoint(videoRect.right() - preview.width() - 12, videoRect.top() + 12), preview);
painter.setPen(QPen(Qt::white, 2)); painter.drawRect(target.adjusted(-1, -1, 1, 1)); painter.drawImage(target, local);
}
painter.end(); presented();
}
private:
void presented() {
if (engine && serial) { engine->imagePresented(serial); serial = 0; }
}
void toggleFullScreen() {
if (isFullScreen()) { showNormal(); if (!m_geometry.isEmpty()) restoreGeometry(m_geometry); }
else { m_geometry = saveGeometry(); showFullScreen(); }
@@ -271,6 +282,7 @@ CallWindow::CallWindow(QWidget* parent)
lay->addWidget(m_bgpPathLabel);
lay->addWidget(m_statsLabel);
m_video = new CallVideoEngine(this);
m_video->setPresentationDriven(true); m_videoWindow->engine = m_video;
m_localPreview = new QLabel(this); m_localPreview->setAlignment(Qt::AlignCenter); m_localPreview->hide();
lay->addWidget(m_localPreview, 0, Qt::AlignHCenter);
m_videoStatus = new QLabel(this); m_videoStatus->setWordWrap(true); lay->addWidget(m_videoStatus);
@@ -317,10 +329,12 @@ CallWindow::CallWindow(QWidget* parent)
connect(m_video, &CallVideoEngine::statusChanged, this, [this](const QString& text) {
m_videoStatus->setText(text); m_videoStatus->setVisible(m_state == Active && !text.isEmpty());
});
connect(m_video, &CallVideoEngine::imageChanged, this, [this](const QImage& image) {
connect(m_video, &CallVideoEngine::imageChanged, this, [this](const QImage& image, quint64 serial) {
bool changed = m_remoteImage.isNull() != image.isNull(); m_remoteImage = image;
m_videoWindow->remote = image; m_videoWindow->update();
m_videoWindow->remote = image; m_videoWindow->serial = serial; m_videoWindow->update();
if (changed) updateVideoPresentation();
// Скрытое окно продолжает принимать поток, не ждёт невозможного paintEvent.
if (!m_videoWindow->isVisible() || m_videoWindow->isMinimized()) m_video->imagePresented(serial);
update();
});
connect(m_video, &CallVideoEngine::localImageChanged, this, [this](const QImage& image) {