feat: add auto-start functionality for last used audio device on server startup
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user