Skip to content

Commit 698c297

Browse files
committed
add perfcli tools
1 parent 523e833 commit 698c297

4 files changed

Lines changed: 261 additions & 14 deletions

File tree

src/bin/perfcli.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use anyhow::Result;
2+
use clap::Parser;
3+
use dotenv::dotenv;
4+
use futures::{SinkExt, StreamExt};
5+
use rustpbx::{
6+
event::SessionEvent,
7+
handler::{CallOption, Command},
8+
media::{
9+
codecs::{g722::G722Encoder, resample::resample_mono, CodecType, Encoder},
10+
track::{file::read_wav_file, webrtc::WebrtcTrack},
11+
vad::VADOption,
12+
},
13+
version,
14+
};
15+
use std::{
16+
sync::Arc,
17+
time::{Duration, SystemTime},
18+
};
19+
use tokio::{select, time};
20+
use tokio_tungstenite::tungstenite;
21+
use tracing::{error, info, level_filters::LevelFilter, warn};
22+
use uuid::Uuid;
23+
use webrtc::{
24+
api::APIBuilder,
25+
peer_connection::{
26+
configuration::RTCConfiguration, sdp::session_description::RTCSessionDescription,
27+
},
28+
track::track_local::track_local_static_sample::TrackLocalStaticSample,
29+
};
30+
31+
#[derive(Parser, Debug, Clone)]
32+
#[command(
33+
author,
34+
version = version::get_short_version(),
35+
about = "A WebRTC performance test client",
36+
long_about = version::get_version_info()
37+
)]
38+
struct Cli {
39+
/// Number of concurrent connections
40+
#[clap(long, help = "Number of concurrent connections", default_value = "10")]
41+
clients: u32,
42+
43+
/// Endpoint of the server
44+
#[clap(
45+
long,
46+
help = "Endpoint of the server",
47+
default_value = "ws://localhost:8080/call/webrtc"
48+
)]
49+
endpoint: String,
50+
51+
/// Path to the file to play
52+
#[clap(long, help = "Url to the file to play(Server side)")]
53+
play_file: String,
54+
55+
/// Path to the file to play
56+
#[clap(long, help = "Path to the file to play(Client side)")]
57+
input_file: String,
58+
59+
/// Verbose
60+
#[clap(long, help = "Verbose")]
61+
verbose: bool,
62+
}
63+
64+
async fn serve_client(cli: Cli, id: u32) -> Result<()> {
65+
// Create WebRTC client
66+
let media_engine = WebrtcTrack::get_media_engine(None)?;
67+
let api = APIBuilder::new().with_media_engine(media_engine).build();
68+
let config = RTCConfiguration {
69+
..Default::default()
70+
};
71+
72+
let peer_connection = Arc::new(api.new_peer_connection(config).await?);
73+
// Create an audio track
74+
let track = WebrtcTrack::create_audio_track(CodecType::G722, None);
75+
// Add the track to the peer connection
76+
let _rtp_sender = peer_connection
77+
.add_track(Arc::clone(&track) as Arc<TrackLocalStaticSample>)
78+
.await?;
79+
80+
// Create a channel to collect ICE candidates
81+
let (ice_candidates_tx, mut ice_candidates_rx) = tokio::sync::mpsc::channel(1);
82+
let ice_candidates_tx = Arc::new(tokio::sync::Mutex::new(ice_candidates_tx));
83+
84+
// Set up ICE candidate handler
85+
let ice_candidates_tx_clone = Arc::clone(&ice_candidates_tx);
86+
peer_connection.on_ice_candidate(Box::new(
87+
move |candidate: Option<webrtc::ice_transport::ice_candidate::RTCIceCandidate>| {
88+
info!("ICE candidate received: {:?}", candidate);
89+
let ice_candidates_tx_clone = ice_candidates_tx_clone.clone();
90+
Box::pin(async move {
91+
if candidate.is_none() {
92+
ice_candidates_tx_clone.lock().await.send(()).await.ok();
93+
}
94+
})
95+
},
96+
));
97+
98+
// Create offer
99+
let offer = peer_connection.create_offer(None).await?;
100+
peer_connection.set_local_description(offer.clone()).await?;
101+
102+
select! {
103+
_ = ice_candidates_rx.recv() => {
104+
info!("ICE gathering ok");
105+
}
106+
_ = time::sleep(std::time::Duration::from_secs(3)) => {
107+
error!("ICE gathering timeout");
108+
}
109+
}
110+
111+
let offer = peer_connection
112+
.local_description()
113+
.await
114+
.ok_or(anyhow::anyhow!("Failed to get local description"))?;
115+
116+
// Connect to WebSocket
117+
let session_id = Uuid::new_v4().to_string();
118+
let url = format!("{}?id=prefcli_{}_{}", cli.endpoint, session_id, id);
119+
info!(id, "Connecting to WebSocket: {}", url);
120+
let ws_stream = match tokio_tungstenite::connect_async(url).await {
121+
Ok((ws_stream, resp)) => {
122+
info!(id, "Connected to WebSocket: {}", resp.status());
123+
ws_stream
124+
}
125+
Err(e) => {
126+
match e {
127+
tungstenite::Error::Http(resp) => {
128+
let body = resp.body();
129+
let body_str = String::from_utf8_lossy(&body.as_ref().unwrap());
130+
error!(id, "Failed to connect to WebSocket: {}", body_str);
131+
}
132+
_ => {
133+
error!(id, "Failed to connect to WebSocket: {:?}", e);
134+
}
135+
}
136+
return Err(anyhow::anyhow!("Failed to connect to WebSocket"));
137+
}
138+
};
139+
140+
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
141+
142+
// Create the invite command with proper options
143+
let option = CallOption {
144+
offer: Some(offer.sdp.clone()),
145+
vad: Some(VADOption::default()),
146+
..Default::default()
147+
};
148+
149+
let command = Command::Invite { option };
150+
// Send the invite command
151+
let command_str = serde_json::to_string_pretty(&command)?;
152+
ws_sender
153+
.send(tungstenite::Message::Text(command_str.into()))
154+
.await?;
155+
156+
// Wait for transcription event
157+
let recv_event_loop = async move {
158+
while let Some(Ok(msg)) = ws_receiver.next().await {
159+
let event: SessionEvent = serde_json::from_str(&msg.to_string())?;
160+
match event {
161+
SessionEvent::Answer { sdp, .. } => {
162+
info!(id, "Received answer: {}", sdp);
163+
let offer = RTCSessionDescription::answer(sdp)?;
164+
peer_connection.set_remote_description(offer).await?;
165+
}
166+
SessionEvent::Error { error, .. } => {
167+
error!(id, "Received error: {}", error);
168+
break;
169+
}
170+
_ => {
171+
info!(id, "Received unexpected event: {:?}", event);
172+
continue;
173+
}
174+
}
175+
}
176+
Ok::<(), anyhow::Error>(())
177+
};
178+
let send_audio_loop = async move {
179+
// Read test audio file
180+
let (mut audio_samples, sample_rate) =
181+
read_wav_file(&cli.input_file).expect("Failed to read input file");
182+
let mut ticker = time::interval(Duration::from_millis(20));
183+
let mut encoder = G722Encoder::new();
184+
let mut packet_timestamp = 0;
185+
let chunk_size = if encoder.sample_rate() == 16000 {
186+
320
187+
} else {
188+
160
189+
};
190+
if sample_rate != encoder.sample_rate() {
191+
audio_samples = resample_mono(&audio_samples, sample_rate, encoder.sample_rate());
192+
}
193+
let start = SystemTime::now();
194+
for chunk in audio_samples.chunks(chunk_size) {
195+
let encoded = encoder.encode(&chunk);
196+
let sample = webrtc::media::Sample {
197+
data: encoded.into(),
198+
duration: Duration::from_millis(20),
199+
timestamp: SystemTime::now(),
200+
packet_timestamp,
201+
..Default::default()
202+
};
203+
track.write_sample(&sample).await.unwrap();
204+
packet_timestamp += chunk_size as u32;
205+
ticker.tick().await;
206+
}
207+
let duration = start.elapsed().unwrap();
208+
warn!(id, "Audio sent done, duration: {:?}", duration);
209+
};
210+
211+
select! {
212+
_ = recv_event_loop => {
213+
info!(id, "Transcription received");
214+
}
215+
_ = send_audio_loop => {
216+
}
217+
}
218+
219+
Ok(())
220+
}
221+
222+
#[tokio::main]
223+
async fn main() -> Result<()> {
224+
rustls::crypto::ring::default_provider()
225+
.install_default()
226+
.expect("Failed to install rustls crypto provider");
227+
sqlx::any::install_default_drivers();
228+
229+
let cli = Cli::parse();
230+
231+
// Set up logging
232+
tracing_subscriber::fmt()
233+
.with_max_level(if cli.verbose {
234+
LevelFilter::DEBUG
235+
} else {
236+
LevelFilter::WARN
237+
})
238+
.with_file(true)
239+
.with_line_number(true)
240+
.try_init()
241+
.ok();
242+
dotenv().ok();
243+
244+
let mut handles = Vec::new();
245+
for id in 0..cli.clients {
246+
let cli = cli.clone();
247+
handles.push(tokio::spawn(async move {
248+
loop {
249+
serve_client(cli.clone(), id)
250+
.await
251+
.expect("Failed to serve client")
252+
}
253+
}));
254+
}
255+
256+
info!("Waiting for {} clients to connect...", cli.clients);
257+
for handle in handles {
258+
handle.await.expect("Failed to join client");
259+
}
260+
Ok(())
261+
}

