from contextlib import contextmanager import json from typing import Generator, Optional from tornado.web import stream_request_body from platypush.backend.http.app.utils import send_request from platypush.config import Config from .. import StreamingRoute @stream_request_body class SoundRoute(StreamingRoute): """ Route for audio streams. """ _redis_queue_prefix = f'_platypush/{Config.get("device_id") or ""}/sound' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._audio_headers_written: bool = False """Send the audio file headers before we send the first audio frame.""" @classmethod def path(cls) -> str: return r"/sound/stream\.?([a-zA-Z0-9_]+)?" @contextmanager def _audio_stream(self, **kwargs) -> Generator[None, None, None]: response = send_request( 'sound.record', dtype='int16', **kwargs, ) assert response and not response.is_error(), ( 'Streaming error: ' + str(response.errors) if response else '(unknown)' ) yield send_request('sound.stop_recording') @classmethod def _get_redis_queue(cls, *_, device: Optional[str] = None, **__) -> str: return '/'.join([cls._redis_queue_prefix, *([device] if device else [])]) def _get_args(self, **kwargs): kwargs.update({k: v[0].decode() for k, v in self.request.arguments.items()}) device = kwargs.get('device') return { 'device': device, 'sample_rate': int(kwargs.get('sample_rate', 44100)), 'blocksize': int(kwargs.get('blocksize', 512)), 'latency': float(kwargs.get('latency', 0)), 'channels': int(kwargs.get('channels', 1)), 'format': kwargs.get('format', 'wav'), 'redis_queue': kwargs.get('redis_queue', self._get_redis_queue(device)), } @staticmethod def _content_type_by_extension(extension: str) -> str: if extension == 'mp3': return 'audio/mpeg' if extension == 'ogg': return 'audio/ogg' if extension == 'wav': return 'audio/wav;codec=pcm' if extension == 'flac': return 'audio/flac' if extension == 'aac': return 'audio/aac' return 'application/octet-stream' def get(self, extension: Optional[str] = None) -> None: ext = extension or 'wav' args = self._get_args(format=ext) try: with self._audio_stream(**args): self.set_header('Content-Type', self._content_type_by_extension(ext)) self.forward_stream(**args) self.finish() except AssertionError as e: self.set_header("Content-Type", "application/json") self.set_status(400, str(e)) self.finish(json.dumps({"error": str(e)})) except Exception as e: self.set_header("Content-Type", "application/json") self.logger.exception(e) self.set_status(500, str(e)) self.finish(json.dumps({"error": str(e)}))