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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user