Kstars

media.cpp
1 /*
2  SPDX-FileCopyrightText: 2018 Jasem Mutlaq <[email protected]>
3 
4  Media Channel
5 
6  SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "media.h"
10 #include "commands.h"
11 #include "profileinfo.h"
12 #include "skymapcomposite.h"
13 #include "fitsviewer/fitsview.h"
14 #include "fitsviewer/fitsdata.h"
15 #include "hips/hipsfinder.h"
16 #include "ekos/auxiliary/darklibrary.h"
17 #include "kspaths.h"
18 #include "Options.h"
19 
20 #include "ekos_debug.h"
21 
22 #include <QtConcurrent>
23 #include <KFormat>
24 
25 namespace EkosLive
26 {
27 
28 Media::Media(Ekos::Manager * manager): m_Manager(manager)
29 {
30  connect(&m_WebSocket, &QWebSocket::connected, this, &Media::onConnected);
31  connect(&m_WebSocket, &QWebSocket::disconnected, this, &Media::onDisconnected);
32  connect(&m_WebSocket, static_cast<void(QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error), this,
33  &Media::onError);
34 
35 
36  connect(this, &Media::newMetadata, this, &Media::uploadMetadata);
37  connect(this, &Media::newImage, this, [this](const QByteArray & image)
38  {
39  uploadImage(image);
40  m_TemporaryView.clear();
41  });
42 }
43 
44 void Media::connectServer()
45 {
46  QUrl requestURL(m_URL);
47 
49  query.addQueryItem("username", m_AuthResponse["username"].toString());
50  query.addQueryItem("token", m_AuthResponse["token"].toString());
51  if (m_AuthResponse.contains("remoteToken"))
52  query.addQueryItem("remoteToken", m_AuthResponse["remoteToken"].toString());
53  if (m_Options[OPTION_SET_CLOUD_STORAGE])
54  query.addQueryItem("cloudEnabled", "true");
55  query.addQueryItem("email", m_AuthResponse["email"].toString());
56  query.addQueryItem("from_date", m_AuthResponse["from_date"].toString());
57  query.addQueryItem("to_date", m_AuthResponse["to_date"].toString());
58  query.addQueryItem("plan_id", m_AuthResponse["plan_id"].toString());
59  query.addQueryItem("type", m_AuthResponse["type"].toString());
60 
61  requestURL.setPath("/media/ekos");
62  requestURL.setQuery(query);
63 
64 
65  m_WebSocket.open(requestURL);
66 
67  qCInfo(KSTARS_EKOS) << "Connecting to Websocket server at" << requestURL.toDisplayString();
68 }
69 
70 void Media::disconnectServer()
71 {
72  m_WebSocket.close();
73 }
74 
75 void Media::onConnected()
76 {
77  qCInfo(KSTARS_EKOS) << "Connected to media Websocket server at" << m_URL.toDisplayString();
78 
79  connect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Media::onTextReceived, Qt::UniqueConnection);
80  connect(&m_WebSocket, &QWebSocket::binaryMessageReceived, this, &Media::onBinaryReceived, Qt::UniqueConnection);
81 
82  m_isConnected = true;
83  m_ReconnectTries = 0;
84 
85  emit connected();
86 }
87 
88 void Media::onDisconnected()
89 {
90  qCInfo(KSTARS_EKOS) << "Disconnected from media Websocket server.";
91  m_isConnected = false;
92 
93  disconnect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Media::onTextReceived);
94  disconnect(&m_WebSocket, &QWebSocket::binaryMessageReceived, this, &Media::onBinaryReceived);
95 
96  m_sendBlobs = true;
97 
98  for (const QString &oneFile : temporaryFiles)
99  QFile::remove(oneFile);
100  temporaryFiles.clear();
101 
102  emit disconnected();
103 }
104 
105 void Media::onError(QAbstractSocket::SocketError error)
106 {
107  qCritical(KSTARS_EKOS) << "Media Websocket connection error" << m_WebSocket.errorString();
110  {
111  if (m_ReconnectTries++ < RECONNECT_MAX_TRIES)
112  QTimer::singleShot(RECONNECT_INTERVAL, this, SLOT(connectServer()));
113  }
114 }
115 
116 void Media::onTextReceived(const QString &message)
117 {
118  qCInfo(KSTARS_EKOS) << "Media Text Websocket Message" << message;
120  auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error);
121  if (error.error != QJsonParseError::NoError)
122  {
123  qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString();
124  return;
125  }
126 
127  const QJsonObject msgObj = serverMessage.object();
128  const QString command = msgObj["type"].toString();
129  const QJsonObject payload = msgObj["payload"].toObject();
130 
131  if (command == commands[ALIGN_SET_FILE_EXTENSION])
132  extension = payload["ext"].toString();
133  else if (command == commands[SET_BLOBS])
134  m_sendBlobs = msgObj["payload"].toBool();
135  // Get a list of object based on criteria
136  else if (command == commands[ASTRO_GET_OBJECTS_IMAGE])
137  {
138  int level = payload["level"].toInt(5);
139  double zoom = payload["zoom"].toInt(20000);
140 
141  // Object Names
142  QVariantList objectNames = payload["names"].toArray().toVariantList();
143 
144  for (auto &oneName : objectNames)
145  {
146  const QString name = oneName.toString();
147  SkyObject *oneObject = KStarsData::Instance()->skyComposite()->findByName(name, false);
148  if (oneObject)
149  {
150  QImage centerImage(HIPS_TILE_WIDTH, HIPS_TILE_HEIGHT, QImage::Format_ARGB32_Premultiplied);
151  double fov_w = 0, fov_h = 0;
152 
153  if (oneObject->type() == SkyObject::MOON || oneObject->type() == SkyObject::PLANET)
154  {
155  QProcess xplanetProcess;
156  const QString output = KSPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + "xplanet.jpg";
157  xplanetProcess.start(Options::xplanetPath(), QStringList()
158  << "--num_times" << "1"
159  << "--geometry" << QString("%1x%2").arg(HIPS_TILE_WIDTH).arg(HIPS_TILE_HEIGHT)
160  << "--body" << name.toLower()
161  << "--output" << output);
162  xplanetProcess.waitForFinished(5000);
163  centerImage.load(output);
164  }
165  else
166  HIPSFinder::Instance()->render(oneObject, level, zoom, &centerImage, fov_w, fov_h);
167 
168  if (!centerImage.isNull())
169  {
170  // Send everything as strings
171  QJsonObject metadata =
172  {
173  {"uuid", "hips"},
174  {"name", name},
175  {"resolution", QString("%1x%2").arg(HIPS_TILE_WIDTH).arg(HIPS_TILE_HEIGHT)},
176  {"bin", "1x1"},
177  {"fov_w", QString::number(fov_w)},
178  {"fov_h", QString::number(fov_h)},
179  {"ext", "jpg"}
180  };
181 
182  QByteArray jpegData;
183  QBuffer buffer(&jpegData);
184  buffer.open(QIODevice::WriteOnly);
185 
186  // First METADATA_PACKET bytes of the binary data is always allocated
187  // to the metadata, the rest to the image data.
189  meta = meta.leftJustified(METADATA_PACKET, 0);
190  buffer.write(meta);
191  centerImage.save(&buffer, "jpg", 90);
192  buffer.close();
193 
194  emit newImage(jpegData);
195  }
196  }
197  }
198  }
199 }
200 
201 void Media::onBinaryReceived(const QByteArray &message)
202 {
203  // Sometimes this is triggered even though it's a text message
204  Ekos::Align * align = m_Manager->alignModule();
205  if (align)
206  {
207  QString metadataString = message.left(METADATA_PACKET);
208  QJsonDocument metadataDocument = QJsonDocument::fromJson(metadataString.toLatin1());
209  QJsonObject metadataJSON = metadataDocument.object();
210  QString extension = metadataJSON.value("ext").toString();
211  align->loadAndSlew(message.mid(METADATA_PACKET), extension);
212  }
213 }
214 
215 void Media::sendData(const QSharedPointer<FITSData> &data, const QString &uuid)
216 {
217  if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false)
218  return;
219 
220  m_UUID = uuid;
221 
222  m_TemporaryView.reset(new FITSView());
223  m_TemporaryView->loadData(data);
224  QtConcurrent::run(this, &Media::upload, m_TemporaryView);
225 }
226 
227 void Media::sendFile(const QString &filename, const QString &uuid)
228 {
229  if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false)
230  return;
231 
232  m_UUID = uuid;
233 
234  QSharedPointer<FITSView> previewImage(new FITSView());
235  connect(previewImage.get(), &FITSView::loaded, this, [this, previewImage]()
236  {
237  QtConcurrent::run(this, &Media::upload, previewImage);
238  });
239  previewImage->loadFile(filename);
240 }
241 
242 void Media::sendView(const QSharedPointer<FITSView> &view, const QString &uuid)
243 {
244  if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false)
245  return;
246 
247  m_UUID = uuid;
248 
249  upload(view);
250 }
251 
252 void Media::upload(const QSharedPointer<FITSView> &view)
253 {
254  const QString ext = "jpg";
255  QByteArray jpegData;
256  QBuffer buffer(&jpegData);
257  buffer.open(QIODevice::WriteOnly);
258 
259  const QSharedPointer<FITSData> imageData = view->imageData();
260  QString resolution = QString("%1x%2").arg(imageData->width()).arg(imageData->height());
261  QString sizeBytes = KFormat().formatByteSize(imageData->size());
262  QVariant xbin(1), ybin(1), exposure(0), focal_length(0), gain(0), pixel_size(0), aperture(0);
263  imageData->getRecordValue("XBINNING", xbin);
264  imageData->getRecordValue("YBINNING", ybin);
265  imageData->getRecordValue("EXPTIME", exposure);
266  imageData->getRecordValue("GAIN", gain);
267  imageData->getRecordValue("PIXSIZE1", pixel_size);
268  imageData->getRecordValue("FOCALLEN", focal_length);
269  imageData->getRecordValue("APTDIA", aperture);
270 
271  // Account for binning
272  const double binned_pixel = pixel_size.toDouble() * xbin.toInt();
273  // Send everything as strings
274  QJsonObject metadata =
275  {
276  {"resolution", resolution},
277  {"size", sizeBytes},
278  {"channels", imageData->channels()},
279  {"mean", imageData->getAverageMean()},
280  {"median", imageData->getAverageMedian()},
281  {"stddev", imageData->getAverageStdDev()},
282  {"bin", QString("%1x%2").arg(xbin.toString(), ybin.toString())},
283  {"bpp", QString::number(imageData->bpp())},
284  {"uuid", m_UUID},
285  {"exposure", exposure.toString()},
286  {"focal_length", focal_length.toString()},
287  {"aperture", aperture.toString()},
288  {"gain", gain.toString()},
289  {"pixel_size", QString::number(binned_pixel, 'f', 4)},
290  {"ext", ext}
291  };
292 
293  // First METADATA_PACKET bytes of the binary data is always allocated
294  // to the metadata
295  // the rest to the image data.
297  meta = meta.leftJustified(METADATA_PACKET, 0);
298  buffer.write(meta);
299 
300  auto sendPixmap = (!m_Options[OPTION_SET_HIGH_BANDWIDTH] || m_UUID[0] == "+");
301  auto scaleWidth = sendPixmap ? HB_IMAGE_WIDTH / 2 : HB_IMAGE_WIDTH;
302 
303  // For low bandwidth images
304  // Except for dark frames +D
305  if (sendPixmap)
306  {
307  QPixmap scaledImage = view->getDisplayPixmap().width() > scaleWidth ?
308  view->getDisplayPixmap().scaledToWidth(scaleWidth, Qt::FastTransformation) :
309  view->getDisplayPixmap();
310  scaledImage.save(&buffer, ext.toLatin1().constData(), HB_IMAGE_QUALITY);
311  }
312  // For high bandwidth images
313  else
314  {
315  QImage scaledImage = view->getDisplayImage().width() > scaleWidth ?
316  view->getDisplayImage().scaledToWidth(scaleWidth, Qt::SmoothTransformation) :
317  view->getDisplayImage();
318  scaledImage.save(&buffer, ext.toLatin1().constData(), HB_IMAGE_QUALITY);
319  }
320  buffer.close();
321 
322  emit newImage(jpegData);
323 }
324 
325 void Media::sendUpdatedFrame(const QSharedPointer<FITSView> &view)
326 {
327  QString ext = "jpg";
328  QByteArray jpegData;
329  QBuffer buffer(&jpegData);
330  buffer.open(QIODevice::WriteOnly);
331 
332  const QSharedPointer<FITSData> imageData = view->imageData();
333 
334  if (!imageData)
335  return;
336 
337  const int32_t width = imageData->width();
338  const int32_t height = imageData->height();
339  QString resolution = QString("%1x%2").arg(width).arg(height);
340  QString sizeBytes = KFormat().formatByteSize(imageData->size());
341  QVariant xbin(1), ybin(1), exposure(0), focal_length(0), gain(0), pixel_size(0), aperture(0);
342  imageData->getRecordValue("XBINNING", xbin);
343  imageData->getRecordValue("YBINNING", ybin);
344  imageData->getRecordValue("EXPTIME", exposure);
345  imageData->getRecordValue("GAIN", gain);
346  imageData->getRecordValue("PIXSIZE1", pixel_size);
347  imageData->getRecordValue("FOCALLEN", focal_length);
348  imageData->getRecordValue("APTDIA", aperture);
349 
350  // Account for binning
351  const double binned_pixel = pixel_size.toDouble() * xbin.toInt();
352  // Send everything as strings
353  QJsonObject metadata =
354  {
355  {"resolution", resolution},
356  {"size", sizeBytes},
357  {"channels", imageData->channels()},
358  {"mean", imageData->getAverageMean()},
359  {"median", imageData->getAverageMedian()},
360  {"stddev", imageData->getAverageStdDev()},
361  {"bin", QString("%1x%2").arg(xbin.toString()).arg(ybin.toString())},
362  {"bpp", QString::number(imageData->bpp())},
363  {"uuid", "+A"},
364  {"exposure", exposure.toString()},
365  {"focal_length", focal_length.toString()},
366  {"aperture", aperture.toString()},
367  {"gain", gain.toString()},
368  {"pixel_size", QString::number(binned_pixel, 'f', 4)},
369  {"ext", ext}
370  };
371 
372  // First METADATA_PACKET bytes of the binary data is always allocated
373  // to the metadata
374  // the rest to the image data.
376  meta = meta.leftJustified(METADATA_PACKET, 0);
377  buffer.write(meta);
378 
379  // For low bandwidth images
380  QPixmap scaledImage;
381  // Align images
382  if (correctionVector.isNull() == false)
383  {
384  scaledImage = view->getDisplayPixmap();
385  const double currentZoom = view->getCurrentZoom();
386  const double normalizedZoom = currentZoom / 100;
387  // If zoom level is not 100%, then scale.
388  if (fabs(normalizedZoom - 1) > 0.001)
389  scaledImage = view->getDisplayPixmap().scaledToWidth(view->zoomedWidth());
390  else
391  scaledImage = view->getDisplayPixmap();
392  // as we factor in the zoom level, we adjust center and length accordingly
393  QPointF center = 0.5 * correctionVector.p1() * normalizedZoom + 0.5 * correctionVector.p2() * normalizedZoom;
394  uint32_t length = qMax(correctionVector.length() / normalizedZoom, 100 / normalizedZoom);
395 
396  QRect boundingRectable;
397  boundingRectable.setSize(QSize(length * 2, length * 2));
398  QPoint topLeft = (center - QPointF(length, length)).toPoint();
399  boundingRectable.moveTo(topLeft);
400  boundingRectable = boundingRectable.intersected(scaledImage.rect());
401 
402  emit newBoundingRect(boundingRectable, scaledImage.size(), currentZoom);
403 
404  scaledImage = scaledImage.copy(boundingRectable);
405  }
406  else
407  {
408  scaledImage = view->getDisplayPixmap().width() > HB_IMAGE_WIDTH / 2 ?
409  view->getDisplayPixmap().scaledToWidth(HB_IMAGE_WIDTH / 2, Qt::FastTransformation) :
410  view->getDisplayPixmap();
411  emit newBoundingRect(QRect(), QSize(), 100);
412  }
413 
414  scaledImage.save(&buffer, ext.toLatin1().constData(), HB_IMAGE_QUALITY);
415  buffer.close();
416  emit newImage(jpegData);
417 }
418 
419 void Media::sendVideoFrame(const QSharedPointer<QImage> &frame)
420 {
421  if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false || !frame)
422  return;
423 
424  int32_t width = m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_VIDEO_WIDTH : HB_VIDEO_WIDTH / 2;
425  QByteArray image;
426  QBuffer buffer(&image);
427  buffer.open(QIODevice::WriteOnly);
428 
429  QImage videoImage = (frame->width() > width) ? frame->scaledToWidth(width) : *frame;
430 
431  QString resolution = QString("%1x%2").arg(videoImage.width()).arg(videoImage.height());
432 
433  // First METADATA_PACKET bytes of the binary data is always allocated
434  // to the metadata
435  // the rest to the image data.
436  QJsonObject metadata =
437  {
438  {"resolution", resolution},
439  {"ext", "jpg"}
440  };
442  meta = meta.leftJustified(METADATA_PACKET, 0);
443  buffer.write(meta);
444 
445  QImageWriter writer;
446  writer.setDevice(&buffer);
447  writer.setFormat("JPG");
448  writer.setCompression(6);
449  writer.write(videoImage);
450  buffer.close();
451 
452  m_WebSocket.sendBinaryMessage(image);
453 }
454 
455 void Media::registerCameras()
456 {
457  if (m_isConnected == false)
458  return;
459 
460  for(auto &oneDevice : m_Manager->getAllDevices())
461  {
462  auto camera = dynamic_cast<ISD::Camera*>(oneDevice->getConcreteDevice(INDI::BaseDevice::CCD_INTERFACE));
463  if (!camera)
464  continue;
465  connect(camera, &ISD::Camera::newVideoFrame, this, &Media::sendVideoFrame, Qt::UniqueConnection);
466  }
467 }
468 
469 void Media::resetPolarView()
470 {
471  this->correctionVector = QLineF();
472 
473  m_Manager->alignModule()->zoomAlignView();
474 }
475 
476 void Media::uploadMetadata(const QByteArray &metadata)
477 {
478  m_WebSocket.sendTextMessage(metadata);
479 }
480 
481 void Media::uploadImage(const QByteArray &image)
482 {
483  m_WebSocket.sendBinaryMessage(image);
484 }
485 
486 void Media::processNewBLOB(IBLOB *bp)
487 {
488  Q_UNUSED(bp)
489 }
490 
491 void Media::sendModuleFrame(const QSharedPointer<FITSView> &view)
492 {
493  if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false)
494  return;
495 
496  if (qobject_cast<Ekos::Align*>(sender()) == m_Manager->alignModule())
497  sendView(view, "+A");
498  else if (qobject_cast<Ekos::Focus*>(sender()) == m_Manager->focusModule())
499  sendView(view, "+F");
500  else if (qobject_cast<Ekos::Guide*>(sender()) == m_Manager->guideModule())
501  sendView(view, "+G");
502  else if (qobject_cast<Ekos::DarkLibrary*>(sender()) == Ekos::DarkLibrary::Instance())
503  sendView(view, "+D");
504 }
505 }
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QRect rect() const const
void setSize(const QSize &size)
QJsonObject object() const const
QFuture< T > run(Function function,...)
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
Format_ARGB32_Premultiplied
QString number(int n, int base)
int height() const const
bool waitForFinished(int msecs)
bool remove()
QPixmap copy(int x, int y, int width, int height) const const
QSize size() const const
QAbstractSocket::SocketError error() const const
QChar separator()
QStringView level(QStringView ifopt)
QString toString() const const
QByteArray toLatin1() const const
QRect intersected(const QRect &rectangle) const const
bool loadAndSlew(const QByteArray &image, const QString &extension)
DBUS interface function.
Definition: align.cpp:3076
void binaryMessageReceived(const QByteArray &message)
int type(void) const
Definition: skyobject.h:188
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
SkyObject * findByName(const QString &name, bool exact=true) override
Search the children of this SkyMapComposite for a SkyObject whose name matches the argument.
QAction * zoom(const QObject *recvr, const char *slot, QObject *parent)
char * toString(const T &value)
bool save(const QString &fileName, const char *format, int quality) const const
void setFormat(const QByteArray &format)
UniqueConnection
QJsonValue value(const QString &key) const const
void setCompression(int compression)
void disconnected()
void connected()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
SkyMapComposite * skyComposite()
Definition: kstarsdata.h:165
QString toLower() const const
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString left(int n) const const
Generic record interfaces and implementations.
Definition: cloud.cpp:23
QString name(StandardShortcut id)
QByteArray toJson() const const
QByteArray leftJustified(int width, char fill, bool truncate) const const
QTextStream & center(QTextStream &stream)
void moveTo(int x, int y)
Align class handles plate-solving and polar alignment measurement and correction using astrometry....
Definition: align.h:73
void textMessageReceived(const QString &message)
FastTransformation
QString mid(int position, int n) const const
void setDevice(QIODevice *device)
Information about an object in the sky.
Definition: skyobject.h:41
bool save(const QString &fileName, const char *format, int quality) const const
QString message
bool write(const QImage &image)
int width() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu Aug 11 2022 04:00:01 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.