Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qsurfacecapturegrabber_p.h"
#include <QtCore/qchronotimer.h>
#include <QtCore/qelapsedtimer.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qthread.h>
#include <QtCore/qtimer.h>
#include <mutex>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(qLcScreenCaptureGrabber, "qt.multimedia.ffmpeg.surfacecapturegrabber");
class QSurfaceCaptureGrabber::GrabbingProfiler
{
public:
auto measure()
{
m_elapsedTimer.start();
return qScopeGuard([&]() {
const auto nsecsElapsed = m_elapsedTimer.nsecsElapsed();
++m_number;
m_wholeTime += nsecsElapsed;
#ifdef DUMP_SCREEN_CAPTURE_PROFILING
qDebug() << "screen grabbing time:" << nsecsElapsed << "avg:" << avgTime()
<< "number:" << m_number;
#endif
});
}
qreal avgTime() const
{
return m_number ? m_wholeTime / (m_number * 1000000.) : 0.;
}
qint64 number() const
{
return m_number;
}
private:
QElapsedTimer m_elapsedTimer;
qint64 m_wholeTime = 0;
qint64 m_number = 0;
};
struct QSurfaceCaptureGrabber::GrabbingContext
{
GrabbingProfiler profiler;
QChronoTimer timer;
QElapsedTimer elapsedTimer;
qint64 lastFrameTime = 0;
};
class QSurfaceCaptureGrabber::GrabbingThread : public QThread
{
public:
GrabbingThread(QSurfaceCaptureGrabber& grabber)
: m_grabber(grabber)
{}
protected:
void run() override
{
m_grabber.initializeGrabbingContext();
if (!m_grabber.isGrabbingContextInitialized())
return;
exec();
m_grabber.finalizeGrabbingContext();
}
private:
QSurfaceCaptureGrabber& m_grabber;
};
QSurfaceCaptureGrabber::QSurfaceCaptureGrabber(ThreadPolicy threadPolicy)
{
setFrameRate(DefaultScreenCaptureFrameRate);
if (threadPolicy == CreateGrabbingThread)
m_thread = std::make_unique<GrabbingThread>(*this);
}
void QSurfaceCaptureGrabber::start()
{
Q_ASSERT(thread()->isCurrentThread());
{
// Locked: pairs with the read in updateTimerInterval().
std::lock_guard locker{ m_mutex };
m_activeRate = m_rate;
}
if (m_thread)
m_thread->start();
else if (!isGrabbingContextInitialized())
initializeGrabbingContext();
}
QSurfaceCaptureGrabber::~QSurfaceCaptureGrabber() = default;
void QSurfaceCaptureGrabber::setFrameRate(std::optional<qreal> rate)
{
if (!rate)
rate = DefaultScreenCaptureFrameRate;
// Bound to a minimum
if (*rate < MinScreenCaptureFrameRate)
rate = MinScreenCaptureFrameRate;
// New rate applies to the next stream only.
Q_ASSERT(thread()->isCurrentThread());
Q_ASSERT(!isGrabbingContextInitialized());
if (m_rate == *rate)
return;
m_rate = *rate;
m_activeRate = *rate; // Also visible pre-start via activeFrameRate().
qCDebug(qLcScreenCaptureGrabber) << "Screen capture rate has been changed:" << *rate;
}
qreal QSurfaceCaptureGrabber::frameRate() const
{
Q_ASSERT(thread()->isCurrentThread());
return m_rate;
}
qreal QSurfaceCaptureGrabber::activeFrameRate() const
{
// Owning thread before start(), grabbing thread after.
return m_activeRate;
}
void QSurfaceCaptureGrabber::stop()
{
if (m_thread)
{
m_thread->quit();
m_thread->wait();
}
else if (isGrabbingContextInitialized())
{
finalizeGrabbingContext();
}
}
void QSurfaceCaptureGrabber::updateError(QPlatformSurfaceCapture::Error error,
const QString &description)
{
std::optional<QPlatformSurfaceCapture::Error> prevError;
{
std::lock_guard locker{ m_mutex };
prevError = std::exchange(m_prevError, error);
}
if (error != QPlatformSurfaceCapture::Error::NoError
|| prevError != QPlatformSurfaceCapture::Error::NoError) {
Q_EMIT errorUpdated(error, description);
}
updateTimerInterval();
}
void QSurfaceCaptureGrabber::updateTimerInterval()
{
using namespace std::chrono;
std::lock_guard locker{ m_mutex };
if (!m_context)
return;
const qreal rate = m_prevError && *m_prevError != QPlatformSurfaceCapture::Error::NoError
? MinScreenCaptureFrameRate
: m_activeRate;
const nanoseconds interval = round<nanoseconds>(nanoseconds(1s) / rate);
if (m_timerInterval == interval)
return;
QChronoTimer *timer = &m_context->timer;
if (timer->thread()->isCurrentThread()) {
timer->setInterval(interval);
m_timerInterval = interval;
} else {
const quint64 generation = m_contextGeneration;
QMetaObject::invokeMethod(timer, [this, interval, generation] {
setTimerInterval(interval, generation);
}, Qt::QueuedConnection);
}
}
void QSurfaceCaptureGrabber::setTimerInterval(std::chrono::nanoseconds interval, quint64 generation)
{
std::lock_guard locker{ m_mutex };
if (!m_context || generation != m_contextGeneration || m_timerInterval == interval)
return;
m_context->timer.setInterval(interval);
m_timerInterval = interval;
}
void QSurfaceCaptureGrabber::initializeGrabbingContext()
{
Q_ASSERT(!isGrabbingContextInitialized());
qCDebug(qLcScreenCaptureGrabber) << "screen capture started";
{
std::lock_guard locker{ m_mutex };
m_context = std::make_unique<GrabbingContext>();
++m_contextGeneration;
m_timerInterval = std::chrono::nanoseconds{ 0 };
}
m_context->timer.setTimerType(Qt::PreciseTimer);
updateTimerInterval();
m_context->elapsedTimer.start();
auto doGrab = [this]() {
auto measure = m_context->profiler.measure();
auto frame = grabFrame();
if (frame.isValid()) {
frame.setStartTime(m_context->lastFrameTime);
frame.setEndTime(m_context->elapsedTimer.nsecsElapsed() / 1000);
m_context->lastFrameTime = frame.endTime();
updateError(QPlatformSurfaceCapture::Error::NoError);
Q_EMIT frameGrabbed(frame);
}
};
doGrab();
m_context->timer.callOnTimeout(&m_context->timer, doGrab);
m_context->timer.start();
}
void QSurfaceCaptureGrabber::finalizeGrabbingContext()
{
Q_ASSERT(isGrabbingContextInitialized());
qCDebug(qLcScreenCaptureGrabber)
<< "end screen capture thread; avg grabbing time:" << m_context->profiler.avgTime()
<< "ms, grabbings number:" << m_context->profiler.number();
{
// Bump the generation first so queued interval updates posted from
// another thread are dropped instead of touching a dead timer.
std::lock_guard locker{ m_mutex };
++m_contextGeneration;
m_timerInterval = std::chrono::nanoseconds{ 0 };
m_context.reset();
}
}
bool QSurfaceCaptureGrabber::isGrabbingContextInitialized() const
{
std::lock_guard locker{ m_mutex };
return m_context != nullptr;
}
void QSurfaceCaptureGrabber::injectContextToGrabbingThread(QObject *context)
{
Q_ASSERT(m_thread);
Q_ASSERT(QThread::currentThread() == context->thread());
context->moveToThread(m_thread.get());
}
QT_END_NAMESPACE
#include "moc_qsurfacecapturegrabber_p.cpp"