fix usb device update behavior

This commit is contained in:
2025-06-17 16:41:15 +02:00
parent 0a4e6b08a3
commit efc3870963
2 changed files with 47 additions and 22 deletions

View File

@@ -66,7 +66,12 @@ if audio_mode in ["Webapp", "USB"]:
if default_input not in input_options: if default_input not in input_options:
default_input = input_options[0] default_input = input_options[0]
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
with col1:
selected_option = st.selectbox("Input Device", input_options, index=input_options.index(default_input)) selected_option = st.selectbox("Input Device", input_options, index=input_options.index(default_input))
with col2:
if st.button("Refresh"):
st.rerun()
# We send only the numeric/card identifier (before :) or 'default' # We send only the numeric/card identifier (before :) or 'default'
input_device = selected_option.split(":", 1)[0] if ":" in selected_option else selected_option input_device = selected_option.split(":", 1)[0] if ":" in selected_option else selected_option
else: else:
@@ -76,23 +81,22 @@ if audio_mode in ["Webapp", "USB"]:
if stop_stream: if stop_stream:
try: try:
r = requests.post(f"{BACKEND_URL}/stop_audio") r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r.status_code == 200: if r['was_running']:
st.success("Stream Stopped!") st.success("Stream Stopped!")
else: else:
st.error(f"Failed to stop: {r.text}") st.success("Stream was not running.")
except Exception as e: except Exception as e:
st.error(f"Error: {e}") st.error(f"Error: {e}")
if start_stream: if start_stream:
# Always send stop to ensure backend is in a clean state, regardless of current status # Always send stop to ensure backend is in a clean state, regardless of current status
try: r = requests.post(f"{BACKEND_URL}/stop_audio").json()
requests.post(f"{BACKEND_URL}/stop_audio", timeout=5) if r['was_running']:
except Exception: st.success("Stream Stopped!")
# Ignore connection or 500 errors backend may not be running yet
pass
# Small pause lets backend fully release audio devices before re-init # Small pause lets backend fully release audio devices before re-init
import time; time.sleep(0.7) import time; time.sleep(1)
# Prepare config using the model (do NOT send qos_config, only relevant fields) # Prepare config using the model (do NOT send qos_config, only relevant fields)
q = quality_map[quality] q = quality_map[quality]
config = auracast_config.AuracastConfigGroup( config = auracast_config.AuracastConfigGroup(

View File

@@ -3,6 +3,7 @@ import os
import logging as log import logging as log
import uuid import uuid
import json import json
import sys
from datetime import datetime from datetime import datetime
import asyncio import asyncio
import numpy as np import numpy as np
@@ -14,9 +15,18 @@ from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
import av import av
import av.audio.layout import av.audio.layout
import sounddevice as sd # type: ignore import sounddevice as sd # type: ignore
from typing import List, Set from typing import Set
import traceback import traceback
PTIME = 40 # TODO: seems to have no effect at all
pcs: Set[RTCPeerConnection] = set() # keep refs so they dont GC early
class Offer(BaseModel):
sdp: str
type: str
# Path to persist stream settings # Path to persist stream settings
STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.json') STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.json')
@@ -38,6 +48,7 @@ def save_stream_settings(settings: dict):
except Exception as e: except Exception as e:
log.error('Unable to persist stream settings: %s', e) log.error('Unable to persist stream settings: %s', e)
app = FastAPI() app = FastAPI()
# Allow CORS for frontend on localhost # Allow CORS for frontend on localhost
@@ -151,9 +162,12 @@ async def stop_audio():
await asyncio.gather(*close_tasks, return_exceptions=True) await asyncio.gather(*close_tasks, return_exceptions=True)
# Now shut down the multicaster and release audio devices # Now shut down the multicaster and release audio devices
running=False
if multicaster is not None: if multicaster is not None:
await multicaster.stop_streaming() await multicaster.stop_streaming()
return {"status": "stopped"} running=True
return {"status": "stopped", "was_running": running}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@@ -172,25 +186,25 @@ async def get_status():
async def list_audio_inputs(): async def list_audio_inputs():
"""Return available hardware audio input devices for USB mode.""" """Return available hardware audio input devices for USB mode."""
try: try:
# Re-scan devices on Linux, see https://github.com/spatialaudio/python-sounddevice/issues/16
if sys.platform == 'linux':
log.info("Re-initializing sounddevice to scan for new devices")
sd._terminate()
sd._initialize()
devs = sd.query_devices() devs = sd.query_devices()
inputs = [ inputs = [
{"id": idx, "name": d["name"]} {"id": idx, "name": d["name"]}
for idx, d in enumerate(devs) for idx, d in enumerate(devs)
if d.get("max_input_channels", 0) > 0 and ("(hw:" in d["name"].lower() or "usb" in d["name"].lower()) if d.get("max_input_channels", 0) > 0 and ("(hw:" in d["name"].lower() or "usb" in d["name"].lower())
] ]
log.info('Found %d audio input devices:', len(inputs))
for i in inputs:
log.info(' %s', i)
return {"inputs": inputs} return {"inputs": inputs}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
PTIME = 160 # TODO: seems to have no effect at all
pcs: Set[RTCPeerConnection] = set() # keep refs so they dont GC early
class Offer(BaseModel):
sdp: str
type: str
@app.post("/offer") @app.post("/offer")
async def offer(offer: Offer): async def offer(offer: Offer):
log.info("/offer endpoint called") log.info("/offer endpoint called")
@@ -210,7 +224,9 @@ async def offer(offer: Offer):
# create directory for records - only for testing # create directory for records - only for testing
os.makedirs("./records", exist_ok=True) os.makedirs("./records", exist_ok=True)
await multicaster.start_streaming() # Do NOT start the streamer yet we'll start it lazily once we actually
# receive the first audio frame, ensuring WebRTCAudioInput is ready and
# avoiding race-conditions on restarts.
@pc.on("track") @pc.on("track")
async def on_track(track: MediaStreamTrack): async def on_track(track: MediaStreamTrack):
log.info(f"{id_}: track {track.kind} received") log.info(f"{id_}: track {track.kind} received")
@@ -224,6 +240,12 @@ async def offer(offer: Offer):
log.info( log.info(
f"{id_}: frame sample_rate={frame.sample_rate}, samples_per_channel={frame.samples}, planes={frame.planes}" f"{id_}: frame sample_rate={frame.sample_rate}, samples_per_channel={frame.samples}, planes={frame.planes}"
) )
# Lazily start the streamer now that we know a track exists.
if multicaster.streamer is None:
await multicaster.start_streaming()
# Yield control so the Streamer coroutine has a chance to
# create the WebRTCAudioInput before we push samples.
await asyncio.sleep(0)
first = False first = False
# in stereo case this is interleaved data format # in stereo case this is interleaved data format
frame_array = frame.to_ndarray() frame_array = frame.to_ndarray()
@@ -243,7 +265,6 @@ async def offer(offer: Offer):
log.info(f"mono_array.shape: {mono_array.shape}") log.info(f"mono_array.shape: {mono_array.shape}")
frame_array = frame.to_ndarray() frame_array = frame.to_ndarray()
# Flatten in case it's (1, N) or (N,) # Flatten in case it's (1, N) or (N,)