src/bin/text2wav.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ async fn main() -> Result<()> {
110110
rustls::crypto::ring::default_provider()
111111
.install_default()
112112
.expect("Failed to install rustls crypto provider");
113-
// Set up logging
114-
tracing_subscriber::fmt::init();
115113
dotenv().ok();
116114
// Parse command line arguments
117115
let args = Args::parse();

src/transcription/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ pub enum TranscriptionType {
3333
#[serde(default)]
3434
pub struct TranscriptionOption {
3535
pub provider: Option<TranscriptionType>,
36-
pub model: Option<String>,
3736
pub language: Option<String>,
3837
pub app_id: Option<String>,
3938
pub secret_id: Option<String>,
@@ -76,7 +75,6 @@ impl Default for TranscriptionOption {
7675
fn default() -> Self {
7776
Self {
7877
provider: None,
79-
model: None,
8078
language: None,
8179
app_id: None,
8280
secret_id: None,

src/transcription/voiceapi.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,6 @@ impl VoiceApiAsrClientBuilder {
8383
self
8484
}
8585

86-
pub fn with_host(mut self, host: String) -> Self {
87-
self.option.model = Some(host);
88-
self
89-
}
90-
91-
pub fn with_port(mut self, port: String) -> Self {
92-
self.option.language = Some(port);
93-
self
94-
}
95-
9686
pub async fn build(self) -> Result<VoiceApiAsrClient> {
9787
let (audio_tx, audio_rx) = mpsc::unbounded_channel();
9888

0 commit comments

Comments
 (0)