Added picamera streaming route and web panel tab

This commit is contained in:
Fabio Manganiello 2019-12-17 19:54:38 +01:00
parent bce4c7c51e
commit fb744dbc74
5 changed files with 164 additions and 9 deletions

View file

@ -0,0 +1,54 @@
import os
import tempfile
from flask import Response, Blueprint, send_from_directory
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, send_request
camera_pi = Blueprint('camera.pi', __name__, template_folder=template_folder)
filename = os.path.join(tempfile.gettempdir(), 'camera_pi.jpg')
# Declare routes list
__routes__ = [
camera_pi,
]
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'].get(0, 'Unable to capture an image file')
return response['output']['image_file']
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()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
finally:
send_request(action='camera.pi.close')
@camera_pi.route('/camera/pi/frame', methods=['GET'])
@authenticate()
def get_frame():
frame_file = get_frame_file()
return send_from_directory(os.path.dirname(frame_file),
os.path.basename(frame_file))
@camera_pi.route('/camera/pi/stream', methods=['GET'])
@authenticate()
def get_stream_feed():
return Response(video_feed(),
mimetype='multipart/x-mixed-replace; boundary=frame')
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1 @@
camera

View file

@ -0,0 +1,52 @@
Vue.component('camera-pi', {
template: '#tmpl-camera-pi',
props: ['config'],
data: function() {
return {
bus: new Vue({}),
streaming: false,
capturing: false,
};
},
methods: {
startStreaming: function() {
if (this.streaming)
return;
this.streaming = true;
this.capturing = false;
this.$refs.frame.setAttribute('src', '/camera/pi/stream');
},
stopStreaming: function() {
if (!this.streaming)
return;
this.streaming = false;
this.capturing = false;
this.$refs.frame.removeAttribute('src');
},
capture: function() {
if (this.capturing)
return;
this.streaming = false;
this.capturing = true;
this.$refs.frame.setAttribute('src', '/camera/pi/frame?t=' + (new Date()).getTime());
},
onFrameLoaded: function(event) {
if (this.capturing) {
this.capturing = false;
}
},
},
mounted: function() {
this.$refs.frame.addEventListener('load', this.onFrameLoaded);
},
});

View file

@ -0,0 +1,23 @@
<script type="text/x-template" id="tmpl-camera-pi">
<div class="camera">
<div class="camera-container">
<div class="no-frame" v-if="!streaming && !capturing">The camera is not active</div>
<img class="frame" ref="frame">
</div>
<div class="controls">
<button type="button" @click="startStreaming" :disabled="capturing" v-if="!streaming">
<i class="fa fa-play"></i>&nbsp; Start streaming
</button>
<button type="button" @click="stopStreaming" :disabled="capturing" v-else>
<i class="fa fa-stop"></i>&nbsp; Stop streaming
</button>
<button type="button" @click="capture" :disabled="streaming || capturing">
<i class="fas fa-camera"></i>&nbsp; Take snapshot
</button>
</div>
</div>
</script>

View file

