refractori to use one common radio_init for both primary and secondary radio
This commit is contained in:
@@ -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, "
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user