introduce basic auracast config capability

This commit is contained in:
2025-06-06 16:28:08 +02:00
parent 83c7fcb596
commit 1294e40deb
3 changed files with 103 additions and 44 deletions

View File

@@ -562,11 +562,11 @@ if __name__ == "__main__":
#big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR #big.code = 'ff'*16 # returns hci/HCI_ENCRYPTION_MODE_NOT_ACCEPTABLE_ERROR
#big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae' #big.code = '78 e5 dc f1 34 ab 42 bf c1 92 ef dd 3a fd 67 ae'
big.precode_wav = True big.precode_wav = True
#big.audio_source = big.audio_source.replace('.wav', '_10_16_32.lc3') #lc3 precoded files big.audio_source = big.audio_source.replace('.wav', '_10_16_32.lc3') #lc3 precoded files
#big.audio_source = read_lc3_file(big.audio_source) # load files in advance big.audio_source = read_lc3_file(big.audio_source) # load files in advance
# --- Network_uncoded mode using NetworkAudioReceiver --- # --- Network_uncoded mode using NetworkAudioReceiver ---
big.audio_source = NetworkAudioReceiverUncoded(port=50007, samplerate=16000, channels=1, chunk_size=1024) #big.audio_source = NetworkAudioReceiverUncoded(port=50007, samplerate=16000, channels=1, chunk_size=1024)
# 16kHz works reliably with 3 streams # 16kHz works reliably with 3 streams
# 24kHz is only working with 2 streams - probably airtime constraint # 24kHz is only working with 2 streams - probably airtime constraint

View File

