File indexing completed on 2025-02-02 07:09:08
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 }