|
@@ -0,0 +1,113 @@
|
|
|
+from UM.Logger import Logger
|
|
|
+
|
|
|
+from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
|
|
+from PyQt5.QtGui import QImage
|
|
|
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
|
|
+
|
|
|
+
|
|
|
+class NetworkCamera(QObject):
|
|
|
+ newImage = pyqtSignal()
|
|
|
+
|
|
|
+ def __init__(self, target = None, parent = None):
|
|
|
+ super().__init__(parent)
|
|
|
+ self._stream_buffer = b""
|
|
|
+ self._stream_buffer_start_index = -1
|
|
|
+ self._manager = None
|
|
|
+ self._image_request = None
|
|
|
+ self._image_reply = None
|
|
|
+ self._image = QImage()
|
|
|
+ self._image_id = 0
|
|
|
+
|
|
|
+ self._target = target
|
|
|
+ self._started = False
|
|
|
+
|
|
|
+ @pyqtSlot(str)
|
|
|
+ def setTarget(self, target):
|
|
|
+ restart_required = False
|
|
|
+ if self._started:
|
|
|
+ self.stop()
|
|
|
+ restart_required = True
|
|
|
+
|
|
|
+ self._target = target
|
|
|
+
|
|
|
+ if restart_required:
|
|
|
+ self.start()
|
|
|
+
|
|
|
+ @pyqtProperty(QUrl, notify=newImage)
|
|
|
+ def latestImage(self):
|
|
|
+ self._image_id += 1
|
|
|
+ # There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
|
|
+ # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
|
|
+ # as new (instead of relying on cached version and thus forces an update.
|
|
|
+ temp = "image://camera/" + str(self._image_id)
|
|
|
+
|
|
|
+ return QUrl(temp, QUrl.TolerantMode)
|
|
|
+
|
|
|
+ @pyqtSlot()
|
|
|
+ def start(self):
|
|
|
+ if self._target is None:
|
|
|
+ Logger.log("w", "Unable to start camera stream without target!")
|
|
|
+ return
|
|
|
+ self._started = True
|
|
|
+ url = QUrl(self._target)
|
|
|
+ self._image_request = QNetworkRequest(url)
|
|
|
+ if self._manager is None:
|
|
|
+ self._manager = QNetworkAccessManager()
|
|
|
+
|
|
|
+ self._image_reply = self._manager.get(self._image_request)
|
|
|
+ self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
|
|
+
|
|
|
+ @pyqtSlot()
|
|
|
+ def stop(self):
|
|
|
+ self._manager = None
|
|
|
+
|
|
|
+ self._stream_buffer = b""
|
|
|
+ self._stream_buffer_start_index = -1
|
|
|
+
|
|
|
+ if self._image_reply:
|
|
|
+ try:
|
|
|
+ # disconnect the signal
|
|
|
+ try:
|
|
|
+ self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+ # abort the request if it's not finished
|
|
|
+ if not self._image_reply.isFinished():
|
|
|
+ self._image_reply.close()
|
|
|
+ except Exception as e: # RuntimeError
|
|
|
+ pass # It can happen that the wrapped c++ object is already deleted.
|
|
|
+
|
|
|
+ self._image_reply = None
|
|
|
+ self._image_request = None
|
|
|
+
|
|
|
+ self._started = False
|
|
|
+
|
|
|
+ def getImage(self):
|
|
|
+ return self._image
|
|
|
+
|
|
|
+ def _onStreamDownloadProgress(self, bytes_received, bytes_total):
|
|
|
+ # An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
|
|
+ # JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
|
|
+ if self._image_reply is None:
|
|
|
+ return
|
|
|
+ self._stream_buffer += self._image_reply.readAll()
|
|
|
+
|
|
|
+ if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
|
|
+ Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
|
|
+ self.stop() # resets stream buffer and start index
|
|
|
+ self.start()
|
|
|
+ return
|
|
|
+
|
|
|
+ if self._stream_buffer_start_index == -1:
|
|
|
+ self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
|
|
+ stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
|
|
+ # If this happens to be more than a single frame, then so be it; the JPG decoder will
|
|
|
+ # ignore the extra data. We do it like this in order not to get a buildup of frames
|
|
|
+
|
|
|
+ if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
|
|
+ jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
|
|
+ self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
|
|
+ self._stream_buffer_start_index = -1
|
|
|
+ self._image.loadFromData(jpg_data)
|
|
|
+
|
|
|
+ self.newImage.emit()
|