Using an io memory buffer to write the streamed images instead of a

physical file to improve streaming performance
This commit is contained in:
Fabio Manganiello 2019-12-17 21:32:56 +01:00
parent 60d5e7a0f6
commit 17af488b32
3 changed files with 81 additions and 15 deletions

View file

@ -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():
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:

View file

@ -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.

View file

@ -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):