@ -7,6 +7,8 @@ import socket
import threading import threading
import time import time
from typing import Optional
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.camera import CameraPlugin from platypush.plugins.camera import CameraPlugin
@ -21,14 +23,18 @@ class CameraPiPlugin(CameraPlugin):
""" """
_default_resolution = (800, 600) _default_resolution = (800, 600)
_default_listen_port = 5000
def __init__(self, resolution=(_default_resolution[0], _default_resolution[1]), framerate=24, def __init__(self, resolution=(_default_resolution[0], _default_resolution[1]), framerate=24,
hflip=False, vflip=False, sharpness=0, contrast=0, brightness=50, video_stabilization=False, iso=0, hflip=False, vflip=False, sharpness=0, contrast=0, brightness=50, video_stabilization=False, iso=0,
exposure_compensation=0, exposure_mode='auto', meter_mode='average', awb_mode='auto', exposure_compensation=0, exposure_mode='auto', meter_mode='average', awb_mode='auto',
image_effect='none', color_effects=None, rotation=0, crop=(0.0, 0.0, 1.0, 1.0), **kwargs): image_effect='none', color_effects=None, rotation=0, crop=(0.0, 0.0, 1.0, 1.0),
listen_port: int = _default_listen_port, **kwargs):
""" """
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
for a detailed reference about the Pi camera options. for a detailed reference about the Pi camera options.
:param listen_port: Default port that will be used for streaming the feed (default: 5000)
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
@ -53,6 +59,7 @@ class CameraPiPlugin(CameraPlugin):
} }
self._camera = None self._camera = None
self.listen_port = listen_port
self._time_lapse_thread = None self._time_lapse_thread = None
self._recording_thread = None self._recording_thread = None
@ -76,6 +83,17 @@ class CameraPiPlugin(CameraPlugin):
return self._camera return self._camera
@action
def close(self):
"""
Close an active connection to the camera.
"""
if not self._camera or self._camera.closed:
self.logger.info('Camera connection already closed')
self._camera.close()
self._camera = None
@action @action
def start_preview(self, **opts): def start_preview(self, **opts):
""" """
@ -99,7 +117,7 @@ class CameraPiPlugin(CameraPlugin):
self.logger.warning(str(e)) self.logger.warning(str(e))
@action @action
def take_picture(self, image_file, preview=False, warmup_time=2, resize=None, **opts): def take_picture(self, image_file, preview=False, warmup_time=2, resize=None, close=True, **opts):
""" """
Take a picture. Take a picture.
@ -118,6 +136,11 @@ class CameraPiPlugin(CameraPlugin):
:param opts: Extra options to pass to the camera (see :param opts: Extra options to pass to the camera (see
https://www.raspberrypi.org/documentation/usage/camera/python/README.md) https://www.raspberrypi.org/documentation/usage/camera/python/README.md)
:param close: If True (default) close the connection to the camera after capturing,
otherwise keep the connection open (e.g. if you want to take a sequence of pictures).
If you set close=False you should remember to call ``close`` when you don't need
the connection anymore.
:return: dict:: :return: dict::
{"image_file": path_to_the_image} {"image_file": path_to_the_image}
@ -144,10 +167,11 @@ class CameraPiPlugin(CameraPlugin):
if preview: if preview:
camera.stop_preview() camera.stop_preview()
return {'image_file': image_file} return {'image_file': image_file}
finally: finally:
if camera: if camera and close:
camera.close() self.close()
@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,
@ -183,8 +207,6 @@ class CameraPiPlugin(CameraPlugin):
""" """
camera = None
try: try:
camera = self._get_camera(**opts) camera = self._get_camera(**opts)
directory = os.path.abspath(os.path.expanduser(directory)) directory = os.path.abspath(os.path.expanduser(directory))
@ -213,7 +235,7 @@ class CameraPiPlugin(CameraPlugin):
return {'image_files': images} return {'image_files': images}
finally: finally:
camera.close() self.close()
@action @action
def start_time_lapse(self, directory, n_images=None, interval=0, warmup_time=2, def start_time_lapse(self, directory, n_images=None, interval=0, warmup_time=2,
@ -413,11 +435,11 @@ class CameraPiPlugin(CameraPlugin):
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins
@action @action
def start_streaming(self, listen_port=5000, format='h264', **opts): def start_streaming(self, listen_port: Optional[int] = None, format='h264', **opts):
""" """
Start recording to a network stream Start recording to a network stream
:param listen_port: TCP listen port (default: 5000) :param listen_port: TCP listen port (default: `listen_port` configured value or 5000)
:type listen_port: int :type listen_port: int
:param format: Video stream format (default: h264) :param format: Video stream format (default: h264)
@ -430,6 +452,9 @@ class CameraPiPlugin(CameraPlugin):
if self._streaming_thread: if self._streaming_thread:
return None, 'A streaming thread is already running' return None, 'A streaming thread is already running'
if not listen_port:
listen_port = self.listen_port
camera = self._get_camera(**opts) camera = self._get_camera(**opts)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)