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