forked from platypush/platypush
Added picamera streaming route and web panel tab
This commit is contained in:
parent
bce4c7c51e
commit
fb744dbc74
5 changed files with 164 additions and 9 deletions
54
platypush/backend/http/app/routes/plugins/camera/pi.py
Normal file
54
platypush/backend/http/app/routes/plugins/camera/pi.py
Normal 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:
|
|
@ -0,0 +1 @@
|
|||
camera
|
52
platypush/backend/http/static/js/plugins/camera.pi/index.js
Normal file
52
platypush/backend/http/static/js/plugins/camera.pi/index.js
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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> Start streaming
|
||||
</button>
|
||||
|
||||
<button type="button" @click="stopStreaming" :disabled="capturing" v-else>
|
||||
<i class="fa fa-stop"></i> Stop streaming
|
||||
</button>
|
||||
|
||||
<button type="button" @click="capture" :disabled="streaming || capturing">
|
||||
<i class="fas fa-camera"></i> Take snapshot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
|
@ -7,6 +7,8 @@ import socket
|
|||
import threading
|
||||
import time
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.camera import CameraPlugin
|
||||
|
||||
|
@ -21,14 +23,18 @@ class CameraPiPlugin(CameraPlugin):
|
|||
"""
|
||||
|
||||
_default_resolution = (800, 600)
|
||||
_default_listen_port = 5000
|
||||
|
||||
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,
|
||||
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
|
||||
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)
|
||||
|
||||
|
@ -53,6 +59,7 @@ class CameraPiPlugin(CameraPlugin):
|
|||
}
|
||||
|
||||
self._camera = None
|
||||
self.listen_port = listen_port
|
||||
|
||||
self._time_lapse_thread = None
|
||||
self._recording_thread = None
|
||||
|
@ -76,6 +83,17 @@ class CameraPiPlugin(CameraPlugin):
|
|||
|
||||
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
|
||||
def start_preview(self, **opts):
|
||||
"""
|
||||
|
@ -99,7 +117,7 @@ class CameraPiPlugin(CameraPlugin):
|
|||
self.logger.warning(str(e))
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -118,6 +136,11 @@ class CameraPiPlugin(CameraPlugin):
|
|||
:param opts: Extra options to pass to the camera (see
|
||||
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::
|
||||
|
||||
{"image_file": path_to_the_image}
|
||||
|
@ -144,10 +167,11 @@ class CameraPiPlugin(CameraPlugin):
|
|||
|
||||
if preview:
|
||||
camera.stop_preview()
|
||||
|
||||
return {'image_file': image_file}
|
||||
finally:
|
||||
if camera:
|
||||
camera.close()
|
||||
if camera and close:
|
||||
self.close()
|
||||
|
||||
@action
|
||||
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:
|
||||
camera = self._get_camera(**opts)
|
||||
directory = os.path.abspath(os.path.expanduser(directory))
|
||||
|
@ -213,7 +235,7 @@ class CameraPiPlugin(CameraPlugin):
|
|||
|
||||
return {'image_files': images}
|
||||
finally:
|
||||
camera.close()
|
||||
self.close()
|
||||
|
||||
@action
|
||||
def start_time_lapse(self, directory, n_images=None, interval=0, warmup_time=2,
|
||||
|
@ -413,11 +435,11 @@ class CameraPiPlugin(CameraPlugin):
|
|||
|
||||
# noinspection PyShadowingBuiltins
|
||||
@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
|
||||
|
||||
: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
|
||||
|
||||
:param format: Video stream format (default: h264)
|
||||
|
@ -430,6 +452,9 @@ class CameraPiPlugin(CameraPlugin):
|
|||
if self._streaming_thread:
|
||||
return None, 'A streaming thread is already running'
|
||||
|
||||
if not listen_port:
|
||||
listen_port = self.listen_port
|
||||
|
||||
camera = self._get_camera(**opts)
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
|
Loading…
Reference in a new issue