123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- 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):
- # Ensure that previous requests (if any) are stopped.
- self.stop()
- 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._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._manager = None
- self._started = False
- def getImage(self):
- return self._image
- ## Ensure that close gets called when object is destroyed
- def __del__(self):
- self.stop()
- 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()
|