refractori to use one common radio_init for both primary and secondary radio

This commit is contained in:
2025-11-17 18:12:28 +01:00
parent 572eb4b600
commit 1b744d52d8
3 changed files with 209 additions and 227 deletions

View File

@@ -40,8 +40,6 @@ class AuracastGlobalConfig(BaseModel):
# so receivers may render earlier than the presentation delay for lower latency.
immediate_rendering: bool = False
assisted_listening_stream: bool = False
# Adaptive frame dropping: discard sub-frame samples when buffer exceeds threshold
enable_adaptive_frame_dropping: bool = False
# "Audio input. "
# "'device' -> use the host's default sound input device, "

View File

@@ -885,9 +885,6 @@ if __name__ == "__main__":
#config.immediate_rendering = True
#config.debug = True
config.enable_adaptive_frame_dropping=False
# Enable clock drift compensation to prevent latency accumulation
run_async(
broadcast(
config,

View File

@@ -191,25 +191,15 @@ async def _stream_lc3(audio_data: dict[str, str], bigs_template: list) -> None:
multicaster1.big_conf = bigs_template
await multicaster1.start_streaming()
@app.post("/init")
async def initialize(conf: auracast_config.AuracastConfigGroup):
"""Initializes the primary broadcaster on the streamer thread."""
global global_config_group
async with _stream_lock:
async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup, current_mc: multicast_control.Multicaster | None):
try:
global_config_group = conf
log.info('Initializing multicaster1 with config:\n %s', conf.model_dump_json(indent=2))
# Inline of _init_primary
global multicaster1
if multicaster1 is not None:
await multicaster1.shutdown()
multicaster1 = None
log.info('Initializing multicaster with transport %s and config:\n %s', transport, conf.model_dump_json(indent=2))
conf.transport = TRANSPORT1
conf.enable_adaptive_frame_dropping = any(
isinstance(big.audio_source, str) and big.audio_source.startswith('device:')
for big in conf.bigs
)
if current_mc is not None:
await current_mc.shutdown()
current_mc = None
conf.transport = transport
first_source = conf.bigs[0].audio_source if conf.bigs else ''
input_device_name = None
@@ -240,11 +230,12 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
for big in conf.bigs:
big.random_address = gen_random_add()
multicaster1 = multicast_control.Multicaster(conf, conf.bigs)
await multicaster1.init_broadcast()
mc = multicast_control.Multicaster(conf, conf.bigs)
await mc.init_broadcast()
auto_started = False
if any(isinstance(big.audio_source, str) and (big.audio_source.startswith("device:") or big.audio_source.startswith("file:")) for big in conf.bigs):
await multicaster1.start_streaming()
await mc.start_streaming()
auto_started = True
demo_count = sum(1 for big in conf.bigs if isinstance(big.audio_source, str) and big.audio_source.startswith('file:'))
@@ -273,100 +264,31 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
'demo_stream_type': demo_type,
'is_streaming': auto_started,
}
# Persist returned settings (avoid touching from worker thread)
save_settings(persisted, secondary=False)
return mc, persisted
except HTTPException:
raise
except Exception as e:
log.error("Exception in /init: %s", traceback.format_exc())
log.error("Exception in init_radio: %s", traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.post("/init")
async def initialize(conf: auracast_config.AuracastConfigGroup):
"""Initializes the primary broadcaster on the streamer thread."""
async with _stream_lock:
global multicaster1, global_config_group
mc, persisted = await init_radio(TRANSPORT1, conf, multicaster1)
multicaster1 = mc
global_config_group = conf
save_settings(persisted, secondary=False)
@app.post("/init2")
async def initialize2(conf: auracast_config.AuracastConfigGroup):
"""Initializes the secondary broadcaster on the streamer thread."""
try:
log.info('Initializing multicaster2 with config:\n %s', conf.model_dump_json(indent=2))
# Inline of _init_secondary
async with _stream_lock:
global multicaster2
if multicaster2 is not None:
await multicaster2.shutdown()
multicaster2 = None
conf.transport = TRANSPORT2
conf.enable_adaptive_frame_dropping = any(
isinstance(big.audio_source, str) and big.audio_source.startswith('device:')
for big in conf.bigs
)
conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3
for big in conf.bigs:
if isinstance(big.audio_source, str) and big.audio_source.startswith('device:'):
device_name = big.audio_source.split(':', 1)[1]
net_names = {d.get('name') for _, d in get_network_pw_inputs()}
alsa_usb_names = {d.get('name') for _, d in get_alsa_usb_inputs()}
device_index = resolve_input_device_index(device_name)
if device_index is None:
raise HTTPException(status_code=400, detail=f"Audio device '{device_name}' not found.")
big.audio_source = f'device:{device_index}'
multicaster2 = multicast_control.Multicaster(conf, conf.bigs)
await multicaster2.init_broadcast()
if any(isinstance(big.audio_source, str) and (big.audio_source.startswith("device:") or big.audio_source.startswith("file:")) for big in conf.bigs):
await multicaster2.start_streaming()
# Build and persist secondary settings analogous to primary
first_source = conf.bigs[0].audio_source if conf.bigs else ''
input_device_name = None
audio_mode_persist = 'Demo'
if isinstance(first_source, str) and first_source.startswith('device:'):
input_device_name = first_source.split(':', 1)[1] if ':' in first_source else None
try:
net_names = {d.get('name') for _, d in get_network_pw_inputs()}
except Exception:
net_names = set()
audio_mode_persist = 'Network' if (input_device_name in net_names) else 'USB'
demo_count = sum(1 for big in conf.bigs if isinstance(big.audio_source, str) and big.audio_source.startswith('file:'))
demo_rate = int(conf.auracast_sampling_rate_hz or 0)
demo_type = None
if demo_count > 0 and demo_rate > 0:
if demo_rate in (48000, 24000, 16000):
demo_type = f"{demo_count} × {demo_rate//1000}kHz"
else:
demo_type = f"{demo_count} × {demo_rate}Hz"
persisted2 = {
'channel_names': [big.name for big in conf.bigs],
'languages': [big.language for big in conf.bigs],
'audio_mode': audio_mode_persist,
'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,
'presentation_delay_us': getattr(conf, 'presentation_delay_us', None),
'rtn': getattr(getattr(conf, 'qos_config', None), 'number_of_retransmissions', None),
'immediate_rendering': getattr(conf, 'immediate_rendering', False),
'assisted_listening_stream': getattr(conf, 'assisted_listening_stream', False),
'stream_password': (conf.bigs[0].code if conf.bigs and getattr(conf.bigs[0], 'code', None) else None),
'demo_total_streams': demo_count,
'demo_stream_type': demo_type,
'is_streaming': any(isinstance(big.audio_source, str) and (big.audio_source.startswith("device:") or big.audio_source.startswith("file:")) for big in conf.bigs),
}
save_settings(persisted2, secondary=True)
is_demo = any(isinstance(big.audio_source, str) and big.audio_source.startswith('file:') for big in conf.bigs)
if is_demo:
settings = load_stream_settings() or {}
primary_count = int(settings.get('demo_total_streams') or len(settings.get('channel_names') or []))
secondary_count = len(conf.bigs or [])
total = primary_count + secondary_count
settings['demo_total_streams'] = total
demo_rate = int(conf.auracast_sampling_rate_hz or 0)
if demo_rate > 0:
if demo_rate in (48000, 24000, 16000):
settings['demo_stream_type'] = f"{total} × {demo_rate//1000}kHz"
else:
settings['demo_stream_type'] = f"{total} × {demo_rate}Hz"
settings['timestamp'] = datetime.utcnow().isoformat()
save_stream_settings(settings)
except Exception as e:
log.error("Exception in /init2: %s", traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
mc, persisted = await init_radio(TRANSPORT2, conf, multicaster2)
multicaster2 = mc
save_settings(persisted, secondary=True)
@app.post("/stop_audio")
async def stop_audio():
@@ -407,13 +329,12 @@ async def get_status():
return status
async def _autostart_from_settings():
"""Background task: auto-start last selected device-based input at server startup.
settings1 = load_stream_settings() or {}
settings2 = load_stream_settings2() or {}
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.
"""
settings = load_stream_settings() or {}
async def do_primary():
global multicaster1, global_config_group
settings = settings1
audio_mode = settings.get('audio_mode')
input_device_name = settings.get('input_device')
rate = settings.get('auracast_sampling_rate_hz')
@@ -429,41 +350,26 @@ async def _autostart_from_settings():
original_ts = settings.get('timestamp')
previously_streaming = bool(settings.get('is_streaming'))
# Only auto-start if the previous state was streaming and it's a device-based input.
if not previously_streaming:
if not previously_streaming or not input_device_name or rate is None or octets is None:
return
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
current = await _status_primary()
if current.get('is_streaming'):
return
while True:
# Do not interfere if user started a stream manually in the meantime
current = await _status_primary()
if current.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
# Check against the cached device lists
usb = [d for _, d in get_alsa_usb_inputs()]
net = [d for _, d in get_network_pw_inputs()]
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(
code=stream_password,
@@ -471,7 +377,6 @@ async def _autostart_from_settings():
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 is intentionally omitted to use the default
iso_que_len=1,
sampling_frequency=rate,
octets_per_frame=octets,
@@ -486,19 +391,101 @@ async def _autostart_from_settings():
presentation_delay_us=pres_delay if pres_delay is not None else 40000,
bigs=bigs,
)
# Attach QoS if saved_rtn present
conf.qos_config = auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(saved_rtn),
max_transport_latency_ms=int(saved_rtn) * 10 + 3,
)
# Initialize and start
await asyncio.sleep(2)
await initialize(conf)
async with _stream_lock:
mc, persisted = await init_radio(TRANSPORT1, conf, multicaster1)
multicaster1 = mc
global_config_group = conf
save_settings(persisted, secondary=False)
return
await asyncio.sleep(2)
async def do_secondary():
global multicaster2
settings = settings2
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')
pres_delay = settings.get('presentation_delay_us')
saved_rtn = settings.get('rtn')
immediate_rendering = settings.get('immediate_rendering', False)
assisted_listening_stream = settings.get('assisted_listening_stream', False)
channel_names = settings.get('channel_names') or ["Broadcast0"]
program_info = settings.get('program_info') or channel_names
languages = settings.get('languages') or ["deu"]
stream_password = settings.get('stream_password')
original_ts = settings.get('timestamp')
previously_streaming = bool(settings.get('is_streaming'))
if not previously_streaming or not input_device_name or rate is None or octets is None:
return
if multicaster2 is not None:
try:
if multicaster2.get_status().get('is_streaming'):
return
except Exception:
pass
while True:
if multicaster2 is not None:
try:
if multicaster2.get_status().get('is_streaming'):
return
except Exception:
pass
current_settings = load_stream_settings2() or {}
if current_settings.get('timestamp') != original_ts:
if (
current_settings.get('input_device') != input_device_name or
current_settings.get('audio_mode') != audio_mode
):
return
usb = [d for _, d in get_alsa_usb_inputs()]
net = [d for _, d in get_network_pw_inputs()]
names = {d.get('name') for d in usb} | {d.get('name') for d in net}
if input_device_name in names:
bigs = [
auracast_config.AuracastBigConfig(
code=stream_password,
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}",
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=TRANSPORT2,
immediate_rendering=immediate_rendering,
assisted_listening_stream=assisted_listening_stream,
presentation_delay_us=pres_delay if pres_delay is not None else 40000,
bigs=bigs,
)
conf.qos_config = auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(saved_rtn),
max_transport_latency_ms=int(saved_rtn) * 10 + 3,
)
await asyncio.sleep(2)
async with _stream_lock:
mc, persisted = await init_radio(TRANSPORT2, conf, multicaster2)
multicaster2 = mc
save_settings(persisted, secondary=True)
return
await asyncio.sleep(2)
await do_primary()
await do_secondary()
@app.on_event("startup")
async def _startup_autostart_event():
# Spawn the autostart task without blocking startup