use std::sync::Arc;
use anyhow::Result;
use super::codec::{self, UplinkDecoder, DOWNLINK_RATE, FRAME_MS, UPLINK_RATE};
use super::protocol::{AudioFormat, Caps, DeviceType, Emotion, Mode, RhpServerMsg};
use crate::ingest::MeetingIngest;
pub mod live {
use super::{RhpServerMsg, SessionOutput};
use std::collections::HashMap;
use std::sync::OnceLock;
use tokio::sync::{mpsc, Mutex};
static REGISTRY: OnceLock<Mutex<HashMap<String, mpsc::Sender<SessionOutput>>>> =
OnceLock::new();
fn registry() -> &'static Mutex<HashMap<String, mpsc::Sender<SessionOutput>>> {
REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
pub async fn register(device_id: &str, tx: mpsc::Sender<SessionOutput>) {
registry().lock().await.insert(device_id.to_string(), tx);
}
pub async fn unregister(device_id: &str) {
registry().lock().await.remove(device_id);
}
pub async fn is_connected(device_id: &str) -> bool {
registry().lock().await.contains_key(device_id)
}
pub async fn send(device_id: &str, msg: RhpServerMsg) -> bool {
let tx = {
let map = registry().lock().await;
map.get(device_id).cloned()
};
match tx {
Some(tx) => match tx.try_send(SessionOutput::Control(msg)) {
Ok(()) => true,
Err(mpsc::error::TrySendError::Closed(_)) => {
unregister(device_id).await;
false
}
Err(mpsc::error::TrySendError::Full(_)) => false,
},
None => false,
}
}
}
pub enum SessionOutput {
Control(RhpServerMsg),
Audio(Vec<u8>),
}
impl SessionOutput {
fn control(msg: RhpServerMsg) -> Self {
SessionOutput::Control(msg)
}
}
pub struct HardwareSession {
pub device_id: String,
pub device_type: DeviceType,
pub caps: Caps,
pub mode: Mode,
pub ambient_session_id: Option<String>,
pub conversation_id: String,
pub agent_id: Option<String>,
meetings: Arc<dyn MeetingIngest>,
uplink: UplinkDecoder,
chat_pcm: Vec<i16>,
ambient_pcm: Vec<i16>,
}
const AMBIENT_FLUSH_SAMPLES: usize = UPLINK_RATE as usize;
impl HardwareSession {
pub fn new(
device_id: String,
device_type: DeviceType,
caps: Caps,
ambient_session_id: Option<String>,
meetings: Arc<dyn MeetingIngest>,
agent_id: Option<String>,
) -> Result<Self> {
Ok(Self {
conversation_id: format!("hw_{device_id}"),
device_id,
device_type,
caps,
mode: Mode::Idle,
ambient_session_id,
agent_id,
meetings,
uplink: UplinkDecoder::new()?,
chat_pcm: Vec::new(),
ambient_pcm: Vec::new(),
})
}
pub fn tts_format() -> AudioFormat {
AudioFormat {
codec: "opus".to_string(),
sample_rate: DOWNLINK_RATE,
frame_ms: FRAME_MS,
}
}
pub fn set_mode(&mut self, mode: Mode) {
if mode == Mode::Chat {
self.chat_pcm.clear();
}
self.mode = mode;
}
pub fn on_listen_start(&mut self) {
self.chat_pcm.clear();
}
pub async fn on_audio(&mut self, opus_packet: &[u8]) -> Result<Vec<SessionOutput>> {
let pcm = self.uplink.decode(opus_packet)?;
match self.mode {
Mode::Chat => {
self.chat_pcm.extend_from_slice(&pcm);
Ok(Vec::new())
}
Mode::Ambient => {
self.ambient_pcm.extend_from_slice(&pcm);
if self.ambient_pcm.len() >= AMBIENT_FLUSH_SAMPLES {
self.flush_ambient().await
} else {
Ok(Vec::new())
}
}
Mode::Idle => Ok(Vec::new()),
}
}
async fn flush_ambient(&mut self) -> Result<Vec<SessionOutput>> {
let pcm = std::mem::take(&mut self.ambient_pcm);
let Some(meeting_id) = self.ambient_session_id.clone() else {
return Ok(vec![SessionOutput::control(RhpServerMsg::AmbientSkip {
reason: "no ambient session".to_string(),
})]);
};
let wav = codec::pcm16_to_wav(&pcm, UPLINK_RATE)?;
match self
.meetings
.append_segment(&meeting_id, wav, "ambient.wav".to_string())
.await
{
Ok(segment_id) => Ok(vec![SessionOutput::control(RhpServerMsg::AmbientAck {
segment_id,
})]),
Err(e) if e.contains("silence") || e.contains("empty") => {
Ok(vec![SessionOutput::control(RhpServerMsg::AmbientSkip {
reason: "silence".to_string(),
})])
}
Err(e) => Ok(vec![SessionOutput::control(RhpServerMsg::AmbientSkip {
reason: e,
})]),
}
}
pub fn take_voice_turn(&mut self) -> Option<TurnInput> {
let pcm = std::mem::take(&mut self.chat_pcm);
if pcm.is_empty() {
return None;
}
Some(TurnInput::Voice(pcm))
}
pub fn take_text_turn(&mut self, content: &str) -> Option<TurnInput> {
let content = content.trim();
if content.is_empty() {
return None;
}
Some(TurnInput::Text(content.to_string()))
}
pub fn emotion_for_phase(&self) -> Emotion {
match self.mode {
Mode::Chat => Emotion::Listening,
Mode::Ambient => Emotion::Neutral,
Mode::Idle => Emotion::Neutral,
}
}
}
pub enum TurnInput {
Voice(Vec<i16>),
Text(String),
}