feat: improve audio streaming UI and configuration

- Renamed "AES67" mode to "Network" for clearer user understanding
- Added structured stream controls with consistent start/stop buttons and status display
- Changed presentation delay input from microseconds to milliseconds for better usability
- Restricted retransmission (RTN) options to valid range of 1-4
- Added help tooltips for assisted listening and immediate rendering options
- Fixed portaudio configuration to enable ALSA support and remove
This commit is contained in:
pstruebi
2025-10-30 14:08:13 +01:00
parent 00a832a1fd
commit cf74244674
2 changed files with 79 additions and 45 deletions
+2 -1
View File
@@ -170,6 +170,7 @@ option snd_usb_audio nrpacks=1
sudo apt install -y --no-install-recommends \
git build-essential cmake pkg-config \
libasound2-dev libpulse-dev pipewire ethtool linuxptp
sudo apt remove -y libportaudio2 portaudio19-dev libportaudiocpp0
git clone https://github.com/PortAudio/portaudio.git
cd portaudio
@@ -177,7 +178,7 @@ git checkout 9abe5fe7db729280080a0bbc1397a528cd3ce658
rm -rf build
cmake -S . -B build -G"Unix Makefiles" \
-DBUILD_SHARED_LIBS=ON \
-DPA_USE_ALSA=OFF \
-DPA_USE_ALSA=ON \
-DPA_USE_PULSEAUDIO=ON \
-DPA_USE_JACK=OFF
cmake --build build -j$(nproc)
+77 -44
View File
@@ -104,18 +104,32 @@ is_streaming = bool(saved_settings.get("is_streaming", False))
st.title("Auracast Audio Mode Control")
def render_stream_controls(status_streaming: bool, start_label: str, stop_label: str, mode_label: str):
c_start, c_stop, c_spacer, c_status = st.columns([1, 1, 1, 2], gap="small", vertical_alignment="center")
with c_start:
start_clicked = st.button(start_label, disabled=status_streaming)
with c_stop:
stop_clicked = st.button(stop_label, disabled=not status_streaming)
with c_status:
st.write(
(
f"Mode: {mode_label} · " + ("🟢 Streaming" if status_streaming else "🔴 Stopped")
)
)
return start_clicked, stop_clicked
# 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",
"AES67",
"Network",
# "Webapp"
]
saved_audio_mode = saved_settings.get("audio_mode", "Demo")
if saved_audio_mode not in options:
# Map legacy/unknown modes to closest
mapping = {"USB/Network": "USB", "Network": "AES67"}
mapping = {"USB/Network": "USB", "AES67": "Network"}
saved_audio_mode = mapping.get(saved_audio_mode, "Demo")
audio_mode = st.selectbox(
@@ -124,11 +138,29 @@ audio_mode = st.selectbox(
index=options.index(saved_audio_mode) if saved_audio_mode in options else options.index("Demo"),
help=(
"Select the audio input source. Choose 'Webapp' for browser microphone, "
"'USB' for a connected USB audio device (via PipeWire), 'AES67' for network RTP/AES67 sources, "
"'USB' for a connected USB audio device (via PipeWire), 'Network' (AES67) for network RTP/AES67 sources, "
"or 'Demo' for a simulated stream."
)
)
# Determine the displayed mode label:
# - While streaming, prefer the backend-reported mode
# - When not streaming, show the currently selected mode
backend_mode_raw = saved_settings.get("audio_mode")
backend_mode_mapped = None
if isinstance(backend_mode_raw, str):
if backend_mode_raw == "AES67":
backend_mode_mapped = "Network"
elif backend_mode_raw == "USB/Network":
backend_mode_mapped = "USB"
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
is_started = False
is_stopped = False
if audio_mode == "Demo":
demo_stream_map = {
"1 × 48kHz": {"quality": "High (48kHz)", "streams": 1},
@@ -158,36 +190,37 @@ if audio_mode == "Demo":
with col_flags1:
assisted_listening = st.checkbox(
"Assistive listening",
value=bool(saved_settings.get('assisted_listening_stream', False))
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))
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:
presentation_delay_us = st.number_input(
"Presentation delay (µs)",
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
presentation_delay_ms = st.number_input(
"Presentation 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(
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
"Retransmissions (RTN)", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
)
#st.info(f"Demo mode selected: {demo_selected} (Streams: {demo_stream_map[demo_selected]['streams']}, Rate: {demo_stream_map[demo_selected]['rate']} Hz)")
# Start/Stop buttons for demo mode
if 'demo_stream_started' not in st.session_state:
st.session_state['demo_stream_started'] = False
col1, col2 = st.columns(2)
with col1:
start_demo = st.button("Start Demo Stream", disabled=is_streaming)
with col2:
stop_demo = st.button("Stop Demo Stream", disabled=not is_streaming)
start_demo, stop_demo = render_stream_controls(is_streaming, "Start Demo Stream", "Stop Demo Stream", running_mode)
if start_demo:
# Always stop any running stream for clean state
try:
@@ -232,7 +265,7 @@ if audio_mode == "Demo":
transport='', # is set in baccol_qoskend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=presentation_delay_us,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(rtn),
@@ -248,7 +281,7 @@ if audio_mode == "Demo":
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=presentation_delay_us,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(rtn),
@@ -257,12 +290,14 @@ if audio_mode == "Demo":
bigs=bigs2
)
# Call /init and /init2
is_started = False
try:
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
if r1.status_code == 200:
msg = f"Demo stream started on multicaster 1 ({len(bigs1)} streams)"
st.session_state['demo_stream_started'] = True
st.success(msg)
is_started = True
else:
st.session_state['demo_stream_started'] = False
st.error(f"Failed to initialize multicaster 1: {r1.text}")
@@ -270,18 +305,21 @@ if audio_mode == "Demo":
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
if r2.status_code == 200:
st.success(f"Demo stream started on multicaster 2 ({len(bigs2)} streams)")
is_started = True
else:
st.error(f"Failed to initialize multicaster 2: {r2.text}")
except Exception as e:
st.session_state['demo_stream_started'] = False
st.error(f"Error: {e}")
if is_started:
pass
elif stop_demo:
try:
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
st.session_state['demo_stream_started'] = False
if r.get('was_running'):
st.info("Demo stream stopped.")
st.rerun()
is_stopped = True
else:
st.info("Demo stream was not running.")
except Exception as e:
@@ -293,7 +331,6 @@ 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(
@@ -337,25 +374,30 @@ else:
with col_flags1:
assisted_listening = st.checkbox(
"Assistive listening",
value=bool(saved_settings.get('assisted_listening_stream', False))
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))
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:
presentation_delay_us = st.number_input(
"Presentation delay (µs)",
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
presentation_delay_ms = st.number_input(
"Presentation 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(
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
"Retransmissions (RTN)", options=rtn_options, index=rtn_options.index(default_rtn_clamped),
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
)
# Gain slider for Webapp mode
@@ -365,7 +407,7 @@ else:
mic_gain = 1.0
# Input device selection for USB or AES67 mode
if audio_mode in ("USB", "AES67"):
if audio_mode in ("USB", "Network"):
if not is_streaming:
# Only query device lists when NOT streaming to avoid extra backend calls
try:
@@ -438,20 +480,7 @@ else:
)
else:
input_device = None
# Buttons and status on a single row (4 columns: start, stop, spacer, status)
c_start, c_stop, c_spacer, c_status = st.columns([1, 1, 1, 2], gap="small", vertical_alignment="center")
with c_start:
start_stream = st.button("Start Auracast", disabled=is_streaming)
with c_stop:
stop_stream = st.button("Stop Auracast", disabled=not is_streaming)
# c_spacer intentionally left empty to push status to the far right
with c_status:
# Fetch current status from backend and render using Streamlit widgets (no HTML)
# The is_streaming variable is now defined at the top of the script.
# We only need to re-fetch here if we want the absolute latest status for the display,
# but for UI consistency, we can just use the value from the top of the script run.
st.write("🟢 Streaming" if is_streaming else "🔴 Stopped")
start_stream, stop_stream = render_stream_controls(is_streaming, "Start Auracast", "Stop Auracast", running_mode)
# If gain slider moved while streaming, send update to JS without restarting
if audio_mode == "Webapp" and st.session_state.get('stream_started'):
@@ -468,7 +497,7 @@ else:
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r['was_running']:
st.success("Stream Stopped!")
st.rerun()
is_stopped = True
else:
st.success("Stream was not running.")
except Exception as e:
@@ -504,7 +533,7 @@ else:
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=presentation_delay_us,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=auracast_config.AuracastQoSConfig(
iso_int_multiple_10ms=1,
number_of_retransmissions=int(rtn),
@@ -517,11 +546,11 @@ else:
program_info=program_info,
language=language,
audio_source=(
f"device:{input_device}" if audio_mode in ("USB", "AES67") else (
f"device:{input_device}" if audio_mode in ("USB", "Network") else (
"webrtc" if audio_mode == "Webapp" else "network"
)
),
input_format=(f"int16le,{q['rate']},1" if audio_mode in ("USB", "AES67") else "auto"),
input_format=(f"int16le,{q['rate']},1" if audio_mode in ("USB", "Network") else "auto"),
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
@@ -533,7 +562,7 @@ else:
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
if r.status_code == 200:
st.success("Stream Started!")
st.rerun()
is_started = True
else:
st.error(f"Failed to initialize: {r.text}")
except Exception as e:
@@ -595,6 +624,10 @@ else:
"""
st.components.v1.html(component, height=0)
st.session_state['stream_started'] = True
# Centralized rerun based on start/stop outcomes
if is_started or is_stopped:
st.rerun()
#else:
# st.header("Advertised Streams (Cloud Announcements)")
# st.info("This feature requires backend support to list advertised streams.")