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