NetworkMJPGImage.py 5.4 KB

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