diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index d17916a..977d74c 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -7,6 +7,8 @@ import sys from datetime import datetime import asyncio import numpy as np +from dotenv import load_dotenv + from pydantic import BaseModel from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -21,7 +23,7 @@ from auracast.utils.sounddevice_utils import ( list_usb_pw_inputs, list_network_pw_inputs, ) - +load_dotenv() PTIME = 40 # TODO: seems to have no effect at all pcs: Set[RTCPeerConnection] = set() # keep refs so they don’t GC early @@ -85,22 +87,19 @@ global_config_group = auracast_config.AuracastConfigGroup() multicaster1: multicast_control.Multicaster | None = None multicaster2: multicast_control.Multicaster | None = None + +# Raspberry Pi UART transports +TRANSPORT1 = os.getenv('TRANSPORT1', 'serial:/dev/ttyAMA3,1000000,rtscts') # transport for raspberry pi gpio header +TRANSPORT2 = os.getenv('TRANSPORT2', 'serial:/dev/ttyAMA4,1000000,rtscts') # transport for raspberry pi gpio header + @app.post("/init") async def initialize(conf: auracast_config.AuracastConfigGroup): """Initializes the primary broadcaster (multicaster1).""" global global_config_group global multicaster1 try: - if conf.transport == 'auto': - serial_devices = glob.glob('/dev/serial/by-id/*') - log.info('Found serial devices: %s', serial_devices) - for device in serial_devices: - if 'usb-ZEPHYR_Zephyr_HCI_UART_sample' in device: - log.info('Using: %s', device) - conf.transport = f'serial:{device},115200,rtscts' - break - if conf.transport == 'auto': - raise HTTPException(status_code=500, detail='No suitable transport found.') + + conf.transport = TRANSPORT1 # Derive audio_mode and input_device from first BIG audio_source first_source = conf.bigs[0].audio_source if conf.bigs else '' if first_source.startswith('device:'): @@ -132,6 +131,8 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): 'input_device': input_device_name, 'program_info': [getattr(big, 'program_info', None) for big in conf.bigs], 'gain': [getattr(big, 'input_gain', 1.0) for big in conf.bigs], + 'auracast_sampling_rate_hz': conf.auracast_sampling_rate_hz, + 'octets_per_frame': conf.octets_per_frame, 'timestamp': datetime.utcnow().isoformat() }) global_config_group = conf @@ -155,16 +156,7 @@ async def initialize2(conf: auracast_config.AuracastConfigGroup): """Initializes the secondary broadcaster (multicaster2). Does NOT persist stream settings.""" global multicaster2 try: - if conf.transport == 'auto': - serial_devices = glob.glob('/dev/serial/by-id/*') - log.info('Found serial devices: %s', serial_devices) - for device in serial_devices: - if 'usb-ZEPHYR_Zephyr_HCI_UART_sample' in device: - log.info('Using: %s', device) - conf.transport = f'serial:{device},115200,rtscts' - break - if conf.transport == 'auto': - raise HTTPException(status_code=500, detail='No suitable transport found.') + conf.transport = TRANSPORT2 # Patch device name to index for sounddevice for big in conf.bigs: if big.audio_source.startswith('device:'): @@ -242,6 +234,90 @@ async def get_status(): return status +async def _autostart_from_settings(): + """Background task: auto-start last selected device-based input at server startup. + + Skips Webapp (webrtc) and Demo (file) modes. Polls every 2 seconds until the + saved device name appears in either USB or Network lists, then builds a config + and initializes streaming. + """ + try: + settings = load_stream_settings() or {} + audio_mode = settings.get('audio_mode') + input_device_name = settings.get('input_device') + rate = settings.get('auracast_sampling_rate_hz') + octets = settings.get('octets_per_frame') + channel_names = settings.get('channel_names') or ["Broadcast0"] + program_info = settings.get('program_info') or channel_names + languages = settings.get('languages') or ["deu"] + original_ts = settings.get('timestamp') + + # Only auto-start device-based inputs; Webapp and Demo require external sources/UI + if not input_device_name: + return + if rate is None or octets is None: + # Not enough info to reconstruct stream reliably + return + + # Avoid duplicate start if already streaming + if multicaster1 and multicaster1.get_status().get('is_streaming'): + return + + while True: + try: + # Do not interfere if user started a stream manually in the meantime + if multicaster1 and multicaster1.get_status().get('is_streaming'): + return + # Abort if saved settings changed to a different target while we were polling + current_settings = load_stream_settings() or {} + if current_settings.get('timestamp') != original_ts: + # Settings were updated (likely by user via /init) + # If the target device or mode changed, stop autostart + if ( + current_settings.get('input_device') != input_device_name or + current_settings.get('audio_mode') != audio_mode + ): + return + # Avoid refreshing PortAudio while we poll + usb = [d for _, d in list_usb_pw_inputs(refresh=False)] + net = [d for _, d in list_network_pw_inputs(refresh=False)] + names = {d.get('name') for d in usb} | {d.get('name') for d in net} + if input_device_name in names: + # Build a minimal config based on saved fields + bigs = [ + auracast_config.AuracastBigConfig( + name=channel_names[0] if channel_names else "Broadcast0", + program_info=program_info[0] if isinstance(program_info, list) and program_info else program_info, + language=languages[0] if languages else "deu", + audio_source=f"device:{input_device_name}", + input_format=f"int16le,{rate},1", + iso_que_len=1, + sampling_frequency=rate, + octets_per_frame=octets, + ) + ] + conf = auracast_config.AuracastConfigGroup( + auracast_sampling_rate_hz=rate, + octets_per_frame=octets, + transport=TRANSPORT1, + bigs=bigs, + ) + # Initialize and start + await initialize(conf) + return + except Exception: + log.warning("Autostart polling encountered an error", exc_info=True) + await asyncio.sleep(2) + except Exception: + log.warning("Autostart task failed", exc_info=True) + + +@app.on_event("startup") +async def _startup_autostart_event(): + # Spawn the autostart task without blocking startup + asyncio.create_task(_autostart_from_settings()) + + @app.post("/refresh_audio_inputs") async def refresh_audio_inputs(force: bool = False): """Triggers a re-scan of audio devices.