introduce basic auracast config capability
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|
||||||
|
|||||||
@@ -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 don’t GC early
|
pcs: Set[RTCPeerConnection] = set() # keep refs so they don’t 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)
|
||||||
Reference in New Issue
Block a user