From 17af488b326fbef9d71f5ecffc35e85e97cae801 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 17 Dec 2019 21:32:56 +0100 Subject: [PATCH] Using an io memory buffer to write the streamed images instead of a physical file to improve streaming performance --- .../http/app/routes/plugins/camera/pi.py | 52 ++++++++++++++----- platypush/plugins/camera/__init__.py | 19 +++++++ platypush/plugins/camera/pi.py | 25 ++++++++- 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/platypush/backend/http/app/routes/plugins/camera/pi.py b/platypush/backend/http/app/routes/plugins/camera/pi.py index 52c29bc9bd..719c1cb70b 100644 --- a/platypush/backend/http/app/routes/plugins/camera/pi.py +++ b/platypush/backend/http/app/routes/plugins/camera/pi.py @@ -2,9 +2,12 @@ import os import tempfile from flask import Response, Blueprint, send_from_directory +from typing import Optional from platypush.backend.http.app import template_folder from platypush.backend.http.app.utils import authenticate, send_request +from platypush.config import Config +from platypush.plugins.camera.pi import CameraPiPlugin camera_pi = Blueprint('camera.pi', __name__, template_folder=template_folder) filename = os.path.join(tempfile.gettempdir(), 'camera_pi.jpg') @@ -14,22 +17,34 @@ __routes__ = [ camera_pi, ] +_camera: Optional[CameraPiPlugin] = None -def get_frame_file(*args, **kwargs): - response = send_request(*args, action='camera.pi.take_picture', image_file=filename, **kwargs) - assert response.output and 'image_file' in response.output,\ - (response.errors[0] if response.errors else 'Unable to capture frame from the picamera') - return response.output['image_file'] +def get_camera() -> CameraPiPlugin: + global _camera + + # noinspection PyProtectedMember + if _camera and _camera._camera and not _camera._camera.closed: + return _camera + + camera_conf = Config.get('camera.pi') or {} + _camera = CameraPiPlugin(**camera_conf) + return _camera + + +def get_frame(): + camera = get_camera() + output = camera.get_output_stream() + + with output.ready: + output.ready.wait() + return output.frame def video_feed(): try: while True: - frame_file = get_frame_file(warmup_time=0, close=False) - with open(frame_file, 'rb') as f: - frame = f.read() - + frame = get_frame() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') finally: @@ -38,8 +53,11 @@ def video_feed(): @camera_pi.route('/camera/pi/frame', methods=['GET']) @authenticate() -def get_frame(): - frame_file = get_frame_file() +def get_frame_img(): + response = send_request('camera.pi.take_picture', image_file=filename) + frame_file = (response.output or {}).get('image_file') + assert frame_file is not None + return send_from_directory(os.path.dirname(frame_file), os.path.basename(frame_file)) @@ -47,8 +65,16 @@ def get_frame(): @camera_pi.route('/camera/pi/stream', methods=['GET']) @authenticate() def get_stream_feed(): - return Response(video_feed(), - mimetype='multipart/x-mixed-replace; boundary=frame') + global _camera + + try: + return Response(video_feed(), + headers={'Cache-Control': 'no-cache, private', 'Pragma': 'no-cache', 'Age': 0}, + mimetype='multipart/x-mixed-replace; boundary=frame') + finally: + if _camera: + _camera.close_output_stream() + _camera = None # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/__init__.py b/platypush/plugins/camera/__init__.py index 5e666c3b09..d00afa66e4 100644 --- a/platypush/plugins/camera/__init__.py +++ b/platypush/plugins/camera/__init__.py @@ -1,3 +1,4 @@ +import io import os import re import shutil @@ -15,6 +16,24 @@ from platypush.message.event.camera import CameraRecordingStartedEvent, \ from platypush.plugins import Plugin, action +class StreamingOutput: + def __init__(self): + self.frame = None + self.buffer = io.BytesIO() + self.ready = threading.Condition() + + def write(self, buf): + if buf.startswith(b'\xff\xd8'): + # New frame, copy the existing buffer's content and notify all clients that it's available + self.buffer.truncate() + with self.ready: + self.frame = self.buffer.getvalue() + self.ready.notify_all() + self.buffer.seek(0) + + return self.buffer.write(buf) + + class CameraPlugin(Plugin): """ Plugin to control generic cameras over OpenCV. diff --git a/platypush/plugins/camera/pi.py b/platypush/plugins/camera/pi.py index 591690aaf2..b2410facdf 100644 --- a/platypush/plugins/camera/pi.py +++ b/platypush/plugins/camera/pi.py @@ -7,10 +7,10 @@ import socket import threading import time -from typing import Optional +from typing import Optional, Union from platypush.plugins import action -from platypush.plugins.camera import CameraPlugin +from platypush.plugins.camera import CameraPlugin, StreamingOutput class CameraPiPlugin(CameraPlugin): @@ -67,6 +67,7 @@ class CameraPiPlugin(CameraPlugin): self._time_lapse_stop_condition = threading.Condition() self._recording_stop_condition = threading.Condition() self._streaming_stop_condition = threading.Condition() + self._output: StreamingOutput = None # noinspection PyUnresolvedReferences,PyPackageRequirements def _get_camera(self, **opts): @@ -173,6 +174,26 @@ class CameraPiPlugin(CameraPlugin): if camera and close: self.close() + def get_output_stream(self, resize: Union[tuple, list] = None, **opts) -> StreamingOutput: + camera = self._get_camera(**opts) + + if self._output and not camera.closed: + return self._output + + capture_opts = {} + if resize: + capture_opts['resize'] = tuple(resize) + + self._output = StreamingOutput() + camera.start_recording(self._output, format='mjpeg', **capture_opts) + return self._output + + def close_output_stream(self): + if self._camera and not self._camera.closed: + self._camera.stop_recording() + + self._output = None + @action def capture_sequence(self, n_images, directory, name_format='image_%04d.jpg', preview=False, warmup_time=2, resize=None, **opts):