platypush/platypush/backend/http/app/streaming/plugins/camera.py

139 lines
4.2 KiB
Python

from enum import Enum
import json
from typing import Optional
from typing_extensions import override
from tornado.web import stream_request_body
from platypush.context import get_plugin
from platypush.config import Config
from platypush.plugins.camera import Camera, CameraPlugin, StreamWriter
from platypush.utils import get_plugin_name_by_class
from .. import StreamingRoute
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.
"""
_redis_queue_prefix = f'_platypush/{Config.get("device_id") or ""}/camera'
def __init__(self, *args, **kwargs):
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
return None
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
@override
@classmethod
def _get_redis_queue(cls, camera: CameraPlugin, *_, **__) -> str:
plugin_name = get_plugin_name_by_class(camera.__class__)
assert plugin_name, f'No such plugin: {plugin_name}'
return '/'.join(
[
cls._redis_queue_prefix,
plugin_name,
*map(
str,
[camera.camera_info.device] if camera.camera_info.device else [],
),
]
)
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)
redis_queue = self._get_redis_queue(camera)
self.set_header('Content-Type', stream_class.mimetype)
with camera.open(
stream=True,
stream_format=self._extension,
frames_dir=None,
redis_queue=redis_queue,
**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.forward_stream(camera)
self.finish()