2026-06-26 17:55:15 +08:00
|
|
|
|
#include "DroneScrewCtrlPresenter.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
|
#include <QFont>
|
2026-06-29 13:59:12 +08:00
|
|
|
|
#include <QFontMetrics>
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
|
#include <QDateTime>
|
|
|
|
|
|
#include <QEventLoop>
|
|
|
|
|
|
#include <QElapsedTimer>
|
|
|
|
|
|
#include <QSize>
|
|
|
|
|
|
#include <QThread>
|
|
|
|
|
|
#include <QUrl>
|
|
|
|
|
|
#include <QtGlobal>
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
constexpr int kDefaultRtspPullPort = 8554;
|
|
|
|
|
|
constexpr int kLegacyRtmpPublishPort = 1935;
|
|
|
|
|
|
constexpr unsigned int kRtspOpenTimeoutMs = 5000;
|
|
|
|
|
|
constexpr int kRtspPullMaxAttempts = 5;
|
|
|
|
|
|
constexpr int kRtspPullRetryDelayMs = 500;
|
|
|
|
|
|
constexpr int kRtspFirstFrameWaitMs = 5000;
|
|
|
|
|
|
constexpr size_t kMaxPendingMatchFrames = 30;
|
|
|
|
|
|
constexpr qint64 kMaxPendingMatchAgeMs = 15000;
|
|
|
|
|
|
|
|
|
|
|
|
bool hasReadableImageRows(const void* data,
|
|
|
|
|
|
int width,
|
|
|
|
|
|
int height,
|
|
|
|
|
|
int stride,
|
|
|
|
|
|
size_t size,
|
|
|
|
|
|
unsigned int bytesPerPixel)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!data || width <= 0 || height <= 0 || stride <= 0)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
const size_t minStride = static_cast<size_t>(width) * bytesPerPixel;
|
|
|
|
|
|
if (static_cast<size_t>(stride) < minStride)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
const size_t minSize = static_cast<size_t>(stride) * static_cast<size_t>(height);
|
|
|
|
|
|
return size >= minSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool hasReadableRows(const VrFFPulledFrame& frame, unsigned int bytesPerPixel)
|
|
|
|
|
|
{
|
|
|
|
|
|
return hasReadableImageRows(frame.data,
|
|
|
|
|
|
static_cast<int>(frame.width),
|
|
|
|
|
|
static_cast<int>(frame.height),
|
|
|
|
|
|
static_cast<int>(frame.stride),
|
|
|
|
|
|
frame.size,
|
|
|
|
|
|
bytesPerPixel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString configuredStreamUrl(const DroneScrewCtrlConfigResult& cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString host = QString::fromStdString(cfg.server.serverIp).trimmed();
|
|
|
|
|
|
if (host.isEmpty())
|
|
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
|
|
|
|
int port = cfg.server.rtspPort > 0 ? cfg.server.rtspPort : kDefaultRtspPullPort;
|
|
|
|
|
|
if (port == kLegacyRtmpPublishPort)
|
|
|
|
|
|
port = kDefaultRtspPullPort;
|
|
|
|
|
|
|
|
|
|
|
|
QString path = QString::fromStdString(cfg.server.rtspPath).trimmed();
|
|
|
|
|
|
if (path.isEmpty())
|
|
|
|
|
|
path = QStringLiteral("/live/dronescrew");
|
|
|
|
|
|
if (!path.startsWith('/'))
|
|
|
|
|
|
path.prepend('/');
|
|
|
|
|
|
|
|
|
|
|
|
QString auth;
|
|
|
|
|
|
const QString user = QString::fromStdString(cfg.server.rtspUser);
|
|
|
|
|
|
const QString pass = QString::fromStdString(cfg.server.rtspPass);
|
|
|
|
|
|
if (!user.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
auth = user;
|
|
|
|
|
|
if (!pass.isEmpty())
|
|
|
|
|
|
auth += QStringLiteral(":") + pass;
|
|
|
|
|
|
auth += QStringLiteral("@");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return QStringLiteral("rtsp://%1%2:%3%4").arg(auth).arg(host).arg(port).arg(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString normalizeStreamUrl(QString url, const DroneScrewCtrlConfigResult& cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
url = url.trimmed();
|
|
|
|
|
|
if (url.isEmpty())
|
|
|
|
|
|
return configuredStreamUrl(cfg);
|
|
|
|
|
|
|
|
|
|
|
|
QUrl parsed(url);
|
|
|
|
|
|
if (parsed.isValid() && !parsed.scheme().isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString scheme = parsed.scheme().toLower();
|
|
|
|
|
|
if (scheme == QStringLiteral("rtsp") || scheme == QStringLiteral("rtmp"))
|
|
|
|
|
|
{
|
|
|
|
|
|
parsed.setScheme(QStringLiteral("rtsp"));
|
|
|
|
|
|
const QString configuredHost = QString::fromStdString(cfg.server.serverIp).trimmed();
|
|
|
|
|
|
const QString parsedHost = parsed.host().trimmed();
|
|
|
|
|
|
if (!configuredHost.isEmpty() &&
|
|
|
|
|
|
(parsedHost.isEmpty() ||
|
|
|
|
|
|
parsedHost == QStringLiteral("0.0.0.0") ||
|
|
|
|
|
|
parsedHost == QStringLiteral("127.0.0.1") ||
|
|
|
|
|
|
parsedHost.compare(QStringLiteral("localhost"), Qt::CaseInsensitive) == 0))
|
|
|
|
|
|
{
|
|
|
|
|
|
parsed.setHost(configuredHost);
|
|
|
|
|
|
}
|
|
|
|
|
|
const int configuredPort =
|
|
|
|
|
|
(cfg.server.rtspPort > 0 && cfg.server.rtspPort != kLegacyRtmpPublishPort)
|
|
|
|
|
|
? cfg.server.rtspPort
|
|
|
|
|
|
: kDefaultRtspPullPort;
|
|
|
|
|
|
if (parsed.port(-1) <= 0 || parsed.port(-1) == kLegacyRtmpPublishPort)
|
|
|
|
|
|
parsed.setPort(configuredPort);
|
|
|
|
|
|
return parsed.toString(QUrl::FullyEncoded);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (url.startsWith(QStringLiteral("rtmp://"), Qt::CaseInsensitive))
|
|
|
|
|
|
url.replace(0, 4, QStringLiteral("rtsp"));
|
|
|
|
|
|
url.replace(QStringLiteral(":1935/"), QStringLiteral(":8554/"));
|
|
|
|
|
|
return url;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QSize overlayCoordinateSize(const CtrlDetectionFrame& frame, const QImage& img, const QSize& sourceSize)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sourceSize.width() > 0 && sourceSize.height() > 0)
|
|
|
|
|
|
return sourceSize;
|
|
|
|
|
|
return QSize(frame.imageWidth > 0 ? frame.imageWidth : img.width(),
|
|
|
|
|
|
frame.imageHeight > 0 ? frame.imageHeight : img.height());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
qint64 nowMs()
|
|
|
|
|
|
{
|
|
|
|
|
|
return QDateTime::currentMSecsSinceEpoch();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void processEventsForMs(int waitMs)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (waitMs <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
QElapsedTimer timer;
|
|
|
|
|
|
timer.start();
|
|
|
|
|
|
while (timer.elapsed() < waitMs)
|
|
|
|
|
|
{
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
|
|
|
|
|
QThread::msleep(10);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-26 17:55:15 +08:00
|
|
|
|
|
|
|
|
|
|
DroneScrewCtrlPresenter::DroneScrewCtrlPresenter(QObject* parent)
|
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pReconnectTimer = new QTimer(this);
|
|
|
|
|
|
m_pReconnectTimer->setInterval(3000);
|
|
|
|
|
|
connect(m_pReconnectTimer, &QTimer::timeout, this, &DroneScrewCtrlPresenter::onReconnectTimer);
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
connect(this, &DroneScrewCtrlPresenter::displayFrameReady,
|
2026-06-26 17:55:15 +08:00
|
|
|
|
this, &DroneScrewCtrlPresenter::onOverlayFrameReady,
|
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
connect(this, &DroneScrewCtrlPresenter::detectionResultReady,
|
|
|
|
|
|
this, &DroneScrewCtrlPresenter::onResultReceived,
|
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DroneScrewCtrlPresenter::~DroneScrewCtrlPresenter()
|
|
|
|
|
|
{
|
|
|
|
|
|
DeinitApp();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::ApplyConfig(const DroneScrewCtrlConfigResult& cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_cfg = cfg;
|
|
|
|
|
|
if (m_bRunning.load())
|
|
|
|
|
|
{
|
|
|
|
|
|
DeinitApp();
|
|
|
|
|
|
InitApp();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int DroneScrewCtrlPresenter::InitApp()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_bRunning.load()) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 1) 创建 WDRemoteReceiver 并注入回调(控制 + 结果 + 原始图)
|
|
|
|
|
|
if (IWDRemoteReceiver::CreateInstance(&m_pRemote) != 0 || !m_pRemote)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_bConnected = false;
|
|
|
|
|
|
emit serverConnectionChanged(false);
|
|
|
|
|
|
m_pReconnectTimer->start();
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
m_pRemote->SetEventCallback(
|
|
|
|
|
|
[this](WDRemoteEventType ev, const std::string& msg) {
|
|
|
|
|
|
this->onRemoteEvent(ev, msg);
|
|
|
|
|
|
});
|
|
|
|
|
|
m_pRemote->SetDetectionCallback(
|
|
|
|
|
|
[this](const WDRemoteDetectionFrame& f) {
|
|
|
|
|
|
this->onRemoteDetection(f);
|
|
|
|
|
|
});
|
|
|
|
|
|
// 检测模式下板一关闭 RTSP、改用 ZMQ 原始图通道发图;订阅之以显示检测结果图像
|
|
|
|
|
|
m_pRemote->SetRawImageCallback(
|
|
|
|
|
|
[this](const WDRemoteBinocularRawImage& img) {
|
|
|
|
|
|
this->onRemoteRawImage(img);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 2) UDP 搜索设备 → 默认自动打开第一个;搜不到不再按固定 IP 直接交互。
|
|
|
|
|
|
bool opened = false;
|
|
|
|
|
|
std::vector<WDRemoteDeviceInfo> devices;
|
|
|
|
|
|
if (m_pRemote->SearchDevices(devices, 500) == 0 && !devices.empty())
|
|
|
|
|
|
{
|
|
|
|
|
|
const WDRemoteDeviceInfo& d = devices.front();
|
|
|
|
|
|
if (m_pRemote->Open(d.machineCode) == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 用发现到的设备信息更新连接参数,供 RTSP 拉流使用
|
|
|
|
|
|
if (!d.serverIp.empty()) m_cfg.server.serverIp = d.serverIp;
|
|
|
|
|
|
if (d.controlPort > 0) m_cfg.server.zmqControlPort = d.controlPort;
|
|
|
|
|
|
if (d.resultPort > 0) m_cfg.server.zmqResultPort = d.resultPort;
|
|
|
|
|
|
m_cfg.server.zmqRawImagePort = d.rawImagePort;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
m_streamUrl = resolveStreamUrl(QString::fromStdString(d.rtspUrl));
|
2026-06-26 17:55:15 +08:00
|
|
|
|
opened = true;
|
|
|
|
|
|
emit statusMessage(QStringLiteral("已自动打开设备 %1 (%2)")
|
|
|
|
|
|
.arg(QString::fromStdString(d.deviceName),
|
|
|
|
|
|
QString::fromStdString(d.serverIp)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!opened)
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("未发现 DroneScrewServer,等待自动搜索"));
|
|
|
|
|
|
m_bConnected = false;
|
|
|
|
|
|
emit serverConnectionChanged(false);
|
|
|
|
|
|
delete m_pRemote;
|
|
|
|
|
|
m_pRemote = nullptr;
|
|
|
|
|
|
m_pReconnectTimer->start();
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 拉流器等 start_stream 成功后再打开;地址以 Server get_info/discovery 返回为准。
|
|
|
|
|
|
|
|
|
|
|
|
m_bConnected = true;
|
|
|
|
|
|
m_bRunning = true;
|
|
|
|
|
|
emit serverConnectionChanged(true);
|
|
|
|
|
|
emit statusMessage(QStringLiteral("已连接到服务器"));
|
|
|
|
|
|
|
|
|
|
|
|
// 4) 延迟从 Server 获取参数(相机参数、算法参数)并更新 UI
|
|
|
|
|
|
QTimer::singleShot(200, this, [this]() {
|
|
|
|
|
|
if (m_pRemote && m_bConnected.load())
|
|
|
|
|
|
{
|
|
|
|
|
|
WDRemoteServerInfo info = m_pRemote->GetServerInfo();
|
|
|
|
|
|
if (info.ok)
|
|
|
|
|
|
{
|
|
|
|
|
|
updateStreamInfo(info);
|
|
|
|
|
|
emit serverInfoReceived(info);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::DeinitApp()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop();
|
|
|
|
|
|
|
|
|
|
|
|
releasePuller();
|
|
|
|
|
|
|
|
|
|
|
|
if (m_pRemote)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pRemote->Disconnect();
|
|
|
|
|
|
delete m_pRemote;
|
|
|
|
|
|
m_pRemote = nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_bRunning = false;
|
|
|
|
|
|
m_bConnected = false;
|
|
|
|
|
|
m_streamUrl.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onReconnectTimer()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_bRunning.load() && m_bConnected.load()) { m_pReconnectTimer->stop(); return; }
|
|
|
|
|
|
emit statusMessage(QStringLiteral("尝试重新连接..."));
|
|
|
|
|
|
DeinitApp();
|
|
|
|
|
|
if (InitApp() == 0) m_pReconnectTimer->stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::updateStreamInfo(const WDRemoteServerInfo& info)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!info.ok) return;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
m_streamUrl = resolveStreamUrl(QString::fromStdString(info.rtspUrl));
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::releasePuller()
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
m_rtspReceiving = false;
|
|
|
|
|
|
m_rtspFirstFrameReported = false;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
if (!m_pPuller) return;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
IVrFFMediaPuller* puller = m_pPuller;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
m_pPuller = nullptr;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
puller->SetFrameCallback(FFPulledFrameCallback{});
|
|
|
|
|
|
puller->Stop();
|
|
|
|
|
|
puller->UnInit();
|
|
|
|
|
|
delete puller;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) const
|
|
|
|
|
|
{
|
|
|
|
|
|
return normalizeStreamUrl(streamUrl, m_cfg);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl)
|
|
|
|
|
|
{
|
|
|
|
|
|
releasePuller();
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const QString url = resolveStreamUrl(streamUrl);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
if (url.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("Server 未返回拉流地址"));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller)
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("创建拉流器失败"));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VrFFPullConfig pc;
|
|
|
|
|
|
pc.rtspUrl = url.toStdString();
|
|
|
|
|
|
pc.useTcp = m_cfg.server.rtspUseTcp;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
pc.networkTimeoutMs = kRtspOpenTimeoutMs;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
pc.outputFormat = EFFPullPixelFormat::NV12;
|
|
|
|
|
|
|
|
|
|
|
|
m_pPuller->SetFrameCallback([this](const VrFFPulledFrame& f) {
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!m_rtspReceiving.load() || !m_bRunning.load())
|
|
|
|
|
|
return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
this->onRtspFrame(f);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const int ret = m_pPuller->Init(pc);
|
|
|
|
|
|
if (ret != 0)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url));
|
2026-06-26 17:55:15 +08:00
|
|
|
|
releasePuller();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl)
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString url = resolveStreamUrl(streamUrl);
|
|
|
|
|
|
if (url.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
m_streamUrl = url;
|
|
|
|
|
|
|
|
|
|
|
|
emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url));
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
|
|
|
|
|
if (initPuller(url))
|
|
|
|
|
|
{
|
|
|
|
|
|
m_rtspFirstFrameReported = false;
|
|
|
|
|
|
m_rtspReceiving = true;
|
|
|
|
|
|
const int startRet = m_pPuller ? m_pPuller->Start() : -1;
|
|
|
|
|
|
if (startRet == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_streamUrl = url;
|
|
|
|
|
|
emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
m_rtspReceiving = false;
|
|
|
|
|
|
emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url));
|
|
|
|
|
|
releasePuller();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::waitForRtspFirstFrame(int timeoutMs)
|
|
|
|
|
|
{
|
|
|
|
|
|
QElapsedTimer timer;
|
|
|
|
|
|
timer.start();
|
|
|
|
|
|
while (timer.elapsed() < timeoutMs)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_rtspFirstFrameReported.load())
|
|
|
|
|
|
return true;
|
|
|
|
|
|
if (!m_rtspReceiving.load())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
|
|
|
|
|
QThread::msleep(10);
|
|
|
|
|
|
}
|
|
|
|
|
|
return m_rtspFirstFrameReported.load();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::startPullerWithRetry(const QString& streamUrl)
|
|
|
|
|
|
{
|
|
|
|
|
|
const QString url = resolveStreamUrl(streamUrl);
|
|
|
|
|
|
if (url.isEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
m_streamUrl = url;
|
|
|
|
|
|
|
|
|
|
|
|
for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt)
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("正在拉流(%1/%2): %3")
|
|
|
|
|
|
.arg(attempt)
|
|
|
|
|
|
.arg(kRtspPullMaxAttempts)
|
|
|
|
|
|
.arg(url));
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
|
|
|
|
|
|
|
|
|
|
|
if (startPullerOnce(url) && waitForRtspFirstFrame(kRtspFirstFrameWaitMs))
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
|
|
emit statusMessage(QStringLiteral("本地 RTSP 未收到首帧(%1/%2): %3")
|
|
|
|
|
|
.arg(attempt)
|
|
|
|
|
|
.arg(kRtspPullMaxAttempts)
|
|
|
|
|
|
.arg(url));
|
|
|
|
|
|
releasePuller();
|
|
|
|
|
|
|
|
|
|
|
|
if (attempt < kRtspPullMaxAttempts)
|
|
|
|
|
|
{
|
|
|
|
|
|
emit statusMessage(QStringLiteral("本地 RTSP 拉流失败,准备重试(%1/%2): %3")
|
|
|
|
|
|
.arg(attempt + 1)
|
|
|
|
|
|
.arg(kRtspPullMaxAttempts)
|
|
|
|
|
|
.arg(url));
|
|
|
|
|
|
processEventsForMs(kRtspPullRetryDelayMs);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:55:15 +08:00
|
|
|
|
bool DroneScrewCtrlPresenter::TriggerSingleDetection()
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_pRemote && m_pRemote->RequestSingleDetection() == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::StartLiveStream()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
releasePuller();
|
2026-06-29 13:59:12 +08:00
|
|
|
|
clearCachedDisplayState();
|
2026-06-26 17:55:15 +08:00
|
|
|
|
m_pRemote->StopWork(WDRemoteWorkMode::Detection);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit statusMessage(QStringLiteral("正在请求服务端启动距离检测..."));
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream) == 0;
|
|
|
|
|
|
if (ok)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit statusMessage(QStringLiteral("服务端已启动,正在获取拉流地址..."));
|
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
WDRemoteServerInfo info = m_pRemote->GetServerInfo();
|
|
|
|
|
|
if (info.ok)
|
|
|
|
|
|
{
|
|
|
|
|
|
updateStreamInfo(info);
|
|
|
|
|
|
emit serverInfoReceived(info);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const bool pullOk = startPullerWithRetry(m_streamUrl);
|
|
|
|
|
|
if (!pullOk)
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit statusMessage(QStringLiteral("服务端已开流,但本地 RTSP 多次拉流未启动: %1").arg(m_streamUrl));
|
|
|
|
|
|
return true;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit statusMessage(QStringLiteral("Server 已开流,RTSP 已出图: %1").arg(m_streamUrl));
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::StopLiveStream()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
// 停止 RTSP 拉流
|
|
|
|
|
|
releasePuller();
|
|
|
|
|
|
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream) == 0;
|
|
|
|
|
|
if (ok) emit statusMessage(QStringLiteral("Server 已关流"));
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::StartDetection()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
// 检测模式下停止 RTSP 拉流(改用 ZMQ 原始图像通道)
|
|
|
|
|
|
releasePuller();
|
2026-06-29 13:59:12 +08:00
|
|
|
|
clearCachedDisplayState();
|
2026-06-26 17:55:15 +08:00
|
|
|
|
m_pRemote->StopWork(WDRemoteWorkMode::LiveStream);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用单目传输模式:仅接收左目图像,减少网络带宽消耗
|
|
|
|
|
|
m_pRemote->SetDetectMode("mono");
|
|
|
|
|
|
|
|
|
|
|
|
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::Detection) == 0;
|
|
|
|
|
|
if (ok) emit statusMessage(QStringLiteral("Server 已开始检测(单目传输)"));
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::StopDetection()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::Detection) == 0;
|
|
|
|
|
|
if (ok) emit statusMessage(QStringLiteral("Server 已停止检测"));
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::StopAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
// 停止 RTSP 拉流
|
|
|
|
|
|
releasePuller();
|
2026-06-29 13:59:12 +08:00
|
|
|
|
clearCachedDisplayState();
|
2026-06-26 17:55:15 +08:00
|
|
|
|
// 两种模式都停掉,保证服务端回到空闲(幂等)
|
|
|
|
|
|
m_pRemote->StopWork(WDRemoteWorkMode::Detection, 1000);
|
|
|
|
|
|
m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, 1000);
|
|
|
|
|
|
emit statusMessage(QStringLiteral("Server 已停止(实时/检测)"));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::SetServerExposure(double exposureTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_pRemote && m_pRemote->SetExposure(exposureTime) == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::SetServerGain(double gain)
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_pRemote && m_pRemote->SetGain(gain) == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& params)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_pRemote) return false;
|
|
|
|
|
|
WDRemoteAlgoParams p;
|
|
|
|
|
|
p.scoreThreshold = params.scoreThreshold;
|
|
|
|
|
|
p.nmsThreshold = params.nmsThreshold;
|
|
|
|
|
|
p.inputWidth = params.inputWidth;
|
|
|
|
|
|
p.inputHeight = params.inputHeight;
|
|
|
|
|
|
p.modelType = params.modelType;
|
|
|
|
|
|
p.modelPath = params.modelPath;
|
|
|
|
|
|
return m_pRemote->SetAlgoParams(p) == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// WDRemoteReceiver 回调
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f)
|
|
|
|
|
|
{
|
|
|
|
|
|
CtrlDetectionFrame frame = toCtrlFrame(f);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (m_rtspReceiving.load())
|
|
|
|
|
|
{
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastResultMutex);
|
|
|
|
|
|
m_lastResult = frame;
|
|
|
|
|
|
m_bHasResult = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
emit detectionResultReady(frame);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DisplayFrame matchedFrame;
|
|
|
|
|
|
bool hasMatchedImage = false;
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastImgMutex);
|
|
|
|
|
|
auto rawIt = m_pendingRawFrames.find(frame.frameId);
|
|
|
|
|
|
if (rawIt != m_pendingRawFrames.end())
|
|
|
|
|
|
{
|
|
|
|
|
|
matchedFrame = rawIt->second.frame;
|
|
|
|
|
|
m_pendingRawFrames.erase(rawIt);
|
|
|
|
|
|
prunePendingMatchesLocked();
|
|
|
|
|
|
hasMatchedImage = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pendingDetectionResults[frame.frameId] = PendingDetectionResult{frame, nowMs()};
|
|
|
|
|
|
prunePendingMatchesLocked();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasMatchedImage)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastResultMutex);
|
|
|
|
|
|
m_lastResult = frame;
|
|
|
|
|
|
m_bHasResult = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
emit detectionResultReady(frame);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
emit displayFrameReady(matchedFrame.image, matchedFrame.sourceSize, frame.frameId, false, frame, true);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& img)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。
|
|
|
|
|
|
// 转为 QImage,走与 RTSP 相同的叠加+显示通路(onOverlayFrameReady 会叠加检测框)。
|
|
|
|
|
|
if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (m_rtspReceiving.load()) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
|
|
|
|
|
|
QImage q;
|
|
|
|
|
|
switch (img.leftPixelFormat)
|
|
|
|
|
|
{
|
|
|
|
|
|
case WDRemotePixelFormat::MONO8:
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
|
|
|
|
|
|
img.leftStride, img.leftDataSize, 1)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
|
|
|
|
|
|
img.leftWidth, img.leftHeight, img.leftStride,
|
|
|
|
|
|
QImage::Format_Grayscale8).copy();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case WDRemotePixelFormat::RGB8:
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
|
|
|
|
|
|
img.leftStride, img.leftDataSize, 3)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
|
|
|
|
|
|
img.leftWidth, img.leftHeight, img.leftStride,
|
|
|
|
|
|
QImage::Format_RGB888).copy();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case WDRemotePixelFormat::BGR8:
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
|
|
|
|
|
|
img.leftStride, img.leftDataSize, 3)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
|
|
|
|
|
|
img.leftWidth, img.leftHeight, img.leftStride,
|
|
|
|
|
|
QImage::Format_RGB888).rgbSwapped();
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!q.isNull())
|
|
|
|
|
|
{
|
|
|
|
|
|
const qint64 frameId = static_cast<qint64>(img.frameId);
|
|
|
|
|
|
const QSize sourceSize(img.leftSourceWidth, img.leftSourceHeight);
|
|
|
|
|
|
const DisplayFrame displayFrame{q, sourceSize};
|
|
|
|
|
|
CtrlDetectionFrame matchedResult;
|
|
|
|
|
|
bool hasMatchedResult = false;
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastImgMutex);
|
|
|
|
|
|
auto resultIt = m_pendingDetectionResults.find(frameId);
|
|
|
|
|
|
if (resultIt != m_pendingDetectionResults.end())
|
|
|
|
|
|
{
|
|
|
|
|
|
matchedResult = resultIt->second.result;
|
|
|
|
|
|
m_pendingDetectionResults.erase(resultIt);
|
|
|
|
|
|
prunePendingMatchesLocked();
|
|
|
|
|
|
hasMatchedResult = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pendingRawFrames[frameId] = PendingRawFrame{displayFrame, nowMs()};
|
|
|
|
|
|
prunePendingMatchesLocked();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasMatchedResult)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastResultMutex);
|
|
|
|
|
|
m_lastResult = matchedResult;
|
|
|
|
|
|
m_bHasResult = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
emit detectionResultReady(matchedResult);
|
|
|
|
|
|
emit displayFrameReady(q, sourceSize, frameId, false, matchedResult, true);
|
|
|
|
|
|
}
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onRemoteEvent(WDRemoteEventType ev, const std::string& msg)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (ev)
|
|
|
|
|
|
{
|
|
|
|
|
|
case WDRemoteEventType::CONNECTED:
|
|
|
|
|
|
m_bConnected = true;
|
|
|
|
|
|
emit serverConnectionChanged(true);
|
|
|
|
|
|
emit statusMessage(QStringLiteral("控制通道已连接"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case WDRemoteEventType::DISCONNECTED:
|
|
|
|
|
|
m_bConnected = false;
|
|
|
|
|
|
emit serverConnectionChanged(false);
|
|
|
|
|
|
emit statusMessage(QStringLiteral("控制通道已断开"));
|
|
|
|
|
|
if (m_bRunning.load()) m_pReconnectTimer->start();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case WDRemoteEventType::CONNECTION_ERROR:
|
|
|
|
|
|
m_bConnected = false;
|
|
|
|
|
|
emit serverConnectionChanged(false);
|
|
|
|
|
|
emit statusMessage(QStringLiteral("连接错误: %1").arg(QString::fromStdString(msg)));
|
|
|
|
|
|
if (m_bRunning.load()) m_pReconnectTimer->start();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case WDRemoteEventType::REQUEST_TIMEOUT:
|
|
|
|
|
|
emit statusMessage(QStringLiteral("请求超时"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CtrlDetectionFrame DroneScrewCtrlPresenter::toCtrlFrame(const WDRemoteDetectionFrame& f)
|
|
|
|
|
|
{
|
|
|
|
|
|
CtrlDetectionFrame frame;
|
|
|
|
|
|
frame.frameId = static_cast<qint64>(f.frameId);
|
|
|
|
|
|
frame.timestampUs = f.timestampUs;
|
|
|
|
|
|
frame.imageWidth = f.imageWidth;
|
|
|
|
|
|
frame.imageHeight = f.imageHeight;
|
|
|
|
|
|
frame.success = f.success;
|
|
|
|
|
|
frame.errorCode = f.errorCode;
|
|
|
|
|
|
frame.message = QString::fromStdString(f.message);
|
|
|
|
|
|
frame.boxes.reserve(f.boxes.size());
|
|
|
|
|
|
for (const auto& b : f.boxes)
|
|
|
|
|
|
{
|
|
|
|
|
|
CtrlDetectionBox cb;
|
|
|
|
|
|
cb.classId = b.classId;
|
|
|
|
|
|
cb.score = b.score;
|
|
|
|
|
|
cb.x = b.x;
|
|
|
|
|
|
cb.y = b.y;
|
|
|
|
|
|
cb.width = b.width;
|
|
|
|
|
|
cb.height = b.height;
|
|
|
|
|
|
frame.boxes.push_back(cb);
|
|
|
|
|
|
}
|
|
|
|
|
|
frame.distances.reserve(f.distances.size());
|
|
|
|
|
|
for (const auto& d : f.distances)
|
|
|
|
|
|
{
|
|
|
|
|
|
CtrlDetectionDistance cd;
|
|
|
|
|
|
cd.fromId = d.fromId;
|
|
|
|
|
|
cd.toId = d.toId;
|
|
|
|
|
|
cd.distanceMm = d.distanceMm;
|
|
|
|
|
|
frame.distances.push_back(cd);
|
|
|
|
|
|
}
|
|
|
|
|
|
return frame;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// RTSP
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onRtspFrame(const VrFFPulledFrame& frame)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!m_rtspReceiving.load()) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
|
|
|
|
|
|
QImage img;
|
|
|
|
|
|
if (frame.format == EFFPullPixelFormat::NV12)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableRows(frame, 1)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
img = QImage(static_cast<const uchar*>(frame.data),
|
|
|
|
|
|
frame.width, frame.height, frame.stride,
|
|
|
|
|
|
QImage::Format_Grayscale8).copy();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (frame.format == EFFPullPixelFormat::RGBA32)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableRows(frame, 4)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
img = QImage(static_cast<const uchar*>(frame.data),
|
|
|
|
|
|
frame.width, frame.height, frame.stride,
|
|
|
|
|
|
QImage::Format_RGBA8888).copy();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (frame.format == EFFPullPixelFormat::BGR24)
|
|
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!hasReadableRows(frame, 3)) return;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
img = QImage(static_cast<const uchar*>(frame.data),
|
|
|
|
|
|
frame.width, frame.height, frame.stride,
|
|
|
|
|
|
QImage::Format_BGR888).copy();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (!m_rtspFirstFrameReported.exchange(true))
|
|
|
|
|
|
emit statusMessage(QStringLiteral("RTSP 已出图"));
|
|
|
|
|
|
emit displayFrameReady(img, img.size(), -1, true, CtrlDetectionFrame{}, false);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img,
|
|
|
|
|
|
const QSize& sourceSize,
|
|
|
|
|
|
qint64 frameId,
|
|
|
|
|
|
bool rtspFrame,
|
|
|
|
|
|
const CtrlDetectionFrame& result,
|
|
|
|
|
|
bool hasResult)
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
QImage frame = img;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
if (hasResult && !rtspFrame)
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
overlayDetectionBoxes(frame, result, sourceSize);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastImgMutex);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
m_lastSourceImage = img;
|
|
|
|
|
|
m_lastSourceFrameId = frameId;
|
|
|
|
|
|
m_lastSourceIsRtsp = rtspFrame;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
m_lastDisplay = frame;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (m_pReceiver) m_pReceiver->OnRtspFrame(frame);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::onResultReceived(const CtrlDetectionFrame& result)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_pReceiver) m_pReceiver->OnDetectionResult(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
void DroneScrewCtrlPresenter::clearCachedDisplayState()
|
|
|
|
|
|
{
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastResultMutex);
|
|
|
|
|
|
m_bHasResult = false;
|
|
|
|
|
|
m_lastResult = CtrlDetectionFrame{};
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastImgMutex);
|
|
|
|
|
|
m_lastSourceImage = QImage();
|
|
|
|
|
|
m_lastSourceFrameId = -1;
|
|
|
|
|
|
m_lastSourceIsRtsp = false;
|
|
|
|
|
|
m_lastDisplay = QImage();
|
|
|
|
|
|
m_pendingRawFrames.clear();
|
|
|
|
|
|
m_pendingDetectionResults.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::prunePendingMatchesLocked()
|
|
|
|
|
|
{
|
|
|
|
|
|
const qint64 expireBefore = nowMs() - kMaxPendingMatchAgeMs;
|
|
|
|
|
|
for (auto it = m_pendingRawFrames.begin(); it != m_pendingRawFrames.end(); )
|
|
|
|
|
|
{
|
|
|
|
|
|
if (it->second.receivedAtMs < expireBefore)
|
|
|
|
|
|
it = m_pendingRawFrames.erase(it);
|
|
|
|
|
|
else
|
|
|
|
|
|
++it;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (auto it = m_pendingDetectionResults.begin(); it != m_pendingDetectionResults.end(); )
|
|
|
|
|
|
{
|
|
|
|
|
|
if (it->second.receivedAtMs < expireBefore)
|
|
|
|
|
|
it = m_pendingDetectionResults.erase(it);
|
|
|
|
|
|
else
|
|
|
|
|
|
++it;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
while (m_pendingRawFrames.size() > kMaxPendingMatchFrames)
|
|
|
|
|
|
m_pendingRawFrames.erase(m_pendingRawFrames.begin());
|
|
|
|
|
|
while (m_pendingDetectionResults.size() > kMaxPendingMatchFrames)
|
|
|
|
|
|
m_pendingDetectionResults.erase(m_pendingDetectionResults.begin());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img,
|
|
|
|
|
|
const CtrlDetectionFrame& frame,
|
|
|
|
|
|
const QSize& sourceSize)
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (!m_cfg.display.drawBoxes || frame.boxes.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
QPainter p(&img);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
|
|
const int thickness = m_cfg.display.boxThickness > 0 ? m_cfg.display.boxThickness : 1;
|
|
|
|
|
|
QPen pen(QColor(0, 0, 0), thickness);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
p.setPen(pen);
|
2026-06-29 13:59:12 +08:00
|
|
|
|
QFont f = p.font();
|
|
|
|
|
|
f.setPointSize(28);
|
|
|
|
|
|
f.setBold(true);
|
|
|
|
|
|
p.setFont(f);
|
|
|
|
|
|
const QFontMetrics fm(f);
|
|
|
|
|
|
|
|
|
|
|
|
const QSize coordSize = overlayCoordinateSize(frame, img, sourceSize);
|
|
|
|
|
|
const int coordWidth = coordSize.width();
|
|
|
|
|
|
const int coordHeight = coordSize.height();
|
|
|
|
|
|
|
|
|
|
|
|
const double scaleX = (coordWidth > 0)
|
|
|
|
|
|
? static_cast<double>(img.width()) / coordWidth
|
2026-06-26 17:55:15 +08:00
|
|
|
|
: 1.0;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const double scaleY = (coordHeight > 0)
|
|
|
|
|
|
? static_cast<double>(img.height()) / coordHeight
|
2026-06-26 17:55:15 +08:00
|
|
|
|
: 1.0;
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const QRect imageRect(0, 0, img.width(), img.height());
|
2026-06-26 17:55:15 +08:00
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
for (size_t i = 0; i < frame.boxes.size(); ++i)
|
2026-06-26 17:55:15 +08:00
|
|
|
|
{
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const auto& b = frame.boxes[i];
|
|
|
|
|
|
QRect r(qRound(static_cast<double>(b.x) * scaleX),
|
|
|
|
|
|
qRound(static_cast<double>(b.y) * scaleY),
|
|
|
|
|
|
qRound(static_cast<double>(b.width) * scaleX),
|
|
|
|
|
|
qRound(static_cast<double>(b.height) * scaleY));
|
|
|
|
|
|
r = r.normalized().intersected(imageRect);
|
|
|
|
|
|
if (r.isEmpty())
|
|
|
|
|
|
continue;
|
2026-06-26 17:55:15 +08:00
|
|
|
|
p.drawRect(r);
|
|
|
|
|
|
|
2026-06-29 13:59:12 +08:00
|
|
|
|
const QString label = QString::number(static_cast<int>(i + 1));
|
|
|
|
|
|
const QPoint labelPos(r.right() + 4, r.top() + fm.ascent());
|
|
|
|
|
|
p.drawText(labelPos, label);
|
2026-06-26 17:55:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QImage DroneScrewCtrlPresenter::GetLastDisplayImage() const
|
|
|
|
|
|
{
|
|
|
|
|
|
QMutexLocker lk(&m_lastImgMutex);
|
|
|
|
|
|
return m_lastDisplay;
|
|
|
|
|
|
}
|