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