improve changing streaming modes

This commit is contained in:
2025-06-16 18:23:27 +02:00
parent 005c3b550e
commit 0a4e6b08a3
3 changed files with 68 additions and 15 deletions

View File

@@ -51,14 +51,11 @@ if audio_mode in ["Webapp", "USB"]:
# Input device selection for USB mode # Input device selection for USB mode
if audio_mode == "USB": if audio_mode == "USB":
try: try:
import sounddevice as sd # type: ignore resp = requests.get(f"{BACKEND_URL}/audio_inputs", timeout=1)
devs = sd.query_devices() if resp.status_code == 200:
log.info('Found audio devices: %s', devs) input_options = [f"{d['id']}:{d['name']}" for d in resp.json().get('inputs', [])]
input_options = [ else:
f"{idx}:{d['name']}" input_options = []
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())
]
except Exception: except Exception:
input_options = [] input_options = []
@@ -75,8 +72,27 @@ if audio_mode in ["Webapp", "USB"]:
else: else:
input_device = None input_device = None
start_stream = st.button("Start Auracast") start_stream = st.button("Start Auracast")
stop_stream = st.button("Stop Auracast")
if stop_stream:
try:
r = requests.post(f"{BACKEND_URL}/stop_audio")
if r.status_code == 200:
st.success("Stream Stopped!")
else:
st.error(f"Failed to stop: {r.text}")
except Exception as 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
try:
requests.post(f"{BACKEND_URL}/stop_audio", timeout=5)
except Exception:
# Ignore connection or 500 errors backend may not be running yet
pass
# Small pause lets backend fully release audio devices before re-init
import time; time.sleep(0.7)
# 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

@@ -4,7 +4,7 @@ import logging as log
import uuid import uuid
import json import json
from datetime import datetime from datetime import datetime
import asyncio
import numpy as np import numpy as np
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
@@ -13,6 +13,7 @@ from auracast import multicast_control, auracast_config
from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
import av import av
import av.audio.layout import av.audio.layout
import sounddevice as sd # type: ignore
from typing import List, Set from typing import List, Set
import traceback import traceback
@@ -105,15 +106,16 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
log.info( log.info(
'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2) 'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2)
) )
# TODO: check if multicaster is already initialized
multicaster = multicast_control.Multicaster( multicaster = multicast_control.Multicaster(
conf, conf,
conf.bigs, conf.bigs,
) )
await multicaster.init_broadcast() await multicaster.init_broadcast()
# Auto-start streaming for USB microphone mode # Auto-start streaming only when using a local USB audio device. For Webapp mode the
if any(big.audio_source.startswith('device:') for big in conf.bigs): # streamer is started by the /offer handler once the WebRTC track arrives so we know
# the peer connection is established.
if any(big.audio_source.startswith("device:") for big in conf.bigs):
await multicaster.start_streaming() await multicaster.start_streaming()
except Exception as e: except Exception as e:
log.error("Exception in /init: %s", traceback.format_exc()) log.error("Exception in /init: %s", traceback.format_exc())
@@ -142,6 +144,14 @@ async def send_audio(audio_data: dict[str, str]):
async def stop_audio(): async def stop_audio():
"""Stops streaming.""" """Stops streaming."""
try: try:
# First close any active WebRTC peer connections so their track loops finish cleanly
close_tasks = [pc.close() for pc in list(pcs)]
pcs.clear()
if close_tasks:
await asyncio.gather(*close_tasks, return_exceptions=True)
# Now shut down the multicaster and release audio devices
if multicaster is not None:
await multicaster.stop_streaming() await multicaster.stop_streaming()
return {"status": "stopped"} return {"status": "stopped"}
except Exception as e: except Exception as e:
@@ -158,6 +168,20 @@ async def get_status():
status.update(load_stream_settings()) status.update(load_stream_settings())
return status return status
@app.get("/audio_inputs")
async def list_audio_inputs():
"""Return available hardware audio input devices for USB mode."""
try:
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())
]
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 PTIME = 160 # TODO: seems to have no effect at all
pcs: Set[RTCPeerConnection] = set() # keep refs so they dont GC early pcs: Set[RTCPeerConnection] = set() # keep refs so they dont GC early
@@ -171,6 +195,13 @@ class Offer(BaseModel):
async def offer(offer: Offer): async def offer(offer: Offer):
log.info("/offer endpoint called") log.info("/offer endpoint called")
# If a previous PeerConnection is still alive, close it so we only ever keep one active.
if pcs:
log.info("Closing %d existing PeerConnection(s) before creating a new one", len(pcs))
close_tasks = [p.close() for p in list(pcs)]
await asyncio.gather(*close_tasks, return_exceptions=True)
pcs.clear()
pc = RTCPeerConnection() # No STUN needed for localhost pc = RTCPeerConnection() # No STUN needed for localhost
pcs.add(pc) pcs.add(pc)
id_ = uuid.uuid4().hex[:8] id_ = uuid.uuid4().hex[:8]
@@ -211,7 +242,6 @@ async def offer(offer: Offer):
log.info(f"mono_array.shape: {mono_array.shape}") log.info(f"mono_array.shape: {mono_array.shape}")
audio_input = list(multicaster.bigs.values())[0]['audio_input']
frame_array = frame.to_ndarray() frame_array = frame.to_ndarray()
@@ -225,6 +255,12 @@ async def offer(offer: Offer):
else: else:
mono_array = samples mono_array = samples
# Get current WebRTC audio input (streamer may have been restarted)
big0 = list(multicaster.bigs.values())[0]
audio_input = big0.get('audio_input')
# Wait until the streamer has instantiated the WebRTCAudioInput
if audio_input is None or getattr(audio_input, 'closed', False):
continue
# Feed mono PCM samples to the global WebRTC audio input # Feed mono PCM samples to the global WebRTC audio input
await audio_input.put_samples(mono_array.astype(np.int16)) await audio_input.put_samples(mono_array.astype(np.int16))

View File

@@ -36,6 +36,7 @@ class WebRTCAudioInput:
logging.debug(f"WebRTCAudioInput: Added {len(samples)} samples, buffer now has {len(self.buffer)} samples.") logging.debug(f"WebRTCAudioInput: Added {len(samples)} samples, buffer now has {len(self.buffer)} samples.")
self.data_available.set() self.data_available.set()
def close(self): async def close(self):
"""Mark the input closed so frames() stops yielding."""
self.closed = True self.closed = True
self.data_available.set() self.data_available.set()