forked from platypush/platypush
Moved camera routes.
Camera routes migrated from Flask blueprints to Tornado handlers.
This commit is contained in:
parent
b4d714df8a
commit
4bf9c01ac9
10 changed files with 235 additions and 171 deletions
|
@ -16,7 +16,7 @@ from tornado.web import Application, FallbackHandler
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.backend.http.app import application
|
from platypush.backend.http.app import application
|
||||||
from platypush.backend.http.app.utils import get_ws_routes
|
from platypush.backend.http.app.utils import get_streaming_routes, get_ws_routes
|
||||||
from platypush.backend.http.app.ws.events import events_redis_topic
|
from platypush.backend.http.app.ws.events import events_redis_topic
|
||||||
|
|
||||||
from platypush.bus.redis import RedisBus
|
from platypush.bus.redis import RedisBus
|
||||||
|
@ -331,7 +331,10 @@ class HttpBackend(Backend):
|
||||||
container = WSGIContainer(application)
|
container = WSGIContainer(application)
|
||||||
tornado_app = Application(
|
tornado_app = Application(
|
||||||
[
|
[
|
||||||
*[(route.path(), route) for route in get_ws_routes()],
|
*[
|
||||||
|
(route.path(), route)
|
||||||
|
for route in [*get_ws_routes(), *get_streaming_routes()]
|
||||||
|
],
|
||||||
(r'.*', FallbackHandler, {'fallback': container}),
|
(r'.*', FallbackHandler, {'fallback': container}),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -352,7 +355,7 @@ class HttpBackend(Backend):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.use_werkzeug_server:
|
if self.use_werkzeug_server:
|
||||||
application.config['redis_queue'] = self.bus.redis_queue
|
application.config['redis_queue'] = self.bus.redis_queue # type: ignore
|
||||||
application.run(
|
application.run(
|
||||||
host=self.bind_address,
|
host=self.bind_address,
|
||||||
port=self.port,
|
port=self.port,
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import json
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from flask import Blueprint, request
|
|
||||||
from flask.wrappers import Response
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
|
||||||
from platypush.backend.http.app.utils import authenticate
|
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.plugins.camera import CameraPlugin, Camera, StreamWriter
|
|
||||||
|
|
||||||
camera = Blueprint('camera', __name__, template_folder=template_folder)
|
|
||||||
|
|
||||||
# Declare routes list
|
|
||||||
__routes__ = [
|
|
||||||
camera,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_camera(plugin: str) -> CameraPlugin:
|
|
||||||
plugin_name = f'camera.{plugin}'
|
|
||||||
p = get_plugin(plugin_name)
|
|
||||||
assert p, f'No such plugin: {plugin_name}'
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
def get_frame(session: Camera, timeout: Optional[float] = None) -> Optional[bytes]:
|
|
||||||
if session.stream:
|
|
||||||
with session.stream.ready:
|
|
||||||
session.stream.ready.wait(timeout=timeout)
|
|
||||||
return session.stream.frame
|
|
||||||
|
|
||||||
|
|
||||||
def feed(camera: CameraPlugin, **kwargs):
|
|
||||||
with camera.open(**kwargs) as session:
|
|
||||||
camera.start_camera(session)
|
|
||||||
while True:
|
|
||||||
frame = get_frame(session, timeout=5.0)
|
|
||||||
if frame:
|
|
||||||
yield frame
|
|
||||||
|
|
||||||
|
|
||||||
def get_args(kwargs):
|
|
||||||
kwargs = kwargs.copy()
|
|
||||||
if 't' in kwargs:
|
|
||||||
del kwargs['t']
|
|
||||||
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
if k == 'resolution':
|
|
||||||
v = json.loads('[{}]'.format(v))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
v = int(v)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
try:
|
|
||||||
v = float(v)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
kwargs[k] = v
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/photo.<extension>', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo(plugin, extension):
|
|
||||||
plugin = get_camera(plugin)
|
|
||||||
extension = 'jpeg' if extension in ('jpg', 'jpeg') else extension
|
|
||||||
|
|
||||||
with plugin.open(stream=True, stream_format=extension, frames_dir=None, **get_args(request.args)) as session:
|
|
||||||
plugin.start_camera(session)
|
|
||||||
frame = None
|
|
||||||
for _ in range(session.info.warmup_frames):
|
|
||||||
frame = get_frame(session)
|
|
||||||
|
|
||||||
return Response(frame, mimetype=session.stream.mimetype)
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/video.<extension>', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video(plugin, extension):
|
|
||||||
stream_class = StreamWriter.get_class_by_name(extension)
|
|
||||||
camera = get_camera(plugin)
|
|
||||||
return Response(
|
|
||||||
feed(camera, stream=True, stream_format=extension, frames_dir=None,
|
|
||||||
**get_args(request.args)
|
|
||||||
), mimetype=stream_class.mimetype
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/photo', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo_default(plugin):
|
|
||||||
return get_photo(plugin, 'jpeg')
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/video', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video_default(plugin):
|
|
||||||
return get_video(plugin, 'mjpeg')
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/frame', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo_deprecated(plugin):
|
|
||||||
return get_photo_default(plugin)
|
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/feed', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video_deprecated(plugin):
|
|
||||||
return get_video_default(plugin)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,51 +0,0 @@
|
||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
|
||||||
from platypush.backend.http.app.routes.plugins.camera import get_photo, get_video
|
|
||||||
from platypush.backend.http.app.utils import authenticate
|
|
||||||
|
|
||||||
camera_ir_mlx90640 = Blueprint('camera-ir-mlx90640', __name__, template_folder=template_folder)
|
|
||||||
|
|
||||||
# Declare routes list
|
|
||||||
__routes__ = [
|
|
||||||
camera_ir_mlx90640,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/photo.<extension>', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo_route(extension):
|
|
||||||
return get_photo('ir.mlx90640', extension)
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/video.<extension>', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video_route(extension):
|
|
||||||
return get_video('ir.mlx90640', extension)
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/photo', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo_route_default():
|
|
||||||
return get_photo_route('jpeg')
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/video', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video_route_default():
|
|
||||||
return get_video_route('mjpeg')
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/frame', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_photo_route_deprecated():
|
|
||||||
return get_photo_route_default()
|
|
||||||
|
|
||||||
|
|
||||||
@camera_ir_mlx90640.route('/camera/ir/mlx90640/feed', methods=['GET'])
|
|
||||||
@authenticate()
|
|
||||||
def get_video_route_deprecated():
|
|
||||||
return get_video_route_default()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
3
platypush/backend/http/app/streaming/__init__.py
Normal file
3
platypush/backend/http/app/streaming/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from ._base import StreamingRoute, logger
|
||||||
|
|
||||||
|
__all__ = ['StreamingRoute', 'logger']
|
50
platypush/backend/http/app/streaming/_base.py
Normal file
50
platypush/backend/http/app/streaming/_base.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from http.client import responses
|
||||||
|
import json
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Optional
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from tornado.web import RequestHandler, stream_request_body
|
||||||
|
|
||||||
|
from platypush.backend.http.app.utils.auth import AuthStatus, get_auth_status
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@stream_request_body
|
||||||
|
class StreamingRoute(RequestHandler, ABC):
|
||||||
|
"""
|
||||||
|
Base class for Tornado streaming routes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@override
|
||||||
|
def prepare(self):
|
||||||
|
# Perform authentication
|
||||||
|
if self.auth_required:
|
||||||
|
auth_status = get_auth_status(self.request)
|
||||||
|
if auth_status != AuthStatus.OK:
|
||||||
|
self.send_error(auth_status.value.code, error=auth_status.value.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Client %s connected to %s', self.request.remote_ip, self.request.path
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def write_error(self, status_code: int, error: Optional[str] = None, **_):
|
||||||
|
self.set_header("Content-Type", "application/json")
|
||||||
|
self.finish(
|
||||||
|
json.dumps(
|
||||||
|
{"status": status_code, "error": error or responses[status_code]}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def path(cls) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_required(self):
|
||||||
|
return True
|
134
platypush/backend/http/app/streaming/plugins/camera.py
Normal file
134
platypush/backend/http/app/streaming/plugins/camera.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
from enum import Enum
|
||||||
|
import json
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Optional
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from tornado.web import stream_request_body
|
||||||
|
from platypush.context import get_plugin
|
||||||
|
|
||||||
|
from platypush.plugins.camera import Camera, CameraPlugin, StreamWriter
|
||||||
|
|
||||||
|
from .. import StreamingRoute
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestType(Enum):
|
||||||
|
"""
|
||||||
|
Models the camera route request type (video or photo)
|
||||||
|
"""
|
||||||
|
|
||||||
|
UNKNOWN = ''
|
||||||
|
PHOTO = 'photo'
|
||||||
|
VIDEO = 'video'
|
||||||
|
|
||||||
|
|
||||||
|
@stream_request_body
|
||||||
|
class CameraRoute(StreamingRoute):
|
||||||
|
"""
|
||||||
|
Route for camera streams.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# TODO Support multiple concurrent requests
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._camera: Optional[Camera] = None
|
||||||
|
self._request_type = RequestType.UNKNOWN
|
||||||
|
self._extension: str = ''
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def path(cls) -> str:
|
||||||
|
return r"/camera/([a-zA-Z0-9_./]+)/([a-zA-Z0-9_]+)\.?([a-zA-Z0-9_]+)?"
|
||||||
|
|
||||||
|
def _get_camera(self, plugin: str) -> CameraPlugin:
|
||||||
|
plugin_name = f'camera.{plugin.replace("/", ".")}'
|
||||||
|
p = get_plugin(plugin_name)
|
||||||
|
assert p, f'No such plugin: {plugin_name}'
|
||||||
|
return p
|
||||||
|
|
||||||
|
def _get_frame(
|
||||||
|
self, camera: Camera, timeout: Optional[float] = None
|
||||||
|
) -> Optional[bytes]:
|
||||||
|
if camera.stream:
|
||||||
|
with camera.stream.ready:
|
||||||
|
camera.stream.ready.wait(timeout=timeout)
|
||||||
|
return camera.stream.frame
|
||||||
|
|
||||||
|
def _should_stop(self):
|
||||||
|
if self._finished:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.request.connection and getattr(self.request.connection, 'stream', None):
|
||||||
|
return self.request.connection.stream.closed() # type: ignore
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def send_feed(self, camera: Camera):
|
||||||
|
while not self._should_stop():
|
||||||
|
frame = self._get_frame(camera, timeout=5.0)
|
||||||
|
if frame:
|
||||||
|
self.write(frame)
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def send_frame(self, camera: Camera):
|
||||||
|
frame = None
|
||||||
|
for _ in range(camera.info.warmup_frames):
|
||||||
|
frame = self._get_frame(camera)
|
||||||
|
|
||||||
|
if frame:
|
||||||
|
self.write(frame)
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def _set_request_type_and_extension(self, route: str, extension: str):
|
||||||
|
if route in {'photo', 'frame'}:
|
||||||
|
self._request_type = RequestType.PHOTO
|
||||||
|
if extension == 'jpg':
|
||||||
|
extension = 'jpeg'
|
||||||
|
self._extension = extension or 'jpeg'
|
||||||
|
elif route in {'video', 'feed'}:
|
||||||
|
self._request_type = RequestType.VIDEO
|
||||||
|
self._extension = extension or 'mjpeg'
|
||||||
|
|
||||||
|
def _get_args(self, kwargs: dict):
|
||||||
|
kwargs = {k: v[0].decode() for k, v in kwargs.items() if k != 't'}
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if k == 'resolution':
|
||||||
|
v = json.loads(f'[{v}]')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
v = int(v)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
kwargs[k] = v
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get(self, plugin: str, route: str, extension: str = '') -> None:
|
||||||
|
self._set_request_type_and_extension(route, extension)
|
||||||
|
if not (self._request_type and self._extension):
|
||||||
|
self.write_error(404, 'Not Found')
|
||||||
|
return
|
||||||
|
|
||||||
|
stream_class = StreamWriter.get_class_by_name(self._extension)
|
||||||
|
camera = self._get_camera(plugin)
|
||||||
|
self.set_header('Content-Type', stream_class.mimetype)
|
||||||
|
|
||||||
|
with camera.open(
|
||||||
|
stream=True,
|
||||||
|
stream_format=self._extension,
|
||||||
|
frames_dir=None,
|
||||||
|
**self._get_args(self.request.arguments),
|
||||||
|
) as session:
|
||||||
|
camera.start_camera(session)
|
||||||
|
if self._request_type == RequestType.PHOTO:
|
||||||
|
self.send_frame(session)
|
||||||
|
elif self._request_type == RequestType.VIDEO:
|
||||||
|
self.send_feed(session)
|
||||||
|
|
||||||
|
self.finish()
|
|
@ -13,6 +13,7 @@ from .routes import (
|
||||||
get_remote_base_url,
|
get_remote_base_url,
|
||||||
get_routes,
|
get_routes,
|
||||||
)
|
)
|
||||||
|
from .streaming import get_streaming_routes
|
||||||
from .ws import get_ws_routes
|
from .ws import get_ws_routes
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -27,6 +28,7 @@ __all__ = [
|
||||||
'get_message_response',
|
'get_message_response',
|
||||||
'get_remote_base_url',
|
'get_remote_base_url',
|
||||||
'get_routes',
|
'get_routes',
|
||||||
|
'get_streaming_routes',
|
||||||
'get_ws_routes',
|
'get_ws_routes',
|
||||||
'logger',
|
'logger',
|
||||||
'send_message',
|
'send_message',
|
||||||
|
|
39
platypush/backend/http/app/utils/streaming.py
Normal file
39
platypush/backend/http/app/utils/streaming.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
from typing import List, Type
|
||||||
|
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
from ..streaming import StreamingRoute, logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_streaming_routes() -> List[Type[StreamingRoute]]:
|
||||||
|
"""
|
||||||
|
Scans for streaming routes.
|
||||||
|
"""
|
||||||
|
from platypush.backend.http import HttpBackend
|
||||||
|
|
||||||
|
base_pkg = '.'.join([HttpBackend.__module__, 'app', 'streaming'])
|
||||||
|
base_dir = os.path.join(
|
||||||
|
os.path.dirname(inspect.getfile(HttpBackend)), 'app', 'streaming'
|
||||||
|
)
|
||||||
|
routes = []
|
||||||
|
|
||||||
|
for _, mod_name, _ in pkgutil.walk_packages([base_dir], prefix=base_pkg + '.'):
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(mod_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('Could not import module %s', mod_name)
|
||||||
|
logger.exception(e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for _, obj in inspect.getmembers(module):
|
||||||
|
if (
|
||||||
|
inspect.isclass(obj)
|
||||||
|
and not inspect.isabstract(obj)
|
||||||
|
and issubclass(obj, StreamingRoute)
|
||||||
|
):
|
||||||
|
routes.append(obj)
|
||||||
|
|
||||||
|
return routes
|
|
@ -834,7 +834,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
if device:
|
if device:
|
||||||
return self._status(device)
|
return self._status(device)
|
||||||
|
|
||||||
return {id: self._status(device) for id, camera in self._devices.items()}
|
return {id: self._status(id) for id in self._devices}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def transform_frame(frame, color_transform):
|
def transform_frame(frame, color_transform):
|
||||||
|
|
Loading…
Reference in a new issue