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 import tempfile
from flask import Response, Blueprint, send_from_directory 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 import template_folder
from platypush.backend.http.app.utils import authenticate, send_request 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) camera_pi = Blueprint('camera.pi', __name__, template_folder=template_folder)
filename = os.path.join(tempfile.gettempdir(), 'camera_pi.jpg') filename = os.path.join(tempfile.gettempdir(), 'camera_pi.jpg')
@ -14,22 +17,34 @@ __routes__ = [
camera_pi, 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(): def video_feed():
try: try:
while True: while True:
frame_file = get_frame_file(warmup_time=0, close=False) frame = get_frame()
with open(frame_file, 'rb') as f:
frame = f.read()
yield (b'--frame\r\n' yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
finally: finally:
@ -38,8 +53,11 @@ def video_feed():
@camera_pi.route('/camera/pi/frame', methods=['GET']) @camera_pi.route('/camera/pi/frame', methods=['GET'])
@authenticate() @authenticate()
def get_frame(): def get_frame_img():
frame_file = get_frame_file() 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), return send_from_directory(os.path.dirname(frame_file),
os.path.basename(frame_file)) os.path.basename(frame_file))
@ -47,8 +65,16 @@ def get_frame():
@camera_pi.route('/camera/pi/stream', methods=['GET']) @camera_pi.route('/camera/pi/stream', methods=['GET'])
@authenticate() @authenticate()
def get_stream_feed(): def get_stream_feed():
global _camera
try:
return Response(video_feed(), return Response(video_feed(),
headers={'Cache-Control': 'no-cache, private', 'Pragma': 'no-cache', 'Age': 0},
mimetype='multipart/x-mixed-replace; boundary=frame') mimetype='multipart/x-mixed-replace; boundary=frame')
finally:
if _camera:
_camera.close_output_stream()
_camera = None
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,3 +1,4 @@
import io
import os import os
import re import re
import shutil import shutil
@ -15,6 +16,24 @@ from platypush.message.event.camera import CameraRecordingStartedEvent, \
from platypush.plugins import Plugin, action 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): class CameraPlugin(Plugin):
""" """
Plugin to control generic cameras over OpenCV. Plugin to control generic cameras over OpenCV.

View file

@ -7,10 +7,10 @@ import socket
import threading import threading
import time import time
from typing import Optional from typing import Optional, Union
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.camera import CameraPlugin from platypush.plugins.camera import CameraPlugin, StreamingOutput
class CameraPiPlugin(CameraPlugin): class CameraPiPlugin(CameraPlugin):
@ -67,6 +67,7 @@ class CameraPiPlugin(CameraPlugin):
self._time_lapse_stop_condition = threading.Condition() self._time_lapse_stop_condition = threading.Condition()
self._recording_stop_condition = threading.Condition() self._recording_stop_condition = threading.Condition()
self._streaming_stop_condition = threading.Condition() self._streaming_stop_condition = threading.Condition()
self._output: StreamingOutput = None
# noinspection PyUnresolvedReferences,PyPackageRequirements # noinspection PyUnresolvedReferences,PyPackageRequirements
def _get_camera(self, **opts): def _get_camera(self, **opts):
@ -173,6 +174,26 @@ class CameraPiPlugin(CameraPlugin):
if camera and close: if camera and close:
self.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 @action
def capture_sequence(self, n_images, directory, name_format='image_%04d.jpg', preview=False, warmup_time=2, def capture_sequence(self, n_images, directory, name_format='image_%04d.jpg', preview=False, warmup_time=2,
resize=None, **opts): resize=None, **opts):