diff --git a/tools/chatgui/src/call_video_engine.cpp b/tools/chatgui/src/call_video_engine.cpp index 6a80fcb1..1e0fc619 100644 --- a/tools/chatgui/src/call_video_engine.cpp +++ b/tools/chatgui/src/call_video_engine.cpp @@ -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 guard(m->lock); m->presentationDriven = enabled; +} +void CallVideoEngine::imagePresented(quint64 serial) { { std::lock_guard 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 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 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 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 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; diff --git a/tools/chatgui/src/call_video_engine.h b/tools/chatgui/src/call_video_engine.h index df72ffcb..d909bc2d 100644 --- a/tools/chatgui/src/call_video_engine.h +++ b/tools/chatgui/src/call_video_engine.h @@ -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: diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index d29b2b1f..4d8ab95a 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -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) {