QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
VideoManager.cc
Go to the documentation of this file.
1#include "VideoManager.h"
2#include "AppSettings.h"
5#include "AppMessages.h"
6#include "QGCApplication.h"
7#include "QGCCameraManager.h"
8#include "QGCCorePlugin.h"
10#include "QGCNetworkHelper.h"
11#include "QGCVideoStreamInfo.h"
12#include "SettingsManager.h"
13#include "SubtitleWriter.h"
14#include "Vehicle.h"
15#include "VehicleLinkManager.h"
16#include "VideoReceiver.h"
17#include "VideoSettings.h"
18#include "UVCReceiver.h"
19#include "VideoBackend.h"
20
21#include <algorithm>
22#include <climits>
23
24#include <QtConcurrent/QtConcurrent>
25#include <QtCore/QApplicationStatic>
26#include <QtCore/QCoreApplication>
27#include <QtCore/QDir>
28#include <QtCore/QEventLoop>
29#include <QtCore/QFutureWatcher>
30#include <QtCore/QRunnable>
31#include <QtCore/QTimer>
32#include <QtQml/QQmlEngine>
33#include <QtQuick/QQuickItem>
34#include <QtQuick/QQuickWindow>
35
36QGC_LOGGING_CATEGORY(VideoManagerLog, "Video.VideoManager")
37
38static constexpr const char *kFileExtension[VideoReceiver::FILE_FORMAT_MAX + 1] = {
39 "mkv",
40 "mov",
41 "mp4"
42};
43
44Q_APPLICATION_STATIC(VideoManager, _videoManagerInstance);
45
47 : QObject(parent)
48 , _subtitleWriter(new SubtitleWriter(this))
49 , _videoSettings(SettingsManager::instance()->videoSettings())
50{
51 qCDebug(VideoManagerLog) << this;
52
53 (void) qRegisterMetaType<VideoReceiver::STATUS>("STATUS");
54
56 _backendDisabledForTests = VideoBackend::disabledForUnitTests();
57 if (_backendDisabledForTests) {
58 qCInfo(VideoManagerLog) << "Skipping video backend initialization for unit tests";
59 }
60 }
61}
62
64{
65 qCDebug(VideoManagerLog) << this;
66}
67
69{
70 return _videoManagerInstance();
71}
72
74{
75 if (!VideoBackend::needsAsyncInit()) return;
76
77 if (_backendDisabledForTests) {
78 _initState.store(InitState::BackendReady);
79 qCInfo(VideoManagerLog) << "video initialization disabled for unit tests";
80 return;
81 }
82
83 // CAS-gate NotStarted -> Pending: init() (GUI thread) and waitForVideoBackendReady() (other threads)
84 // both enter here; without it both launch VideoBackend::initialize() -> double-init SIGABRT.
85 // The mutex holds back waiters until _backendInitFuture is assigned.
86 QMutexLocker lock(&_initFutureMutex);
87 InitState expected = InitState::NotStarted;
88 if (!_initState.compare_exchange_strong(expected, InitState::Pending)) {
89 qCWarning(VideoManagerLog) << "video init already started";
90 return;
91 }
92
94 // Snapshot argv + env result here on the GUI thread; QCoreApplication::arguments() is not thread-safe.
95 _backendInitFuture = QtConcurrent::run(&VideoBackend::initialize, QCoreApplication::arguments(), envResult);
96
97 _backendInitFuture.then(this, [this](bool success) {
98 _onBackendInitComplete(success);
99 }).onCanceled(this, [this] {
100 _onBackendInitComplete(false);
101 });
102}
103
104bool VideoManager::waitForVideoBackendReady(std::chrono::milliseconds timeout)
105{
106 if (!VideoBackend::needsAsyncInit()) return true;
107
108 if (_backendDisabledForTests) {
109 return true;
110 }
111
112 if (_initState.load() == InitState::NotStarted) {
114 }
115
116 switch (_initState.load()) {
117 case InitState::Failed:
118 return false;
119 case InitState::BackendReady:
120 case InitState::Running:
121 return true;
122 case InitState::NotStarted:
123 case InitState::Pending:
124 case InitState::QmlReady:
125 break;
126 }
127
128 QFuture<bool> future;
129 {
130 QMutexLocker lock(&_initFutureMutex);
131 future = _backendInitFuture;
132 }
133 if (!future.isValid()) {
134 qCCritical(VideoManagerLog) << "waitForVideoBackendReady: no valid future";
135 return false;
136 }
137
138 QEventLoop loop;
139 QTimer timer;
140 timer.setSingleShot(true);
141 QFutureWatcher<bool> watcher;
142 (void) connect(&watcher, &QFutureWatcher<bool>::finished, &loop, &QEventLoop::quit);
143 (void) connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
144
145 watcher.setFuture(future);
146 if (!watcher.isFinished()) {
147 timer.start(timeout);
148 loop.exec();
149 }
150
151 if (!watcher.isFinished()) {
152 qCCritical(VideoManagerLog) << "Timed out waiting for video init";
153 return false;
154 }
155
156 const bool success = watcher.result();
157 if (_initState.load() == InitState::Pending || _initState.load() == InitState::QmlReady) {
158 _onBackendInitComplete(success);
159 }
160 return _initState.load() != InitState::Failed;
161}
162
163void VideoManager::init(QQuickWindow *mainWindow)
164{
165 if (_initialized) {
166 qCDebug(VideoManagerLog) << "Video Manager already initialized";
167 return;
168 }
169
170 if (!mainWindow) {
171 qCCritical(VideoManagerLog) << "Failed To Init Video Manager - mainWindow is NULL";
172 return;
173 }
174 _mainWindow = mainWindow;
175
177
178 (void) connect(_videoSettings->videoSource(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
179 (void) connect(_videoSettings->udpUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
180 (void) connect(_videoSettings->rtspUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
181 (void) connect(_videoSettings->tcpUrl(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged);
182 (void) connect(_videoSettings->aspectRatio(), &Fact::rawValueChanged, this, &VideoManager::aspectRatioChanged);
183 (void) connect(_videoSettings->lowLatencyMode(), &Fact::rawValueChanged, this, [this](const QVariant &value) { Q_UNUSED(value); _restartAllVideos(); });
184 // rtpJitterLatencyMs needs a pipeline restart; route through _videoSourceChanged so _updateSettings
185 // pushes the new value to each receiver and restarts exactly once (no double restart).
186 (void) connect(_videoSettings->rtpJitterLatencyMs(), &Fact::rawValueChanged, this, [this](const QVariant &value) { Q_UNUSED(value); _videoSourceChanged(); });
187 // autoReconnect is a live setting — push without restart so an in-flight reconnect
188 // can be cancelled mid-backoff.
189 (void) connect(_videoSettings->rtspAutoReconnect(), &Fact::rawValueChanged, this, [this](const QVariant &value) {
190 const bool enabled = value.toBool();
191 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
192 receiver->setAutoReconnect(enabled);
193 }
194 });
195 VideoBackend::bindDebugLevelFact(SettingsManager::instance()->appSettings()->gstDebugLevel(), this);
196 (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &VideoManager::_setActiveVehicle);
197
198 (void) connect(this, &VideoManager::autoStreamConfiguredChanged, this, &VideoManager::_videoSourceChanged);
199
200 if (VideoBackend::needsAsyncInit() && _initState.load() == InitState::NotStarted) {
202 }
203
204 _mainWindow->scheduleRenderJob(
205 QRunnable::create([this] {
206 QMetaObject::invokeMethod(this, &VideoManager::_initAfterQmlIsReady, Qt::QueuedConnection);
207 }),
208 QQuickWindow::AfterSynchronizingStage);
209
210 _initialized = true;
211}
212
213void VideoManager::_initAfterQmlIsReady()
214{
215 if (!_mainWindow) {
216 qCCritical(VideoManagerLog) << "_initAfterQmlIsReady called with NULL mainWindow";
217 return;
218 }
219
220 qCDebug(VideoManagerLog) << "_initAfterQmlIsReady";
221
223 switch (_initState.load()) {
224 case InitState::Pending:
225 _initState.store(InitState::QmlReady);
226 qCDebug(VideoManagerLog) << "QML ready, waiting for video";
227 return;
228 case InitState::BackendReady:
229 _initState.store(InitState::Running);
230 qCDebug(VideoManagerLog) << "QML ready, video already done — creating receivers";
231 break;
232 case InitState::Failed:
233 qCWarning(VideoManagerLog) << "QML ready but video init failed";
234 return;
235 case InitState::NotStarted:
236 case InitState::QmlReady:
237 case InitState::Running:
238 qCWarning(VideoManagerLog) << "_initAfterQmlIsReady: unexpected state" << static_cast<int>(_initState.load());
239 return;
240 }
241 }
242 _createVideoReceivers();
243}
244
245void VideoManager::_onBackendInitComplete(bool success)
246{
247 if (!success) {
248 _initState.store(InitState::Failed);
249 qCCritical(VideoManagerLog) << "video initialization failed";
250 return;
251 }
252
253 if (VideoBackend::needsAsyncInit() && _videoSettings) {
254 _videoSettings->pruneUnavailableDecoders();
255 VideoBackend::applyDecoderPriorities(_videoSettings->forceVideoDecoder()->rawValue().toInt());
256 }
257
258 switch (_initState.load()) {
259 case InitState::Pending:
260 _initState.store(InitState::BackendReady);
261 qCDebug(VideoManagerLog) << "video ready, waiting for QML";
262 return;
263 case InitState::QmlReady:
264 _initState.store(InitState::Running);
265 qCDebug(VideoManagerLog) << "video ready, QML already done — creating receivers";
266 _createVideoReceivers();
267 return;
268 default:
269 qCWarning(VideoManagerLog) << "_onBackendInitComplete: unexpected state" << static_cast<int>(_initState.load());
270 return;
271 }
272}
273
274void VideoManager::_createVideoReceivers()
275{
276#ifdef QGC_UNITTEST_BUILD
277 if (_createVideoReceiversForTest) {
278 _createVideoReceiversForTest();
279 return;
280 }
281#endif
282 static const QStringList videoStreamList = {
283 "videoContent",
284 "thermalVideo"
285 };
286
287 QStringList existing;
288 existing.reserve(_videoReceivers.size());
289 for (const VideoReceiver *receiver : std::as_const(_videoReceivers)) {
290 existing.append(receiver->name());
291 }
292
293 for (const QString &streamName : videoStreamList) {
294 // Skip only names that already initialized; a once-failed receiver was removed from the
295 // list, so re-entry retries it instead of being blocked by an all-or-nothing guard.
296 if (existing.contains(streamName)) {
297 continue;
298 }
300 if (!receiver) {
301 continue;
302 }
303 receiver->setName(streamName);
304
305 _initVideoReceiver(receiver, _mainWindow);
306 }
307}
308
310{
311 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
313 }
314}
315
316void VideoManager::_cleanupOldVideos()
317{
318 if (!SettingsManager::instance()->videoSettings()->enableStorageLimit()->rawValue().toBool()) {
319 return;
320 }
321
322 const QString savePath = SettingsManager::instance()->appSettings()->videoSavePath();
323 QDir videoDir = QDir(savePath);
324 videoDir.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable);
325 videoDir.setSorting(QDir::Time);
326
327 QStringList nameFilters;
328 for (size_t i = 0; i < std::size(kFileExtension); i++) {
329 nameFilters << QStringLiteral("*.") + kFileExtension[i];
330 }
331
332 videoDir.setNameFilters(nameFilters);
333 QFileInfoList vidList = videoDir.entryInfoList();
334 if (vidList.isEmpty()) {
335 return;
336 }
337
338 uint64_t total = 0;
339 for (const QFileInfo &video : std::as_const(vidList)) {
340 total += video.size();
341 }
342
343 const uint64_t maxSize = SettingsManager::instance()->videoSettings()->maxVideoSize()->rawValue().toUInt() * qPow(1024, 2);
344 while ((total >= maxSize) && !vidList.isEmpty()) {
345 const QFileInfo info = vidList.takeLast();
346 total -= info.size();
347 const QString path = info.filePath();
348 qCDebug(VideoManagerLog) << "Removing old video file:" << path;
349 (void) QFile::remove(path);
350 }
351}
352
353void VideoManager::startRecording(const QString &videoFile)
354{
355 const VideoReceiver::FILE_FORMAT fileFormat = static_cast<VideoReceiver::FILE_FORMAT>(_videoSettings->recordingFormat()->rawValue().toInt());
356 if (!VideoReceiver::isValidFileFormat(fileFormat)) {
357 QGC::showAppMessage(tr("Invalid video format defined."));
358 return;
359 }
360
361 _cleanupOldVideos();
362
363 const QString savePath = SettingsManager::instance()->appSettings()->videoSavePath();
364 if (savePath.isEmpty()) {
365 QGC::showAppMessage(tr("Unable to record video. Video save path must be specified in Settings."));
366 return;
367 }
368
369 const QString videoFileUrl = videoFile.isEmpty() ? QDateTime::currentDateTime().toString("yyyy-MM-dd_hh.mm.ss") : videoFile;
370 const QString ext = kFileExtension[fileFormat];
371
372 const QString videoFileNameTemplate = savePath + "/" + videoFileUrl + ".%1" + ext;
373
374 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
375 if (!receiver->started()) {
376 qCDebug(VideoManagerLog) << "Video receiver is not ready.";
377 continue;
378 }
379 const QString streamName = (receiver->name() == QStringLiteral("videoContent")) ? "" : (receiver->name() + ".");
380 const QString videoFileName = videoFileNameTemplate.arg(streamName);
381 receiver->startRecording(videoFileName, fileFormat);
382 }
383}
384
386{
387 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
388 receiver->stopRecording();
389 }
390}
391
392void VideoManager::grabImage(const QString &imageFile)
393{
394 if (imageFile.isEmpty()) {
396 _imageFile += QStringLiteral("/") + QDateTime::currentDateTime().toString("yyyy-MM-dd_hh.mm.ss.zzz") + QStringLiteral(".jpg");
397 } else {
398 _imageFile = imageFile;
399 }
400
401 emit imageFileChanged(_imageFile);
402
403 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
404 receiver->takeScreenshot(_imageFile);
405 // QSharedPointer<QQuickItemGrabResult> result = receiver->widget()->grabToImage(const QSize &targetSize = QSize())
406 }
407}
408
410{
411 // Live decoded resolution wins — set by VideoReceiver::videoSizeChanged once frames flow.
412 if (!_videoSize.isEmpty()) {
413 return static_cast<double>(_videoSize.width()) / _videoSize.height();
414 }
415
416 for (VideoReceiver *receiver : _videoReceivers) {
417 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
418 if (!receiver->isThermal() && pInfo && !pInfo->isThermal()) {
419 return pInfo->aspectRatio();
420 }
421 }
422
423 return _videoSettings->aspectRatio()->rawValue().toDouble();
424}
425
427{
428 for (VideoReceiver *receiver : _videoReceivers) {
429 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
430 if (receiver->isThermal() && pInfo && pInfo->isThermal()) {
431 return pInfo->aspectRatio();
432 }
433 }
434
435 return 1.0;
436}
437
438double VideoManager::hfov() const
439{
440 for (VideoReceiver *receiver : _videoReceivers) {
441 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
442 if (!receiver->isThermal() && pInfo && !pInfo->isThermal()) {
443 return pInfo->hfov();
444 }
445 }
446
447 return 1.0;
448}
449
451{
452 for (VideoReceiver *receiver : _videoReceivers) {
453 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
454 if (receiver->isThermal() && pInfo && pInfo->isThermal()) {
455 return pInfo->hfov();
456 }
457 }
458
459 return _videoSettings->aspectRatio()->rawValue().toDouble();
460}
461
463{
464 for (VideoReceiver *receiver : _videoReceivers) {
465 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
466 if (receiver->isThermal() && pInfo && pInfo->isThermal()) {
467 return true;
468 }
469 }
470
471 return false;
472}
473
475{
476 return (_videoSettings->streamEnabled()->rawValue().toBool() && _videoSettings->streamConfigured());
477}
478
480{
481 return (!_uvcVideoSourceID.isEmpty() && UVCReceiver::enabled() && hasVideo());
482}
483
485{
486 if (on) {
487 if (!_activeVehicle || _activeVehicle->vehicleLinkManager()->communicationLost()) {
488 on = false;
489 }
490 }
491
492 if (on != _fullScreen) {
493 _fullScreen = on;
494 emit fullScreenChanged();
495 }
496}
497
499{
500 static const QStringList videoSourceList = {
511 };
512 const QString videoSource = _videoSettings->videoSource()->rawValue().toString();
513 return (videoSourceList.contains(videoSource) || autoStreamConfigured());
514}
515
516void VideoManager::_videoSourceChanged()
517{
518 bool changed = false;
519 if (_activeVehicle) {
520 QGCCameraManager* camMgr = _activeVehicle->cameraManager();
521 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
522 QGCVideoStreamInfo* info = nullptr;
523 if (receiver->isThermal()) {
524 info = camMgr ? camMgr->thermalStreamInstance() : nullptr;
525 } else {
526 info = camMgr ? camMgr->currentStreamInstance() : nullptr;
527 }
528 receiver->setVideoStreamInfo(info);
529 changed |= _updateSettings(receiver);
530 }
531 } else {
532 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
533 receiver->setVideoStreamInfo(nullptr);
534 changed |= _updateSettings(receiver);
535 }
536 }
537
538 if (changed) {
539 emit hasVideoChanged();
541 emit isAutoStreamChanged();
542
543 if (hasVideo()) {
544 _restartAllVideos();
545 } else {
546 stopVideo();
547 }
548
549 qCDebug(VideoManagerLog) << "New Video Source:" << _videoSettings->videoSource()->rawValue().toString();
550 }
551}
552
553bool VideoManager::_updateUVC(VideoReceiver * /*receiver*/)
554{
555 bool result = false;
556
557 const QString oldUvcVideoSrcID = _uvcVideoSourceID;
558
559 if (!UVCReceiver::enabled() || !hasVideo() || isStreamSource()) {
560 _uvcVideoSourceID = QString();
561 } else {
562 _uvcVideoSourceID = UVCReceiver::getSourceId();
563 }
564
565 if (oldUvcVideoSrcID != _uvcVideoSourceID) {
566 qCDebug(VideoManagerLog) << "UVC changed from [" << oldUvcVideoSrcID << "] to [" << _uvcVideoSourceID << "]";
567 if (!_uvcVideoSourceID.isEmpty()) {
569 }
570 result = true;
572 emit isUvcChanged();
573 }
574
575 return result;
576}
577
579{
580 for (VideoReceiver *receiver : _videoReceivers) {
581 QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
582 if (!receiver->isThermal() && pInfo && !pInfo->isThermal()) {
583 return !pInfo->uri().isEmpty();
584 }
585 }
586
587 return false;
588}
589
590bool VideoManager::_updateAutoStream(VideoReceiver *receiver)
591{
592 const QGCVideoStreamInfo *pInfo = receiver->videoStreamInfo();
593 if (!pInfo) {
594 return false;
595 }
596
597 qCDebug(VideoManagerLog) << QString("Configure stream (%1):").arg(receiver->name())
599
600 QString source, url;
601 switch (pInfo->type()) {
602 case VIDEO_STREAM_TYPE_RTSP:
604 url = pInfo->uri();
605 if (source == VideoSettings::videoSourceRTSP) {
606 _videoSettings->rtspUrl()->setRawValue(url);
607 }
608 break;
609 case VIDEO_STREAM_TYPE_TCP_MPEG:
611 url = pInfo->uri();
612 break;
613 case VIDEO_STREAM_TYPE_RTPUDP:
614 if (pInfo->encoding() == VIDEO_STREAM_ENCODING_H265) {
616 url = pInfo->uri().contains("udp265://") ? pInfo->uri() : QStringLiteral("udp265://0.0.0.0:%1").arg(pInfo->uri());
617 } else {
619 url = pInfo->uri().contains("udp://") ? pInfo->uri() : QStringLiteral("udp://0.0.0.0:%1").arg(pInfo->uri());
620 }
621 break;
622 case VIDEO_STREAM_TYPE_MPEG_TS:
624 url = pInfo->uri().contains("mpegts://") ? pInfo->uri() : QStringLiteral("mpegts://0.0.0.0:%1").arg(pInfo->uri());
625 break;
626 default:
627 qCWarning(VideoManagerLog) << "Unknown VIDEO_STREAM_TYPE";
629 url = pInfo->uri();
630 break;
631 }
632
633 const bool settingsChanged = _updateVideoUri(receiver, url);
634 if (settingsChanged) {
635 if (!receiver->isThermal()) {
636 _videoSettings->videoSource()->setRawValue(source);
637 }
638
640 }
641
642 return settingsChanged;
643}
644
645bool VideoManager::_updateVideoUri(VideoReceiver *receiver, const QString &uri)
646{
647 if (!receiver) {
648 qCDebug(VideoManagerLog) << "VideoReceiver is NULL";
649 return false;
650 }
651
652 if ((uri == receiver->uri()) && !receiver->uri().isNull()) {
653 return false;
654 }
655
656 qCDebug(VideoManagerLog) << "New Video URI" << QGCNetworkHelper::redactedUrlForLogging(uri);
657
658 receiver->setUri(uri);
659
660 return true;
661}
662
663bool VideoManager::_updateSettings(VideoReceiver *receiver)
664{
665 if (!receiver) {
666 qCDebug(VideoManagerLog) << "VideoReceiver is NULL";
667 return false;
668 }
669
670 bool settingsChanged = false;
671
672 const bool lowLatency = _videoSettings->lowLatencyMode()->rawValue().toBool();
673 if (lowLatency != receiver->lowLatency()) {
674 receiver->setLowLatency(lowLatency);
675 settingsChanged = true;
676 }
677
678 const int rtpLatencyMs = static_cast<int>(std::min(_videoSettings->rtpJitterLatencyMs()->rawValue().toUInt(), static_cast<uint>(INT_MAX)));
679 if (rtpLatencyMs != receiver->rtpJitterLatencyMs()) {
680 receiver->setRtpJitterLatencyMs(rtpLatencyMs);
681 settingsChanged = true;
682 }
683
684 const bool autoReconnect = _videoSettings->rtspAutoReconnect()->rawValue().toBool();
685 if (autoReconnect != receiver->autoReconnect()) {
686 receiver->setAutoReconnect(autoReconnect);
687 // No settingsChanged: autoReconnect is live, doesn't require pipeline restart.
688 }
689
690 if (receiver->isThermal()) {
691 return settingsChanged;
692 }
693
694 settingsChanged |= _updateUVC(receiver);
695 settingsChanged |= _updateAutoStream(receiver);
696
697 const QString source = _videoSettings->videoSource()->rawValue().toString();
698 if (source == VideoSettings::videoSourceUDPH264) {
699 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("udp://%1").arg(_videoSettings->udpUrl()->rawValue().toString()));
700 } else if (source == VideoSettings::videoSourceUDPH265) {
701 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("udp265://%1").arg(_videoSettings->udpUrl()->rawValue().toString()));
702 } else if (source == VideoSettings::videoSourceMPEGTS) {
703 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("mpegts://%1").arg(_videoSettings->udpUrl()->rawValue().toString()));
704 } else if (source == VideoSettings::videoSourceRTSP) {
705 settingsChanged |= _updateVideoUri(receiver, _videoSettings->rtspUrl()->rawValue().toString());
706 } else if (source == VideoSettings::videoSourceTCP) {
707 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("tcp://%1").arg(_videoSettings->tcpUrl()->rawValue().toString()));
708 } else if (source == VideoSettings::videoSource3DRSolo) {
709 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("udp://0.0.0.0:5600"));
710 } else if (source == VideoSettings::videoSourceParrotDiscovery) {
711 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("udp://0.0.0.0:8888"));
712 } else if (source == VideoSettings::videoSourceYuneecMantisG) {
713 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("rtsp://192.168.42.1:554/live"));
714 } else if (source == VideoSettings::videoSourceHerelinkAirUnit) {
715 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("rtsp://192.168.0.10:8554/H264Video"));
716 } else if (source == VideoSettings::videoSourceHerelinkHotspot) {
717 settingsChanged |= _updateVideoUri(receiver, QStringLiteral("rtsp://192.168.43.1:8554/fpv_stream"));
718 } else if ((source == VideoSettings::videoDisabled) || (source == VideoSettings::videoSourceNoVideo)) {
719 settingsChanged |= _updateVideoUri(receiver, QString());
720 } else {
721 settingsChanged |= _updateVideoUri(receiver, QString());
722 if (!isUvc()) {
723 qCCritical(VideoManagerLog) << "Video source URI \"" << source << "\" is not supported. Please add support!";
724 }
725 }
726
727 return settingsChanged;
728}
729
730void VideoManager::_setActiveVehicle(Vehicle *vehicle)
731{
732 qCDebug(VideoManagerLog) << Q_FUNC_INFO << "new vehicle" << vehicle << "old active vehicle" << _activeVehicle;
733
734 if (_activeVehicle) {
735 (void) disconnect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged);
736 auto cameraManager = _activeVehicle->cameraManager();
737 if (cameraManager) {
738 MavlinkCameraControlInterface *pCamera = cameraManager->currentCameraInstance();
739 if (pCamera) {
740 pCamera->stopStream();
741 }
742 (void) disconnect(cameraManager, &QGCCameraManager::streamChanged, this, &VideoManager::_videoSourceChanged);
743 }
744
745 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
746 // disconnect(receiver->videoStreamInfo(), &QGCVideoStreamInfo::infoChanged, ))
747 receiver->setVideoStreamInfo(nullptr);
748 }
749 }
750
751 _activeVehicle = vehicle;
752 if (_activeVehicle) {
753 (void) connect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged);
754 if (_activeVehicle->cameraManager()) {
755 (void) connect(_activeVehicle->cameraManager(), &QGCCameraManager::streamChanged, this, &VideoManager::_videoSourceChanged);
756 MavlinkCameraControlInterface *pCamera = _activeVehicle->cameraManager()->currentCameraInstance();
757 if (pCamera) {
758 pCamera->resumeStream();
759 }
760 }
761
762 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
763 if (_activeVehicle->cameraManager()) {
764 if (receiver->isThermal()) {
765 receiver->setVideoStreamInfo(_activeVehicle->cameraManager()->thermalStreamInstance());
766 } else {
767 receiver->setVideoStreamInfo(_activeVehicle->cameraManager()->currentStreamInstance());
768 }
769 } else {
770 receiver->setVideoStreamInfo(nullptr);
771 }
772 // connect(receiver->videoStreamInfo(), &QGCVideoStreamInfo::infoChanged, ))
773 }
774 } else {
775 setfullScreen(false);
776 }
777}
778
779void VideoManager::_communicationLostChanged(bool connectionLost)
780{
781 if (connectionLost) {
782 setfullScreen(false);
783 }
784}
785
786void VideoManager::_restartAllVideos()
787{
788 for (VideoReceiver *videoReceiver : std::as_const(_videoReceivers)) {
789 _restartVideo(videoReceiver);
790 }
791}
792
793void VideoManager::_restartVideo(VideoReceiver *receiver)
794{
795 if (!receiver) {
796 qCDebug(VideoManagerLog) << "VideoReceiver is NULL";
797 return;
798 }
799
800 qCDebug(VideoManagerLog) << "Restart video receiver" << receiver->name();
801
802 if (receiver->started()) {
803 _stopReceiver(receiver);
804 // onStopComplete Signal Will Restart It
805 } else {
806 _startReceiver(receiver);
807 }
808}
809
810void VideoManager::_stopReceiver(VideoReceiver *receiver)
811{
812 if (!receiver) {
813 qCDebug(VideoManagerLog) << "VideoReceiver is NULL";
814 return;
815 }
816
817 if (receiver->started()) {
818 receiver->stop();
819 }
820}
821
823{
824 for (VideoReceiver *receiver : std::as_const(_videoReceivers)) {
825 _stopReceiver(receiver);
826 }
827}
828
829void VideoManager::_startReceiver(VideoReceiver *receiver)
830{
831 if (!receiver) {
832 qCDebug(VideoManagerLog) << "VideoReceiver is NULL";
833 return;
834 }
835
836 if (receiver->started()) {
837 qCDebug(VideoManagerLog) << "VideoReceiver is already started" << receiver->name();
838 return;
839 }
840
841 if (receiver->uri().isEmpty()) {
842 qCDebug(VideoManagerLog) << "VideoUri is NULL" << receiver->name();
843 return;
844 }
845
846 const QString source = _videoSettings->videoSource()->rawValue().toString();
847 const uint32_t timeout = ((source == VideoSettings::videoSourceRTSP) ? _videoSettings->rtspTimeout()->rawValue().toUInt() : 3);
848
849 receiver->start(timeout);
850}
851
852void VideoManager::_initVideoReceiver(VideoReceiver *receiver, QQuickWindow *window)
853{
854 if (_videoReceivers.contains(receiver)) {
855 qCWarning(VideoManagerLog) << "Receiver already initialized";
856 return;
857 }
858
859 // Register before any setup so re-entry is blocked at every point below; error paths remove it.
860 _videoReceivers.append(receiver);
861
862 QQuickItem *widget = window->findChild<QQuickItem*>(receiver->name());
863 if (!widget) {
864 qCCritical(VideoManagerLog) << "stream widget not found" << receiver->name();
865 _videoReceivers.removeOne(receiver);
866 receiver->deleteLater();
867 return;
868 }
869 receiver->setWidget(widget);
870
871 void *sink = QGCCorePlugin::instance()->createVideoSink(receiver->widget(), receiver);
872 if (!sink) {
873 qCCritical(VideoManagerLog) << "createVideoSink() failed" << receiver->name();
874 _videoReceivers.removeOne(receiver);
875 receiver->deleteLater();
876 return;
877 }
878 receiver->setSink(sink);
879
880 VideoBackend::attachSink(receiver, sink, widget);
881
882 (void) connect(receiver, &VideoReceiver::onStartComplete, this, [this, receiver](VideoReceiver::STATUS status) {
883 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "Start complete, status:" << status;
884 switch (status) {
886 receiver->setStarted(true);
887 if (receiver->sink()) {
888 receiver->startDecoding(receiver->sink());
889 }
890 break;
893 break;
894 default:
895 // Rate limit restarts on start failure.
896 QTimer::singleShot(1000, receiver, [this, receiver]() {
897 _restartVideo(receiver);
898 });
899 break;
900 }
901 });
902
903 (void) connect(receiver, &VideoReceiver::onStopComplete, this, [this, receiver](VideoReceiver::STATUS status) {
904 qCDebug(VideoManagerLog) << "Stop complete" << receiver->name()
906 << ", status:" << status;
907 receiver->setStarted(false);
908 if (status == VideoReceiver::STATUS_INVALID_URL) {
909 qCDebug(VideoManagerLog) << "Invalid video URL. Not restarting";
910 } else {
911 QTimer::singleShot(1000, receiver, [this, receiver]() {
912 qCDebug(VideoManagerLog) << "Restarting video receiver" << receiver->name()
914 _startReceiver(receiver);
915 });
916 }
917 });
918
919 (void) connect(receiver, &VideoReceiver::streamingChanged, this, [this, receiver](bool active) {
920 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "streaming changed, active:" << (active ? "yes" : "no");
921 if (!receiver->isThermal()) {
922 _streaming = active;
923 emit streamingChanged();
924 }
925 });
926
927 (void) connect(receiver, &VideoReceiver::decodingChanged, this, [this, receiver](bool active) {
928 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "decoding changed, active:" << (active ? "yes" : "no");
929 if (!receiver->isThermal()) {
930 _decoding = active;
931 emit decodingChanged();
932 }
933 });
934
935 (void) connect(receiver, &VideoReceiver::recordingChanged, this, [this, receiver](bool active) {
936 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "recording changed, active:" << (active ? "yes" : "no");
937 if (!receiver->isThermal()) {
938 _recording = active;
939 if (!active) {
940 _subtitleWriter->stopCapturingTelemetry();
941 }
942 emit recordingChanged(_recording);
943 }
944 });
945
946 (void) connect(receiver, &VideoReceiver::recordingStarted, this, [this, receiver](const QString &filename) {
947 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "recording started";
948 if (!receiver->isThermal()) {
949 _subtitleWriter->startCapturingTelemetry(filename, videoSize());
950 }
951 });
952
953 (void) connect(receiver, &VideoReceiver::videoSizeChanged, this, [this, receiver](QSize size) {
954 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "resized. New resolution:" << size.width() << "x" << size.height();
955 if (!receiver->isThermal()) {
956 _videoSize = size;
957 emit videoSizeChanged();
958 emit aspectRatioChanged();
959 }
960 });
961
962 (void) connect(receiver, &VideoReceiver::onTakeScreenshotComplete, this, [receiver](VideoReceiver::STATUS status) {
963 if (status == VideoReceiver::STATUS_OK) {
964 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "screenshot taken";
965 } else {
966 qCWarning(VideoManagerLog) << "Video" << receiver->name() << "screenshot failed";
967 }
968 });
969
970 (void) connect(receiver, &VideoReceiver::videoStreamInfoChanged, this, [this, receiver]() {
971 const QGCVideoStreamInfo *videoStreamInfo = receiver->videoStreamInfo();
972 qCDebug(VideoManagerLog) << "Video" << receiver->name() << "stream info:" << (videoStreamInfo ? "received" : "lost");
973
974 (void) _updateAutoStream(receiver);
975 });
976
977 (void) _updateSettings(receiver);
978
979 if (hasVideo()) {
980 _startReceiver(receiver);
981 }
982}
983
985{
986 qCDebug(VideoManagerLog) << "startVideo";
987
988 if (!hasVideo()) {
989 qCDebug(VideoManagerLog) << "Stream not enabled/configured";
990 return;
991 }
992
993 _restartAllVideos();
994}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static constexpr const char * kFileExtension[VideoReceiver::FILE_FORMAT_MAX+1]
Q_APPLICATION_STATIC(VideoManager, _videoManagerInstance)
QString videoSavePath()
QString photoSavePath()
void rawValueChanged(const QVariant &value)
Abstract base class for all camera controls: real and simulated.
virtual Q_INVOKABLE void stopStream()=0
virtual Q_INVOKABLE void resumeStream()=0
static MultiVehicleManager * instance()
void activeVehicleChanged(Vehicle *activeVehicle)
Camera Manager.
QGCVideoStreamInfo * currentStreamInstance()
QGCVideoStreamInfo * thermalStreamInstance()
virtual void * createVideoSink(QQuickItem *widget, QObject *parent)
Allows the plugin to override the creation of VideoSink.
virtual VideoReceiver * createVideoReceiver(QObject *parent)
Allows the plugin to override the creation of VideoReceiver.
virtual void releaseVideoSink(void *sink)
Allows the plugin to override the release of VideoSink.
static QGCCorePlugin * instance()
Encapsulates the contents of a VIDEO_STREAM_INFORMATION message.
quint8 encoding() const
Provides access to all app settings.
static SettingsManager * instance()
VideoSettings * videoSettings() const
AppSettings * appSettings() const
void stopCapturingTelemetry()
void startCapturingTelemetry(const QString &videoFile, QSize size)
static bool enabled()
static QString getSourceId()
static void checkPermission()
void communicationLostChanged(bool communicationLost)
bool communicationLost() const
QGCCameraManager * cameraManager()
Definition Vehicle.h:1312
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:597
bool isStreamSource() const
void decodingChanged()
bool isUvc() const
void setfullScreen(bool on)
double hfov() const
void startVideoBackendInit()
Q_INVOKABLE void stopRecording()
bool waitForVideoBackendReady(std::chrono::milliseconds timeout=std::chrono::minutes(1))
void hasVideoChanged()
void uvcVideoSourceIDChanged()
Q_INVOKABLE void startVideo()
bool hasThermal() const
bool autoStreamConfigured() const
Q_INVOKABLE void startRecording(const QString &videoFile=QString())
double thermalAspectRatio() const
void isStreamSourceChanged()
static VideoManager * instance()
VideoManager(QObject *parent=nullptr)
void autoStreamConfiguredChanged()
void streamingChanged()
void fullScreenChanged()
bool hasVideo() const
void init(QQuickWindow *mainWindow)
Q_INVOKABLE void stopVideo()
double aspectRatio() const
void isUvcChanged()
double thermalHfov() const
void imageFileChanged(const QString &filename)
QSize videoSize() const
void recordingChanged(bool recording)
void isAutoStreamChanged()
void videoSizeChanged()
QString imageFile() const
void aspectRatioChanged()
Q_INVOKABLE void grabImage(const QString &imageFile=QString())
void recordingStarted(const QString &filename)
bool lowLatency() const
void setLowLatency(bool lowLatency)
void setName(const QString &name)
void videoSizeChanged(QSize size)
void streamingChanged(bool active)
virtual void stopRecording()=0
virtual void startRecording(const QString &videoFile, FILE_FORMAT format)=0
bool started() const
QQuickItem * widget()
QString uri() const
void recordingChanged(bool active)
VideoSinkHandle sink() const
void videoStreamInfoChanged()
virtual void setSink(VideoSinkHandle sink)
bool isThermal() const
virtual void start(uint32_t timeout)=0
void onTakeScreenshotComplete(STATUS status)
bool autoReconnect() const
void decodingChanged(bool active)
void onStartComplete(STATUS status)
virtual void startDecoding(VideoSinkHandle sink)=0
void setStarted(bool started)
void setUri(const QString &uri)
static bool isValidFileFormat(FILE_FORMAT format)
QString name() const
virtual void setWidget(QQuickItem *widget)
void setAutoReconnect(bool enabled)
void setVideoStreamInfo(QGCVideoStreamInfo *videoStreamInfo)
void setRtpJitterLatencyMs(int ms)
void onStopComplete(STATUS status)
int rtpJitterLatencyMs() const
virtual void stop()=0
virtual void takeScreenshot(const QString &imageFile)=0
QGCVideoStreamInfo * videoStreamInfo()
static constexpr const char * videoSource3DRSolo
static constexpr const char * videoSourceParrotDiscovery
static constexpr const char * videoSourceTCP
static constexpr const char * videoSourceUDPH264
static constexpr const char * videoSourceHerelinkHotspot
bool streamConfigured()
static constexpr const char * videoSourceRTSP
static constexpr const char * videoDisabled
void pruneUnavailableDecoders()
static constexpr const char * videoSourceYuneecMantisG
static constexpr const char * videoSourceUDPH265
static constexpr const char * videoSourceNoVideo
static constexpr const char * videoSourceMPEGTS
static constexpr const char * videoSourceHerelinkAirUnit
QString redactedUrlForLogging(const QUrl &url)
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9
EnvPrepResult prepareEnvironment()
bool initialize(const QStringList &arguments, const EnvPrepResult &envResult)
void applyDecoderPriorities(int rawOption)
void attachSink(QObject *receiver, void *sink, QQuickItem *widget)
void onMainWindowReady(QQuickWindow *window)
bool disabledForUnitTests()
True when the backend should be skipped under unit tests (opt back in with QGC_TEST_ENABLE_GSTREAMER)...
constexpr bool needsAsyncInit() noexcept
void bindDebugLevelFact(Fact *fact, QObject *context)