Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use NetworkMJPGImage instead of NetworkCamera
In Qt > 5.8, changing the source of an Image repeatedly seems to leak a lot of memory. NetworkMJPGImage is a custom QML component that replaces the NetworkCamera -> CameraImageProvider -> QML Image route with an integrated streaming-mjpeg-displaying QML item, foregoing a lot of signaling between pyqt and qml.
- Loading branch information
1 parent
4231561
commit 8d2003f
Showing
4 changed files
with
163 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,76 @@ | ||
import QtQuick 2.2 | ||
import OctoPrintPlugin 1.0 as OctoPrintPlugin | ||
|
||
Component | ||
{ | ||
Item | ||
{ | ||
Image | ||
OctoPrintPlugin.NetworkMJPGImage | ||
{ | ||
id: cameraImage | ||
visible: OutputDevice != null ? OutputDevice.showCamera : false | ||
property real maximumZoom: 2 | ||
property bool rotatedImage: (OutputDevice.cameraOrientation.rotation / 90) % 2 | ||
property bool proportionalHeight: | ||
{ | ||
if (sourceSize.height == 0 || maximumHeight == 0) | ||
if (imageHeight == 0 || maximumHeight == 0) | ||
{ | ||
return true; | ||
} | ||
if (!rotatedImage) | ||
{ | ||
return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight); | ||
return (imageWidth / imageHeight) > (maximumWidth / maximumHeight); | ||
} | ||
else | ||
{ | ||
return (sourceSize.width / sourceSize.height) > (maximumHeight / maximumWidth); | ||
return (imageWidth / imageHeight) > (maximumHeight / maximumWidth); | ||
} | ||
} | ||
property real _width: | ||
{ | ||
if (!rotatedImage) | ||
{ | ||
return Math.min(maximumWidth, sourceSize.width * screenScaleFactor * maximumZoom); | ||
return Math.min(maximumWidth, imageWidth * screenScaleFactor * maximumZoom); | ||
} | ||
else | ||
{ | ||
return Math.min(maximumHeight, sourceSize.width * screenScaleFactor * maximumZoom); | ||
return Math.min(maximumHeight, imageWidth * screenScaleFactor * maximumZoom); | ||
} | ||
} | ||
property real _height: | ||
{ | ||
if (!rotatedImage) | ||
{ | ||
return Math.min(maximumHeight, sourceSize.height * screenScaleFactor * maximumZoom); | ||
return Math.min(maximumHeight, imageHeight * screenScaleFactor * maximumZoom); | ||
} | ||
else | ||
{ | ||
return Math.min(maximumWidth, sourceSize.height * screenScaleFactor * maximumZoom); | ||
return Math.min(maximumWidth, imageHeight * screenScaleFactor * maximumZoom); | ||
} | ||
} | ||
width: proportionalHeight ? _width : sourceSize.width * _height / sourceSize.height | ||
height: !proportionalHeight ? _height : sourceSize.height * _width / sourceSize.width | ||
width: proportionalHeight ? _width : imageWidth * _height / imageHeight | ||
height: !proportionalHeight ? _height : imageHeight * _width / imageWidth | ||
anchors.horizontalCenter: parent.horizontalCenter | ||
anchors.verticalCenter: parent.verticalCenter | ||
|
||
Component.onCompleted: | ||
{ | ||
if (visible && OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) | ||
{ | ||
OutputDevice.activePrinter.camera.start(); | ||
} | ||
start(); | ||
} | ||
onVisibleChanged: | ||
{ | ||
if (OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) | ||
if (visible) | ||
{ | ||
if (visible) | ||
{ | ||
OutputDevice.activePrinter.camera.start(); | ||
} else | ||
{ | ||
OutputDevice.activePrinter.camera.stop(); | ||
} | ||
} | ||
} | ||
source: | ||
{ | ||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) | ||
start(); | ||
} else | ||
{ | ||
return OutputDevice.activePrinter.camera.latestImage; | ||
stop(); | ||
} | ||
return ""; | ||
} | ||
source: OutputDevice.cameraUrl | ||
|
||
rotation: OutputDevice.cameraOrientation.rotation | ||
mirror: OutputDevice.cameraOrientation.mirror | ||
//mirror: OutputDevice.cameraOrientation.mirror | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# Copyright (c) 2018 Aldo Hoeben / fieldOfView | ||
# NetworkMJPGImage is released under the terms of the AGPLv3 or higher. | ||
|
||
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect | ||
from PyQt5.QtGui import QImage | ||
from PyQt5.QtQuick import QQuickPaintedItem | ||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager | ||
|
||
from UM.Logger import Logger | ||
|
||
# | ||
# A QQuickPaintedItem that progressively downloads a network mjpeg stream, | ||
# picks it apart in individual jpeg frames, and paints it. | ||
# | ||
class NetworkMJPGImage(QQuickPaintedItem): | ||
|
||
def __init__(self, *args, **kwargs) -> None: | ||
super().__init__(*args, **kwargs) | ||
|
||
self._stream_buffer = b"" | ||
self._stream_buffer_start_index = -1 | ||
self._network_manager = None | ||
self._image_request = None | ||
self._image_reply = None | ||
self._image = QImage() | ||
self._image_rect = QRect() | ||
|
||
self._source_url = QUrl() | ||
self._started = False | ||
|
||
## Ensure that close gets called when object is destroyed | ||
def __del__(self) -> None: | ||
self.stop() | ||
|
||
|
||
def paint(self, painter: "QPainter") -> None: | ||
painter.drawImage(self.contentsBoundingRect(), self._image) | ||
|
||
|
||
def setSourceURL(self, source_url: "QUrl") -> None: | ||
self._source_url = source_url | ||
self.sourceURLChanged.emit() | ||
if self._started: | ||
self.start() | ||
|
||
def getSourceURL(self) -> "QUrl": | ||
return self._source_url | ||
|
||
sourceURLChanged = pyqtSignal() | ||
source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged) | ||
|
||
|
||
imageSizeChanged = pyqtSignal() | ||
|
||
@pyqtProperty(int, notify = imageSizeChanged) | ||
def imageWidth(self) -> int: | ||
return self._image.width() | ||
|
||
@pyqtProperty(int, notify = imageSizeChanged) | ||
def imageHeight(self) -> int: | ||
return self._image.height() | ||
|
||
|
||
@pyqtSlot() | ||
def start(self) -> None: | ||
self.stop() # Ensure that previous requests (if any) are stopped. | ||
|
||
if not self._source_url: | ||
Logger.log("w", "Unable to start camera stream without target!") | ||
return | ||
self._started = True | ||
|
||
self._image_request = QNetworkRequest(self._source_url) | ||
if self._network_manager is None: | ||
self._network_manager = QNetworkAccessManager() | ||
|
||
self._image_reply = self._network_manager.get(self._image_request) | ||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) | ||
|
||
@pyqtSlot() | ||
def stop(self) -> None: | ||
self._stream_buffer = b"" | ||
self._stream_buffer_start_index = -1 | ||
|
||
if self._image_reply: | ||
try: | ||
try: | ||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) | ||
except Exception: | ||
pass | ||
|
||
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._network_manager = None | ||
|
||
self._started = False | ||
|
||
|
||
def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None: | ||
# 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) | ||
|
||
if self._image.rect() != self._image_rect: | ||
self.imageSizeChanged.emit() | ||
|
||
self.update() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters