diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index 0982a95..730b733 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -154,7 +154,7 @@ if audio_mode == "Demo": type=("password"), help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast." ) - col_flags1, col_flags2, col_placeholder = st.columns([1, 1, 2]) + col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small") with col_flags1: assisted_listening = st.checkbox( "Assistive listening", @@ -165,6 +165,20 @@ if audio_mode == "Demo": "Immediate rendering", value=bool(saved_settings.get('immediate_rendering', False)) ) + # 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, + help="Delay between capture and presentation for receivers." + ) + default_rtn = int(saved_settings.get('rtn', 4) or 4) + with col_rtn: + rtn = st.selectbox( + "Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn), + 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: @@ -215,9 +229,15 @@ if audio_mode == "Demo": config1 = auracast_config.AuracastConfigGroup( auracast_sampling_rate_hz=q['rate'], octets_per_frame=q['octets'], - transport='', # is set in backend + transport='', # is set in baccol_qoskend assisted_listening_stream=assisted_listening, immediate_rendering=immediate_rendering, + presentation_delay_us=presentation_delay_us, + qos_config=auracast_config.AuracastQoSConfig( + iso_int_multiple_10ms=1, + number_of_retransmissions=int(rtn), + max_transport_latency_ms=int(rtn)*10 + 3, + ), bigs=bigs1 ) config2 = None @@ -228,6 +248,12 @@ if audio_mode == "Demo": transport='', # is set in backend assisted_listening_stream=assisted_listening, immediate_rendering=immediate_rendering, + presentation_delay_us=presentation_delay_us, + qos_config=auracast_config.AuracastQoSConfig( + iso_int_multiple_10ms=1, + number_of_retransmissions=int(rtn), + max_transport_latency_ms=int(rtn)*10 + 3, + ), bigs=bigs2 ) # Call /init and /init2 @@ -306,8 +332,8 @@ else: type="password", help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast." ) - # Flags: Assistive Listening and Immediate Rendering (one row) - col_flags1, col_flags2, col_placeholder = st.columns([1, 1, 2]) + # Flags and QoS row (compact, four columns) + col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small") with col_flags1: assisted_listening = st.checkbox( "Assistive listening", @@ -318,6 +344,20 @@ else: "Immediate rendering", value=bool(saved_settings.get('immediate_rendering', False)) ) + # 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, + help="Delay between capture and presentation for receivers." + ) + default_rtn = int(saved_settings.get('rtn', 4) or 4) + with col_rtn: + rtn = st.selectbox( + "Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn), + help="Number of ISO retransmissions (higher improves robustness at cost of airtime)." + ) # Gain slider for Webapp mode if audio_mode == "Webapp": mic_gain = st.slider("Microphone Gain", 0.0, 2.0, 1.0, 0.1, help="Adjust microphone volume sent to Auracast") @@ -464,7 +504,12 @@ else: transport='', # is set in backend assisted_listening_stream=assisted_listening, immediate_rendering=immediate_rendering, - presentation_delay_us=40000, + presentation_delay_us=presentation_delay_us, + 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), diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index 9984c8b..da7c15c 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -200,6 +200,10 @@ class StreamerWorker: for big in conf.bigs: big.input_format = f"int16le,{capture_rate},{channels}" + # Coerce QoS: compute max_transport_latency from RTN if qos_config present + if getattr(conf, 'qos_config', None) and getattr(conf.qos_config, 'number_of_retransmissions', None) is not None: + conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3 + # Create and init multicaster1 self._multicaster1 = multicast_control.Multicaster(conf, conf.bigs) await reset_nrf54l(1) @@ -219,6 +223,8 @@ class StreamerWorker: 'gain': [getattr(big, 'input_gain', 1.0) for big in conf.bigs], 'auracast_sampling_rate_hz': conf.auracast_sampling_rate_hz, 'octets_per_frame': conf.octets_per_frame, + 'presentation_delay_us': getattr(conf, 'presentation_delay_us', None), + 'rtn': getattr(getattr(conf, 'qos_config', None), 'number_of_retransmissions', None), '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), @@ -241,6 +247,11 @@ class StreamerWorker: if device_index is None: raise HTTPException(status_code=400, detail=f"Audio device '{device_name}' not found.") big.audio_source = f'device:{device_index}' + # Coerce QoS: compute max_transport_latency from RTN if qos_config present + if getattr(conf, 'qos_config', None) and getattr(conf.qos_config, 'number_of_retransmissions', None) is not None: + conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3 + + self._multicaster2 = multicast_control.Multicaster(conf, conf.bigs) await reset_nrf54l(0) await self._multicaster2.init_broadcast() @@ -379,6 +390,8 @@ async def _autostart_from_settings(): input_device_name = settings.get('input_device') rate = settings.get('auracast_sampling_rate_hz') octets = settings.get('octets_per_frame') + pres_delay = settings.get('presentation_delay_us') + saved_rtn = settings.get('rtn') immediate_rendering = settings.get('immediate_rendering', False) assisted_listening_stream = settings.get('assisted_listening_stream', False) channel_names = settings.get('channel_names') or ["Broadcast0"] @@ -442,8 +455,16 @@ async def _autostart_from_settings(): transport=TRANSPORT1, immediate_rendering=immediate_rendering, assisted_listening_stream=assisted_listening_stream, + presentation_delay_us=pres_delay if pres_delay is not None else 40000, bigs=bigs, ) + # Attach QoS if saved_rtn present + conf.qos_config = auracast_config.AuracastQoSConfig( + iso_int_multiple_10ms=1, + number_of_retransmissions=int(saved_rtn), + max_transport_latency_ms=int(saved_rtn) * 10 + 3, + ) + # Initialize and start await asyncio.sleep(0.5) await initialize(conf)