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 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)
|
||||||
|
|
Loading…
Reference in a new issue