feat: add auto-start functionality for last used audio device on server startup

This commit is contained in:
2025-09-08 14:29:33 +02:00
parent e80ff79d67
commit fe3ed1636d
+97 -21
View File
@@ -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 dont 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.