Moved camera routes.

Camera routes migrated from Flask blueprints to Tornado handlers.
This commit is contained in:
Fabio Manganiello 2023-05-27 22:24:45 +02:00
parent b4d714df8a
commit 4bf9c01ac9
Signed by: blacklight
GPG key ID: D90FBA7F76362774
10 changed files with 235 additions and 171 deletions

View file

@ -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,

View file

@ -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:

View file

@ -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:

View file

@ -0,0 +1,3 @@
from ._base import StreamingRoute, logger
__all__ = ['StreamingRoute', 'logger']

View 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

View 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()

View file

@ -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',

View 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

View file

@ -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):