add demo with up to 6 streams
This commit is contained in:
@@ -13,7 +13,8 @@ if 'stream_started' not in st.session_state:
|
||||
# Global: desired packetization time in ms for Opus (should match backend)
|
||||
PTIME = 40
|
||||
BACKEND_URL = "http://localhost:5000"
|
||||
TRANSPORT1 = "auto" #'serial:/dev/ttyAMA3,1000000,rtscts', # transport for raspberry pi gpio header
|
||||
TRANSPORT1 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_B53C372677E14460-if00,115200,rtscts" #'serial:/dev/ttyAMA3,1000000,rtscts', # transport for raspberry pi gpio header
|
||||
TRANSPORT2 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_CC69A2912F84AE5E-if00,115200,rtscts" #'serial:/dev/ttyAMA4,1000000,rtscts', # transport for raspberry pi gpio header
|
||||
QUALITY_MAP = {
|
||||
"High (48kHz)": {"rate": 48000, "octets": 120},
|
||||
"Good (32kHz)": {"rate": 32000, "octets": 80},
|
||||
@@ -47,9 +48,12 @@ audio_mode = st.selectbox(
|
||||
|
||||
if audio_mode == "Demo":
|
||||
demo_stream_map = {
|
||||
"1 × 48kHz": {"quality": "High (48kHz)", "streams": 1,},
|
||||
"2 × 24kHz": {"quality": "Medium (24kHz)", "streams": 2,},
|
||||
"3 × 16kHz": {"quality": "Fair (16kHz)", "streams": 3,},
|
||||
"1 × 48kHz": {"quality": "High (48kHz)", "streams": 1},
|
||||
"2 × 24kHz": {"quality": "Medium (24kHz)", "streams": 2},
|
||||
"3 × 16kHz": {"quality": "Fair (16kHz)", "streams": 3},
|
||||
"2 × 48kHz": {"quality": "High (48kHz)", "streams": 2},
|
||||
"4 × 24kHz": {"quality": "Medium (24kHz)", "streams": 4},
|
||||
"6 × 16kHz": {"quality": "Fair (16kHz)", "streams": 6},
|
||||
}
|
||||
demo_options = list(demo_stream_map.keys())
|
||||
default_demo = demo_options[0]
|
||||
@@ -78,50 +82,63 @@ if audio_mode == "Demo":
|
||||
demo_cfg = demo_stream_map[demo_selected]
|
||||
# Octets per frame logic matches quality_map
|
||||
q = QUALITY_MAP[demo_cfg['quality']]
|
||||
|
||||
if demo_cfg['streams'] >= 1:
|
||||
bigs = [
|
||||
auracast_config.AuracastBigConfigDeu(
|
||||
audio_source=f'file:../testdata/wave_particle_5min_de_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||
iso_que_len=32,
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
)
|
||||
]
|
||||
if demo_cfg['streams'] >= 2:
|
||||
bigs += [
|
||||
auracast_config.AuracastBigConfigEng(
|
||||
audio_source=f'file:../testdata/wave_particle_5min_en_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||
iso_que_len=32,
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
),
|
||||
]
|
||||
if demo_cfg['streams'] >= 3:
|
||||
bigs += [
|
||||
auracast_config.AuracastBigConfigFra(
|
||||
audio_source=f'file:../testdata/wave_particle_5min_fr_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||
iso_que_len=32,
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
),
|
||||
]
|
||||
|
||||
config = auracast_config.AuracastConfigGroup(
|
||||
# Language configs and test files
|
||||
lang_cfgs = [
|
||||
(auracast_config.AuracastBigConfigDeu, 'de'),
|
||||
(auracast_config.AuracastBigConfigEng, 'en'),
|
||||
(auracast_config.AuracastBigConfigFra, 'fr'),
|
||||
(auracast_config.AuracastBigConfigSpa, 'es'),
|
||||
(auracast_config.AuracastBigConfigIta, 'it'),
|
||||
(auracast_config.AuracastBigConfigPol, 'pl'),
|
||||
]
|
||||
bigs1 = []
|
||||
for i in range(demo_cfg['streams']):
|
||||
cfg_cls, lang = lang_cfgs[i % len(lang_cfgs)]
|
||||
bigs1.append(cfg_cls(
|
||||
audio_source=f'file:../testdata/wave_particle_5min_{lang}_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||
iso_que_len=32,
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
))
|
||||
|
||||
# Split bigs into two configs if needed
|
||||
max_per_mc = {48000: 1, 24000: 2, 16000: 3}
|
||||
max_streams = max_per_mc.get(q['rate'], 3)
|
||||
bigs2 = []
|
||||
if len(bigs1) > max_streams:
|
||||
bigs2 = bigs1[max_streams:]
|
||||
bigs1 = bigs1[:max_streams]
|
||||
config1 = auracast_config.AuracastConfigGroup(
|
||||
auracast_sampling_rate_hz=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
transport=TRANSPORT1, # transport for raspberry pi gpio header
|
||||
bigs = bigs
|
||||
transport=TRANSPORT1,
|
||||
bigs=bigs1
|
||||
)
|
||||
|
||||
config2 = None
|
||||
if bigs2:
|
||||
config2 = auracast_config.AuracastConfigGroup(
|
||||
auracast_sampling_rate_hz=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
transport=TRANSPORT2,
|
||||
bigs=bigs2
|
||||
)
|
||||
# Call /init and /init2
|
||||
try:
|
||||
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
|
||||
if r.status_code == 200:
|
||||
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
|
||||
if r1.status_code == 200:
|
||||
msg = f"Demo stream started on multicaster 1 ({len(bigs1)} streams)"
|
||||
st.session_state['demo_stream_started'] = True
|
||||
st.success(f"Demo stream started: {demo_selected}")
|
||||
st.success(msg)
|
||||
else:
|
||||
st.session_state['demo_stream_started'] = False
|
||||
st.error(f"Failed to initialize demo: {r.text}")
|
||||
st.error(f"Failed to initialize multicaster 1: {r1.text}")
|
||||
if config2:
|
||||
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
|
||||
if r2.status_code == 200:
|
||||
st.success(f"Demo stream started on multicaster 2 ({len(bigs2)} streams)")
|
||||
else:
|
||||
st.error(f"Failed to initialize multicaster 2: {r2.text}")
|
||||
except Exception as e:
|
||||
st.session_state['demo_stream_started'] = False
|
||||
st.error(f"Error: {e}")
|
||||
|
||||
@@ -65,13 +65,14 @@ app.add_middleware(
|
||||
global_config_group = auracast_config.AuracastConfigGroup()
|
||||
|
||||
# Create multicast controller
|
||||
multicaster: multicast_control.Multicaster | None = None
|
||||
multicaster1: multicast_control.Multicaster | None = None
|
||||
multicaster2: multicast_control.Multicaster | None = None
|
||||
|
||||
@app.post("/init")
|
||||
async def initialize(conf: auracast_config.AuracastConfigGroup):
|
||||
"""Initializes the broadcasters."""
|
||||
"""Initializes the primary broadcaster (multicaster1)."""
|
||||
global global_config_group
|
||||
global multicaster
|
||||
global multicaster1
|
||||
try:
|
||||
if conf.transport == 'auto':
|
||||
serial_devices = glob.glob('/dev/serial/by-id/*')
|
||||
@@ -81,13 +82,8 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
|
||||
log.info('Using: %s', device)
|
||||
conf.transport = f'serial:{device},115200,rtscts'
|
||||
break
|
||||
|
||||
# check again if transport is still auto
|
||||
if conf.transport == 'auto':
|
||||
HTTPException(status_code=500, detail='No suitable transport found.')
|
||||
|
||||
# initialize the streams dict
|
||||
# persist stream settings for later retrieval
|
||||
# Derive audio_mode and input_device from first BIG audio_source
|
||||
first_source = conf.bigs[0].audio_source if conf.bigs else ''
|
||||
if first_source.startswith('device:'):
|
||||
@@ -102,7 +98,6 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
|
||||
else:
|
||||
audio_mode_persist = 'Network'
|
||||
input_device = None
|
||||
# Always persist all relevant stream settings
|
||||
save_stream_settings({
|
||||
'channel_names': [big.name for big in conf.bigs],
|
||||
'languages': [big.language for big in conf.bigs],
|
||||
@@ -113,38 +108,56 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
global_config_group = conf
|
||||
# If there is an existing multicaster, cleanly shut it down first so audio devices are released
|
||||
if multicaster is not None:
|
||||
if multicaster1 is not None:
|
||||
try:
|
||||
await multicaster.shutdown()
|
||||
await multicaster1.shutdown()
|
||||
except Exception:
|
||||
log.warning("Failed to shutdown previous multicaster", exc_info=True)
|
||||
|
||||
log.info(
|
||||
'Initializing multicaster with config:\n %s', conf.model_dump_json(indent=2)
|
||||
)
|
||||
multicaster = multicast_control.Multicaster(
|
||||
conf,
|
||||
conf.bigs,
|
||||
)
|
||||
await multicaster.init_broadcast()
|
||||
|
||||
# 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.
|
||||
# TODO rather do a if not webrtc in the future
|
||||
log.info('Initializing multicaster1 with config:\n %s', conf.model_dump_json(indent=2))
|
||||
multicaster1 = multicast_control.Multicaster(conf, conf.bigs)
|
||||
await multicaster1.init_broadcast()
|
||||
if any(big.audio_source.startswith("device:") or big.audio_source.startswith("file:") for big in conf.bigs):
|
||||
log.info("Auto-starting streaming")
|
||||
await multicaster.start_streaming()
|
||||
log.info("Auto-starting streaming on multicaster1")
|
||||
await multicaster1.start_streaming()
|
||||
except Exception as e:
|
||||
log.error("Exception in /init: %s", traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/init2")
|
||||
async def initialize2(conf: auracast_config.AuracastConfigGroup):
|
||||
"""Initializes the secondary broadcaster (multicaster2). Does NOT persist stream settings."""
|
||||
global multicaster2
|
||||
try:
|
||||
if conf.transport == 'auto':
|
||||
serial_devices = glob.glob('/dev/serial/by-id/*')
|
||||
log.info('Found serial devices: %s', serial_devices)
|
||||
for device in serial_devices:
|
||||
if 'usb-ZEPHYR_Zephyr_HCI_UART_sample' in device:
|
||||
log.info('Using: %s', device)
|
||||
conf.transport = f'serial:{device},115200,rtscts'
|
||||
break
|
||||
if conf.transport == 'auto':
|
||||
HTTPException(status_code=500, detail='No suitable transport found.')
|
||||
if multicaster2 is not None:
|
||||
try:
|
||||
await multicaster2.shutdown()
|
||||
except Exception:
|
||||
log.warning("Failed to shutdown previous multicaster2", exc_info=True)
|
||||
log.info('Initializing multicaster2 with config:\n %s', conf.model_dump_json(indent=2))
|
||||
multicaster2 = multicast_control.Multicaster(conf, conf.bigs)
|
||||
await multicaster2.init_broadcast()
|
||||
if any(big.audio_source.startswith("device:") or big.audio_source.startswith("file:") for big in conf.bigs):
|
||||
log.info("Auto-starting streaming on multicaster2")
|
||||
await multicaster2.start_streaming()
|
||||
except Exception as e:
|
||||
log.error("Exception in /init2: %s", traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/stream_lc3")
|
||||
async def send_audio(audio_data: dict[str, str]):
|
||||
"""Sends a block of pre-coded LC3 audio."""
|
||||
if multicaster is None:
|
||||
if multicaster1 is None:
|
||||
raise HTTPException(status_code=500, detail='Auracast endpoint was never intialized')
|
||||
try:
|
||||
for big in global_config_group.bigs:
|
||||
@@ -152,8 +165,8 @@ async def send_audio(audio_data: dict[str, str]):
|
||||
log.info('Received a send audio request for %s', big.language)
|
||||
big.audio_source = audio_data[big.language].encode('latin-1') # TODO: use base64 encoding
|
||||
|
||||
multicaster.big_conf = global_config_group.bigs
|
||||
await multicaster.start_streaming()
|
||||
multicaster1.big_conf = global_config_group.bigs
|
||||
await multicaster1.start_streaming()
|
||||
return {"status": "audio_sent"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -161,7 +174,7 @@ async def send_audio(audio_data: dict[str, str]):
|
||||
|
||||
@app.post("/stop_audio")
|
||||
async def stop_audio():
|
||||
"""Stops streaming."""
|
||||
"""Stops streaming on both multicaster1 and multicaster2."""
|
||||
try:
|
||||
# First close any active WebRTC peer connections so their track loops finish cleanly
|
||||
close_tasks = [pc.close() for pc in list(pcs)]
|
||||
@@ -169,11 +182,14 @@ async def stop_audio():
|
||||
if close_tasks:
|
||||
await asyncio.gather(*close_tasks, return_exceptions=True)
|
||||
|
||||
# Now shut down the multicaster and release audio devices
|
||||
running=False
|
||||
if multicaster is not None:
|
||||
await multicaster.stop_streaming()
|
||||
running=True
|
||||
# Now shut down both multicasters and release audio devices
|
||||
running = False
|
||||
if multicaster1 is not None:
|
||||
await multicaster1.stop_streaming()
|
||||
running = True
|
||||
if multicaster2 is not None:
|
||||
await multicaster2.stop_streaming()
|
||||
running = True
|
||||
|
||||
return {"status": "stopped", "was_running": running}
|
||||
except Exception as e:
|
||||
@@ -184,7 +200,7 @@ async def stop_audio():
|
||||
@app.get("/status")
|
||||
async def get_status():
|
||||
"""Gets the current status of the multicaster together with persisted stream info."""
|
||||
status = multicaster.get_status() if multicaster else {
|
||||
status = multicaster1.get_status() if multicaster1 else {
|
||||
'is_initialized': False,
|
||||
'is_streaming': False,
|
||||
}
|
||||
@@ -269,8 +285,8 @@ async def offer(offer: Offer):
|
||||
f"{id_}: frame sample_rate={frame.sample_rate}, samples_per_channel={frame.samples}, planes={frame.planes}"
|
||||
)
|
||||
# Lazily start the streamer now that we know a track exists.
|
||||
if multicaster.streamer is None:
|
||||
await multicaster.start_streaming()
|
||||
if multicaster1.streamer is None:
|
||||
await multicaster1.start_streaming()
|
||||
# Yield control so the Streamer coroutine has a chance to
|
||||
# create the WebRTCAudioInput before we push samples.
|
||||
await asyncio.sleep(0)
|
||||
@@ -305,7 +321,7 @@ async def offer(offer: Offer):
|
||||
mono_array = samples
|
||||
|
||||
# Get current WebRTC audio input (streamer may have been restarted)
|
||||
big0 = list(multicaster.bigs.values())[0]
|
||||
big0 = list(multicaster1.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):
|
||||
@@ -361,7 +377,7 @@ async def offer(offer: Offer):
|
||||
async def shutdown():
|
||||
"""Stops broadcasting and releases all audio/Bluetooth resources."""
|
||||
try:
|
||||
await multicaster.shutdown()
|
||||
await multicaster1.shutdown()
|
||||
return {"status": "stopped"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
BIN
src/auracast/testdata/wave_particle_5min_pl_16kHz_mono.wav
vendored
Normal file
BIN
src/auracast/testdata/wave_particle_5min_pl_16kHz_mono.wav
vendored
Normal file
Binary file not shown.
BIN
src/auracast/testdata/wave_particle_5min_pl_24kHz_mono.wav
vendored
Normal file
BIN
src/auracast/testdata/wave_particle_5min_pl_24kHz_mono.wav
vendored
Normal file
Binary file not shown.
BIN
src/auracast/testdata/wave_particle_5min_pl_48kHz_mono.wav
vendored
Normal file
BIN
src/auracast/testdata/wave_particle_5min_pl_48kHz_mono.wav
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user