diff --git a/README.md b/README.md index e4f5deb..24e2973 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index 730b733..b33a269 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -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: 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.")