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
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(

View File

@@ -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 dont 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))

View File

@@ -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()