File indexing completed on 2024-07-21 06:28:25

0001 /*
0002     SPDX-FileCopyrightText: 2003 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006     2004-03-16: A class to handle video streaming.
0007 */
0008 
0009 #include "streamwg.h"
0010 
0011 #include "kstars.h"
0012 #include "Options.h"
0013 #include "kstars_debug.h"
0014 #include "collimationoverlayoptions.h"
0015 #include "qobjectdefs.h"
0016 
0017 #include <basedevice.h>
0018 
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 
0022 #include <QLocale>
0023 #include <QDebug>
0024 #include <QPushButton>
0025 #include <QFileDialog>
0026 #include <QRgb>
0027 #include <QSocketNotifier>
0028 #include <QImage>
0029 #include <QPainter>
0030 #include <QDir>
0031 #include <QLayout>
0032 #include <QPaintEvent>
0033 #include <QCloseEvent>
0034 #include <QImageWriter>
0035 #include <QImageReader>
0036 #include <QIcon>
0037 #include <QTimer>
0038 
0039 #include <cstdlib>
0040 #include <fcntl.h>
0041 
0042 RecordOptions::RecordOptions(QWidget *parent) : QDialog(parent)
0043 {
0044 #ifdef Q_OS_OSX
0045     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0046 #endif
0047 
0048     setupUi(this);
0049 
0050     dirPath = QUrl::fromLocalFile(QDir::homePath());
0051 
0052     selectDirB->setIcon(
0053         QIcon::fromTheme("document-open-folder"));
0054     connect(selectDirB, SIGNAL(clicked()), this, SLOT(selectRecordDirectory()));
0055 }
0056 
0057 void RecordOptions::selectRecordDirectory()
0058 {
0059     QString dir =
0060         QFileDialog::getExistingDirectory(KStars::Instance(), i18nc("@title:window", "SER Record Directory"),
0061                                           dirPath.toLocalFile());
0062 
0063     if (dir.isEmpty())
0064         return;
0065 
0066     recordDirectoryEdit->setText(dir);
0067 }
0068 
0069 StreamWG::StreamWG(ISD::Camera *ccd) : QDialog(KStars::Instance())
0070 {
0071     setupUi(this);
0072     m_Camera  = ccd;
0073     streamWidth = streamHeight = -1;
0074     processStream = colorFrame = isRecording = false;
0075 
0076     options = new RecordOptions(this);
0077     connect(optionsB, SIGNAL(clicked()), options, SLOT(show()));
0078 
0079     collimationOptionsB->setIcon(QIcon::fromTheme("run-build-prune"));
0080     connect(collimationOptionsB, &QPushButton::clicked, this, [this]()
0081             {
0082                 CollimationOverlayOptions::Instance(this)->openEditor();
0083             });
0084 
0085     collimationB->setIcon(QIcon::fromTheme("crosshairs"));
0086     connect(CollimationOverlayOptions::Instance(this), SIGNAL(updated()), videoFrame, SLOT(modelChanged()));
0087     connect(collimationB, &QPushButton::clicked, videoFrame, &VideoWG::toggleOverlay);
0088 
0089     QString filename, directory;
0090     ccd->getSERNameDirectory(filename, directory);
0091 
0092     double duration = 0.1;
0093     bool hasStreamExposure = m_Camera->getStreamExposure(&duration);
0094     if (hasStreamExposure)
0095         targetFrameDurationSpin->setValue(duration);
0096     else
0097     {
0098         targetFrameDurationSpin->setEnabled(false);
0099         changeFPSB->setEnabled(false);
0100     }
0101 
0102     options->recordFilenameEdit->setText(filename);
0103     options->recordDirectoryEdit->setText(directory);
0104 
0105     setWindowTitle(i18nc("@title:window", "%1 Live Video", ccd->getDeviceName()));
0106 
0107 #if defined(Q_OS_OSX)
0108     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0109 #else
0110     setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
0111 #endif
0112     recordIcon = QIcon::fromTheme("media-record");
0113     stopIcon   = QIcon::fromTheme("media-playback-stop");
0114 
0115     optionsB->setIcon(QIcon::fromTheme("run-build"));
0116     resetFrameB->setIcon(QIcon::fromTheme("view-restore"));
0117 
0118     connect(resetFrameB, SIGNAL(clicked()), this, SLOT(resetFrame()));
0119 
0120     recordB->setIcon(recordIcon);
0121 
0122     connect(recordB, SIGNAL(clicked()), this, SLOT(toggleRecord()));
0123     connect(ccd, SIGNAL(videoRecordToggled(bool)), this, SLOT(updateRecordStatus(bool)));
0124 
0125     connect(videoFrame, &VideoWG::newSelection, this, &StreamWG::setStreamingFrame);
0126     connect(videoFrame, &VideoWG::imageChanged, this, &StreamWG::imageChanged);
0127 
0128     resize(Options::streamWindowWidth(), Options::streamWindowHeight());
0129 
0130     eoszoom = m_Camera->getProperty("eoszoom");
0131     if (eoszoom == nullptr)
0132     {
0133         zoomLevelCombo->hide();
0134     }
0135     else
0136     {
0137         connect(zoomLevelCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), [&]()
0138         {
0139             auto tvp = eoszoom->getText();
0140             QString zoomLevel = zoomLevelCombo->currentText().remove("x");
0141             tvp->at(0)->setText(zoomLevel.toLatin1().constData());
0142             handLabel->setEnabled(true);
0143             NSSlider->setEnabled(true);
0144             WESlider->setEnabled(true);
0145             // Set it twice!
0146             m_Camera->sendNewProperty(tvp);
0147             QTimer::singleShot(1000, this, [ &, tvp]()
0148             {
0149                 m_Camera->sendNewProperty(tvp);
0150             });
0151 
0152         });
0153     }
0154 
0155     eoszoomposition = m_Camera->getProperty("eoszoomposition");
0156     if (eoszoomposition == nullptr)
0157     {
0158         handLabel->hide();
0159         NSSlider->hide();
0160         WESlider->hide();
0161 
0162         horizontalSpacer->changeSize(1, 1, QSizePolicy::Expanding);
0163     }
0164     else
0165     {
0166         connect(NSSlider, &QSlider::sliderReleased, [&]()
0167         {
0168             auto tvp = eoszoomposition->getText();
0169             QString pos = QString("%1,%2").arg(WESlider->value()).arg(NSSlider->value());
0170             tvp->at(0)->setText(pos.toLatin1().constData());
0171             m_Camera->sendNewProperty(tvp);
0172         });
0173 
0174         connect(WESlider, &QSlider::sliderReleased, [&]()
0175         {
0176             auto tvp = eoszoomposition->getText();
0177             QString pos = QString("%1,%2").arg(WESlider->value()).arg(NSSlider->value());
0178             tvp->at(0)->setText(pos.toLatin1().constData());
0179             m_Camera->sendNewProperty(tvp);
0180         });
0181 
0182         horizontalSpacer->changeSize(1, 1, QSizePolicy::Preferred);
0183     }
0184 
0185     connect(m_Camera, &ISD::Camera::newFPS, this, &StreamWG::updateFPS);
0186     connect(m_Camera, &ISD::Camera::propertyUpdated, this, [this](INDI::Property prop)
0187     {
0188         if (prop.isNameMatch("CCD_INFO") || prop.isNameMatch("CCD_CFA"))
0189             syncDebayerParameters();
0190     });
0191     connect(changeFPSB, &QPushButton::clicked, this, [&]()
0192     {
0193         if (m_Camera)
0194         {
0195             m_Camera->setStreamExposure(targetFrameDurationSpin->value());
0196             m_Camera->setVideoStreamEnabled(false);
0197             QTimer::singleShot(1000, this, [&]()
0198             {
0199                 m_Camera->setVideoStreamEnabled(true);
0200             });
0201         }
0202     });
0203 
0204     debayerB->setIcon(QIcon(":/icons/cfa.svg"));
0205     connect(debayerB, &QPushButton::clicked, this, [this]()
0206     {
0207         m_DebayerActive = !m_DebayerActive;
0208     });
0209     syncDebayerParameters();
0210 }
0211 
0212 void StreamWG::syncDebayerParameters()
0213 {
0214     m_DebayerSupported = queryDebayerParameters();
0215     debayerB->setEnabled(m_DebayerSupported);
0216     m_DebayerActive = m_DebayerSupported;
0217 }
0218 
0219 bool StreamWG::queryDebayerParameters()
0220 {
0221     if (!m_Camera)
0222         return false;
0223 
0224     ISD::CameraChip *targetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0225     if (!targetChip)
0226         return false;
0227 
0228     // DSLRs always send motion JPGs when streaming so
0229     // bayered images are not streamed.
0230     if (targetChip->getISOList().isEmpty() == false)
0231         return false;
0232 
0233     uint16_t w, h;
0234     QString pattern;
0235 
0236     if (targetChip->getImageInfo(w, h, pixelX, pixelY, m_BBP) == false)
0237         return false;
0238 
0239     // Limit only to 8 and 16 bit, nothing in between or less or more.
0240     if (m_BBP > 8)
0241         m_BBP = 16;
0242     else
0243         m_BBP = 8;
0244 
0245     if (targetChip->getBayerInfo(offsetX, offsetY, pattern) == false)
0246         return false;
0247 
0248     m_DebayerParams.method = DC1394_BAYER_METHOD_NEAREST;
0249     m_DebayerParams.filter = DC1394_COLOR_FILTER_RGGB;
0250 
0251     if (pattern == "GBRG")
0252         m_DebayerParams.filter = DC1394_COLOR_FILTER_GBRG;
0253     else if (pattern == "GRBG")
0254         m_DebayerParams.filter = DC1394_COLOR_FILTER_GRBG;
0255     else if (pattern == "BGGR")
0256         m_DebayerParams.filter = DC1394_COLOR_FILTER_BGGR;
0257 
0258     return true;
0259 }
0260 
0261 QSize StreamWG::sizeHint() const
0262 {
0263     QSize size(Options::streamWindowWidth(), Options::streamWindowHeight());
0264     return size;
0265 }
0266 
0267 void StreamWG::showEvent(QShowEvent *ev)
0268 {
0269     if (m_Camera)
0270     {
0271         // Always reset to 1x for DSLRs since they reset whenever they are triggered again.
0272         if (eoszoom)
0273             zoomLevelCombo->setCurrentIndex(0);
0274     }
0275 
0276     ev->accept();
0277 }
0278 
0279 void StreamWG::closeEvent(QCloseEvent * ev)
0280 {
0281     processStream = false;
0282 
0283     Options::setStreamWindowWidth(width());
0284     Options::setStreamWindowHeight(height());
0285 
0286     ev->accept();
0287 
0288     emit hidden();
0289 }
0290 
0291 void StreamWG::setColorFrame(bool color)
0292 {
0293     colorFrame = color;
0294 }
0295 
0296 void StreamWG::enableStream(bool enable)
0297 {
0298     if (enable)
0299     {
0300         processStream = true;
0301         show();
0302     }
0303     else
0304     {
0305         processStream = false;
0306         //instFPS->setText("--");
0307         avgFPS->setText("--");
0308         hide();
0309     }
0310 }
0311 
0312 void StreamWG::setSize(int wd, int ht)
0313 {
0314     if (wd != streamWidth || ht != streamHeight)
0315     {
0316         streamWidth  = wd;
0317         streamHeight = ht;
0318 
0319         NSSlider->setMaximum(ht);
0320         NSSlider->setSingleStep(ht / 30);
0321         WESlider->setMaximum(wd);
0322         WESlider->setSingleStep(wd / 30);
0323 
0324         videoFrame->setSize(wd, ht);
0325     }
0326 }
0327 
0328 /*void StreamWG::resizeEvent(QResizeEvent *ev)
0329 {
0330     streamFrame->resize(ev->size().width() - layout()->margin() * 2, ev->size().height() - playB->height() - layout()->margin() * 4 - layout()->spacing());
0331 
0332 }*/
0333 
0334 void StreamWG::updateRecordStatus(bool enabled)
0335 {
0336     if ((enabled && isRecording) || (!enabled && !isRecording))
0337         return;
0338 
0339     isRecording = enabled;
0340 
0341     if (isRecording)
0342     {
0343         recordB->setIcon(stopIcon);
0344         recordB->setToolTip(i18n("Stop recording"));
0345     }
0346     else
0347     {
0348         recordB->setIcon(recordIcon);
0349         recordB->setToolTip(i18n("Start recording"));
0350     }
0351 }
0352 
0353 void StreamWG::toggleRecord()
0354 {
0355     if (isRecording)
0356     {
0357         recordB->setIcon(recordIcon);
0358         isRecording = false;
0359         recordB->setToolTip(i18n("Start recording"));
0360 
0361         m_Camera->stopRecording();
0362     }
0363     else
0364     {
0365         QString directory, filename;
0366         m_Camera->getSERNameDirectory(filename, directory);
0367         if (filename != options->recordFilenameEdit->text() ||
0368                 directory != options->recordDirectoryEdit->text())
0369         {
0370             m_Camera->setSERNameDirectory(options->recordFilenameEdit->text(), options->recordDirectoryEdit->text());
0371             // Save config in INDI so the filename and directory templates are reloaded next time
0372             m_Camera->setConfig(SAVE_CONFIG);
0373         }
0374 
0375         if (options->recordUntilStoppedR->isChecked())
0376         {
0377             isRecording = m_Camera->startRecording();
0378         }
0379         else if (options->recordDurationR->isChecked())
0380         {
0381             isRecording = m_Camera->startDurationRecording(options->durationSpin->value());
0382         }
0383         else
0384         {
0385             isRecording = m_Camera->startFramesRecording(options->framesSpin->value());
0386         }
0387 
0388         if (isRecording)
0389         {
0390             recordB->setIcon(stopIcon);
0391             recordB->setToolTip(i18n("Stop recording"));
0392         }
0393     }
0394 }
0395 
0396 void StreamWG::newFrame(INDI::Property prop)
0397 {
0398     auto bp = prop.getBLOB()->at(0);
0399 
0400     bool rc = (m_DebayerActive
0401                && !strcmp(bp->getFormat(), ".stream")) ? videoFrame->newBayerFrame(bp, m_DebayerParams) : videoFrame->newFrame(bp);
0402 
0403     if (rc == false)
0404         qCWarning(KSTARS) << "Failed to load video frame.";
0405 }
0406 
0407 void StreamWG::resetFrame()
0408 {
0409     m_Camera->resetStreamingFrame();
0410 }
0411 
0412 void StreamWG::setStreamingFrame(QRect newFrame)
0413 {
0414     if (newFrame.isNull())
0415     {
0416         resetFrame();
0417         return;
0418     }
0419 
0420     if (newFrame.width() < 5 || newFrame.height() < 5)
0421         return;
0422 
0423     int w = newFrame.width();
0424     // Must be divisable by 4
0425     while (w % 4)
0426     {
0427         w++;
0428     }
0429 
0430     m_Camera->setStreamingFrame(newFrame.x(), newFrame.y(), w, newFrame.height());
0431 }
0432 
0433 void StreamWG::updateFPS(double instantFPS, double averageFPS)
0434 {
0435     Q_UNUSED(instantFPS)
0436     //instFPS->setText(QString::number(instantFPS, 'f', 1));
0437     avgFPS->setText(QString::number(averageFPS, 'f', 1));
0438 }
0439 
0440 StreamWG::~StreamWG()
0441 {
0442     CollimationOverlayOptions::Instance(this)->release();
0443 }