feat: add dual-radio analog input mode with independent channel configuration

- Added "Analog" mode to audio source options alongside Demo, USB, and Network
- Implemented dual-radio support for analog inputs (Radio 1 and Radio 2) with separate quality, timing, and metadata settings
- Filter analog devices (ch1/ch2) from USB device list to prevent conflicts between modes
- Added per-radio controls for stream quality, broadcast code, assistive listening flags, presentation delay, and RTN
- Introduce
This commit is contained in:
2025-11-19 12:18:50 +01:00
parent 690d31559b
commit 9bfea25fd2
2 changed files with 491 additions and 163 deletions

View File

@@ -119,9 +119,10 @@ def render_stream_controls(status_streaming: bool, start_label: str, stop_label:
# Audio mode selection with persisted default
# Note: backend persists 'USB' for any device:<name> source (including AES67). We default to 'USB' in that case.
options = [
"Demo",
"USB",
"Network",
"Demo",
"Analog",
"USB",
"Network",
]
saved_audio_mode = saved_settings.get("audio_mode", "Demo")
if saved_audio_mode not in options:
@@ -153,7 +154,12 @@ if isinstance(backend_mode_raw, str):
elif backend_mode_raw in options:
backend_mode_mapped = backend_mode_raw
running_mode = backend_mode_mapped if (is_streaming and backend_mode_mapped) else audio_mode
# When Analog is selected in the UI we always show it as such, even though the
# backend currently persists USB for all device sources.
if audio_mode == "Analog":
running_mode = "Analog"
else:
running_mode = backend_mode_mapped if (is_streaming and backend_mode_mapped) else audio_mode
is_started = False
is_stopped = False
@@ -338,111 +344,103 @@ if audio_mode == "Demo":
quality = None # Not used in demo mode
else:
# Stream quality selection (now enabled)
quality_options = list(QUALITY_MAP.keys())
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
quality = st.selectbox(
"Stream Quality (Sampling Rate)",
quality_options,
index=quality_options.index(default_quality),
help="Select the audio sampling rate for the stream. Lower rates may improve compatibility."
)
# --- Mode-specific configuration ---
default_name = saved_settings.get('channel_names', ["Broadcast0"])[0]
default_lang = saved_settings.get('languages', ["deu"])[0]
default_input = saved_settings.get('input_device') or 'default'
stream_name = st.text_input(
"Channel Name",
value=default_name,
help="The primary name for your broadcast. Like the SSID of a WLAN, it identifies your stream for receivers."
)
raw_program_info = saved_settings.get('program_info', default_name)
if isinstance(raw_program_info, list) and raw_program_info:
default_program_info = raw_program_info[0]
else:
default_program_info = raw_program_info
program_info = st.text_input(
"Program Info",
value=default_program_info,
help="Additional details about the broadcast program, such as its content or purpose. Shown to receivers for more context."
)
language = st.text_input(
"Language (ISO 639-3)",
value=default_lang,
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
)
# Optional broadcast code for coded streams
stream_passwort = st.text_input(
"Stream Passwort",
value="",
type="password",
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
)
# Flags and QoS row (compact, four columns)
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_flags1:
assisted_listening = st.checkbox(
"Assistive listening",
value=bool(saved_settings.get('assisted_listening_stream', False)),
help="tells the receiver that this is an assistive listening stream"
)
with col_flags2:
immediate_rendering = st.checkbox(
"Immediate rendering",
value=bool(saved_settings.get('immediate_rendering', False)),
help="tells the receiver to ignore presentation delay and render immediately if possible."
)
# QoS/presentation controls inline with flags
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
with col_pdelay:
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
presentation_delay_ms = st.number_input(
"Delay (ms)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Delay between capture and presentation for receivers."
)
default_rtn = int(saved_settings.get('rtn', 4) or 4)
with col_rtn:
rtn_options = [1,2,3,4]
default_rtn_clamped = min(4, max(1, default_rtn))
rtn = st.selectbox(
"RTN", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
)
default_lang = saved_settings.get('languages', ["deu"])[0]
# Input device selection for USB or AES67 mode
if audio_mode in ("USB", "Network"):
# Per-mode configuration and controls
input_device = None
radio2_enabled = False
radio1_cfg = None
radio2_cfg = None
if audio_mode == "Analog":
# --- Radio 1 controls ---
st.subheader("Radio 1")
quality_options = list(QUALITY_MAP.keys())
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
quality1 = st.selectbox(
"Stream Quality (Radio 1)",
quality_options,
index=quality_options.index(default_quality),
help="Select the audio sampling rate for Radio 1."
)
stream_passwort1 = st.text_input(
"Stream Passwort (Radio 1)",
value="",
type="password",
help="Optional: Set a broadcast code for Radio 1."
)
col_r1_flags1, col_r1_flags2, col_r1_pdelay, col_r1_rtn = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_r1_flags1:
assisted_listening1 = st.checkbox(
"Assistive listening (R1)",
value=bool(saved_settings.get('assisted_listening_stream', False)),
help="tells the receiver that this is an assistive listening stream"
)
with col_r1_flags2:
immediate_rendering1 = st.checkbox(
"Immediate rendering (R1)",
value=bool(saved_settings.get('immediate_rendering', False)),
help="tells the receiver to ignore presentation delay and render immediately if possible."
)
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
with col_r1_pdelay:
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
presentation_delay_ms1 = st.number_input(
"Delay (ms, R1)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Delay between capture and presentation for Radio 1."
)
default_rtn = int(saved_settings.get('rtn', 4) or 4)
with col_r1_rtn:
rtn_options = [1,2,3,4]
default_rtn_clamped = min(4, max(1, default_rtn))
rtn1 = st.selectbox(
"RTN (R1)", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions for Radio 1."
)
col_r1_name, col_r1_lang = st.columns([2, 1])
with col_r1_name:
stream_name1 = st.text_input(
"Channel Name (Radio 1)",
value=default_name,
help="Name for the first analog radio (Radio 1)."
)
with col_r1_lang:
language1 = st.text_input(
"Language (ISO 639-3) (Radio 1)",
value=default_lang,
help="Language code for Radio 1."
)
program_info1 = st.text_input(
"Program Info (Radio 1)",
value=default_program_info,
help="Program information for Radio 1."
)
# Analog mode exposes only ALSA ch1/ch2 inputs.
if not is_streaming:
# Only query device lists when NOT streaming to avoid extra backend calls
try:
endpoint = "/audio_inputs_pw_usb" if audio_mode == "USB" else "/audio_inputs_pw_network"
resp = requests.get(f"{BACKEND_URL}{endpoint}")
resp = requests.get(f"{BACKEND_URL}/audio_inputs_pw_usb")
device_list = resp.json().get('inputs', [])
except Exception as e:
st.error(f"Failed to fetch devices: {e}")
device_list = []
# Display "name [id]" but use name as value
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
device_names = [d['name'] for d in device_list]
analog_devices = [d for d in device_list if d.get('name') in ('ch1', 'ch2')]
# Determine default input by name (from persisted server state)
default_input_name = saved_settings.get('input_device')
if default_input_name not in device_names and device_names:
default_input_name = device_names[0]
default_input_label = None
for label, name in option_name_map.items():
if name == default_input_name:
default_input_label = label
break
if not input_options:
warn_text = (
"No USB audio input devices found. Connect a USB input and click Refresh."
if audio_mode == "USB" else
"No AES67/Network inputs found."
)
st.warning(warn_text)
if not analog_devices:
st.warning("No Analog (ch1/ch2) ALSA inputs found. Check asound configuration.")
if st.button("Refresh", disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
@@ -451,16 +449,242 @@ else:
except Exception as e:
st.error(f"Failed to refresh devices: {e}")
st.rerun()
input_device = None
analog_names = [d['name'] for d in analog_devices]
else:
analog_devices = []
analog_names = []
if not is_streaming:
if analog_names:
default_r1_idx = 0
input_device1 = st.selectbox(
"Input Device (Radio 1)",
analog_names,
index=default_r1_idx,
)
else:
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
with col1:
selected_option = st.selectbox(
"Input Device",
input_options,
index=input_options.index(default_input_label) if default_input_label in input_options else 0
input_device1 = None
else:
input_device1 = saved_settings.get('input_device')
st.selectbox(
"Input Device (Radio 1)",
[input_device1 or "No device selected"],
index=0,
disabled=True,
help="Stop the stream to change the input device."
)
# --- Radio 2 controls ---
st.subheader("Radio 2")
radio2_enabled = st.checkbox(
"Enable Radio 2",
value=False,
help="Activate a second analog radio with its own quality and timing settings."
)
if radio2_enabled:
quality2 = st.selectbox(
"Stream Quality (Radio 2)",
quality_options,
index=quality_options.index(default_quality),
help="Select the audio sampling rate for Radio 2."
)
stream_passwort2 = st.text_input(
"Stream Passwort (Radio 2)",
value="",
type="password",
help="Optional: Set a broadcast code for Radio 2."
)
col_r2_flags1, col_r2_flags2, col_r2_pdelay, col_r2_rtn = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_r2_flags1:
assisted_listening2 = st.checkbox(
"Assistive listening (R2)",
value=bool(saved_settings.get('assisted_listening_stream', False)),
help="tells the receiver that this is an assistive listening stream"
)
with col_r2_flags2:
immediate_rendering2 = st.checkbox(
"Immediate rendering (R2)",
value=bool(saved_settings.get('immediate_rendering', False)),
help="tells the receiver to ignore presentation delay and render immediately if possible."
)
with col_r2_pdelay:
presentation_delay_ms2 = st.number_input(
"Delay (ms, R2)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Delay between capture and presentation for Radio 2."
)
with col_r2_rtn:
rtn2 = st.selectbox(
"RTN (R2)", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions for Radio 2."
)
col_r2_name, col_r2_lang = st.columns([2, 1])
with col_r2_name:
stream_name2 = st.text_input(
"Channel Name (Radio 2)",
value=f"{default_name}_2",
help="Name for the second analog radio (Radio 2)."
)
with col_r2_lang:
language2 = st.text_input(
"Language (ISO 639-3) (Radio 2)",
value=default_lang,
help="Language code for Radio 2."
)
program_info2 = st.text_input(
"Program Info (Radio 2)",
value=default_program_info,
help="Program information for Radio 2."
)
if not is_streaming:
if analog_names:
default_r2_idx = 1 if len(analog_names) > 1 else 0
input_device2 = st.selectbox(
"Input Device (Radio 2)",
analog_names,
index=default_r2_idx,
)
with col2:
else:
input_device2 = None
else:
input_device2 = saved_settings.get('input_device')
st.selectbox(
"Input Device (Radio 2)",
[input_device2 or "No device selected"],
index=0,
disabled=True,
help="Stop the stream to change the input device."
)
radio2_cfg = {
'id': 1002,
'name': stream_name2,
'program_info': program_info2,
'language': language2,
'input_device': input_device2,
'quality': quality2,
'stream_passwort': stream_passwort2,
'assisted_listening': assisted_listening2,
'immediate_rendering': immediate_rendering2,
'presentation_delay_ms': presentation_delay_ms2,
'rtn': rtn2,
}
radio1_cfg = {
'id': 1001,
'name': stream_name1,
'program_info': program_info1,
'language': language1,
'input_device': input_device1,
'quality': quality1,
'stream_passwort': stream_passwort1,
'assisted_listening': assisted_listening1,
'immediate_rendering': immediate_rendering1,
'presentation_delay_ms': presentation_delay_ms1,
'rtn': rtn1,
}
else:
# USB/Network: single set of controls shared with the single channel
quality_options = list(QUALITY_MAP.keys())
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
quality = st.selectbox(
"Stream Quality (Sampling Rate)",
quality_options,
index=quality_options.index(default_quality),
help="Select the audio sampling rate for the stream. Lower rates may improve compatibility."
)
stream_passwort = st.text_input(
"Stream Passwort",
value="",
type="password",
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
)
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_flags1:
assisted_listening = st.checkbox(
"Assistive listening",
value=bool(saved_settings.get('assisted_listening_stream', False)),
help="tells the receiver that this is an assistive listening stream"
)
with col_flags2:
immediate_rendering = st.checkbox(
"Immediate rendering",
value=bool(saved_settings.get('immediate_rendering', False)),
help="tells the receiver to ignore presentation delay and render immediately if possible."
)
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
with col_pdelay:
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
presentation_delay_ms = st.number_input(
"Delay (ms)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Delay between capture and presentation for receivers."
)
default_rtn = int(saved_settings.get('rtn', 4) or 4)
with col_rtn:
rtn_options = [1,2,3,4]
default_rtn_clamped = min(4, max(1, default_rtn))
rtn = st.selectbox(
"RTN", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
)
stream_name = st.text_input(
"Channel Name",
value=default_name,
help="The primary name for your broadcast. Like the SSID of a WLAN, it identifies your stream for receivers."
)
program_info = st.text_input(
"Program Info",
value=default_program_info,
help="Additional details about the broadcast program, such as its content or purpose. Shown to receivers for more context."
)
language = st.text_input(
"Language (ISO 639-3)",
value=default_lang,
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
)
if audio_mode in ("USB", "Network"):
if not is_streaming:
try:
endpoint = "/audio_inputs_pw_usb" if audio_mode == "USB" else "/audio_inputs_pw_network"
resp = requests.get(f"{BACKEND_URL}{endpoint}")
device_list = resp.json().get('inputs', [])
except Exception as e:
st.error(f"Failed to fetch devices: {e}")
device_list = []
if audio_mode == "USB":
device_list = [d for d in device_list if d.get('name') not in ('ch1', 'ch2')]
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
device_names = [d['name'] for d in device_list]
default_input_name = saved_settings.get('input_device')
if default_input_name not in device_names and device_names:
default_input_name = device_names[0]
default_input_label = None
for label, name in option_name_map.items():
if name == default_input_name:
default_input_label = label
break
if not input_options:
warn_text = (
"No USB audio input devices found. Connect a USB input and click Refresh."
if audio_mode == "USB" else
"No AES67/Network inputs found."
)
st.warning(warn_text)
if st.button("Refresh", disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
@@ -469,21 +693,38 @@ else:
except Exception as e:
st.error(f"Failed to refresh devices: {e}")
st.rerun()
# Send only the device name to backend
input_device = option_name_map.get(selected_option)
input_device = None
else:
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
with col1:
selected_option = st.selectbox(
"Input Device",
input_options,
index=input_options.index(default_input_label) if default_input_label in input_options else 0
)
with col2:
if st.button("Refresh", disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
if not r.ok:
st.error(f"Failed to refresh: {r.text}")
except Exception as e:
st.error(f"Failed to refresh devices: {e}")
st.rerun()
input_device = option_name_map.get(selected_option)
else:
input_device = saved_settings.get('input_device')
current_label = input_device or "No device selected"
st.selectbox(
"Input Device",
[current_label],
index=0,
disabled=True,
help="Stop the stream to change the input device."
)
else:
# When streaming, keep showing the current selection but lock editing.
input_device = saved_settings.get('input_device')
current_label = input_device or "No device selected"
st.selectbox(
"Input Device",
[current_label],
index=0,
disabled=True,
help="Stop the stream to change the input device."
)
else:
input_device = None
input_device = None
start_stream, stop_stream = render_stream_controls(is_streaming, "Start Auracast", "Stop Auracast", running_mode)
if stop_stream:
@@ -499,48 +740,104 @@ else:
if start_stream:
# Always send stop to ensure backend is in a clean state, regardless of current status
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
#if r['was_running']:
# st.success("Stream Stopped!")
# Small pause lets backend fully release audio devices before re-init
time.sleep(1)
# Prepare config using the model (do NOT send qos_config, only relevant fields)
q = QUALITY_MAP[quality]
config = auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(rtn),
max_transport_latency_ms=int(rtn)*10 + 3,
),
bigs = [
auracast_config.AuracastBigConfig(
code=(stream_passwort.strip() or None),
name=stream_name,
program_info=program_info,
language=language,
audio_source=(f"device:{input_device}"),
input_format=(f"int16le,{q['rate']},1"),
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
),
]
)
try:
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
if r.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize: {r.text}")
except Exception as e:
st.error(f"Error: {e}")
if audio_mode == "Analog":
# Build separate configs per radio, each with its own quality and QoS parameters.
is_started = False
def _build_group_from_radio(cfg: dict) -> auracast_config.AuracastConfigGroup | None:
if not cfg or not cfg.get('input_device'):
return None
q = QUALITY_MAP[cfg['quality']]
return auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=bool(cfg['assisted_listening']),
immediate_rendering=bool(cfg['immediate_rendering']),
presentation_delay_us=int(cfg['presentation_delay_ms'] * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(cfg['rtn']),
max_transport_latency_ms=int(cfg['rtn']) * 10 + 3,
),
bigs=[
auracast_config.AuracastBigConfig(
id=cfg.get('id', 123456),
code=(cfg['stream_passwort'].strip() or None),
name=cfg['name'],
program_info=cfg['program_info'],
language=cfg['language'],
audio_source=f"device:{cfg['input_device']}",
input_format=f"int16le,{q['rate']},1",
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
)
],
)
# Radio 1 (always active if a device is selected)
config1 = _build_group_from_radio(radio1_cfg)
# Radio 2 (optional)
config2 = _build_group_from_radio(radio2_cfg) if radio2_enabled else None
try:
if config1 is not None:
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
if r1.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize Radio 1: {r1.text}")
else:
st.error("Radio 1 has no valid input device configured.")
if config2 is not None:
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
if r2.status_code != 200:
st.error(f"Failed to initialize Radio 2: {r2.text}")
except Exception as e:
st.error(f"Error while starting Analog radios: {e}")
else:
# USB/Network: single config as before, using shared controls
q = QUALITY_MAP[quality]
config = auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(rtn),
max_transport_latency_ms=int(rtn)*10 + 3,
),
bigs=[
auracast_config.AuracastBigConfig(
code=(stream_passwort.strip() or None),
name=stream_name,
program_info=program_info,
language=language,
audio_source=(f"device:{input_device}"),
input_format=(f"int16le,{q['rate']},1"),
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
),
],
)
try:
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
if r.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize: {r.text}")
except Exception as e:
st.error(f"Error: {e}")
# Centralized rerun based on start/stop outcomes
if is_started or is_stopped:

