Compare commits
2 Commits
77e8cd5729
...
0.5.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 50761a4b37 | |||
| 5bb31e3f6a |
@@ -71,6 +71,10 @@ if not is_pw_disabled():
|
||||
with st.form("signin_form"):
|
||||
pw = st.text_input("Password", type="password")
|
||||
submitted = st.form_submit_button("Sign in")
|
||||
st.components.v1.html(
|
||||
"<script>setTimeout(()=>window.parent.document.querySelector('input[type=\"password\"]')?.focus(),100)</script>",
|
||||
height=0
|
||||
)
|
||||
if submitted:
|
||||
if verify_password(pw, pw_rec):
|
||||
st.session_state['frontend_authenticated'] = True
|
||||
@@ -490,13 +494,43 @@ else:
|
||||
disabled=is_streaming
|
||||
)
|
||||
|
||||
# Use analog-specific defaults (not from saved settings which may have Dante values)
|
||||
default_name = "Analog_Radio_1"
|
||||
default_program_info = "Analog Radio Broadcast"
|
||||
default_lang = "deu"
|
||||
|
||||
quality_options = list(QUALITY_MAP.keys())
|
||||
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
|
||||
|
||||
# Use saved settings if audio_mode matches, otherwise use analog-specific defaults
|
||||
saved_audio_mode = saved_settings.get('audio_mode')
|
||||
if saved_audio_mode == 'Analog':
|
||||
default_name = saved_settings.get('channel_names', ["Analog_Radio_1"])[0]
|
||||
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
|
||||
default_lang = saved_settings.get('languages', ["deu"])[0]
|
||||
|
||||
# Map saved sampling rate to quality label
|
||||
saved_rate = saved_settings.get('auracast_sampling_rate_hz')
|
||||
if saved_rate == 48000:
|
||||
default_quality = "High (48kHz)"
|
||||
elif saved_rate == 32000:
|
||||
default_quality = "Good (32kHz)"
|
||||
elif saved_rate == 24000:
|
||||
default_quality = "Medium (24kHz)"
|
||||
elif saved_rate == 16000:
|
||||
default_quality = "Fair (16kHz)"
|
||||
else:
|
||||
default_quality = "Medium (24kHz)"
|
||||
|
||||
saved_pwd = saved_settings.get('stream_password', '')
|
||||
else:
|
||||
# Use analog-specific defaults when switching from another mode
|
||||
default_name = "Analog_Radio_1"
|
||||
default_program_info = "Analog Radio Broadcast"
|
||||
default_lang = "deu"
|
||||
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
|
||||
saved_pwd = ''
|
||||
|
||||
if default_quality not in quality_options:
|
||||
default_quality = quality_options[0]
|
||||
quality1 = st.selectbox(
|
||||
"Stream Quality (Radio 1)",
|
||||
quality_options,
|
||||
@@ -507,7 +541,7 @@ else:
|
||||
|
||||
stream_passwort1 = st.text_input(
|
||||
"Stream Passwort (Radio 1)",
|
||||
value="",
|
||||
value=saved_pwd,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
help="Optional: Set a broadcast code for Radio 1."
|
||||
@@ -621,7 +655,10 @@ else:
|
||||
input_device1 = None
|
||||
else:
|
||||
# Mono mode: show all available channels
|
||||
saved_input_device = saved_settings.get('input_device')
|
||||
default_r1_idx = 0
|
||||
if saved_input_device in analog_names:
|
||||
default_r1_idx = analog_names.index(saved_input_device)
|
||||
input_device1 = st.selectbox(
|
||||
"Input Device (Radio 1)",
|
||||
analog_names,
|
||||
@@ -670,22 +707,53 @@ else:
|
||||
)
|
||||
|
||||
if radio2_enabled and not stereo_enabled:
|
||||
# Use analog-specific defaults for Radio 2
|
||||
default_name_r2 = "Analog_Radio_2"
|
||||
default_program_info_r2 = "Analog Radio Broadcast"
|
||||
default_lang_r2 = "deu"
|
||||
# Use saved settings if audio_mode matches, otherwise use analog-specific defaults for Radio 2
|
||||
secondary_settings = saved_settings.get('secondary', {})
|
||||
saved_audio_mode = saved_settings.get('audio_mode')
|
||||
if saved_audio_mode == 'Analog' and secondary_settings:
|
||||
default_name_r2 = secondary_settings.get('channel_names', ["Analog_Radio_2"])[0] if isinstance(secondary_settings.get('channel_names'), list) else secondary_settings.get('channel_names', "Analog_Radio_2")
|
||||
raw_program_info_r2 = secondary_settings.get('program_info', default_name_r2)
|
||||
if isinstance(raw_program_info_r2, list) and raw_program_info_r2:
|
||||
default_program_info_r2 = raw_program_info_r2[0]
|
||||
else:
|
||||
default_program_info_r2 = raw_program_info_r2
|
||||
default_lang_r2 = secondary_settings.get('languages', ["deu"])[0] if isinstance(secondary_settings.get('languages'), list) else secondary_settings.get('languages', 'deu')
|
||||
|
||||
# Map saved sampling rate to quality label
|
||||
saved_rate_r2 = secondary_settings.get('auracast_sampling_rate_hz')
|
||||
if saved_rate_r2 == 48000:
|
||||
default_quality_r2 = "High (48kHz)"
|
||||
elif saved_rate_r2 == 32000:
|
||||
default_quality_r2 = "Good (32kHz)"
|
||||
elif saved_rate_r2 == 24000:
|
||||
default_quality_r2 = "Medium (24kHz)"
|
||||
elif saved_rate_r2 == 16000:
|
||||
default_quality_r2 = "Fair (16kHz)"
|
||||
else:
|
||||
default_quality_r2 = "Medium (24kHz)"
|
||||
|
||||
saved_pwd_r2 = secondary_settings.get('stream_password', '')
|
||||
else:
|
||||
# Use analog-specific defaults when switching from another mode
|
||||
default_name_r2 = "Analog_Radio_2"
|
||||
default_program_info_r2 = "Analog Radio Broadcast"
|
||||
default_lang_r2 = "deu"
|
||||
default_quality_r2 = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
|
||||
saved_pwd_r2 = ''
|
||||
|
||||
if default_quality_r2 not in quality_options:
|
||||
default_quality_r2 = quality_options[0]
|
||||
quality2 = st.selectbox(
|
||||
"Stream Quality (Radio 2)",
|
||||
quality_options,
|
||||
index=quality_options.index(default_quality),
|
||||
index=quality_options.index(default_quality_r2),
|
||||
disabled=is_streaming,
|
||||
help="Select the audio sampling rate for Radio 2."
|
||||
)
|
||||
|
||||
stream_passwort2 = st.text_input(
|
||||
"Stream Passwort (Radio 2)",
|
||||
value="",
|
||||
value=saved_pwd_r2,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
help="Optional: Set a broadcast code for Radio 2."
|
||||
@@ -753,7 +821,11 @@ else:
|
||||
|
||||
if not is_streaming:
|
||||
if analog_names:
|
||||
secondary_settings = saved_settings.get('secondary', {})
|
||||
saved_input_device2 = secondary_settings.get('input_device')
|
||||
default_r2_idx = 1 if len(analog_names) > 1 else 0
|
||||
if saved_input_device2 in analog_names:
|
||||
default_r2_idx = analog_names.index(saved_input_device2)
|
||||
input_device2 = st.selectbox(
|
||||
"Input Device (Radio 2)",
|
||||
analog_names,
|
||||
@@ -847,10 +919,15 @@ else:
|
||||
)
|
||||
|
||||
# Dante stereo mode toggle
|
||||
saved_r1_config = saved_settings.get('dante_radio1', {})
|
||||
saved_audio_mode = saved_settings.get('audio_mode')
|
||||
dante_stereo_enabled = False
|
||||
if saved_audio_mode == 'Network - Dante':
|
||||
# Check if any input device starts with dante_stereo_ to detect stereo mode
|
||||
input_device = saved_settings.get('input_device', '')
|
||||
dante_stereo_enabled = input_device.startswith('dante_stereo_')
|
||||
dante_stereo_enabled = st.checkbox(
|
||||
"🎧 Stereo Mode",
|
||||
value=bool(saved_r1_config.get('dante_stereo_mode', False)),
|
||||
value=dante_stereo_enabled,
|
||||
help="Enable stereo streaming for Dante inputs. Select left and right channels from ASRC channels 1-6. Radio 2 and multi-stream configurations will be disabled in stereo mode.",
|
||||
disabled=is_streaming
|
||||
)
|
||||
@@ -859,13 +936,23 @@ else:
|
||||
dante_left_channel = None
|
||||
dante_right_channel = None
|
||||
if dante_stereo_enabled:
|
||||
dante_channel_options = ["dante_asrc_ch1", "dante_asrc_ch2", "dante_asrc_ch3",
|
||||
dante_channel_options = ["dante_asrc_ch1", "dante_asrc_ch2", "dante_asrc_ch3",
|
||||
"dante_asrc_ch4", "dante_asrc_ch5", "dante_asrc_ch6"]
|
||||
dante_channel_labels = ["CH1", "CH2", "CH3", "CH4", "CH5", "CH6"]
|
||||
|
||||
|
||||
# Parse saved stereo device name to extract left and right channels
|
||||
input_device = saved_settings.get('input_device', '')
|
||||
saved_left = 'dante_asrc_ch1'
|
||||
saved_right = 'dante_asrc_ch2'
|
||||
if input_device.startswith('dante_stereo_'):
|
||||
# Format: dante_stereo_<left>_<right>
|
||||
parts = input_device.split('_')
|
||||
if len(parts) >= 4:
|
||||
saved_left = f"dante_asrc_ch{parts[2]}"
|
||||
saved_right = f"dante_asrc_ch{parts[3]}"
|
||||
|
||||
col_left, col_right = st.columns(2)
|
||||
with col_left:
|
||||
saved_left = saved_r1_config.get('dante_stereo_left', 'dante_asrc_ch1')
|
||||
left_idx = dante_channel_options.index(saved_left) if saved_left in dante_channel_options else 0
|
||||
dante_left_channel = st.selectbox(
|
||||
"Left Channel",
|
||||
@@ -876,7 +963,6 @@ else:
|
||||
help="Select the Dante ASRC channel for the left stereo channel"
|
||||
)
|
||||
with col_right:
|
||||
saved_right = saved_r1_config.get('dante_stereo_right', 'dante_asrc_ch2')
|
||||
right_idx = dante_channel_options.index(saved_right) if saved_right in dante_channel_options else 1
|
||||
dante_right_channel = st.selectbox(
|
||||
"Right Channel",
|
||||
@@ -886,7 +972,7 @@ else:
|
||||
disabled=is_streaming,
|
||||
help="Select the Dante ASRC channel for the right stereo channel"
|
||||
)
|
||||
|
||||
|
||||
if dante_left_channel == dante_right_channel:
|
||||
st.warning("⚠️ Left and right channels are the same. Select different channels for true stereo.")
|
||||
else:
|
||||
@@ -894,7 +980,22 @@ else:
|
||||
|
||||
# Stream count dropdown for Radio 1 (disabled in stereo mode - forced to 1 stream at 48kHz)
|
||||
r1_stream_options = list(dante_stream_options.keys())
|
||||
saved_r1_streams = saved_r1_config.get('stream_config', '1x48')
|
||||
# Infer stream configuration from saved sampling rate
|
||||
saved_rate = saved_settings.get('auracast_sampling_rate_hz')
|
||||
saved_r1_streams = '1 × 48kHz' # default
|
||||
if saved_rate:
|
||||
if saved_rate == 48000:
|
||||
channel_names = saved_settings.get('channel_names', [])
|
||||
if len(channel_names) == 2:
|
||||
saved_r1_streams = '2 × 24kHz'
|
||||
elif len(channel_names) == 3:
|
||||
saved_r1_streams = '3 × 16kHz'
|
||||
else:
|
||||
saved_r1_streams = '1 × 48kHz'
|
||||
elif saved_rate == 24000:
|
||||
saved_r1_streams = '2 × 24kHz'
|
||||
elif saved_rate == 16000:
|
||||
saved_r1_streams = '3 × 16kHz'
|
||||
default_r1_idx = r1_stream_options.index(saved_r1_streams) if saved_r1_streams in r1_stream_options else 0
|
||||
|
||||
if dante_stereo_enabled:
|
||||
@@ -924,15 +1025,25 @@ else:
|
||||
r1_available_qualities = []
|
||||
for quality in ["High (48kHz)", "Good (32kHz)", "Medium (24kHz)", "Fair (16kHz)"]:
|
||||
# Check if this quality is equal to or lower than the max
|
||||
if (r1_max_quality == "High (48kHz)" or
|
||||
if (r1_max_quality == "High (48kHz)" or
|
||||
(r1_max_quality == "Medium (24kHz)" and quality in ["Medium (24kHz)", "Fair (16kHz)"]) or
|
||||
(r1_max_quality == "Fair (16kHz)" and quality == "Fair (16kHz)")):
|
||||
r1_available_qualities.append(quality)
|
||||
|
||||
saved_r1_quality = saved_r1_config.get('radio_quality', r1_max_quality)
|
||||
|
||||
# Map saved sampling rate to quality label
|
||||
saved_r1_quality = r1_max_quality
|
||||
saved_rate = saved_settings.get('auracast_sampling_rate_hz')
|
||||
if saved_rate == 48000:
|
||||
saved_r1_quality = "High (48kHz)"
|
||||
elif saved_rate == 32000:
|
||||
saved_r1_quality = "Good (32kHz)"
|
||||
elif saved_rate == 24000:
|
||||
saved_r1_quality = "Medium (24kHz)"
|
||||
elif saved_rate == 16000:
|
||||
saved_r1_quality = "Fair (16kHz)"
|
||||
if saved_r1_quality not in r1_available_qualities:
|
||||
saved_r1_quality = r1_max_quality
|
||||
|
||||
|
||||
r1_radio_quality = st.selectbox(
|
||||
"Stream Quality (Radio 1)",
|
||||
r1_available_qualities,
|
||||
@@ -940,29 +1051,29 @@ else:
|
||||
disabled=is_streaming,
|
||||
help=f"Select stream quality for Radio 1. Maximum quality based on configuration: {r1_max_quality}"
|
||||
)
|
||||
|
||||
|
||||
# Radio-level settings for Radio 1
|
||||
# First row: Assistive listening, immediate rendering, presentation delay, QoS
|
||||
col_r1_flags1, col_r1_flags2, col_r1_pdelay, col_r1_qos = st.columns([1, 1, 0.7, 0.6], gap="small")
|
||||
|
||||
|
||||
with col_r1_flags1:
|
||||
r1_assisted_listening = st.checkbox(
|
||||
"Assistive (R1)",
|
||||
value=bool(saved_r1_config.get('assisted_listening', False)),
|
||||
value=bool(saved_settings.get('assisted_listening_stream', False)),
|
||||
disabled=is_streaming,
|
||||
help="Assistive listening stream"
|
||||
)
|
||||
|
||||
|
||||
with col_r1_flags2:
|
||||
r1_immediate_rendering = st.checkbox(
|
||||
"Immediate (R1)",
|
||||
value=bool(saved_r1_config.get('immediate_rendering', False)),
|
||||
value=bool(saved_settings.get('immediate_rendering', False)),
|
||||
disabled=is_streaming,
|
||||
help="Ignore presentation delay"
|
||||
)
|
||||
|
||||
with col_r1_pdelay:
|
||||
default_pdelay = int(saved_r1_config.get('presentation_delay_us', 40000) or 40000)
|
||||
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
|
||||
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
|
||||
r1_presentation_delay_ms = st.number_input(
|
||||
"Delay (ms, R1)",
|
||||
@@ -970,10 +1081,10 @@ else:
|
||||
disabled=is_streaming,
|
||||
help="Presentation delay for Radio 1"
|
||||
)
|
||||
|
||||
|
||||
with col_r1_qos:
|
||||
qos_options = list(QOS_PRESET_MAP.keys())
|
||||
saved_qos = saved_r1_config.get('qos_preset', 'Fast')
|
||||
saved_qos = saved_settings.get('qos_preset', 'Fast')
|
||||
default_qos_idx = qos_options.index(saved_qos) if saved_qos in qos_options else 0
|
||||
r1_qos_preset = st.selectbox(
|
||||
"QoS (R1)", options=qos_options, index=default_qos_idx,
|
||||
@@ -998,24 +1109,31 @@ else:
|
||||
if dante_stereo_enabled:
|
||||
# Stereo mode: single stream with combined L+R channels
|
||||
with st.expander("Stereo Stream - Radio 1", expanded=True):
|
||||
saved_streams = saved_r1_config.get('streams', [])
|
||||
saved_stream = saved_streams[0] if saved_streams else {}
|
||||
|
||||
# Read from flat settings structure
|
||||
channel_names = saved_settings.get('channel_names', [])
|
||||
program_infos = saved_settings.get('program_info', [])
|
||||
languages = saved_settings.get('languages', [])
|
||||
|
||||
saved_name = channel_names[0] if channel_names else 'Dante_Stereo'
|
||||
saved_program_info = program_infos[0] if program_infos else saved_name
|
||||
saved_language = languages[0] if languages else 'eng'
|
||||
saved_password = saved_settings.get('stream_password', '')
|
||||
|
||||
# First row: Channel name and password
|
||||
col_name, col_pwd = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_name:
|
||||
stream_name = st.text_input(
|
||||
"Channel Name",
|
||||
value=saved_stream.get('name', 'Dante_Stereo'),
|
||||
value=saved_name,
|
||||
disabled=is_streaming,
|
||||
key="r1_stereo_name"
|
||||
)
|
||||
|
||||
|
||||
with col_pwd:
|
||||
stream_password = st.text_input(
|
||||
"Stream Password",
|
||||
value=saved_stream.get('stream_password', ''),
|
||||
value=saved_password,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
key="r1_stereo_password",
|
||||
@@ -1024,19 +1142,19 @@ else:
|
||||
|
||||
# Second row: Program info and language
|
||||
col_prog, col_lang_code = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_prog:
|
||||
program_info = st.text_input(
|
||||
"Program Info",
|
||||
value=saved_stream.get('program_info', 'Dante Stereo Broadcast'),
|
||||
value=saved_program_info,
|
||||
disabled=is_streaming,
|
||||
key="r1_stereo_program"
|
||||
)
|
||||
|
||||
|
||||
with col_lang_code:
|
||||
language = st.text_input(
|
||||
"Language",
|
||||
value=saved_stream.get('language', 'eng'),
|
||||
value=saved_language,
|
||||
disabled=is_streaming,
|
||||
key="r1_stereo_lang",
|
||||
help="ISO 639-3 language code"
|
||||
@@ -1070,47 +1188,58 @@ else:
|
||||
})
|
||||
else:
|
||||
# Normal mono mode: multiple streams with individual channels
|
||||
# Read from flat settings structure
|
||||
channel_names = saved_settings.get('channel_names', [])
|
||||
program_infos = saved_settings.get('program_info', [])
|
||||
languages = saved_settings.get('languages', [])
|
||||
input_devices = saved_settings.get('input_devices', [])
|
||||
stream_passwords = saved_settings.get('stream_passwords', []) if 'stream_passwords' in saved_settings else []
|
||||
|
||||
for i in range(r1_num_streams):
|
||||
with st.expander(f"Stream {i+1} - Radio 1", expanded=True):
|
||||
saved_streams = saved_r1_config.get('streams', [])
|
||||
saved_stream = saved_streams[i] if i < len(saved_streams) else {}
|
||||
|
||||
# Get saved values from flat structure
|
||||
saved_name = channel_names[i] if i < len(channel_names) else f'Dante_R1_S{i+1}'
|
||||
saved_program_info = program_infos[i] if i < len(program_infos) else f'Dante Radio 1 Stream {i+1}'
|
||||
saved_language = languages[i] if i < len(languages) else 'eng'
|
||||
saved_password = stream_passwords[i] if i < len(stream_passwords) else ''
|
||||
saved_input_device = input_devices[i] if i < len(input_devices) else None
|
||||
|
||||
# First row: Channel name and language
|
||||
col_name, col_lang = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_name:
|
||||
stream_name = st.text_input(
|
||||
f"Channel Name",
|
||||
value=saved_stream.get('name', f'Dante_R1_S{i+1}'),
|
||||
value=saved_name,
|
||||
disabled=is_streaming,
|
||||
key=f"r1_stream_{i}_name"
|
||||
)
|
||||
|
||||
|
||||
with col_lang:
|
||||
stream_password = st.text_input(
|
||||
f"Stream Password",
|
||||
value=saved_stream.get('stream_password', ''),
|
||||
value=saved_password,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
key=f"r1_stream_{i}_password",
|
||||
help="Optional: Set a broadcast code for this stream"
|
||||
)
|
||||
|
||||
|
||||
# Second row: Program info and language
|
||||
col_prog, col_lang_code = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_prog:
|
||||
program_info = st.text_input(
|
||||
f"Program Info",
|
||||
value=saved_stream.get('program_info', f'Dante Radio 1 Stream {i+1}'),
|
||||
value=saved_program_info,
|
||||
disabled=is_streaming,
|
||||
key=f"r1_stream_{i}_program"
|
||||
)
|
||||
|
||||
|
||||
with col_lang_code:
|
||||
language = st.text_input(
|
||||
f"Language",
|
||||
value=saved_stream.get('language', 'eng'),
|
||||
value=saved_language,
|
||||
disabled=is_streaming,
|
||||
key=f"r1_stream_{i}_lang",
|
||||
help="ISO 639-3 language code"
|
||||
@@ -1122,10 +1251,10 @@ else:
|
||||
with col_device:
|
||||
# Session state key for persisting the selection
|
||||
device_session_key = f"r1_stream_{i}_device_saved"
|
||||
|
||||
|
||||
if not is_streaming and input_options:
|
||||
# Get default from session state first, then from saved settings
|
||||
default_input_name = st.session_state.get(device_session_key, saved_stream.get('input_device'))
|
||||
default_input_name = st.session_state.get(device_session_key, saved_input_device)
|
||||
default_input_label = None
|
||||
for label, name in option_name_map.items():
|
||||
if name == default_input_name:
|
||||
@@ -1133,7 +1262,7 @@ else:
|
||||
break
|
||||
if default_input_label not in input_options and input_options:
|
||||
default_input_label = input_options[0]
|
||||
|
||||
|
||||
selected_option = st.selectbox(
|
||||
f"Input Device",
|
||||
input_options,
|
||||
@@ -1146,7 +1275,7 @@ else:
|
||||
st.session_state[device_session_key] = input_device
|
||||
else:
|
||||
# When streaming, get the device from session state
|
||||
current_device = st.session_state.get(device_session_key, saved_stream.get('input_device', 'No device'))
|
||||
current_device = st.session_state.get(device_session_key, saved_input_device or 'No device')
|
||||
|
||||
# Convert internal name to display label
|
||||
display_label = current_device
|
||||
@@ -1175,26 +1304,45 @@ else:
|
||||
# --- Radio 2 Section ---
|
||||
with st.container(border=True):
|
||||
st.subheader("Radio 2")
|
||||
|
||||
|
||||
# Disable Radio 2 in stereo mode
|
||||
saved_r2_config = saved_settings.get('dante_radio2', {})
|
||||
secondary_settings = saved_settings.get('secondary', {})
|
||||
if dante_stereo_enabled:
|
||||
st.info("🎧 Radio 2 is automatically disabled in stereo mode")
|
||||
radio2_enabled = False
|
||||
else:
|
||||
# Enable/disable checkbox for Radio 2
|
||||
# Use saved settings or streaming state to determine default
|
||||
radio2_enabled_default = secondary_is_streaming
|
||||
# Check if secondary radio has saved settings (indicates it was enabled)
|
||||
if secondary_settings.get('auracast_sampling_rate_hz') or secondary_settings.get('channel_names'):
|
||||
radio2_enabled_default = True
|
||||
radio2_enabled = st.checkbox(
|
||||
"Enable Radio 2",
|
||||
value=radio2_enabled_default,
|
||||
disabled=is_streaming,
|
||||
help="Activate a second Dante radio with its own quality and timing settings."
|
||||
)
|
||||
|
||||
|
||||
if radio2_enabled:
|
||||
# Stream count dropdown for Radio 2
|
||||
r2_stream_options = r1_stream_options
|
||||
saved_r2_streams = saved_r2_config.get('stream_config', '1x48')
|
||||
# Infer stream configuration from saved secondary sampling rate
|
||||
saved_rate2 = secondary_settings.get('auracast_sampling_rate_hz')
|
||||
saved_r2_streams = '1 × 48kHz' # default
|
||||
if saved_rate2:
|
||||
if saved_rate2 == 48000:
|
||||
channel_names2 = secondary_settings.get('channel_names', [])
|
||||
if len(channel_names2) == 2:
|
||||
saved_r2_streams = '2 × 24kHz'
|
||||
elif len(channel_names2) == 3:
|
||||
saved_r2_streams = '3 × 16kHz'
|
||||
else:
|
||||
saved_r2_streams = '1 × 48kHz'
|
||||
elif saved_rate2 == 24000:
|
||||
saved_r2_streams = '2 × 24kHz'
|
||||
elif saved_rate2 == 16000:
|
||||
saved_r2_streams = '3 × 16kHz'
|
||||
default_r2_idx = r2_stream_options.index(saved_r2_streams) if saved_r2_streams in r2_stream_options else 0
|
||||
|
||||
r2_stream_config = st.selectbox(
|
||||
@@ -1216,11 +1364,20 @@ else:
|
||||
(r2_max_quality == "Medium (24kHz)" and quality in ["Medium (24kHz)", "Fair (16kHz)"]) or
|
||||
(r2_max_quality == "Fair (16kHz)" and quality == "Fair (16kHz)")):
|
||||
r2_available_qualities.append(quality)
|
||||
|
||||
saved_r2_quality = saved_r2_config.get('radio_quality', r2_max_quality)
|
||||
|
||||
# Map saved secondary sampling rate to quality label
|
||||
saved_r2_quality = r2_max_quality
|
||||
if saved_rate2 == 48000:
|
||||
saved_r2_quality = "High (48kHz)"
|
||||
elif saved_rate2 == 32000:
|
||||
saved_r2_quality = "Good (32kHz)"
|
||||
elif saved_rate2 == 24000:
|
||||
saved_r2_quality = "Medium (24kHz)"
|
||||
elif saved_rate2 == 16000:
|
||||
saved_r2_quality = "Fair (16kHz)"
|
||||
if saved_r2_quality not in r2_available_qualities:
|
||||
saved_r2_quality = r2_max_quality
|
||||
|
||||
|
||||
r2_radio_quality = st.selectbox(
|
||||
"Stream Quality (Radio 2)",
|
||||
r2_available_qualities,
|
||||
@@ -1228,29 +1385,28 @@ else:
|
||||
disabled=is_streaming,
|
||||
help=f"Select stream quality for Radio 2. Maximum quality based on configuration: {r2_max_quality}"
|
||||
)
|
||||
|
||||
|
||||
# Radio-level settings for Radio 2
|
||||
# First row: Assistive listening, immediate rendering, presentation delay, QoS
|
||||
col_r2_flags1, col_r2_flags2, col_r2_pdelay, col_r2_qos = st.columns([1, 1, 0.7, 0.6], gap="small")
|
||||
|
||||
|
||||
with col_r2_flags1:
|
||||
r2_assisted_listening = st.checkbox(
|
||||
"Assistive (R2)",
|
||||
value=bool(saved_r2_config.get('assisted_listening', False)),
|
||||
value=bool(secondary_settings.get('assisted_listening_stream', False)),
|
||||
disabled=is_streaming,
|
||||
help="Assistive listening stream"
|
||||
)
|
||||
|
||||
|
||||
with col_r2_flags2:
|
||||
r2_immediate_rendering = st.checkbox(
|
||||
"Immediate (R2)",
|
||||
value=bool(saved_r2_config.get('immediate_rendering', False)),
|
||||
value=bool(secondary_settings.get('immediate_rendering', False)),
|
||||
disabled=is_streaming,
|
||||
help="Ignore presentation delay"
|
||||
)
|
||||
|
||||
|
||||
with col_r2_pdelay:
|
||||
default_pdelay = int(saved_r2_config.get('presentation_delay_us', 40000) or 40000)
|
||||
default_pdelay = int(secondary_settings.get('presentation_delay_us', 40000) or 40000)
|
||||
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
|
||||
r2_presentation_delay_ms = st.number_input(
|
||||
"Delay (ms, R2)",
|
||||
@@ -1258,13 +1414,13 @@ else:
|
||||
disabled=is_streaming,
|
||||
help="Presentation delay for Radio 2"
|
||||
)
|
||||
|
||||
|
||||
with col_r2_qos:
|
||||
qos_options = list(QOS_PRESET_MAP.keys())
|
||||
saved_qos = saved_r2_config.get('qos_preset', 'Fast')
|
||||
default_qos_idx = qos_options.index(saved_qos) if saved_qos in qos_options else 0
|
||||
saved_qos = secondary_settings.get('qos_preset', 'Fast')
|
||||
default_qos_idx2 = qos_options.index(saved_qos) if saved_qos in qos_options else 0
|
||||
r2_qos_preset = st.selectbox(
|
||||
"QoS (R2)", options=qos_options, index=default_qos_idx,
|
||||
"QoS (R2)", options=qos_options, index=default_qos_idx2,
|
||||
disabled=is_streaming,
|
||||
help="Quality of Service preset for Radio 2"
|
||||
)
|
||||
@@ -1279,48 +1435,59 @@ else:
|
||||
# Per-stream configuration for Radio 2
|
||||
st.write("**Stream Configuration (Radio 2)**")
|
||||
r2_streams = []
|
||||
|
||||
|
||||
# Read from flat secondary settings structure
|
||||
channel_names2 = secondary_settings.get('channel_names', [])
|
||||
program_infos2 = secondary_settings.get('program_info', [])
|
||||
languages2 = secondary_settings.get('languages', [])
|
||||
input_devices2 = secondary_settings.get('input_devices', [])
|
||||
stream_passwords2 = secondary_settings.get('stream_passwords', []) if 'stream_passwords' in secondary_settings else []
|
||||
|
||||
for i in range(r2_num_streams):
|
||||
with st.expander(f"Stream {i+1} - Radio 2", expanded=True):
|
||||
saved_streams = saved_r2_config.get('streams', [])
|
||||
saved_stream = saved_streams[i] if i < len(saved_streams) else {}
|
||||
# Get saved values from flat secondary structure
|
||||
saved_name2 = channel_names2[i] if i < len(channel_names2) else f'Dante_R2_S{i+1}'
|
||||
saved_program_info2 = program_infos2[i] if i < len(program_infos2) else f'Dante Radio 2 Stream {i+1}'
|
||||
saved_language2 = languages2[i] if i < len(languages2) else 'eng'
|
||||
saved_password2 = stream_passwords2[i] if i < len(stream_passwords2) else ''
|
||||
saved_input_device2 = input_devices2[i] if i < len(input_devices2) else None
|
||||
|
||||
# First row: Channel name and password
|
||||
col_name, col_pwd = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_name:
|
||||
stream_name = st.text_input(
|
||||
f"Channel Name",
|
||||
value=saved_stream.get('name', f'Dante_R2_S{i+1}'),
|
||||
value=saved_name2,
|
||||
disabled=is_streaming,
|
||||
key=f"r2_stream_{i}_name"
|
||||
)
|
||||
|
||||
|
||||
with col_pwd:
|
||||
stream_password = st.text_input(
|
||||
f"Stream Password",
|
||||
value=saved_stream.get('stream_password', ''),
|
||||
value=saved_password2,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
key=f"r2_stream_{i}_password",
|
||||
help="Optional: Set a broadcast code for this stream"
|
||||
)
|
||||
|
||||
|
||||
# Second row: Program info and language
|
||||
col_prog, col_lang = st.columns([2, 1])
|
||||
|
||||
|
||||
with col_prog:
|
||||
program_info = st.text_input(
|
||||
f"Program Info",
|
||||
value=saved_stream.get('program_info', f'Dante Radio 2 Stream {i+1}'),
|
||||
value=saved_program_info2,
|
||||
disabled=is_streaming,
|
||||
key=f"r2_stream_{i}_program"
|
||||
)
|
||||
|
||||
|
||||
with col_lang:
|
||||
language = st.text_input(
|
||||
f"Language",
|
||||
value=saved_stream.get('language', 'eng'),
|
||||
value=saved_language2,
|
||||
disabled=is_streaming,
|
||||
key=f"r2_stream_{i}_lang",
|
||||
help="ISO 639-3 language code"
|
||||
@@ -1332,10 +1499,10 @@ else:
|
||||
with col_device:
|
||||
# Session state key for persisting the selection
|
||||
device_session_key = f"r2_stream_{i}_device_saved"
|
||||
|
||||
|
||||
if not is_streaming and input_options:
|
||||
# Get default from session state first, then from saved settings
|
||||
default_input_name = st.session_state.get(device_session_key, saved_stream.get('input_device'))
|
||||
default_input_name = st.session_state.get(device_session_key, saved_input_device2)
|
||||
default_input_label = None
|
||||
for label, name in option_name_map.items():
|
||||
if name == default_input_name:
|
||||
@@ -1343,7 +1510,7 @@ else:
|
||||
break
|
||||
if default_input_label not in input_options and input_options:
|
||||
default_input_label = input_options[0]
|
||||
|
||||
|
||||
selected_option = st.selectbox(
|
||||
f"Input Device",
|
||||
input_options,
|
||||
@@ -1356,7 +1523,7 @@ else:
|
||||
st.session_state[device_session_key] = input_device
|
||||
else:
|
||||
# When streaming, get the device from session state
|
||||
current_device = st.session_state.get(device_session_key, saved_stream.get('input_device', 'No device'))
|
||||
current_device = st.session_state.get(device_session_key, saved_input_device2 or 'No device')
|
||||
|
||||
# Convert internal name to display label
|
||||
display_label = current_device
|
||||
@@ -1444,8 +1611,30 @@ else:
|
||||
|
||||
if audio_mode in ("USB", "Network"):
|
||||
# USB/Network: single set of controls shared with the single channel
|
||||
# Use saved settings if audio_mode matches, otherwise use defaults
|
||||
saved_audio_mode = saved_settings.get('audio_mode')
|
||||
if saved_audio_mode in ("USB", "Network"):
|
||||
# Map saved sampling rate to quality label
|
||||
saved_rate = saved_settings.get('auracast_sampling_rate_hz')
|
||||
if saved_rate == 48000:
|
||||
default_quality = "High (48kHz)"
|
||||
elif saved_rate == 32000:
|
||||
default_quality = "Good (32kHz)"
|
||||
elif saved_rate == 24000:
|
||||
default_quality = "Medium (24kHz)"
|
||||
elif saved_rate == 16000:
|
||||
default_quality = "Fair (16kHz)"
|
||||
else:
|
||||
default_quality = "Medium (24kHz)"
|
||||
saved_pwd = saved_settings.get('stream_password', '')
|
||||
else:
|
||||
# Use defaults when switching from another mode
|
||||
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
|
||||
saved_pwd = ''
|
||||
|
||||
quality_options = list(QUALITY_MAP.keys())
|
||||
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
|
||||
if default_quality not in quality_options:
|
||||
default_quality = quality_options[0]
|
||||
quality = st.selectbox(
|
||||
"Stream Quality (Sampling Rate)",
|
||||
quality_options,
|
||||
@@ -1456,7 +1645,7 @@ else:
|
||||
|
||||
stream_passwort = st.text_input(
|
||||
"Stream Passwort",
|
||||
value="",
|
||||
value=saved_pwd,
|
||||
type="password",
|
||||
disabled=is_streaming,
|
||||
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
|
||||
@@ -1728,7 +1917,7 @@ if start_stream:
|
||||
analog_gain_db_right=cfg.get('analog_gain_db_right', 0.0),
|
||||
bigs=[
|
||||
auracast_config.AuracastBigConfig(
|
||||
code=(cfg['stream_passwort'].strip() or None),
|
||||
code=((cfg['stream_passwort'] or '').strip() or None),
|
||||
name=cfg['name'],
|
||||
program_info=cfg['program_info'],
|
||||
language=cfg['language'],
|
||||
|
||||
@@ -445,6 +445,11 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
first_source = conf.bigs[0].audio_source if conf.bigs else ''
|
||||
input_device_name = None
|
||||
audio_mode_persist = 'Demo'
|
||||
# Capture original per-BIG device names before transformation
|
||||
original_input_devices = [
|
||||
big.audio_source.split(':', 1)[1] if (isinstance(big.audio_source, str) and big.audio_source.startswith('device:')) else None
|
||||
for big in conf.bigs
|
||||
]
|
||||
if any(isinstance(b.audio_source, str) and b.audio_source.startswith('device:') for b in conf.bigs):
|
||||
if isinstance(first_source, str) and first_source.startswith('device:'):
|
||||
input_device_name = first_source.split(':', 1)[1] if ':' in first_source else None
|
||||
@@ -604,6 +609,7 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
|
||||
'languages': [big.language for big in conf.bigs],
|
||||
'audio_mode': audio_mode_persist,
|
||||
'input_device': input_device_name,
|
||||
'input_devices': original_input_devices,
|
||||
'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,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# using nmcli device modify (active session only, NOT saved to the profile).
|
||||
# The persistent profile always keeps ipv4.link-local=enabled so that
|
||||
# direct-connect (no DHCP) plug-ins always activate and trigger events.
|
||||
# Avahi is reloaded on each event — no /etc/avahi/hosts file, avahi uses
|
||||
# natural per-interface advertisement so each segment gets the right IP.
|
||||
#
|
||||
# Triggers: up, down, dhcp4-change on ethernet interfaces
|
||||
# Install to: /etc/NetworkManager/dispatcher.d/10-link-local-mgmt
|
||||
@@ -12,47 +14,31 @@
|
||||
|
||||
INTERFACE="$1"
|
||||
ACTION="$2"
|
||||
CONNECTION_NAME="${CONNECTION_ID:-}"
|
||||
|
||||
# Only handle ethernet interfaces
|
||||
if [[ ! "$INTERFACE" =~ ^eth ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If CONNECTION_ID env var is not set, look up the active connection for this interface
|
||||
if [ -z "$CONNECTION_NAME" ]; then
|
||||
CONNECTION_NAME=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null \
|
||||
| grep ":${INTERFACE}$" | cut -d: -f1 | head -n1)
|
||||
[ -z "$CONNECTION_NAME" ] && exit 0
|
||||
fi
|
||||
|
||||
# Update /etc/avahi/hosts to point mDNS hostname at the best available DHCP address
|
||||
# across all ethernet interfaces (so Avahi doesn't advertise a link-local address).
|
||||
update_avahi() {
|
||||
local hostname
|
||||
hostname=$(hostname)
|
||||
# Find first non-link-local IPv4 across all ethernet interfaces
|
||||
local dhcp_ip
|
||||
dhcp_ip=$(ip -4 addr show 2>/dev/null \
|
||||
| grep -A5 ': eth' \
|
||||
| grep -oP '(?<=inet\s)\d+(\.\d+){3}' \
|
||||
| grep -v '^127\.' \
|
||||
| grep -v '^169\.254\.' \
|
||||
| head -n1)
|
||||
|
||||
if [ -n "$dhcp_ip" ]; then
|
||||
mkdir -p /etc/avahi
|
||||
echo "$dhcp_ip $hostname $hostname.local" > /etc/avahi/hosts
|
||||
logger -t nm-link-local "Avahi: pinned $hostname -> $dhcp_ip"
|
||||
else
|
||||
rm -f /etc/avahi/hosts
|
||||
logger -t nm-link-local "Avahi: removed hosts pin, using all addresses"
|
||||
fi
|
||||
systemctl restart avahi-daemon 2>/dev/null
|
||||
reload_avahi() {
|
||||
systemctl reload avahi-daemon 2>/dev/null || systemctl restart avahi-daemon 2>/dev/null
|
||||
logger -t nm-link-local "[$INTERFACE] $ACTION — avahi reloaded"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
up|dhcp4-change)
|
||||
up)
|
||||
# On 'up' the interface may still carry a stale DHCP address from the previous
|
||||
# session (NM hasn't cleaned it up yet). Reading ip-addr here is unreliable.
|
||||
# Always re-enable link-local as a clean slate; let dhcp4-change suppress it
|
||||
# later if a real DHCP lease is obtained.
|
||||
logger -t nm-link-local "[$INTERFACE] Up — ensuring link-local active (clean slate)"
|
||||
(sleep 2 && nmcli device modify "$INTERFACE" ipv4.link-local enabled 2>/dev/null \
|
||||
&& logger -t nm-link-local "[$INTERFACE] Link-local explicitly enabled on up") &
|
||||
reload_avahi
|
||||
;;
|
||||
|
||||
dhcp4-change)
|
||||
# dhcp4-change fires only when DHCP actually succeeds (new/renewed lease).
|
||||
# At this point the DHCP IP is reliably present — safe to read and suppress link-local.
|
||||
DHCP_IP=$(ip -4 addr show "$INTERFACE" 2>/dev/null \
|
||||
| grep -oP '(?<=inet\s)\d+(\.\d+){3}' \
|
||||
| grep -v '^127\.' \
|
||||
@@ -60,24 +46,19 @@ case "$ACTION" in
|
||||
| head -n1)
|
||||
|
||||
if [ -n "$DHCP_IP" ]; then
|
||||
logger -t nm-link-local "[$INTERFACE] DHCP $DHCP_IP detected — suppressing link-local (session only)"
|
||||
# Use device modify (not connection modify) so the persistent profile keeps
|
||||
# ipv4.link-local=enabled. This ensures direct-connect plug-ins always activate.
|
||||
logger -t nm-link-local "[$INTERFACE] DHCP $DHCP_IP confirmed — suppressing link-local (session only)"
|
||||
# Run in background after a delay — nmcli blocks on NM, which is waiting for
|
||||
# this dispatcher to return, causing a deadlock if called synchronously.
|
||||
(sleep 2 && nmcli device modify "$INTERFACE" ipv4.link-local disabled 2>/dev/null \
|
||||
&& logger -t nm-link-local "[$INTERFACE] Link-local suppressed for current session") &
|
||||
else
|
||||
logger -t nm-link-local "[$INTERFACE] No DHCP on $INTERFACE — keeping link-local active"
|
||||
fi
|
||||
update_avahi
|
||||
reload_avahi
|
||||
;;
|
||||
|
||||
down)
|
||||
# Profile always has ipv4.link-local=enabled so no action needed here.
|
||||
# The suppression from device modify was session-only and is gone when the
|
||||
# connection goes down.
|
||||
logger -t nm-link-local "[$INTERFACE] Down — link-local will be active on next connect"
|
||||
update_avahi
|
||||
# NOTE: a carrier-change does NOT fully reset session-level 'device modify' state.
|
||||
# The re-enable is therefore handled in the 'up' handler when no DHCP is detected.
|
||||
logger -t nm-link-local "[$INTERFACE] Down — link-local will be re-enabled on next up without DHCP"
|
||||
reload_avahi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -8,13 +8,28 @@ set -e
|
||||
# Enable link-local for all wired ethernet connections
|
||||
while IFS=: read -r name type; do
|
||||
if [[ "$type" == *"ethernet"* ]]; then
|
||||
echo "Enabling IPv4 link-local for connection: $name"
|
||||
echo "Configuring connection: $name"
|
||||
# link-local: always enabled so direct-connect (no DHCP) works immediately
|
||||
sudo nmcli connection modify "$name" ipv4.link-local enabled 2>/dev/null || echo "Failed to modify $name"
|
||||
# may-fail=yes: do NOT tear down the connection when DHCP times out.
|
||||
# Without this, NM declares ip-config-unavailable after the 45s DHCP timeout
|
||||
# and enters a reconnect loop that causes ~1.5 min outages every ~45 seconds.
|
||||
sudo nmcli connection modify "$name" ipv4.may-fail yes 2>/dev/null || echo "Failed to set may-fail on $name"
|
||||
# Infinite DHCP timeout: NM keeps retrying DHCP in the background but never
|
||||
# declares ip-config-unavailable. This prevents the 45s reconnect loop that
|
||||
# kills the link-local address in direct-connect (no DHCP server) scenarios.
|
||||
sudo nmcli connection modify "$name" ipv4.dhcp-timeout infinity 2>/dev/null || echo "Failed to set dhcp-timeout on $name"
|
||||
sudo nmcli connection up "$name" 2>/dev/null || echo "Failed to bring up $name"
|
||||
fi
|
||||
done < <(nmcli -t -f NAME,TYPE connection show)
|
||||
|
||||
|
||||
# Remove stale avahi hosts pin — this file overrides per-interface advertisement
|
||||
# and causes mDNS to always resolve to eth0's IP regardless of which interface
|
||||
# the query arrived on, breaking eth1 mDNS entirely.
|
||||
sudo rm -f /etc/avahi/hosts
|
||||
sudo systemctl restart avahi-daemon
|
||||
|
||||
# Ensure Loopback is loaded with a fixed name and index
|
||||
# Needed for dante
|
||||
# TODO image when we create the next image this should be part of it
|
||||
|
||||
Reference in New Issue
Block a user