From 0a4e6b08a3b94ff6b5e0eb8ba18a237c332b8f6b Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 16 Jun 2025 18:23:27 +0200 Subject: [PATCH] improve changing streaming modes --- src/auracast/server/multicast_frontend.py | 32 +++++++++++---- src/auracast/server/multicast_server.py | 48 ++++++++++++++++++++--- src/auracast/utils/webrtc_audio_input.py | 3 +- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index 8cfbb27..5ff654e 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -51,14 +51,11 @@ if audio_mode in ["Webapp", "USB"]: # Input device selection for USB mode if audio_mode == "USB": try: - import sounddevice as sd # type: ignore - devs = sd.query_devices() - log.info('Found audio devices: %s', devs) - input_options = [ - f"{idx}:{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()) - ] + resp = requests.get(f"{BACKEND_URL}/audio_inputs", timeout=1) + if resp.status_code == 200: + input_options = [f"{d['id']}:{d['name']}" for d in resp.json().get('inputs', [])] + else: + input_options = [] except Exception: input_options = [] @@ -75,8 +72,27 @@ if audio_mode in ["Webapp", "USB"]: else: input_device = None 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: + # 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) q = quality_map[quality] config = auracast_config.AuracastConfigGroup( diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index b4a532f..aac6f5b 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -4,7 +4,7 @@ import logging as log import uuid import json from datetime import datetime - +import asyncio import numpy as np from pydantic import BaseModel from fastapi import FastAPI, HTTPException @@ -13,6 +13,7 @@ from auracast import multicast_control, auracast_config from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack import av import av.audio.layout +import sounddevice as sd # type: ignore from typing import List, Set import traceback @@ -105,15 +106,16 @@ async def initialize(conf: auracast_config.AuracastConfigGroup): log.info( 'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2) ) - # TODO: check if multicaster is already initialized multicaster = multicast_control.Multicaster( conf, conf.bigs, ) await multicaster.init_broadcast() - # Auto-start streaming for USB microphone mode - if any(big.audio_source.startswith('device:') for big in conf.bigs): + # Auto-start streaming only when using a local USB audio device. For Webapp mode the + # 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() except Exception as e: log.error("Exception in /init: %s", traceback.format_exc()) @@ -142,7 +144,15 @@ async def send_audio(audio_data: dict[str, str]): async def stop_audio(): """Stops streaming.""" try: - await multicaster.stop_streaming() + # 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() return {"status": "stopped"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -158,6 +168,20 @@ async def get_status(): status.update(load_stream_settings()) 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 pcs: Set[RTCPeerConnection] = set() # keep refs so they don’t GC early @@ -171,6 +195,13 @@ class Offer(BaseModel): async def offer(offer: Offer): 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 pcs.add(pc) id_ = uuid.uuid4().hex[:8] @@ -211,7 +242,6 @@ async def offer(offer: Offer): log.info(f"mono_array.shape: {mono_array.shape}") - audio_input = list(multicaster.bigs.values())[0]['audio_input'] frame_array = frame.to_ndarray() @@ -225,6 +255,12 @@ async def offer(offer: Offer): else: 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 await audio_input.put_samples(mono_array.astype(np.int16)) diff --git a/src/auracast/utils/webrtc_audio_input.py b/src/auracast/utils/webrtc_audio_input.py index 0e6b1a4..d13f267 100644 --- a/src/auracast/utils/webrtc_audio_input.py +++ b/src/auracast/utils/webrtc_audio_input.py @@ -36,6 +36,7 @@ class WebRTCAudioInput: logging.debug(f"WebRTCAudioInput: Added {len(samples)} samples, buffer now has {len(self.buffer)} samples.") self.data_available.set() - def close(self): + async def close(self): + """Mark the input closed so frames() stops yielding.""" self.closed = True self.data_available.set()