View File

@@ -33,6 +33,12 @@ TRANSPORT2 = os.getenv('TRANSPORT2', 'serial:/dev/ttyAMA4,1000000,rtscts') # tr
os.environ["PULSE_LATENCY_MSEC"] = "3"
# Defaults from the AuracastBigConfig model, used to detect whether random_address/id
# were explicitly set or are still at their model default values.
_DEFAULT_BIG = auracast_config.AuracastBigConfig()
DEFAULT_BIG_ID = _DEFAULT_BIG.id
DEFAULT_RANDOM_ADDRESS = _DEFAULT_BIG.random_address
# In-memory caches to avoid disk I/O on hot paths like /status
SETTINGS_CACHE1: dict = {}
SETTINGS_CACHE2: dict = {}
@@ -208,7 +214,11 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
input_device_name = first_source.split(':', 1)[1] if ':' in first_source else None
alsa_usb_names = {d.get('name') for _, d in get_alsa_usb_inputs()}
net_names = {d.get('name') for _, d in get_network_pw_inputs()}
audio_mode_persist = 'Network' if (input_device_name in net_names) else 'USB'
if input_device_name in ('ch1', 'ch2'):
# Explicitly treat ch1/ch2 as Analog input mode
audio_mode_persist = 'Analog'
else:
audio_mode_persist = 'Network' if (input_device_name in net_names) else 'USB'
if input_device_name and input_device_name.isdigit():
device_index = int(input_device_name)
@@ -227,8 +237,13 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3
# Only generate a new random_address if the BIG is still at the model default.
for big in conf.bigs:
big.random_address = gen_random_add()
if not getattr(big, 'random_address', None) or big.random_address == DEFAULT_RANDOM_ADDRESS:
big.random_address = gen_random_add()
# Log the final, fully-updated configuration just before creating the Multicaster
log.info('Final multicaster config (transport=%s):\n %s', transport, conf.model_dump_json(indent=2))
mc = multicast_control.Multicaster(conf, conf.bigs)
await mc.init_broadcast()
@@ -260,6 +275,8 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
'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),
'big_ids': [getattr(big, 'id', DEFAULT_BIG_ID) for big in conf.bigs],
'big_random_addresses': [getattr(big, 'random_address', DEFAULT_RANDOM_ADDRESS) for big in conf.bigs],
'demo_total_streams': demo_count,
'demo_stream_type': demo_type,
'is_streaming': auto_started,
@@ -297,13 +314,19 @@ async def stop_audio():
try:
was_running = await _stop_all()
# Persist is_streaming=False
# Persist is_streaming=False for both primary and secondary
try:
settings = load_stream_settings() or {}
if settings.get('is_streaming'):
settings['is_streaming'] = False
settings['timestamp'] = datetime.utcnow().isoformat()
save_stream_settings(settings)
settings1 = load_stream_settings() or {}
if settings1.get('is_streaming'):
settings1['is_streaming'] = False
settings1['timestamp'] = datetime.utcnow().isoformat()
save_stream_settings(settings1)
settings2 = load_stream_settings2() or {}
if settings2.get('is_streaming'):
settings2['is_streaming'] = False
settings2['timestamp'] = datetime.utcnow().isoformat()
save_stream_settings2(settings2)
except Exception:
log.warning("Failed to persist is_streaming=False during stop_audio", exc_info=True)
@@ -349,6 +372,8 @@ async def _autostart_from_settings():
channel_names = settings.get('channel_names') or ["Broadcast0"]
program_info = settings.get('program_info') or channel_names
languages = settings.get('languages') or ["deu"]
big_ids = settings.get('big_ids') or []
big_addrs = settings.get('big_random_addresses') or []
stream_password = settings.get('stream_password')
original_ts = settings.get('timestamp')
previously_streaming = bool(settings.get('is_streaming'))
@@ -386,6 +411,8 @@ async def _autostart_from_settings():
lang = languages[i] if i < len(languages) else (languages[0] if languages else "deu")
bigs.append(
auracast_config.AuracastBigConfig(
id=big_ids[i] if i < len(big_ids) else DEFAULT_BIG_ID,
random_address=big_addrs[i] if i < len(big_addrs) else DEFAULT_RANDOM_ADDRESS,
code=stream_password,
name=name,
program_info=pinfo,
@@ -459,6 +486,8 @@ async def _autostart_from_settings():
log.info("[AUTOSTART][PRIMARY] Device '%s' detected, starting autostart", input_device_name)
bigs = [
auracast_config.AuracastBigConfig(
id=big_ids[0] if big_ids else DEFAULT_BIG_ID,
random_address=big_addrs[0] if big_addrs else DEFAULT_RANDOM_ADDRESS,
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,
@@ -613,6 +642,8 @@ async def _autostart_from_settings():
if input_device_name in names:
bigs = [
auracast_config.AuracastBigConfig(
id=big_ids[0] if big_ids else DEFAULT_BIG_ID,
random_address=big_addrs[0] if big_addrs else DEFAULT_RANDOM_ADDRESS,
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,