use active_call::app::AppStateBuilder;
use active_call::call::Command;
use active_call::config::Config;
use active_call::event::EventSender;
use active_call::handler::call_router;
use active_call::media::SourcePacket;
use active_call::{
event::SessionEvent,
media::Sample,
media::TrackId,
media::engine::StreamEngine,
synthesis::{SynthesisClient, SynthesisEvent, SynthesisOption, SynthesisType},
transcription::{TranscriptionClient, TranscriptionOption, TranscriptionType},
};
use anyhow::Result;
use async_trait::async_trait;
use futures::stream::BoxStream;
use futures::{SinkExt, StreamExt};
use rustrtc::{PeerConnection, RtcConfiguration, media::frame::MediaKind, media::sample_track};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tokio_util::sync::CancellationToken;
struct MockAsrClient {
audio_tx: mpsc::UnboundedSender<Vec<Sample>>,
}
#[async_trait]
impl TranscriptionClient for MockAsrClient {
fn send_audio(&self, samples: &[Sample], _src_packet: Option<&SourcePacket>) -> Result<()> {
let _ = self.audio_tx.send(samples.to_vec());
Ok(())
}
}
struct MockAsrClientBuilder;
impl MockAsrClientBuilder {
pub fn create(
track_id: TrackId,
token: CancellationToken,
_option: TranscriptionOption,
event_sender: EventSender,
) -> Pin<Box<dyn Future<Output = Result<Box<dyn TranscriptionClient>>> + Send>> {
Box::pin(async move {
let (audio_tx, mut audio_rx) = mpsc::unbounded_channel::<Vec<Sample>>();
tokio::spawn(async move {
let mut count = 0;
while let Some(_samples) = audio_rx.recv().await {
tracing::info!("MockAsrClient received audio chunk {}", count);
count += 1;
if count == 10 {
tracing::info!("MockAsrClient sending transcription");
let event = SessionEvent::AsrFinal {
track_id: track_id.clone(),
index: 1,
text: "mock transcription".to_string(),
timestamp: active_call::media::get_timestamp(),
start_time: Some(active_call::media::get_timestamp()),
end_time: Some(active_call::media::get_timestamp() + 100),
is_filler: None,
confidence: None,
task_id: None,
refer: None,
};
let _ = event_sender.send(event);
}
if token.is_cancelled() {
break;
}
}
});
Ok(Box::new(MockAsrClient { audio_tx }) as Box<dyn TranscriptionClient>)
})
}
}
struct MockTts {
_streaming: bool,
}
#[async_trait]
impl SynthesisClient for MockTts {
fn provider(&self) -> SynthesisType {
SynthesisType::Other("mock".to_string())
}
async fn start(
&mut self,
) -> Result<BoxStream<'static, (Option<usize>, Result<SynthesisEvent>)>> {
let (_tx, rx) = mpsc::channel(10);
Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
async fn synthesize(
&mut self,
_text: &str,
_cmd_seq: Option<usize>,
_option: Option<SynthesisOption>,
) -> Result<()> {
Ok(())
}
async fn stop(&mut self) -> Result<()> {
Ok(())
}
}
#[tokio::test]
async fn test_webrtc_call_workflow() -> Result<()> {
let _ = tracing_subscriber::fmt().with_env_filter("info").try_init();
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let mut stream_engine = StreamEngine::new();
stream_engine.register_asr(
TranscriptionType::Other("mock".to_string()),
Box::new(MockAsrClientBuilder::create),
);
stream_engine.register_tts(
SynthesisType::Other("mock".to_string()),
|streaming, _opt| {
Ok(Box::new(MockTts {
_streaming: streaming,
}) as Box<dyn SynthesisClient>)
},
);
let stream_engine = Arc::new(stream_engine);
let port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
listener.local_addr()?.port()
};
let mut config = Config::default();
config.http_addr = format!("127.0.0.1:{}", port);
config.udp_port = 0;
let http_addr = config.http_addr.clone();
let app_state = AppStateBuilder::new()
.with_config(config)
.with_stream_engine(stream_engine)
.build()
.await?;
let listener = TcpListener::bind(&http_addr).await?;
let router = call_router().with_state(app_state.clone());
let http_shutdown = CancellationToken::new();
let http_server = {
let shutdown = http_shutdown.clone();
tokio::spawn(async move {
axum::serve(listener, router)
.with_graceful_shutdown(async move {
shutdown.cancelled().await;
})
.await
.ok();
})
};
let app_state_clone = app_state.clone();
tokio::spawn(async move {
app_state_clone.serve().await.ok();
});
tokio::time::sleep(Duration::from_millis(500)).await;
let ws_url = format!("ws://127.0.0.1:{}/call/webrtc?id=test-session", port);
let (mut ws_stream, _) = connect_async(&ws_url).await?;
let mut rtc_config = RtcConfiguration::default();
rtc_config.ice_servers = vec![];
let pc = Arc::new(PeerConnection::new(rtc_config));
let (source, track, _) = sample_track(MediaKind::Audio, 100);
let params = rustrtc::RtpCodecParameters {
clock_rate: 48000,
channels: 1,
payload_type: 111,
..Default::default()
};
pc.add_track(track, params).expect("Failed to add track");
let offer = pc.create_offer().await?;
pc.set_local_description(offer.clone())?;
let invite_cmd = serde_json::json!({
"command": "invite",
"option": {
"offer": offer.to_sdp_string(),
"tts": {
"speaker": "mock",
"provider": "mock"
},
"asr": {
"provider": "mock"
}
}
});
ws_stream
.send(Message::Text(invite_cmd.to_string().into()))
.await?;
let mut answer_received = false;
while let Some(Ok(msg)) = ws_stream.next().await {
if let Message::Text(text) = msg {
if let Ok(event) = serde_json::from_str::<SessionEvent>(&text.to_string()) {
if let SessionEvent::Answer { sdp, .. } = event {
let desc = rustrtc::SessionDescription::parse(rustrtc::SdpType::Answer, &sdp)?;
pc.set_remote_description(desc).await?;
answer_received = true;
break;
}
}
}
}
assert!(answer_received, "Did not receive Answer event");
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
let mut interval = tokio::time::interval(Duration::from_millis(20));
for i in 0..500 {
if i % 50 == 0 {
}
interval.tick().await;
let data = vec![0xF8, 0xFF, 0xFE];
let frame = rustrtc::media::frame::AudioFrame {
data: bytes::Bytes::from(data),
clock_rate: 48000,
..Default::default()
};
if let Err(e) = source.send_audio(frame).await {
tracing::error!("Failed to send audio frame {}: {:?}", i, e);
}
}
});
let tts_cmd = serde_json::json!({
"command": "tts",
"text": "Hello, this is a test",
"speaker": "mock",
"playId": "test-play",
"autoHangup": false,
"streaming": false,
"endOfStream": true,
"option": {
"speaker": "mock",
"provider": "mock"
}
});
ws_stream
.send(Message::Text(tts_cmd.to_string().into()))
.await?;
let mut track_started = false;
let mut asr_received = false;
let timeout = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(timeout);
loop {
tokio::select! {
msg = ws_stream.next() => {
if let Some(Ok(Message::Text(text))) = msg {
tracing::info!("WS Received: {}", text);
if let Ok(event) = serde_json::from_str::<SessionEvent>(&text.to_string()) {
match event {
SessionEvent::TrackStart { .. } => {
track_started = true;
}
SessionEvent::AsrFinal { text, .. } => {
tracing::info!("Received transcription: {}", text);
asr_received = true;
}
_ => {}
}
if track_started {
break;
}
}
} else {
break;
}
}
_ = &mut timeout => {
break;
}
}
}
assert!(track_started, "Did not receive TrackStart event");
if !asr_received {
tracing::warn!("ASR event not received (expected if test audio is invalid Opus)");
}
let hangup_cmd = Command::Hangup {
reason: None,
initiator: None,
headers: None,
refer: None,
};
ws_stream
.send(Message::Text(serde_json::to_string(&hangup_cmd)?.into()))
.await?;
http_shutdown.cancel();
http_server.await.ok();
Ok(())
}