@@ -1,52 +1,110 @@
# frontend/app.py # frontend/app.py
from itertools import filterfalse
import streamlit as st import streamlit as st
import requests import requests
from auracast import auracast_config
# Global: desired packetization time in ms for Opus (should match backend) # Global: desired packetization time in ms for Opus (should match backend)
PTIME = 40 PTIME = 40
BACKEND_URL = "http://localhost:5000" BACKEND_URL = "http://localhost:5000"
st.title("🎙️ WebRTC mic → backend demo") st.title("🎙️ Auracast Audio Mode Control")
st.markdown("Click start and speak; watch your backend logs to see incoming RTP.")
component = f""" # Audio mode selection
<button id='go'>Start microphone</button> audio_mode = st.selectbox(
<script> "Audio Mode",
const go = document.getElementById('go'); ["Webapp", "Network", "Cloud Announcements"]
go.onclick = async () => {{ )
go.disabled = true;
const pc = new RTCPeerConnection(); // No STUN needed for localhost
const stream = await navigator.mediaDevices.getUserMedia({{audio:true}});
stream.getTracks().forEach(t => pc.addTrack(t, stream));
// --- WebRTC offer/answer exchange --- if audio_mode in ["Webapp", "Network"]:
const offer = await pc.createOffer() # Stream quality selection
// Patch SDP offer to include a=ptime using global PTIME quality = st.selectbox("Stream Quality", ["High (48kHz)", "Mid (24kHz)", "Fair (16kHz)"])
let sdp = offer.sdp; quality_map = {
const ptime_line = 'a=ptime:{PTIME}'; "High (48kHz)": {"rate": 48000, "octets": 120},
const maxptime_line = 'a=maxptime:{PTIME}'; "Mid (24kHz)": {"rate": 24000, "octets": 60},
if (sdp.includes('a=sendrecv')) {{ "Fair (16kHz)": {"rate": 16000, "octets": 40},
sdp = sdp.replace('a=sendrecv', 'a=sendrecv\\n' + ptime_line + '\\n' + maxptime_line); }
}} else {{ stream_name = st.text_input("Channel Name", value="Broadcast0")
sdp += '\\n' + ptime_line + '\\n' + maxptime_line; language = st.text_input("Language (ISO 639-3)", value="deu")
}} start_stream = st.button("Setup Auracast")
const patched_offer = new RTCSessionDescription({{sdp, type: offer.type}})
await pc.setLocalDescription(patched_offer)
// Send offer to backend if start_stream:
const response = await fetch( # Prepare config using the model (do NOT send qos_config, only relevant fields)
"{BACKEND_URL}/offer", q = quality_map[quality]
{{
method: 'POST', config = auracast_config.AuracastConfigGroup(
headers: {{'Content-Type':'application/json'}}, transport="auto",
body: JSON.stringify({{sdp: pc.localDescription.sdp, type: pc.localDescription.type}}) bigs = [
}} auracast_config.AuracastBigConfig(
name=stream_name,
program_info=f"{stream_name} {quality}",
language=language,
audio_source="network" if audio_mode=="Webapp" else "network",
input_format="auto",
iso_que_len=64, # TODO: this should be way less to decrease delay
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
),
]
) )
const answer = await response.json() try:
await pc.setRemoteDescription(new RTCSessionDescription({{sdp: answer.sdp, type: answer.type}})) r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
}}; if r.status_code == 200:
</script> st.success("Stream initialized!")
""" else:
st.components.v1.html(component, height=80) st.error(f"Failed to initialize: {r.text}")
except Exception as e:
st.error(f"Error: {e}")
if audio_mode == "Webapp":
st.markdown("Click start and speak; watch your backend logs to see incoming RTP.")
component = f"""
<button id='go'>Start microphone</button>
<script>
const go = document.getElementById('go');
go.onclick = async () => {{
go.disabled = true;
const pc = new RTCPeerConnection(); // No STUN needed for localhost
const stream = await navigator.mediaDevices.getUserMedia({{audio:true}});
stream.getTracks().forEach(t => pc.addTrack(t, stream));
// --- WebRTC offer/answer exchange ---
const offer = await pc.createOffer()
// Patch SDP offer to include a=ptime using global PTIME
let sdp = offer.sdp;
const ptime_line = 'a=ptime:{PTIME}';
const maxptime_line = 'a=maxptime:{PTIME}';
if (sdp.includes('a=sendrecv')) {{
sdp = sdp.replace('a=sendrecv', 'a=sendrecv\\n' + ptime_line + '\\n' + maxptime_line);
}} else {{
sdp += '\\n' + ptime_line + '\\n' + maxptime_line;
}}
const patched_offer = new RTCSessionDescription({{sdp, type: offer.type}})
await pc.setLocalDescription(patched_offer)
// Send offer to backend
const response = await fetch(
"{BACKEND_URL}/offer",
{{
method: 'POST',
headers: {{'Content-Type':'application/json'}},
body: JSON.stringify({{sdp: pc.localDescription.sdp, type: pc.localDescription.type}})
}}
)
const answer = await response.json()
await pc.setRemoteDescription(new RTCSessionDescription({{sdp: answer.sdp, type: answer.type}}))
}};
</script>
"""
st.components.v1.html(component, height=80)
else:
st.header("Advertised Streams (Cloud Announcements)")
st.info("This feature requires backend support to list advertised streams.")
# Placeholder for future implementation
# Example: r = requests.get(f"{BACKEND_URL}/advertised_streams")
# if r.status_code == 200:
# streams = r.json()
# for s in streams:
# st.write(s)
# else:
# st.error("Could not fetch advertised streams.")

View File

@@ -9,7 +9,7 @@ import av
import av.audio.layout import av.audio.layout
from typing import List, Set from typing import List, Set
from pydantic import BaseModel from pydantic import BaseModel
import traceback
app = FastAPI() app = FastAPI()
@@ -59,6 +59,7 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
) )
await multicaster.init_broadcast() await multicaster.init_broadcast()
except Exception as e: except Exception as e:
log.error("Exception in /init: %s", traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@@ -102,7 +103,7 @@ async def get_status():
} }
PTIME = 40 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
class Offer(BaseModel): class Offer(BaseModel):
@@ -174,7 +175,7 @@ async def shutdown():
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn
log.basicConfig( log.basicConfig(
level=log.INFO, level=log.DEBUG,
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s' format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
) )
uvicorn.run(app, host="0.0.0.0", port=5000) uvicorn.run(app, host="0.0.0.0", port=5000)