diff --git a/App/App.pro b/App/App.pro index fb378196..f1c27549 100644 --- a/App/App.pro +++ b/App/App.pro @@ -45,6 +45,10 @@ isEmpty(TARGET_APP) { SUBDIRS += BinocularMark/BinocularMark.pro } else:equals(TARGET_APP, "WorkpieceProject") { SUBDIRS += WorkpieceProject/WorkpieceProject.pro + } else:equals(TARGET_APP, "WorkpiecePosition") { + SUBDIRS += WorkpieceProject/WorkpieceProject.pro + } else:equals(TARGET_APP, "WorkpieceSplice") { + SUBDIRS += WorkpieceProject/WorkpieceProject.pro } else:equals(TARGET_APP, "TunnelChannel") { SUBDIRS += TunnelChannel/TunnelChannel.pro } else:equals(TARGET_APP, "WheelMeasure") { diff --git a/App/BeltTearing/BeltTearing.pro b/App/BeltTearing/BeltTearing.pro index 18cc223e..6a35d762 100644 --- a/App/BeltTearing/BeltTearing.pro +++ b/App/BeltTearing/BeltTearing.pro @@ -1,6 +1,17 @@ TEMPLATE = subdirs -# 撕裂项目 -SUBDIRS += BeltTearingConfig/BeltTearingConfig.pro -SUBDIRS += BeltTearingApp/BeltTearingApp.pro -SUBDIRS += BeltTearingServer/BeltTearingServer.pro +BeltTearingConfig.file = BeltTearingConfig/BeltTearingConfig.pro +BeltTearingApp.file = BeltTearingApp/BeltTearingApp.pro +BeltTearingServer.file = BeltTearingServer/BeltTearingServer.pro + +SUBDIRS += BeltTearingConfig + +equals(TARGET_APP, "BeltTearing") { + SUBDIRS += BeltTearingServer + BeltTearingServer.depends = BeltTearingConfig +} else { + SUBDIRS += BeltTearingApp + SUBDIRS += BeltTearingServer + BeltTearingApp.depends = BeltTearingConfig + BeltTearingServer.depends = BeltTearingConfig +} diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Inc/DroneScrewCtrlPresenter.h b/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Inc/DroneScrewCtrlPresenter.h index bd06b753..ccc14bdd 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Inc/DroneScrewCtrlPresenter.h +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Inc/DroneScrewCtrlPresenter.h @@ -80,8 +80,8 @@ private slots: private: // RTSP 拉流回调 void onRtspFrame(const VrFFPulledFrame& frame); - bool initPuller(const QString& streamUrl); - bool startPullerOnce(const QString& streamUrl); + bool initPuller(const QString& streamUrl, bool quiet = false); + bool startPullerOnce(const QString& streamUrl, bool quiet = false); bool waitForRtspFirstFrame(int timeoutMs); bool startPullerWithRetry(const QString& streamUrl); void releasePuller(); @@ -128,6 +128,7 @@ private: std::atomic m_bConnected{false}; std::atomic m_bRunning{false}; + std::atomic m_liveStreamMode{false}; std::atomic m_rtspReceiving{false}; std::atomic m_rtspFirstFrameReported{false}; QTimer* m_pReconnectTimer{nullptr}; diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Src/DroneScrewCtrlPresenter.cpp b/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Src/DroneScrewCtrlPresenter.cpp index 65dd8106..64243407 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Src/DroneScrewCtrlPresenter.cpp +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/Presenter/Src/DroneScrewCtrlPresenter.cpp @@ -11,15 +11,18 @@ #include #include #include +#include namespace { constexpr int kDefaultRtspPullPort = 8554; constexpr int kLegacyRtmpPublishPort = 1935; -constexpr unsigned int kRtspOpenTimeoutMs = 5000; -constexpr int kRtspPullMaxAttempts = 5; +constexpr unsigned int kRtspOpenTimeoutMs = 10000; +constexpr int kRtspPullMaxAttempts = 20; constexpr int kRtspPullRetryDelayMs = 500; -constexpr int kRtspFirstFrameWaitMs = 5000; +constexpr int kRtspFirstFrameWaitMs = 10000; +constexpr int kControlStartTimeoutMs = 30000; +constexpr int kControlStopTimeoutMs = 30000; constexpr size_t kMaxPendingMatchFrames = 30; constexpr qint64 kMaxPendingMatchAgeMs = 15000; @@ -124,8 +127,9 @@ QSize overlayCoordinateSize(const CtrlDetectionFrame& frame, const QImage& img, { 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()); + if (frame.imageWidth > 0 && frame.imageHeight > 0) + return QSize(frame.imageWidth, frame.imageHeight); + return img.size(); } qint64 nowMs() @@ -261,6 +265,7 @@ void DroneScrewCtrlPresenter::DeinitApp() { if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop(); + m_liveStreamMode = false; releasePuller(); if (m_pRemote) @@ -307,20 +312,22 @@ QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) cons return normalizeStreamUrl(streamUrl, m_cfg); } -bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl) +bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl, bool quiet) { releasePuller(); const QString url = resolveStreamUrl(streamUrl); if (url.isEmpty()) { - emit statusMessage(QStringLiteral("Server 未返回拉流地址")); + if (!quiet) + emit statusMessage(QStringLiteral("Server 未返回拉流地址")); return false; } if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller) { - emit statusMessage(QStringLiteral("创建拉流器失败")); + if (!quiet) + emit statusMessage(QStringLiteral("创建拉流器失败")); return false; } @@ -339,7 +346,8 @@ bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl) const int ret = m_pPuller->Init(pc); if (ret != 0) { - emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url)); + if (!quiet) + emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url)); releasePuller(); return false; } @@ -347,19 +355,23 @@ bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl) return true; } -bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl) +bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl, bool quiet) { const QString url = resolveStreamUrl(streamUrl); if (url.isEmpty()) { - emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址")); + if (!quiet) + emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址")); return false; } m_streamUrl = url; - emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url)); - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); - if (initPuller(url)) + if (!quiet) + { + emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url)); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); + } + if (initPuller(url, quiet)) { m_rtspFirstFrameReported = false; m_rtspReceiving = true; @@ -367,15 +379,18 @@ bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl) if (startRet == 0) { m_streamUrl = url; - emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url)); + if (!quiet) + emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url)); return true; } m_rtspReceiving = false; - emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url)); + if (!quiet) + emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url)); releasePuller(); } - emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url)); + if (!quiet) + emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url)); return false; } @@ -405,33 +420,24 @@ bool DroneScrewCtrlPresenter::startPullerWithRetry(const QString& streamUrl) } m_streamUrl = url; + emit statusMessage(QStringLiteral("正在拉流: %1").arg(url)); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt) { - emit statusMessage(QStringLiteral("正在拉流(%1/%2): %3") - .arg(attempt) - .arg(kRtspPullMaxAttempts) - .arg(url)); - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); + const bool started = startPullerOnce(url, true); - if (startPullerOnce(url) && waitForRtspFirstFrame(kRtspFirstFrameWaitMs)) + if (started && 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); } } + emit statusMessage(QStringLiteral("本地 RTSP 拉流未收到首帧,Server 推流保持运行: %1").arg(url)); return false; } @@ -445,12 +451,15 @@ bool DroneScrewCtrlPresenter::StartLiveStream() if (!m_pRemote) return false; releasePuller(); clearCachedDisplayState(); - m_pRemote->StopWork(WDRemoteWorkMode::Detection); + m_liveStreamMode = false; emit statusMessage(QStringLiteral("正在请求服务端启动距离检测...")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); - bool ok = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream) == 0; + const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream, + kControlStartTimeoutMs); + bool ok = (startRet == 0); if (ok) { + m_liveStreamMode = true; emit statusMessage(QStringLiteral("服务端已启动,正在获取拉流地址...")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); WDRemoteServerInfo info = m_pRemote->GetServerInfo(); @@ -463,60 +472,85 @@ bool DroneScrewCtrlPresenter::StartLiveStream() const bool pullOk = startPullerWithRetry(m_streamUrl); if (!pullOk) { - emit statusMessage(QStringLiteral("服务端已开流,但本地 RTSP 多次拉流未启动: %1").arg(m_streamUrl)); + releasePuller(); + emit statusMessage(QStringLiteral("Server 已开流,本地暂未拉到图像: %1").arg(m_streamUrl)); return true; } emit statusMessage(QStringLiteral("Server 已开流,RTSP 已出图: %1").arg(m_streamUrl)); } + if (!ok) + { + emit statusMessage(QStringLiteral("Server 启动距离检测失败 ret=%1").arg(startRet)); + m_liveStreamMode = false; + } return ok; } bool DroneScrewCtrlPresenter::StopLiveStream() { if (!m_pRemote) return false; + m_liveStreamMode = false; // 停止 RTSP 拉流 releasePuller(); - bool ok = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream) == 0; + const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, + kControlStopTimeoutMs); + bool ok = (stopRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已关流")); + else emit statusMessage(QStringLiteral("Server 关流失败 ret=%1").arg(stopRet)); return ok; } bool DroneScrewCtrlPresenter::StartDetection() { if (!m_pRemote) return false; + m_liveStreamMode = false; // 检测模式下停止 RTSP 拉流(改用 ZMQ 原始图像通道) releasePuller(); clearCachedDisplayState(); - m_pRemote->StopWork(WDRemoteWorkMode::LiveStream); // 使用单目传输模式:仅接收左目图像,减少网络带宽消耗 m_pRemote->SetDetectMode("mono"); - bool ok = m_pRemote->StartWork(WDRemoteWorkMode::Detection) == 0; + const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::Detection, + kControlStartTimeoutMs); + bool ok = (startRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已开始检测(单目传输)")); + else emit statusMessage(QStringLiteral("Server 启动精准检测失败 ret=%1").arg(startRet)); return ok; } bool DroneScrewCtrlPresenter::StopDetection() { if (!m_pRemote) return false; - bool ok = m_pRemote->StopWork(WDRemoteWorkMode::Detection) == 0; + const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection, + kControlStopTimeoutMs); + bool ok = (stopRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已停止检测")); + else emit statusMessage(QStringLiteral("Server 停止检测失败 ret=%1").arg(stopRet)); return ok; } bool DroneScrewCtrlPresenter::StopAll() { if (!m_pRemote) return false; + m_liveStreamMode = false; // 停止 RTSP 拉流 releasePuller(); clearCachedDisplayState(); // 两种模式都停掉,保证服务端回到空闲(幂等) - m_pRemote->StopWork(WDRemoteWorkMode::Detection, 1000); - m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, 1000); - emit statusMessage(QStringLiteral("Server 已停止(实时/检测)")); - return true; + const int streamRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, + kControlStopTimeoutMs); + const int detectRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection, + kControlStopTimeoutMs); + const bool ok = (streamRet == 0 && detectRet == 0); + if (ok) + emit statusMessage(QStringLiteral("Server 已停止(实时/检测)")); + else + emit statusMessage(QStringLiteral("Server 停止失败 streamRet=%1 detectRet=%2") + .arg(streamRet) + .arg(detectRet)); + return ok; } bool DroneScrewCtrlPresenter::SetServerExposure(double exposureTime) @@ -548,7 +582,7 @@ bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& param void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f) { CtrlDetectionFrame frame = toCtrlFrame(f); - if (m_rtspReceiving.load()) + if (m_liveStreamMode.load() || m_rtspReceiving.load()) { { QMutexLocker lk(&m_lastResultMutex); @@ -579,7 +613,15 @@ void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f) } if (!hasMatchedImage) + { + { + QMutexLocker lk(&m_lastResultMutex); + m_lastResult = frame; + m_bHasResult = true; + } + emit detectionResultReady(frame); return; + } { QMutexLocker lk(&m_lastResultMutex); @@ -595,7 +637,7 @@ void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& // 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。 // 转为 QImage,走与 RTSP 相同的叠加+显示通路(onOverlayFrameReady 会叠加检测框)。 if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return; - if (m_rtspReceiving.load()) return; + if (m_liveStreamMode.load() || m_rtspReceiving.load()) return; QImage q; switch (img.leftPixelFormat) @@ -649,7 +691,9 @@ void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& } if (!hasMatchedResult) + { return; + } { QMutexLocker lk(&m_lastResultMutex); @@ -757,9 +801,21 @@ void DroneScrewCtrlPresenter::onRtspFrame(const VrFFPulledFrame& frame) return; } + CtrlDetectionFrame latestResult; + bool hasLatestResult = false; + if (m_liveStreamMode.load()) + { + QMutexLocker lk(&m_lastResultMutex); + if (m_bHasResult) + { + latestResult = m_lastResult; + hasLatestResult = true; + } + } + if (!m_rtspFirstFrameReported.exchange(true)) emit statusMessage(QStringLiteral("RTSP 已出图")); - emit displayFrameReady(img, img.size(), -1, true, CtrlDetectionFrame{}, false); + emit displayFrameReady(img, img.size(), -1, true, latestResult, hasLatestResult); } void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img, @@ -770,9 +826,16 @@ void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img, bool hasResult) { QImage frame = img; - if (hasResult && !rtspFrame) + if (hasResult && !m_liveStreamMode.load()) { - overlayDetectionBoxes(frame, result, sourceSize); + QSize overlaySourceSize = sourceSize; + if (rtspFrame) + { + overlaySourceSize = (result.imageWidth > 0 && result.imageHeight > 0) + ? QSize(result.imageWidth, result.imageHeight) + : img.size(); + } + overlayDetectionBoxes(frame, result, overlaySourceSize); } { @@ -839,9 +902,10 @@ void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img, if (!m_cfg.display.drawBoxes || frame.boxes.empty()) return; QPainter p(&img); - p.setRenderHint(QPainter::Antialiasing, true); - const int thickness = m_cfg.display.boxThickness > 0 ? m_cfg.display.boxThickness : 1; + p.setRenderHint(QPainter::Antialiasing, false); + const int thickness = std::max(2, m_cfg.display.boxThickness); QPen pen(QColor(0, 0, 0), thickness); + pen.setJoinStyle(Qt::MiterJoin); p.setPen(pen); QFont f = p.font(); f.setPointSize(28); @@ -864,10 +928,15 @@ void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img, for (size_t i = 0; i < frame.boxes.size(); ++i) { const auto& b = frame.boxes[i]; - QRect r(qRound(static_cast(b.x) * scaleX), - qRound(static_cast(b.y) * scaleY), - qRound(static_cast(b.width) * scaleX), - qRound(static_cast(b.height) * scaleY)); + if (b.width <= 0 || b.height <= 0) + continue; + const int x1 = qRound(static_cast(b.x) * scaleX); + const int y1 = qRound(static_cast(b.y) * scaleY); + int x2 = qRound(static_cast(b.x + b.width) * scaleX) - 1; + int y2 = qRound(static_cast(b.y + b.height) * scaleY) - 1; + if (x2 < x1) x2 = x1; + if (y2 < y1) y2 = y1; + QRect r(QPoint(x1, y1), QPoint(x2, y2)); r = r.normalized().intersected(imageRect); if (r.isEmpty()) continue; diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml b/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml index e5eb90bb..f17d7cf9 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml @@ -25,7 +25,7 @@ diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.cpp b/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.cpp index bd57bcd7..77de336b 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.cpp +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.cpp @@ -556,9 +556,16 @@ void MainWindow::on_btn_start_clicked() ui->btn_start->setEnabled(false); ui->btn_detect_start->setEnabled(false); QApplication::processEvents(); - if (m_pPresenter) m_pPresenter->StopAll(); - appendLog(QStringLiteral("已请求 Server 停止")); - applyUiState(UiState::Idle); + const bool ok = m_pPresenter && m_pPresenter->StopAll(); + if (ok) + { + appendLog(QStringLiteral("已请求 Server 停止")); + applyUiState(UiState::Idle); + } + else + { + appendLog(QStringLiteral("Server 停止失败,状态未切换")); + } ui->btn_start->setEnabled(true); ui->btn_detect_start->setEnabled(true); } diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/resultitem.cpp b/App/DroneScrewbolt/DroneScrewCtrlApp/resultitem.cpp index bb9b2bef..687f73cf 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/resultitem.cpp +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/resultitem.cpp @@ -51,6 +51,17 @@ QString distanceText(const CtrlDetectionFrame& frame, int targetIndex) ? QStringLiteral("未测到距离") : distanceParts.join(" "); } + +QString distanceIndexText(const CtrlDetectionFrame& frame, int targetIndex) +{ + const int total = static_cast(frame.distances.size()); + if (targetIndex >= 0 && targetIndex < total) + { + const CtrlDetectionDistance& d = frame.distances[static_cast(targetIndex)]; + return QString::number(d.toId); + } + return QStringLiteral("-"); +} } ResultItem::ResultItem(QWidget* parent) @@ -112,11 +123,7 @@ void ResultItem::setResultData(int targetIndex, .arg(static_cast(frame.boxes.size()))); else if (mode == DisplayMode::Distance) { - const int total = static_cast(frame.distances.size()); - if (targetIndex >= 0 && targetIndex < total) - ui->label_id->setText(QString("%1/%2").arg(targetIndex + 1).arg(total)); - else - ui->label_id->setText(QString("%1/%1").arg(total)); + ui->label_id->setText(distanceIndexText(frame, targetIndex)); } else ui->label_id->setText(QStringLiteral("0/0")); diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp index 2d5be50b..e264f07a 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp @@ -1323,6 +1323,7 @@ int DroneScrewServerPresenter::initRtspPusher(unsigned int width, unsigned int h void DroneScrewServerPresenter::releaseRtspPusher() { + std::lock_guard pusherLock(m_rtspPusherMutex); m_bRtspStarted = false; if (m_pPusher) { @@ -1343,6 +1344,67 @@ void DroneScrewServerPresenter::releaseRtspPusher() m_rtspUvInitialized = false; m_rtspRgaScaleDisabled = false; m_rtspRgaScaleLogged = false; + resetRtspPushState(); +} + +int DroneScrewServerPresenter::startRtspPusher() +{ + std::lock_guard pusherLock(m_rtspPusherMutex); + if (!m_pPusher) + return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); + return m_pPusher->Start(); +} + +void DroneScrewServerPresenter::resetRtspPushState() +{ + { + std::lock_guard lk(m_rtspPushMutex); + m_rtspPushedFrameCounter = 0; + m_rtspLastPushRet = 0; + m_rtspLastPushFrameId = 0; + } +} + +void DroneScrewServerPresenter::recordRtspPushResult(int ret, unsigned long long frameId) +{ + bool logFirstSuccess = false; + bool logFirstFailure = false; + int64_t pushedCount = 0; + const bool pendingEncoderOutput = (ret == ERR_CODE(DEV_RESULT_EMPTY)); + { + std::lock_guard lk(m_rtspPushMutex); + pushedCount = m_rtspPushedFrameCounter.load(); + const int previousRet = m_rtspLastPushRet.load(); + m_rtspLastPushFrameId = frameId; + if (ret == 0) + { + m_rtspLastPushRet = 0; + ++m_rtspPushedFrameCounter; + logFirstSuccess = (pushedCount == 0); + } + else if (!pendingEncoderOutput) + { + m_rtspLastPushRet = ret; + logFirstFailure = (!pendingEncoderOutput && + pushedCount == 0 && + previousRet != ret); + } + else if (previousRet == 0) + { + m_rtspLastPushRet = ret; + } + } + + if (logFirstSuccess) + { + LOG_INFO("[RTMP] first frame pushed frame=%llu pull=%s\n", + frameId, m_rtspAdvertiseUrl.toStdString().c_str()); + } + else if (logFirstFailure) + { + LOG_WARN("[RTMP] push failed before first frame ret=%d frame=%llu\n", + ret, frameId); + } } void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img) @@ -1406,12 +1468,12 @@ void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img) // 直播相机配置为左目时,收到左目立即推流(不等右目)。 if (isLiveStreamCameraRole("left") && - m_pPusher && m_bRtspStarted.load() && img.pData) + m_bRtspStarted.load() && img.pData) { const int rtspRet = pushRtspFrame(img); if (rtspRet == 0) ++rtspPushCnt; - else + else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY)) ++rtspFailCnt; } @@ -1443,47 +1505,49 @@ void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img) { try { - QMutexLocker lk(&m_frameMutex); - - // 检查是否需要重新分配内存 - if (m_rightImageData.dataSize != img.dataSize) { - if (m_rightImageData.pData != nullptr) - { - delete[] m_rightImageData.pData; - m_rightImageData.pData = nullptr; - LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n", - m_rightImageData.dataSize, img.dataSize, img.frameID); - } + QMutexLocker lk(&m_frameMutex); - if (img.dataSize > 0) + // 检查是否需要重新分配内存 + if (m_rightImageData.dataSize != img.dataSize) { - m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize]; - if (!m_rightImageData.pData) + if (m_rightImageData.pData != nullptr) { - m_rightImageData.dataSize = 0; - m_bRightImageReady = false; - LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n", - img.dataSize, img.frameID); - return; + delete[] m_rightImageData.pData; + m_rightImageData.pData = nullptr; + LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n", + m_rightImageData.dataSize, img.dataSize, img.frameID); + } + + if (img.dataSize > 0) + { + m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize]; + if (!m_rightImageData.pData) + { + m_rightImageData.dataSize = 0; + m_bRightImageReady = false; + LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n", + img.dataSize, img.frameID); + return; + } } } + + // 复制图像数据 + m_rightImageData.width = img.width; + m_rightImageData.height = img.height; + m_rightImageData.dataSize = img.dataSize; + m_rightImageData.pixelFormat = img.pixelFormat; + m_rightImageData.frameID = img.frameID; + m_rightImageData.timestamp = img.timestamp; + + if (m_rightImageData.pData != nullptr && img.pData != nullptr) + { + memcpy(m_rightImageData.pData, img.pData, img.dataSize); + } + + m_bRightImageReady = true; } - - // 复制图像数据 - m_rightImageData.width = img.width; - m_rightImageData.height = img.height; - m_rightImageData.dataSize = img.dataSize; - m_rightImageData.pixelFormat = img.pixelFormat; - m_rightImageData.frameID = img.frameID; - m_rightImageData.timestamp = img.timestamp; - - if (m_rightImageData.pData != nullptr && img.pData != nullptr) - { - memcpy(m_rightImageData.pData, img.pData, img.dataSize); - } - - m_bRightImageReady = true; static bool rightFirst = true; if (rightFirst) { std::ostringstream oss; @@ -1496,12 +1560,12 @@ void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img) static int rtspFailCnt = 0; if (isLiveStreamCameraRole("right") && - m_pPusher && m_bRtspStarted.load() && img.pData) + m_bRtspStarted.load() && img.pData) { const int rtspRet = pushRtspFrame(img); if (rtspRet == 0) ++rtspPushCnt; - else + else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY)) ++rtspFailCnt; } @@ -1711,10 +1775,10 @@ int DroneScrewServerPresenter::startDetectionWork() } // 停止 RTSP 推流(检测和推流互斥) - if (m_pPusher && m_bRtspStarted.load()) + if (m_pPusher) { LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n"); - m_pPusher->Stop(); + releaseRtspPusher(); } m_bRtspStarted = false; resetFrameReadyFlags(); @@ -1746,12 +1810,12 @@ int DroneScrewServerPresenter::stopDetectionWork() return 0; } - m_bIsDetecting = false; stopGpioTriggerLoop(); m_bThreadExit = true; if (m_detectThread.joinable()) m_detectThread.join(); stopImageSaveThread(); + m_bIsDetecting = false; m_detectPipelineMode = kDetectPipelinePrecision; m_activeTriggerFps = static_cast(kPrecisionFrameRate); @@ -1768,6 +1832,8 @@ int DroneScrewServerPresenter::startLiveStream() return 0; } + releaseRtspPusher(); + if (m_bIsDetecting.load()) { LOG_WARN("[LIVE] start rejected: detection is running\n"); @@ -1802,6 +1868,9 @@ int DroneScrewServerPresenter::startLiveStream() if (acqRet != 0) { emit statusChanged(QStringLiteral("实时传图相机采集启动失败")); + releaseRtspPusher(); + m_detectPipelineMode = kDetectPipelinePrecision; + m_activeTriggerFps = static_cast(kPrecisionFrameRate); return acqRet; } @@ -1815,20 +1884,25 @@ int DroneScrewServerPresenter::startLiveStream() LOG_ERROR("[LIVE] RTSP pusher init failed ret=%d\n", ret); stopLiveCameras(); releaseRtspPusher(); + m_detectPipelineMode = kDetectPipelinePrecision; + m_activeTriggerFps = static_cast(kPrecisionFrameRate); return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); } - ret = m_pPusher->Start(); + ret = startRtspPusher(); if (ret != 0) { LOG_ERROR("[LIVE] RTSP pusher start failed ret=%d\n", ret); m_bRtspStarted = false; stopLiveCameras(); releaseRtspPusher(); + m_detectPipelineMode = kDetectPipelinePrecision; + m_activeTriggerFps = static_cast(kPrecisionFrameRate); return ERR_CODE(DRONESCREW_ERR_RTSP_START); } m_rtspFrameCounter = 0; // 重置帧计数器 + resetRtspPushState(); m_bRtspStarted = true; m_bLiveStreaming = true; m_bThreadExit = false; @@ -1851,7 +1925,6 @@ int DroneScrewServerPresenter::stopLiveStream() releaseRtspPusher(); else m_bRtspStarted = false; - stopImageSaveThread(); m_rtspFrameCounter = 0; // 重置计数器,防止下次开流时 DTS 残留 return 0; } @@ -2255,11 +2328,6 @@ QString DroneScrewServerPresenter::imageSaveModeName() const if (m_detectPipelineMode.load() == kDetectPipelineDistance) return QStringLiteral("distance"); - const std::string mode = detectMode(); - if (mode == "mono") - return QStringLiteral("precision_mono"); - if (mode == "binocular") - return QStringLiteral("precision_binocular"); return QStringLiteral("precision"); } @@ -2267,9 +2335,13 @@ void DroneScrewServerPresenter::startImageSaveThread(const QString& modeName) { stopImageSaveThread(); - const QString dateDir = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd")); + const QDateTime now = QDateTime::currentDateTime(); + const QString dateDir = now.toString(QStringLiteral("yyyyMMdd")); + const QString sessionName = QStringLiteral("%1_%2") + .arg(modeName, + now.toString(QStringLiteral("HHmmss_zzz"))); const QString sessionDir = - QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(modeName); + QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(sessionName); if (!QDir().mkpath(sessionDir)) { LOG_WARN("[SAVE] create image save dir failed: %s\n", @@ -2294,9 +2366,9 @@ void DroneScrewServerPresenter::stopImageSaveThread() const bool shouldJoin = m_imageSaveThread.joinable(); { std::lock_guard lk(m_imageSaveMutex); + m_imageSaveQueue.clear(); if (!shouldJoin) { - m_imageSaveQueue.clear(); m_imageSaveSessionDir.clear(); m_imageSaveThreadExit = false; return; @@ -2397,12 +2469,11 @@ void DroneScrewServerPresenter::imageSaveThreadFunc() return m_imageSaveThreadExit.load() || !m_imageSaveQueue.empty(); }); + if (m_imageSaveThreadExit.load()) + break; + if (m_imageSaveQueue.empty()) - { - if (m_imageSaveThreadExit.load()) - break; continue; - } job = std::move(m_imageSaveQueue.front()); m_imageSaveQueue.pop_front(); @@ -2422,7 +2493,7 @@ void DroneScrewServerPresenter::imageSaveThreadFunc() static_cast(img.width), QImage::Format_Grayscale8); const QString filePath = QDir(job.dirPath).filePath( - QStringLiteral("%1Image_%2.png").arg(prefix).arg(job.index)); + QStringLiteral("%1_%2Image.png").arg(job.index).arg(prefix)); if (!q.save(filePath, "PNG")) { LOG_WARN("[SAVE] image save failed: %s\n", @@ -3142,20 +3213,34 @@ int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img) { try { + std::lock_guard pusherLock(m_rtspPusherMutex); if (!m_pPusher) return ERR_CODE(DRONESCREW_ERR_RTSP_INIT); if (!m_bLiveStreaming.load() || !m_bRtspStarted.load()) return 0; if (!img.pData || img.dataSize == 0 || img.width == 0 || img.height == 0) - return ERR_CODE(DATA_ERR_INVALID); + { + const int ret = ERR_CODE(DATA_ERR_INVALID); + recordRtspPushResult(ret, img.frameID); + return ret; + } const unsigned int outWidth = m_rtspWidth > 0 ? m_rtspWidth : img.width; const unsigned int outHeight = m_rtspHeight > 0 ? m_rtspHeight : img.height; if (outWidth == 0 || outHeight == 0) - return ERR_CODE(DATA_ERR_INVALID); + { + const int ret = ERR_CODE(DATA_ERR_INVALID); + recordRtspPushResult(ret, img.frameID); + return ret; + } if (img.pixelFormat == kMvsPixelTypeMono8) { const size_t monoSize = static_cast(img.width) * static_cast(img.height); - if (img.dataSize < monoSize) return ERR_CODE(DATA_ERR_LEN); + if (img.dataSize < monoSize) + { + const int ret = ERR_CODE(DATA_ERR_LEN); + recordRtspPushResult(ret, img.frameID); + return ret; + } const size_t ySize = static_cast(outWidth) * static_cast(outHeight); @@ -3236,27 +3321,39 @@ int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img) const int64_t ptsUs = static_cast( (static_cast(frameIdx) / streamFps) * 1000000.0 ); - return m_pPusher->PushFrame(m_rtspFrameBuffer.data(), m_rtspFrameBuffer.size(), ptsUs); + const int ret = m_pPusher->PushFrame(m_rtspFrameBuffer.data(), + m_rtspFrameBuffer.size(), + ptsUs); + recordRtspPushResult(ret, img.frameID); + return ret; } - return ERR_CODE(DATA_ERR_INVALID); + const int ret = ERR_CODE(DATA_ERR_INVALID); + recordRtspPushResult(ret, img.frameID); + return ret; } catch (const std::bad_alloc& e) { LOG_ERROR("[RTSP] push memory allocation failed frame=%llu src=%ux%u: %s\n", img.frameID, img.width, img.height, e.what()); - return ERR_CODE(DATA_ERR_MEM); + const int ret = ERR_CODE(DATA_ERR_MEM); + recordRtspPushResult(ret, img.frameID); + return ret; } catch (const std::exception& e) { LOG_ERROR("[RTSP] push exception frame=%llu src=%ux%u: %s\n", img.frameID, img.width, img.height, e.what()); - return ERR_CODE(DATA_ERR_INVALID); + const int ret = ERR_CODE(DATA_ERR_INVALID); + recordRtspPushResult(ret, img.frameID); + return ret; } catch (...) { LOG_ERROR("[RTSP] push unknown exception frame=%llu src=%ux%u\n", img.frameID, img.width, img.height); - return ERR_CODE(DATA_ERR_INVALID); + const int ret = ERR_CODE(DATA_ERR_INVALID); + recordRtspPushResult(ret, img.frameID); + return ret; } } diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h index 90decb2f..0cb507fe 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h @@ -210,8 +210,11 @@ private: int prepareLiveStreamAcquisition(); void resetFrameReadyFlags(); int initRtspPusher(unsigned int width, unsigned int height, unsigned int fps); + int startRtspPusher(); void releaseRtspPusher(); int pushRtspFrame(const MvsImageData& img); + void resetRtspPushState(); + void recordRtspPushResult(int ret, unsigned long long frameId); bool tryRgaScaleMonoToY(const MvsImageData& img, unsigned char* dstY, unsigned int outWidth, @@ -308,6 +311,11 @@ private: std::atomic m_detectPipelineMode{0}; // 0=precision, 1=distance std::atomic m_activeTriggerFps{2}; std::atomic m_rtspFrameCounter{0}; // 相对帧计数(推流启动时重置) + std::mutex m_rtspPusherMutex; + std::mutex m_rtspPushMutex; + std::atomic m_rtspPushedFrameCounter{0}; + std::atomic m_rtspLastPushRet{0}; + std::atomic m_rtspLastPushFrameId{0}; std::vector m_rtspFrameBuffer; std::vector m_rtspScaleX; std::vector m_rtspScaleY; diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp b/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp index b4f317fa..fb4cb906 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp @@ -25,7 +25,7 @@ namespace { constexpr int kRawImageZlibLevel = 9; -constexpr bool kUseFixedPrecisionMonoRawMjpeg = false; +constexpr bool kUseFixedPrecisionMonoRawMjpeg = true; constexpr int kFixedPrecisionMonoRawMjpegQuality = 25; constexpr int kFixedPrecisionMonoRawScaleDiv = 4; diff --git a/App/DroneScrewbolt/result.png b/App/DroneScrewbolt/result.png new file mode 100644 index 00000000..ad904261 Binary files /dev/null and b/App/DroneScrewbolt/result.png differ diff --git a/App/WorkpieceProject/WorkpieceProject.pro b/App/WorkpieceProject/WorkpieceProject.pro index 8d4334e0..06982dc7 100644 --- a/App/WorkpieceProject/WorkpieceProject.pro +++ b/App/WorkpieceProject/WorkpieceProject.pro @@ -1,9 +1,21 @@ # WorkpieceProject.pro -# 工件项目总工程文件 - TEMPLATE = subdirs -SUBDIRS += \ - WorkpieceProjectConfig/WorkpieceProjectConfig.pro \ - WorkpiecePositionApp/WorkpiecePositionApp.pro \ - WorkpieceSpliceApp/WorkpieceSpliceApp.pro +WorkpieceProjectConfig.file = WorkpieceProjectConfig/WorkpieceProjectConfig.pro +WorkpiecePositionApp.file = WorkpiecePositionApp/WorkpiecePositionApp.pro +WorkpieceSpliceApp.file = WorkpieceSpliceApp/WorkpieceSpliceApp.pro + +SUBDIRS += WorkpieceProjectConfig + +equals(TARGET_APP, "WorkpiecePosition") { + SUBDIRS += WorkpiecePositionApp + WorkpiecePositionApp.depends = WorkpieceProjectConfig +} else:equals(TARGET_APP, "WorkpieceSplice") { + SUBDIRS += WorkpieceSpliceApp + WorkpieceSpliceApp.depends = WorkpieceProjectConfig +} else { + SUBDIRS += WorkpiecePositionApp + SUBDIRS += WorkpieceSpliceApp + WorkpiecePositionApp.depends = WorkpieceProjectConfig + WorkpieceSpliceApp.depends = WorkpieceProjectConfig +} diff --git a/Module/FFMediaStream/Src/CVrFFMediaPusher.cpp b/Module/FFMediaStream/Src/CVrFFMediaPusher.cpp index d295ec4e..2ff4d776 100644 --- a/Module/FFMediaStream/Src/CVrFFMediaPusher.cpp +++ b/Module/FFMediaStream/Src/CVrFFMediaPusher.cpp @@ -585,7 +585,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs) mpp_packet_deinit(&packet); mpp_buffer_put(pktBuf); LOG_DEBUG("FFMediaPusher: encode produced no packet (warmup)\n"); - return SUCCESS; // 跳过,不算失败 + return ERR_CODE(DEV_RESULT_EMPTY); } MppBuffer encodedBuf = mpp_packet_get_buffer(encodedPacket); @@ -618,7 +618,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs) { releaseEncodedPacket(); LOG_DEBUG("FFMediaPusher: encode produced empty packet data\n"); - return SUCCESS; // 跳过,不算失败 + return ERR_CODE(DEV_RESULT_EMPTY); } // 4) 写入 RTSP(独立 AVPacket,pts rescale 到 muxer 时基) @@ -678,8 +678,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs) // 微秒 → muxer 实际时基 av_packet_rescale_ts(avpkt, AVRational{1, 1000000}, m_fmtCtx->streams[0]->time_base); - // Single video stream: write directly to avoid interleave buffering latency. - aret = av_write_frame(m_fmtCtx, avpkt); + aret = av_interleaved_write_frame(m_fmtCtx, avpkt); // RTMP 推流需要强制刷新缓冲区,否则数据会积压不发送 if (aret >= 0 && m_fmtCtx->pb) @@ -689,7 +688,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs) } else { - aret = 0; // 已停止,丢弃本帧 + aret = AVERROR_EOF; // 已停止,丢弃本帧 } } @@ -697,7 +696,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs) if (aret < 0) { - LOG_DEBUG("FFMediaPusher: av_write_frame fail %d\n", aret); + LOG_DEBUG("FFMediaPusher: av_interleaved_write_frame fail %d\n", aret); return ERR_CODE(DEV_CTRL_ERR); } diff --git a/Module/Module.pro b/Module/Module.pro index 57a1d69f..798ace13 100644 --- a/Module/Module.pro +++ b/Module/Module.pro @@ -3,27 +3,59 @@ TEMPLATE = subdirs -SUBDIRS += \ - ShareMem \ - BinocularMarkReceiver \ - ModbusTCPServer \ - ModbusTCPClient \ - AuthModule \ - HandEyeCalib \ - ChessboardDetector \ - FFMediaStream \ - ZeroMQClient \ - ZeroMQServer \ - ZeroMQPubSub \ - WDRemoteReceiver +isEmpty(TARGET_APP) { + SUBDIRS += \ + ShareMem \ + BinocularMarkReceiver \ + ModbusTCPServer \ + ModbusTCPClient \ + AuthModule \ + HandEyeCalib \ + ChessboardDetector \ + FFMediaStream \ + ZeroMQClient \ + ZeroMQServer \ + ZeroMQPubSub \ + WDRemoteReceiver +} else { + SUBDIRS += \ + ShareMem \ + BinocularMarkReceiver \ + ModbusTCPServer \ + AuthModule + + contains(TARGET_APP, "^(BagThreadPosition|HoleDetection|StatorPosition|WorkpieceHole)$") { + SUBDIRS += ModbusTCPClient + } + + contains(TARGET_APP, "^(ScrewPosition|WorkpieceHole|DiscHolePose|TireHolePose|RodAndBarPosition|RodWeldSeam)$") { + SUBDIRS += HandEyeCalib + } + + contains(TARGET_APP, "^(DroneScrewServer|DroneScrewCtrlApp|DroneScrewbolt)$") { + SUBDIRS += FFMediaStream ZeroMQPubSub + } + + contains(TARGET_APP, "^(DroneScrewServer|DroneScrewbolt)$") { + SUBDIRS += ZeroMQServer + } + + contains(TARGET_APP, "^(DroneScrewCtrlApp|DroneScrewbolt)$") { + SUBDIRS += ZeroMQClient WDRemoteReceiver + } +} # 设置依赖关系 # 注意:SUBDIRS 必须用短名(目录名)注册,qmake 会自动查找 子目录/同名.pro, # 这样 .depends 才能生效。若写成 "WDRemoteReceiver/WDRemoteReceiver.pro" 路径形式, # 该依赖不会被 qmake 关联,make -j 并行编译时会出现 "cannot find -lZeroMQClient"。 # WDRemoteReceiver 是动态库,链接 ZeroMQClient / ZeroMQPubSub 静态库,必须先构建它们。 -WDRemoteReceiver.depends = ZeroMQClient ZeroMQPubSub +contains(SUBDIRS, WDRemoteReceiver) { + WDRemoteReceiver.depends = ZeroMQClient ZeroMQPubSub AuthModule +} win32-msvc { SUBDIRS += CloudShow } + +SUBDIRS = $$unique(SUBDIRS) diff --git a/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp b/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp index 2eabf045..6433c0ce 100644 --- a/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp +++ b/Module/WDRemoteReceiver/Src/WDRemoteReceiver.cpp @@ -686,13 +686,9 @@ int WDRemoteReceiver::StartWork(int timeoutMs) int WDRemoteReceiver::StartWork(WDRemoteWorkMode mode, int timeoutMs) { std::lock_guard stateLock(m_mtxWorkState); - const WorkState oldState = m_workState; if (mode == WDRemoteWorkMode::LiveStream) { - if (oldState == WorkState::LiveStream) - return 0; - Json::Value stopReq; stopReq["cmd"] = "stop"; Json::FastWriter w; @@ -734,9 +730,6 @@ int WDRemoteReceiver::StartWork(WDRemoteWorkMode mode, int timeoutMs) return 0; } - if (oldState == WorkState::Detection) - return 0; - Json::Value stopStreamReq; stopStreamReq["cmd"] = "stop_stream"; Json::FastWriter w;