make everything controllable by just one button
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ __pycache__/
|
||||
|
||||
wg_config/wg_confs/
|
||||
records/
|
||||
src/auracast/server/stream_settings.json
|
||||
|
||||
@@ -8,26 +8,44 @@ from auracast import auracast_config
|
||||
PTIME = 40
|
||||
BACKEND_URL = "http://localhost:5000"
|
||||
|
||||
# Try loading persisted settings from backend
|
||||
saved_settings = {}
|
||||
try:
|
||||
resp = requests.get(f"{BACKEND_URL}/status", timeout=1)
|
||||
if resp.status_code == 200:
|
||||
saved_settings = resp.json()
|
||||
except Exception:
|
||||
saved_settings = {}
|
||||
|
||||
st.title("🎙️ Auracast Audio Mode Control")
|
||||
|
||||
# Audio mode selection
|
||||
# Audio mode selection with persisted default
|
||||
options = ["Webapp", "USB"]
|
||||
saved_audio_mode = saved_settings.get("audio_mode", "Webapp")
|
||||
if saved_audio_mode not in options:
|
||||
saved_audio_mode = "Webapp"
|
||||
|
||||
audio_mode = st.selectbox(
|
||||
"Audio Mode",
|
||||
["Webapp", "Network", "Cloud Announcements"]
|
||||
options,
|
||||
index=options.index(saved_audio_mode)
|
||||
)
|
||||
|
||||
if audio_mode in ["Webapp", "Network"]:
|
||||
# Stream quality selection
|
||||
quality = st.selectbox("Stream Quality", ["High (48kHz)", "Mid (24kHz)", "Fair (16kHz)"])
|
||||
if audio_mode in ["Webapp", "USB"]:
|
||||
# Stream quality selection (temporarily disabled)
|
||||
# quality = st.selectbox("Stream Quality", ["High (48kHz)", "Mid (24kHz)", "Fair (16kHz)"])
|
||||
quality_map = {
|
||||
"High (48kHz)": {"rate": 48000, "octets": 120},
|
||||
"Mid (24kHz)": {"rate": 24000, "octets": 60},
|
||||
"Fair (16kHz)": {"rate": 16000, "octets": 40},
|
||||
"High (48kHz)": {"rate": 48000, "octets": 120},
|
||||
"Mid (24kHz)": {"rate": 24000, "octets": 60},
|
||||
"Fair (16kHz)": {"rate": 16000, "octets": 40},
|
||||
}
|
||||
stream_name = st.text_input("Channel Name", value="Broadcast0")
|
||||
language = st.text_input("Language (ISO 639-3)", value="deu")
|
||||
start_stream = st.button("Setup Auracast")
|
||||
# Default to high quality while UI is hidden
|
||||
quality = "High (48kHz)"
|
||||
default_name = saved_settings.get('channel_names', ["Broadcast0"])[0]
|
||||
default_lang = saved_settings.get('languages', ["deu"])[0]
|
||||
stream_name = st.text_input("Channel Name", value=default_name)
|
||||
language = st.text_input("Language (ISO 639-3)", value=default_lang)
|
||||
start_stream = st.button("Start Auracast")
|
||||
|
||||
if start_stream:
|
||||
# Prepare config using the model (do NOT send qos_config, only relevant fields)
|
||||
@@ -40,7 +58,11 @@ if audio_mode in ["Webapp", "Network"]:
|
||||
name=stream_name,
|
||||
program_info=f"{stream_name} {quality}",
|
||||
language=language,
|
||||
audio_source="webrtc" if audio_mode=="Webapp" else "network",
|
||||
audio_source=(
|
||||
"webrtc" if audio_mode == "Webapp" else (
|
||||
"usb" if audio_mode == "USB" else "network"
|
||||
)
|
||||
),
|
||||
input_format="auto",
|
||||
iso_que_len=1, # TODO: this should be way less to decrease delay
|
||||
sampling_frequency=q['rate'],
|
||||
@@ -57,45 +79,42 @@ if audio_mode in ["Webapp", "Network"]:
|
||||
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.")
|
||||
if audio_mode == "Webapp" and start_stream:
|
||||
st.markdown("Starting microphone; allow access if prompted and speak.")
|
||||
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}})
|
||||
(async () => {{
|
||||
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 answer = await response.json()
|
||||
await pc.setRemoteDescription(new RTCSessionDescription({{sdp: answer.sdp, type: answer.type}}))
|
||||
}};
|
||||
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)
|
||||
st.components.v1.html(component, height=0)
|
||||
else:
|
||||
st.header("Advertised Streams (Cloud Announcements)")
|
||||
st.info("This feature requires backend support to list advertised streams.")
|
||||
|
||||
@@ -2,6 +2,8 @@ import glob
|
||||
import os
|
||||
import logging as log
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel
|
||||
@@ -15,6 +17,27 @@ from typing import List, Set
|
||||
import traceback
|
||||
from auracast.utils.webrtc_audio_input import WebRTCAudioInput
|
||||
|
||||
# Path to persist stream settings
|
||||
STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.json')
|
||||
|
||||
def load_stream_settings() -> dict:
|
||||
"""Load persisted stream settings if available."""
|
||||
if os.path.exists(STREAM_SETTINGS_FILE):
|
||||
try:
|
||||
with open(STREAM_SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def save_stream_settings(settings: dict):
|
||||
"""Save stream settings to disk."""
|
||||
try:
|
||||
with open(STREAM_SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
except Exception as e:
|
||||
log.error('Unable to persist stream settings: %s', e)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Allow CORS for frontend on localhost
|
||||
@@ -52,6 +75,20 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
|
||||
HTTPException(status_code=500, detail='No suitable transport found.')
|
||||
|
||||
# initialize the streams dict
|
||||
# persist stream settings for later retrieval
|
||||
# Derive audio_mode from first BIG audio_source
|
||||
first_source = conf.bigs[0].audio_source if conf.bigs else ''
|
||||
audio_mode_persist = (
|
||||
'Webapp' if first_source == 'webrtc' else
|
||||
'USB' if first_source == 'usb' else
|
||||
'Network'
|
||||
)
|
||||
save_stream_settings({
|
||||
'channel_names': [big.name for big in conf.bigs],
|
||||
'languages': [big.language for big in conf.bigs],
|
||||
'audio_mode': audio_mode_persist,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
global_config_group = conf
|
||||
log.info(
|
||||
'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2)
|
||||
@@ -96,14 +133,13 @@ async def stop_audio():
|
||||
|
||||
@app.get("/status")
|
||||
async def get_status():
|
||||
"""Gets the current status of the multicaster."""
|
||||
if multicaster:
|
||||
return multicaster.get_status()
|
||||
else:
|
||||
return {
|
||||
'is_initialized': False,
|
||||
'is_streaming': False,
|
||||
}
|
||||
"""Gets the current status of the multicaster together with persisted stream info."""
|
||||
status = multicaster.get_status() if multicaster else {
|
||||
'is_initialized': False,
|
||||
'is_streaming': False,
|
||||
}
|
||||
status.update(load_stream_settings())
|
||||
return status
|
||||
|
||||
|
||||
PTIME = 160 # TODO: seems to have no effect at all
|
||||
|
||||
Reference in New Issue
Block a user