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:
default_input = input_options[0]
selected_option = st.selectbox("Input Device", input_options, index=input_options.index(default_input))
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))
with col2:
if st.button("Refresh"):
st.rerun()
# We send only the numeric/card identifier (before :) or 'default'
input_device = selected_option.split(":", 1)[0] if ":" in selected_option else selected_option
else:
@@ -76,23 +81,22 @@ if audio_mode in ["Webapp", "USB"]:
if stop_stream:
try:
r = requests.post(f"{BACKEND_URL}/stop_audio")
if r.status_code == 200:
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r['was_running']:
st.success("Stream Stopped!")
else:
st.error(f"Failed to stop: {r.text}")
st.success("Stream was not running.")
except Exception as e:
st.error(f"Error: {e}")
if start_stream:
# Always send stop to ensure backend is in a clean state, regardless of current status
try:
requests.post(f"{BACKEND_URL}/stop_audio", timeout=5)
except Exception:
# Ignore connection or 500 errors backend may not be running yet
pass
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r['was_running']:
st.success("Stream Stopped!")
# 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)
q = quality_map[quality]
config = auracast_config.AuracastConfigGroup(

View File

@@ -3,6 +3,7 @@ import os
import logging as log
import uuid
import json
import sys
from datetime import datetime
import asyncio
import numpy as np
@@ -14,9 +15,18 @@ from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
import av
import av.audio.layout
import sounddevice as sd # type: ignore
from typing import List, Set
from typing import Set
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
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:
log.error('Unable to persist stream settings: %s', e)
app = FastAPI()
# Allow CORS for frontend on localhost
@@ -151,9 +162,12 @@ async def stop_audio():
await asyncio.gather(*close_tasks, return_exceptions=True)
# Now shut down the multicaster and release audio devices
running=False
if multicaster is not None:
await multicaster.stop_streaming()
return {"status": "stopped"}
running=True
return {"status": "stopped", "was_running": running}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -172,25 +186,25 @@ async def get_status():
async def list_audio_inputs():
"""Return available hardware audio input devices for USB mode."""
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()
inputs = [
{"id": idx, "name": d["name"]}
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())
]
log.info('Found %d audio input devices:', len(inputs))
for i in inputs:
log.info(' %s', i)
return {"inputs": inputs}
except Exception as 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")
async def offer(offer: Offer):
log.info("/offer endpoint called")
@@ -210,7 +224,9 @@ async def offer(offer: Offer):
# create directory for records - only for testing
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")
async def on_track(track: MediaStreamTrack):
log.info(f"{id_}: track {track.kind} received")
@@ -224,6 +240,12 @@ async def offer(offer: Offer):
log.info(
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
# in stereo case this is interleaved data format
frame_array = frame.to_ndarray()
@@ -243,7 +265,6 @@ async def offer(offer: Offer):
log.info(f"mono_array.shape: {mono_array.shape}")
frame_array = frame.to_ndarray()
# Flatten in case it's (1, N) or (N,)