diff --git a/src/QGCApplication.h b/src/QGCApplication.h index 9ae1ef1..08a11da 100644 --- a/src/QGCApplication.h +++ b/src/QGCApplication.h @@ -71,7 +71,7 @@ public: void reportMissingParameter(int componentId, const QString& name); /// Show a non-modal message to the user - void showMessage(const QString& message); + Q_SLOT void showMessage(const QString& message); /// @return true: Fake ui into showing mobile interface bool fakeMobile(void) const { return _fakeMobile; } diff --git a/src/Settings/VideoSettings.cc b/src/Settings/VideoSettings.cc index 08fb41d..7e2689c 100644 --- a/src/Settings/VideoSettings.cc +++ b/src/Settings/VideoSettings.cc @@ -167,5 +167,5 @@ bool VideoSettings::streamConfigured(void) void VideoSettings::_configChanged(QVariant) { - emit streamConfiguredChanged(); + emit streamConfiguredChanged(streamConfigured()); } diff --git a/src/Settings/VideoSettings.h b/src/Settings/VideoSettings.h index e8864f2..38da769 100644 --- a/src/Settings/VideoSettings.h +++ b/src/Settings/VideoSettings.h @@ -60,7 +60,7 @@ public: static const char* videoSourceMPEGTS; signals: - void streamConfiguredChanged (); + void streamConfiguredChanged (bool configured); private slots: void _configChanged (QVariant value); diff --git a/src/VideoStreaming/VideoManager.cc b/src/VideoStreaming/VideoManager.cc index 7fd2ef1..8294863 100644 --- a/src/VideoStreaming/VideoManager.cc +++ b/src/VideoStreaming/VideoManager.cc @@ -53,6 +53,8 @@ VideoManager::setToolbox(QGCToolbox *toolbox) QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); qmlRegisterUncreatableType ("QGroundControl.VideoManager", 1, 0, "VideoManager", "Reference only"); qmlRegisterUncreatableType("QGroundControl", 1, 0, "VideoReceiver","Reference only"); + + // TODO: Those connections should be Per Video, not per VideoManager. _videoSettings = toolbox->settingsManager()->videoSettings(); QString videoSource = _videoSettings->videoSource()->rawValue().toString(); connect(_videoSettings->videoSource(), &Fact::rawValueChanged, this, &VideoManager::_videoSourceChanged); @@ -72,7 +74,56 @@ VideoManager::setToolbox(QGCToolbox *toolbox) emit isGStreamerChanged(); qCDebug(VideoManagerLog) << "New Video Source:" << videoSource; _videoReceiver = toolbox->corePlugin()->createVideoReceiver(this); + _videoReceiver->setUnittestMode(qgcApp()->runningUnitTests()); _thermalVideoReceiver = toolbox->corePlugin()->createVideoReceiver(this); + _thermalVideoReceiver->setUnittestMode(qgcApp()->runningUnitTests()); + _videoReceiver->moveToThread(qgcApp()->thread()); + _thermalVideoReceiver->moveToThread(qgcApp()->thread()); + + // Those connects are temporary: In a perfect world those connections are going to be done on the Qml + // but because currently the videoReceiver is created in the C++ world, this is easier. + // The fact returning a QVariant is a quite annoying to use proper signal / slot connection. + _updateSettings(); + + auto appSettings = toolbox->settingsManager()->appSettings(); + for (auto *videoReceiver : { _videoReceiver, _thermalVideoReceiver}) { + // First, Setup the current values from the settings. + videoReceiver->setRtspTimeout(_videoSettings->rtspTimeout()->rawValue().toInt()); + videoReceiver->setStreamEnabled(_videoSettings->streamEnabled()->rawValue().toBool()); + videoReceiver->setRecordingFormatId(_videoSettings->recordingFormat()->rawValue().toInt()); + videoReceiver->setStreamConfigured(_videoSettings->streamConfigured()); + + connect(_videoSettings->rtspTimeout(), &Fact::rawValueChanged, + videoReceiver, [videoReceiver](const QVariant &value) { + videoReceiver->setRtspTimeout(value.toInt()); + } + ); + + connect(_videoSettings->streamEnabled(), &Fact::rawValueChanged, + videoReceiver, [videoReceiver](const QVariant &value) { + videoReceiver->setStreamEnabled(value.toBool()); + } + ); + + connect(_videoSettings->recordingFormat(), &Fact::rawValueChanged, + videoReceiver, [videoReceiver](const QVariant &value) { + videoReceiver->setRecordingFormatId(value.toInt()); + } + ); + + // Why some options are facts while others aren't? + connect(_videoSettings, &VideoSettings::streamConfiguredChanged, videoReceiver, &VideoReceiver::setStreamConfigured); + + // Fix those. + // connect(appSettings, &Fact::rawValueChanged, videoReceiver, &VideoReceiver::setVideoPath); + // connect(appSettings->videoSavePath(), &Fact::rawValueChanged, videoReceiver, &VideoReceiver::setImagePath); + + // Connect the video receiver with the rest of the app. + connect(videoReceiver, &VideoReceiver::restartTimeout, this, &VideoManager::restartVideo); + connect(videoReceiver, &VideoReceiver::sendMessage, qgcApp(), &QGCApplication::showMessage); + connect(videoReceiver, &VideoReceiver::beforeRecording, this, &VideoManager::cleanupOldVideos); + } + _updateSettings(); if(isGStreamer()) { startVideo(); @@ -84,6 +135,55 @@ VideoManager::setToolbox(QGCToolbox *toolbox) #endif } +QStringList VideoManager::videoMuxes() +{ + return {"matroskamux", "qtmux", "mp4mux"}; +} + +QStringList VideoManager::videoExtensions() +{ + return {"mkv", "mov", "mp4"}; +} + +void VideoManager::cleanupOldVideos() +{ +#if defined(QGC_GST_STREAMING) + //-- Only perform cleanup if storage limit is enabled + if(!_videoSettings->enableStorageLimit()->rawValue().toBool()) { + return; + } + QString savePath = qgcApp()->toolbox()->settingsManager()->appSettings()->videoSavePath(); + QDir videoDir = QDir(savePath); + videoDir.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable); + videoDir.setSorting(QDir::Time); + + QStringList nameFilters; + for(const QString& extension : videoExtensions()) { + nameFilters << QString("*.") + extension; + } + videoDir.setNameFilters(nameFilters); + //-- get the list of videos stored + QFileInfoList vidList = videoDir.entryInfoList(); + if(!vidList.isEmpty()) { + uint64_t total = 0; + //-- Settings are stored using MB + uint64_t maxSize = _videoSettings->maxVideoSize()->rawValue().toUInt() * 1024 * 1024; + //-- Compute total used storage + for(int i = 0; i < vidList.size(); i++) { + total += vidList[i].size(); + } + //-- Remove old movies until max size is satisfied. + while(total >= maxSize && !vidList.isEmpty()) { + total -= vidList.last().size(); + qCDebug(VideoReceiverLog) << "Removing old video file:" << vidList.last().filePath(); + QFile file (vidList.last().filePath()); + file.remove(); + vidList.removeLast(); + } + } +#endif +} + //----------------------------------------------------------------------------- void VideoManager::startVideo() diff --git a/src/VideoStreaming/VideoManager.h b/src/VideoStreaming/VideoManager.h index f53f282..54e6051 100644 --- a/src/VideoStreaming/VideoManager.h +++ b/src/VideoStreaming/VideoManager.h @@ -67,6 +67,9 @@ public: virtual VideoReceiver* videoReceiver () { return _videoReceiver; } virtual VideoReceiver* thermalVideoReceiver () { return _thermalVideoReceiver; } + QStringList videoExtensions(); + QStringList videoMuxes(); + #if defined(QGC_DISABLE_UVC) virtual bool uvcEnabled () { return false; } #else @@ -82,6 +85,8 @@ public: Q_INVOKABLE void startVideo (); Q_INVOKABLE void stopVideo (); + void cleanupOldVideos(); + signals: void hasVideoChanged (); void isGStreamerChanged (); diff --git a/src/VideoStreaming/VideoReceiver.cc b/src/VideoStreaming/VideoReceiver.cc index 36b3ef9..8608433 100644 --- a/src/VideoStreaming/VideoReceiver.cc +++ b/src/VideoStreaming/VideoReceiver.cc @@ -15,8 +15,6 @@ */ #include "VideoReceiver.h" -#include "SettingsManager.h" -#include "QGCApplication.h" #include "VideoManager.h" #ifdef QGC_GST_TAISYNC_ENABLED #include "TaisyncHandler.h" @@ -70,14 +68,15 @@ VideoReceiver::VideoReceiver(QObject* parent) #endif , _videoRunning(false) , _showFullScreen(false) - , _videoSettings(nullptr) + , _streamEnabled(false) + , _streamConfigured(false) + , _unittTestMode(false) + , _isTaisync(false) { // FIXME: AV: temporal workaround to allow for Qt::QueuedConnection for gstreamer signals. Need to evaluate proper solution - perhaps QtGst will be helpful - moveToThread(qgcApp()->thread()); - _videoSettings = qgcApp()->toolbox()->settingsManager()->videoSettings(); #if defined(QGC_GST_STREAMING) _restart_timer.setSingleShot(true); - connect(&_restart_timer, &QTimer::timeout, this, &VideoReceiver::_restart_timeout); + connect(&_restart_timer, &QTimer::timeout, this, &VideoReceiver::restartTimeout); connect(this, &VideoReceiver::msgErrorReceived, this, &VideoReceiver::_handleError, Qt::QueuedConnection); connect(this, &VideoReceiver::msgEOSReceived, this, &VideoReceiver::_handleEOS, Qt::QueuedConnection); connect(this, &VideoReceiver::msgStateChangedReceived, this, &VideoReceiver::_handleStateChanged, Qt::QueuedConnection); @@ -433,12 +432,102 @@ VideoReceiver::_makeSource(const QString& uri) return srcbin; } -//----------------------------------------------------------------------------- -void -VideoReceiver::_restart_timeout() +bool VideoReceiver::streamEnabled() const +{ + return _streamEnabled; +} + +void VideoReceiver::setStreamEnabled(bool enabled) +{ + if (_streamEnabled != enabled) { + _streamEnabled = enabled; + emit streamEnabledChanged(); + } +} + +bool VideoReceiver::streamConfigured() const +{ + return _streamConfigured; +} + +void VideoReceiver::setStreamConfigured(bool enabled) +{ + if (_streamConfigured != enabled) { + _streamConfigured = enabled; + emit streamEnabledChanged(); + } +} + +bool VideoReceiver::isTaisync() const +{ + return _isTaisync; +} + +void VideoReceiver::setIsTaysinc(bool enabled) +{ + if (_isTaisync != enabled) { + _isTaisync = enabled; + emit isTaisyncChanged(); + } +} + +QString VideoReceiver::videoPath() const +{ + return _videoPath; +} + +void VideoReceiver::setVideoPath(const QString& value) { - qgcApp()->toolbox()->videoManager()->restartVideo(); + if (_videoPath != value) { + _videoPath = value; + emit videoPathChanged(); + } +} + +QString VideoReceiver::imagePath() const +{ + return _imagePath; +} + +void VideoReceiver::setImagePath(const QString& value) +{ + if (_imagePath != value) { + _imagePath = value; + emit imagePathChanged(); + } } + +int VideoReceiver::recordingFormatId() const +{ + return _recordingFormatId; +} + +void VideoReceiver::setRecordingFormatId(int value) +{ + if (_recordingFormatId != value && value < (int) NUM_MUXES) { + _recordingFormatId = value; + emit recordingFormatIdChanged(); + } +} + +int VideoReceiver::rtspTimeout() const +{ + return _rtspTimeout; +} + +void VideoReceiver::setRtspTimeout(int value) +{ + if (_rtspTimeout != value) { + _rtspTimeout = value; + emit rtspTimeoutChanged(); + } +} + +void VideoReceiver::setUnittestMode(bool runUnitTests) +{ + _unittTestMode = runUnitTests; +} + #endif //----------------------------------------------------------------------------- @@ -454,11 +543,10 @@ void VideoReceiver::start() { qCDebug(VideoReceiverLog) << "Starting " << _uri; - if(qgcApp()->runningUnitTests()) { + if(_unittTestMode) { return; } - if(!_videoSettings->streamEnabled()->rawValue().toBool() || - !_videoSettings->streamConfigured()) { + if(!_streamEnabled || !_streamConfigured) { qCDebug(VideoReceiverLog) << "Stream not enabled/configured"; return; } @@ -470,7 +558,7 @@ VideoReceiver::start() #if defined(QGC_GST_TAISYNC_ENABLED) && (defined(__android__) || defined(__ios__)) //-- Taisync on iOS or Android sends a raw h.264 stream - if (qgcApp()->toolbox()->videoManager()->isTaisync()) { + if (_isTaisync) { uri = QString("tsusb://0.0.0.0:%1").arg(TAISYNC_VIDEO_UDP_PORT); } #endif @@ -609,7 +697,7 @@ VideoReceiver::start() void VideoReceiver::stop() { - if(qgcApp() && qgcApp()->runningUnitTests()) { + if(_unittTestMode) { return; } #if defined(QGC_GST_STREAMING) @@ -765,46 +853,6 @@ VideoReceiver::_onBusMessage(GstBus* bus, GstMessage* msg, gpointer data) //----------------------------------------------------------------------------- #if defined(QGC_GST_STREAMING) void -VideoReceiver::_cleanupOldVideos() -{ - //-- Only perform cleanup if storage limit is enabled - if(_videoSettings->enableStorageLimit()->rawValue().toBool()) { - QString savePath = qgcApp()->toolbox()->settingsManager()->appSettings()->videoSavePath(); - QDir videoDir = QDir(savePath); - videoDir.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable); - videoDir.setSorting(QDir::Time); - //-- All the movie extensions we support - QStringList nameFilters; - for(uint32_t i = 0; i < NUM_MUXES; i++) { - nameFilters << QString("*.") + QString(kVideoExtensions[i]); - } - videoDir.setNameFilters(nameFilters); - //-- get the list of videos stored - QFileInfoList vidList = videoDir.entryInfoList(); - if(!vidList.isEmpty()) { - uint64_t total = 0; - //-- Settings are stored using MB - uint64_t maxSize = (_videoSettings->maxVideoSize()->rawValue().toUInt() * 1024 * 1024); - //-- Compute total used storage - for(int i = 0; i < vidList.size(); i++) { - total += vidList[i].size(); - } - //-- Remove old movies until max size is satisfied. - while(total >= maxSize && !vidList.isEmpty()) { - total -= vidList.last().size(); - qCDebug(VideoReceiverLog) << "Removing old video file:" << vidList.last().filePath(); - QFile file (vidList.last().filePath()); - file.remove(); - vidList.removeLast(); - } - } - } -} -#endif - -//----------------------------------------------------------------------------- -#if defined(QGC_GST_STREAMING) -void VideoReceiver::setVideoSink(GstElement* videoSink) { if(_pipeline != nullptr) { @@ -934,6 +982,7 @@ void VideoReceiver::startRecording(const QString &videoFile) { #if defined(QGC_GST_STREAMING) + emit beforeRecording(); qCDebug(VideoReceiverLog) << "Starting recording"; // exit immediately if we are already recording @@ -942,18 +991,15 @@ VideoReceiver::startRecording(const QString &videoFile) return; } - uint32_t muxIdx = _videoSettings->recordingFormat()->rawValue().toUInt(); + uint32_t muxIdx = _recordingFormatId; if(muxIdx >= NUM_MUXES) { - qgcApp()->showMessage(tr("Invalid video format defined.")); + emit sendMessage(tr("Invalid video format defined.")); return; } - //-- Disk usage maintenance - _cleanupOldVideos(); - - QString savePath = qgcApp()->toolbox()->settingsManager()->appSettings()->videoSavePath(); + QString savePath = _videoPath; if(savePath.isEmpty()) { - qgcApp()->showMessage(tr("Unabled to record video. Video save path must be specified in Settings.")); + emit sendMessage(tr("Unabled to record video. Video save path must be specified in Settings.")); return; } @@ -1175,11 +1221,7 @@ VideoReceiver::_updateTimer() } if(_videoRunning) { - uint32_t timeout = 1; - if(qgcApp()->toolbox() && qgcApp()->toolbox()->settingsManager()) { - timeout = _videoSettings->rtspTimeout()->rawValue().toUInt(); - } - + uint32_t timeout = _rtspTimeout; const qint64 now = QDateTime::currentSecsSinceEpoch(); if(now - _lastFrameTime > timeout) { @@ -1189,7 +1231,7 @@ VideoReceiver::_updateTimer() } } else { // FIXME: AV: if pipeline is _running but not _streaming for some time then we need to restart - if(!_stop && !_running && !_uri.isEmpty() && _videoSettings->streamEnabled()->rawValue().toBool()) { + if(!_stop && !_running && !_uri.isEmpty() && _streamEnabled) { start(); } } diff --git a/src/VideoStreaming/VideoReceiver.h b/src/VideoStreaming/VideoReceiver.h index 7f6e73a..dac93c3 100644 --- a/src/VideoStreaming/VideoReceiver.h +++ b/src/VideoStreaming/VideoReceiver.h @@ -39,11 +39,54 @@ public: Q_PROPERTY(bool videoRunning READ videoRunning NOTIFY videoRunningChanged) Q_PROPERTY(QString imageFile READ imageFile NOTIFY imageFileChanged) Q_PROPERTY(QString videoFile READ videoFile NOTIFY videoFileChanged) + Q_PROPERTY(QString imagePath READ imagePath NOTIFY imagePathChanged) + Q_PROPERTY(QString videoPath READ videoPath NOTIFY videoPathChanged) + Q_PROPERTY(bool showFullScreen READ showFullScreen WRITE setShowFullScreen NOTIFY showFullScreenChanged) + Q_PROPERTY(bool streamEnabled READ streamEnabled WRITE setStreamEnabled NOTIFY streamEnabledChanged) + Q_PROPERTY(bool streamConfigured READ streamConfigured WRITE setStreamConfigured NOTIFY streamConfiguredChanged) + Q_PROPERTY(bool isTaisync READ isTaisync WRITE setIsTaysinc NOTIFY isTaisyncChanged) + + Q_PROPERTY(int recordingFormatId READ recordingFormatId WRITE setRecordingFormatId NOTIFY recordingFormatIdChanged) + Q_PROPERTY(int rtspTimeout READ rtspTimeout WRITE setRtspTimeout NOTIFY rtspTimeoutChanged) explicit VideoReceiver(QObject* parent = nullptr); ~VideoReceiver(); + bool streamEnabled() const; + Q_SLOT void setStreamEnabled(bool enabled); + Q_SIGNAL void streamEnabledChanged(); + + bool streamConfigured() const; + Q_SLOT void setStreamConfigured(bool enabled); + Q_SIGNAL void streamConfiguredChanged(); + + bool isTaisync() const; + Q_SLOT void setIsTaysinc(bool value); + Q_SIGNAL void isTaisyncChanged(); + + QString videoPath() const; + Q_SLOT void setVideoPath(const QString& path); + Q_SIGNAL void videoPathChanged(); + + QString imagePath() const; + Q_SLOT void setImagePath(const QString& path); + Q_SIGNAL void imagePathChanged(); + + int recordingFormatId() const; + Q_SLOT void setRecordingFormatId(int value); + Q_SIGNAL void recordingFormatIdChanged(); + + int rtspTimeout() const; + Q_SLOT void setRtspTimeout(int value); + Q_SIGNAL void rtspTimeoutChanged(); + + Q_SIGNAL void restartTimeout(); + Q_SIGNAL void sendMessage(const QString& message); + + // Emitted before recording starts. + Q_SIGNAL void beforeRecording(); + void setUnittestMode(bool runUnitTests); #if defined(QGC_GST_STREAMING) virtual bool recording () { return _recording; } #endif @@ -86,7 +129,6 @@ protected slots: #if defined(QGC_GST_STREAMING) GstElement* _makeSource (const QString& uri); GstElement* _makeFileSink (const QString& videoFile, unsigned format); - virtual void _restart_timeout (); virtual void _handleError (); virtual void _handleEOS (); virtual void _handleStateChanged (); @@ -122,7 +164,6 @@ protected: virtual void _unlinkRecordingBranch (GstPadProbeInfo* info); virtual void _shutdownRecordingBranch(); virtual void _shutdownPipeline (); - virtual void _cleanupOldVideos (); GstElement* _pipeline; GstElement* _videoSink; @@ -141,8 +182,18 @@ protected: QString _uri; QString _imageFile; QString _videoFile; + QString _videoPath; + QString _imagePath; + bool _videoRunning; bool _showFullScreen; - VideoSettings* _videoSettings; + bool _streamEnabled; + bool _streamConfigured; + bool _storageLimit; + bool _unittTestMode; + bool _isTaisync; + int _recordingFormatId; // 0 - 2, defined in VideoReceiver.cc / kVideoExtensions. TODO: use a better representation. + int _rtspTimeout; + };