NetworkMJPGImage.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # Copyright (c) 2018 Aldo Hoeben / fieldOfView
  2. # NetworkMJPGImage is released under the terms of the LGPLv3 or higher.
  3. from PyQt6.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect, QByteArray
  4. from PyQt6.QtGui import QImage, QPainter
  5. from PyQt6.QtQuick import QQuickPaintedItem
  6. from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
  7. from UM.Logger import Logger
  8. #
  9. # A QQuickPaintedItem that progressively downloads a network mjpeg stream,
  10. # picks it apart in individual jpeg frames, and paints it.
  11. #
  12. class NetworkMJPGImage(QQuickPaintedItem):
  13. def __init__(self, *args, **kwargs) -> None:
  14. super().__init__(*args, **kwargs)
  15. self._stream_buffer = QByteArray()
  16. self._stream_buffer_start_index = -1
  17. self._network_manager = None # type: QNetworkAccessManager
  18. self._image_request = None # type: QNetworkRequest
  19. self._image_reply = None # type: QNetworkReply
  20. self._image = QImage()
  21. self._image_rect = QRect()
  22. self._source_url = QUrl()
  23. self._started = False
  24. self._mirror = False
  25. self.setAntialiasing(True)
  26. def __del__(self) -> None:
  27. """Ensure that close gets called when object is destroyed"""
  28. self.stop()
  29. def paint(self, painter: "QPainter") -> None:
  30. if self._mirror:
  31. painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
  32. return
  33. painter.drawImage(self.contentsBoundingRect(), self._image)
  34. def setSourceURL(self, source_url: "QUrl") -> None:
  35. self._source_url = source_url
  36. self.sourceURLChanged.emit()
  37. if self._started:
  38. self.start()
  39. def getSourceURL(self) -> "QUrl":
  40. return self._source_url
  41. sourceURLChanged = pyqtSignal()
  42. source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)
  43. def setMirror(self, mirror: bool) -> None:
  44. if mirror == self._mirror:
  45. return
  46. self._mirror = mirror
  47. self.mirrorChanged.emit()
  48. self.update()
  49. def getMirror(self) -> bool:
  50. return self._mirror
  51. mirrorChanged = pyqtSignal()
  52. mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)
  53. imageSizeChanged = pyqtSignal()
  54. @pyqtProperty(int, notify = imageSizeChanged)
  55. def imageWidth(self) -> int:
  56. return self._image.width()
  57. @pyqtProperty(int, notify = imageSizeChanged)
  58. def imageHeight(self) -> int:
  59. return self._image.height()
  60. @pyqtSlot()
  61. def start(self) -> None:
  62. self.stop() # Ensure that previous requests (if any) are stopped.
  63. if not self._source_url:
  64. Logger.log("w", "Unable to start camera stream without target!")
  65. return
  66. self._started = True
  67. self._image_request = QNetworkRequest(self._source_url)
  68. if self._network_manager is None:
  69. self._network_manager = QNetworkAccessManager()
  70. self._image_reply = self._network_manager.get(self._image_request)
  71. self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
  72. @pyqtSlot()
  73. def stop(self) -> None:
  74. self._stream_buffer = QByteArray()
  75. self._stream_buffer_start_index = -1
  76. if self._image_reply:
  77. try:
  78. try:
  79. self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
  80. except Exception:
  81. pass
  82. if not self._image_reply.isFinished():
  83. self._image_reply.close()
  84. except Exception: # RuntimeError
  85. pass # It can happen that the wrapped c++ object is already deleted.
  86. self._image_reply = None
  87. self._image_request = None
  88. self._network_manager = None
  89. self._started = False
  90. def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
  91. # An MJPG stream is (for our purpose) a stream of concatenated JPG images.
  92. # JPG images start with the marker 0xFFD8, and end with 0xFFD9
  93. if self._image_reply is None:
  94. return
  95. self._stream_buffer += self._image_reply.readAll()
  96. if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
  97. Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
  98. self.stop() # resets stream buffer and start index
  99. self.start()
  100. return
  101. if self._stream_buffer_start_index == -1:
  102. self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
  103. stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
  104. # If this happens to be more than a single frame, then so be it; the JPG decoder will
  105. # ignore the extra data. We do it like this in order not to get a buildup of frames
  106. if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
  107. jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
  108. self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
  109. self._stream_buffer_start_index = -1
  110. self._image.loadFromData(jpg_data)
  111. if self._image.rect() != self._image_rect:
  112. self.imageSizeChanged.emit()
  113. self.update()