diff --git a/cura/CameraImageProvider.py b/cura/CameraImageProvider.py index ff66170f3c..ddf978f625 100644 --- a/cura/CameraImageProvider.py +++ b/cura/CameraImageProvider.py @@ -12,7 +12,7 @@ class CameraImageProvider(QQuickImageProvider): def requestImage(self, id, size): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): try: - return output_device.getCameraImage(), QSize(15, 15) + return output_device.activePrinter.camera.getImage(), QSize(15, 15) except AttributeError: pass return QImage(), QSize(15, 15) \ No newline at end of file diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py new file mode 100644 index 0000000000..5cb76d2876 --- /dev/null +++ b/cura/PrinterOutput/NetworkCamera.py @@ -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() diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index cb2dc15ea0..aaf9b48968 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -22,6 +22,7 @@ class PrinterOutputModel(QObject): headPositionChanged = pyqtSignal() keyChanged = pyqtSignal() typeChanged = pyqtSignal() + cameraChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) @@ -38,6 +39,17 @@ class PrinterOutputModel(QObject): self._type = "" + self._camera = None + + def setCamera(self, camera): + if self._camera is not camera: + self._camera = camera + self.cameraChanged.emit() + + @pyqtProperty(QObject, notify=cameraChanged) + def camera(self): + return self._camera + @pyqtProperty(str, notify = typeChanged) def type(self): return self._type diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 8114a3eef5..6403bdf14d 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -12,6 +12,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from cura.PrinterOutput.NetworkCamera import NetworkCamera from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController @@ -290,6 +291,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if printer is None: printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) + printer.setCamera(NetworkCamera("http://" + printer_data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) printer_list_changed = True diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml index d0a9e08232..3e6f6a8fd8 100644 --- a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml +++ b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml @@ -32,7 +32,7 @@ Item width: 20 * screenScaleFactor height: 20 * screenScaleFactor - onClicked: OutputDevice.selectAutomaticPrinter() + onClicked: OutputDevice.setActivePrinter(null) style: ButtonStyle { @@ -65,17 +65,23 @@ Item { if(visible) { - OutputDevice.startCamera() + if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) + { + OutputDevice.activePrinter.camera.start() + } } else { - OutputDevice.stopCamera() + if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) + { + OutputDevice.activePrinter.camera.stop() + } } } source: { - if(OutputDevice.cameraImage) + if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) { - return OutputDevice.cameraImage; + return OutputDevice.activePrinter.camera.latestImage; } return ""; }