pub mod ffi;
use std::ffi::{CString, CStr};
use std::sync::{Arc, Mutex};
use std::panic::{catch_unwind, AssertUnwindSafe};
use thiserror::Error;
use lazy_static::lazy_static;
pub use ffi::{
PJSIP_INV_STATE_NULL, PJSIP_INV_STATE_CALLING, PJSIP_INV_STATE_INCOMING,
PJSIP_INV_STATE_EARLY, PJSIP_INV_STATE_CONNECTING, PJSIP_INV_STATE_CONFIRMED,
PJSIP_INV_STATE_DISCONNECTED, PJSIP_SC_OK, PJSIP_SC_RINGING, PJSIP_SC_BUSY_HERE,
PJSUA_STATE_RUNNING,
};
lazy_static! {
static ref INITIALIZED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
static ref CALLBACKS: Arc<Mutex<CallbackHandlers>> = Arc::new(Mutex::new(CallbackHandlers::default()));
}
#[derive(Debug, Clone)]
pub struct CallQuality {
pub call_id: i32,
pub rx_level: f64,
pub tx_level: f64,
pub rx_quality: f64,
pub rx_loss_percent: f64,
pub rtt_ms: i32,
pub jitter_ms: f64,
pub mos_score: f64,
pub codec: String,
}
#[derive(Debug, Clone)]
pub struct CallDetailedInfo {
pub call_id: i32,
pub is_on_hold: bool,
pub is_muted: bool,
pub conf_port: i32,
}
#[derive(Error, Debug)]
pub enum Pjsua2Error {
#[error("Failed to create endpoint: {0}")]
EndpointCreation(i32),
#[error("Failed to initialize endpoint: {0}")]
EndpointInit(i32),
#[error("Failed to create transport: {0}")]
TransportCreation(i32),
#[error("Failed to start PJSUA2: {0}")]
StartError(i32),
#[error("Failed to create account: {0}")]
AccountCreation(i32),
#[error("Failed to make call: {0}")]
CallCreation(i32),
#[error("Failed to answer call: {0}")]
AnswerError(i32),
#[error("Failed to hangup call: {0}")]
HangupError(i32),
#[error("Failed to set audio devices: {0}")]
AudioDeviceError(i32),
#[error("PJSUA2 not initialized")]
NotInitialized,
#[error("Invalid UTF-8 in string")]
Utf8Error(#[from] std::ffi::NulError),
#[error("Device not found")]
DeviceNotFound,
#[error("Internal panic occurred")]
PanicOccurred,
}
pub type Result<T> = std::result::Result<T, Pjsua2Error>;
#[derive(Default)]
struct CallbackHandlers {
on_registration_state: Option<Box<dyn Fn(i32, bool) + Send + Sync>>,
on_incoming_call: Option<Box<dyn Fn(i32, i32, String) + Send + Sync>>,
on_call_state: Option<Box<dyn Fn(i32, CallState) + Send + Sync>>,
on_call_media_state: Option<Box<dyn Fn(i32, bool) + Send + Sync>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CallState {
Null,
Calling,
Incoming,
Early,
Connecting,
Confirmed,
Disconnected,
}
impl From<i32> for CallState {
fn from(state: i32) -> Self {
match state {
0 => CallState::Null,
1 => CallState::Calling,
2 => CallState::Incoming,
3 => CallState::Early,
4 => CallState::Connecting,
5 => CallState::Confirmed,
6 => CallState::Disconnected,
_ => CallState::Null,
}
}
}
extern "C" fn on_reg_state_ffi(acc_id: i32, is_registered: i32) {
let result = catch_unwind(AssertUnwindSafe(|| {
println!("[pjsipua_win FFI] Registration callback triggered - acc_id: {}, is_registered: {}", acc_id, is_registered);
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_registration_state {
println!("[pjsipua_win FFI] Calling Rust registration handler");
handler(acc_id, is_registered != 0);
} else {
println!("[pjsipua_win FFI] No registration handler set!");
}
} else {
println!("[pjsipua_win FFI] Failed to lock CALLBACKS!");
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in registration callback: {:?}", e);
}
}
extern "C" fn on_incoming_call_ffi(acc_id: i32, call_id: i32, remote_uri: *const i8) {
let result = catch_unwind(AssertUnwindSafe(|| {
if remote_uri.is_null() {
eprintln!("[pjsipua_win FFI] Received null remote_uri pointer");
return;
}
let uri = unsafe {
match CStr::from_ptr(remote_uri).to_str() {
Ok(s) => s.to_string(),
Err(e) => {
eprintln!("[pjsipua_win FFI] Invalid UTF-8 in remote_uri: {}", e);
return;
}
}
};
println!("[pjsipua_win FFI] Incoming call - acc_id: {}, call_id: {}, uri: {}", acc_id, call_id, uri);
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_incoming_call {
handler(acc_id, call_id, uri);
} else {
println!("[pjsipua_win FFI] No incoming call handler set!");
}
} else {
println!("[pjsipua_win FFI] Failed to lock CALLBACKS!");
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in incoming call callback: {:?}", e);
}
}
extern "C" fn on_call_state_ffi(call_id: i32, state: i32) {
let result = catch_unwind(AssertUnwindSafe(|| {
println!("[pjsipua_win FFI] Call state changed - call_id: {}, state: {}", call_id, state);
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_call_state {
handler(call_id, CallState::from(state));
}
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in call state callback: {:?}", e);
}
}
extern "C" fn on_call_media_state_ffi(call_id: i32, state: i32) {
let result = catch_unwind(AssertUnwindSafe(|| {
println!("[pjsipua_win FFI] Call media state changed - call_id: {}, state: {}", call_id, state);
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_call_media_state {
handler(call_id, state != 0);
}
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in call media state callback: {:?}", e);
}
}
macro_rules! wrap_ffi_call {
($body:expr) => {{
match catch_unwind(AssertUnwindSafe(|| $body)) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in FFI call: {:?}", e);
Err(Pjsua2Error::PanicOccurred)
}
}
}};
}
pub struct Pjsua2Endpoint {
_private: (),
}
impl Pjsua2Endpoint {
pub fn init(log_level: i32) -> Result<Self> {
wrap_ffi_call!({
let mut initialized = INITIALIZED.lock().unwrap();
if *initialized {
return Ok(Self { _private: () });
}
unsafe {
let result = ffi::pjsua2_create_endpoint();
if result != 0 {
return Err(Pjsua2Error::EndpointCreation(result));
}
let result = ffi::pjsua2_init_endpoint(log_level);
if result != 0 {
return Err(Pjsua2Error::EndpointInit(result));
}
let result = ffi::pjsua2_create_transport(5060);
if result != 0 {
return Err(Pjsua2Error::TransportCreation(result));
}
let result = ffi::pjsua2_start();
if result != 0 {
return Err(Pjsua2Error::StartError(result));
}
*initialized = true;
Ok(Self { _private: () })
}
})
}
pub fn get_call_quality(&self, call_id: i32) -> Result<CallQuality> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let mut rx_level = 0.0;
let mut tx_level = 0.0;
let mut rx_quality = 0.0;
let mut rx_loss_percent = 0.0;
let mut rtt_ms = 0;
let mut jitter_ms = 0.0;
let mut mos_score = 0.0;
let mut codec_buf = vec![0u8; 64];
let result = ffi::pjsua2_get_call_quality(
call_id,
&mut rx_level,
&mut tx_level,
&mut rx_quality,
&mut rx_loss_percent,
&mut rtt_ms,
&mut jitter_ms,
&mut mos_score,
codec_buf.as_mut_ptr() as *mut i8,
codec_buf.len() as i32,
);
if result != 0 {
return Err(Pjsua2Error::CallCreation(result));
}
let codec = CStr::from_ptr(codec_buf.as_ptr() as *const i8)
.to_string_lossy()
.to_string();
Ok(CallQuality {
call_id,
rx_level,
tx_level,
rx_quality,
rx_loss_percent,
rtt_ms,
jitter_ms,
mos_score,
codec,
})
}
})
}
pub fn get_call_audio_devices(&self, call_id: i32) -> Result<(i32, i32)> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let mut capture_dev = -1;
let mut playback_dev = -1;
let result = ffi::pjsua2_get_call_audio_devices(
call_id,
&mut capture_dev,
&mut playback_dev,
);
if result != 0 {
return Err(Pjsua2Error::AudioDeviceError(result));
}
Ok((capture_dev, playback_dev))
}
})
}
pub fn call_has_media(&self, call_id: i32) -> bool {
match catch_unwind(AssertUnwindSafe(|| {
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return false;
}
unsafe {
ffi::pjsua2_call_has_media(call_id) != 0
}
})) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in call_has_media: {:?}", e);
false
}
}
}
pub fn answer_call_with_audio_info(&self, call_id: i32, code: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_answer_call_with_audio_info(call_id, code);
if result != 0 {
Err(Pjsua2Error::AnswerError(result))
} else {
Ok(())
}
}
})
}
pub fn create_account(&self, config: &AccountConfig) -> Result<i32> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
let id_uri = CString::new(&config.id_uri[..])?;
let registrar_uri = CString::new(&config.registrar_uri[..])?;
let username = CString::new(&config.username[..])?;
let password = CString::new(&config.password[..])?;
unsafe {
let account_id = ffi::pjsua2_create_account_simple(
id_uri.as_ptr(),
registrar_uri.as_ptr(),
username.as_ptr(),
password.as_ptr(),
);
if account_id < 0 {
Err(Pjsua2Error::AccountCreation(account_id))
} else {
Ok(account_id)
}
}
})
}
#[deprecated(note = "Use set_callbacks() and create_account() instead")]
pub fn create_account_with_callbacks<F1, F2>(
&self,
config: &AccountConfig,
_on_reg_state: F1,
_on_incoming_call: F2,
) -> Result<i32>
where
F1: Fn(bool) + Send + Sync + 'static,
F2: Fn(i32, String) + Send + Sync + 'static,
{
self.create_account(config)
}
pub fn make_call(&self, account_id: i32, uri: &str) -> Result<i32> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
let uri_c = CString::new(uri)?;
unsafe {
let call_id = ffi::pjsua2_make_call(account_id, uri_c.as_ptr());
if call_id < 0 {
Err(Pjsua2Error::CallCreation(call_id))
} else {
Ok(call_id)
}
}
})
}
pub fn answer_call(&self, call_id: i32, code: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_answer_call(call_id, code);
if result != 0 {
Err(Pjsua2Error::AnswerError(result))
} else {
Ok(())
}
}
})
}
pub fn hangup_call(&self, call_id: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_hangup_call(call_id);
if result != 0 {
Err(Pjsua2Error::HangupError(result))
} else {
Ok(())
}
}
})
}
pub fn hold_call(&self, call_id: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_hold_call(call_id);
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(())
}
}
})
}
pub fn unhold_call(&self, call_id: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_unhold_call(call_id);
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(())
}
}
})
}
pub fn is_call_on_hold(&self, call_id: i32) -> bool {
match catch_unwind(AssertUnwindSafe(|| {
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return false;
}
unsafe {
ffi::pjsua2_is_call_on_hold(call_id) != 0
}
})) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in is_call_on_hold: {:?}", e);
false
}
}
}
pub fn mute_call(&self, call_id: i32, mute: bool) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_mute_call(call_id, if mute { 1 } else { 0 });
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(())
}
}
})
}
pub fn is_call_muted(&self, call_id: i32) -> bool {
match catch_unwind(AssertUnwindSafe(|| {
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return false;
}
unsafe {
ffi::pjsua2_is_call_muted(call_id) != 0
}
})) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in is_call_muted: {:?}", e);
false
}
}
}
pub fn send_dtmf(&self, call_id: i32, digits: &str) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
let digits_c = CString::new(digits)?;
unsafe {
let result = ffi::pjsua2_send_dtmf(call_id, digits_c.as_ptr());
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(())
}
}
})
}
pub fn conference_calls(&self, call_id1: i32, call_id2: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_conference_calls(call_id1, call_id2);
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(())
}
}
})
}
pub fn get_conf_port(&self, call_id: i32) -> Result<i32> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let port = ffi::pjsua2_get_conf_port(call_id);
if port < 0 {
Err(Pjsua2Error::CallCreation(port))
} else {
Ok(port)
}
}
})
}
pub fn get_call_detailed_info(&self, call_id: i32) -> Result<CallDetailedInfo> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let mut is_on_hold = 0;
let mut is_muted = 0;
let mut conf_port = -1;
let result = ffi::pjsua2_get_call_detailed_info(
call_id,
&mut is_on_hold,
&mut is_muted,
&mut conf_port,
);
if result != 0 {
Err(Pjsua2Error::CallCreation(result))
} else {
Ok(CallDetailedInfo {
call_id,
is_on_hold: is_on_hold != 0,
is_muted: is_muted != 0,
conf_port,
})
}
}
})
}
pub fn get_audio_devices(&self) -> Result<Vec<AudioDevice>> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let count = ffi::pjsua2_get_audio_device_count();
let mut devices = Vec::new();
for i in 0..count {
let mut name_buf = vec![0u8; 256];
let mut is_capture = 0;
let mut is_playback = 0;
let dev_id = ffi::pjsua2_get_audio_device_info(
i,
name_buf.as_mut_ptr() as *mut i8,
name_buf.len() as i32,
&mut is_capture,
&mut is_playback,
);
if dev_id >= 0 {
let name = CStr::from_ptr(name_buf.as_ptr() as *const i8)
.to_string_lossy()
.to_string();
devices.push(AudioDevice {
id: dev_id,
name,
is_capture: is_capture != 0,
is_playback: is_playback != 0,
});
}
}
Ok(devices)
}
})
}
pub fn set_audio_devices(&self, capture_id: i32, playback_id: i32) -> Result<()> {
wrap_ffi_call!({
let initialized = INITIALIZED.lock().unwrap();
if !*initialized {
return Err(Pjsua2Error::NotInitialized);
}
unsafe {
let result = ffi::pjsua2_set_audio_devices(capture_id, playback_id);
if result != 0 {
Err(Pjsua2Error::AudioDeviceError(result))
} else {
Ok(())
}
}
})
}
pub fn set_callbacks<F1, F2, F3, F4>(
&self,
on_registration_state: F1,
on_incoming_call: F2,
on_call_state: F3,
on_call_media_state: F4,
) where
F1: Fn(i32, bool) + Send + Sync + 'static,
F2: Fn(i32, i32, String) + Send + Sync + 'static,
F3: Fn(i32, CallState) + Send + Sync + 'static,
F4: Fn(i32, bool) + Send + Sync + 'static,
{
let result = catch_unwind(AssertUnwindSafe(|| {
if let Ok(mut callbacks) = CALLBACKS.lock() {
callbacks.on_registration_state = Some(Box::new(on_registration_state));
callbacks.on_incoming_call = Some(Box::new(on_incoming_call));
callbacks.on_call_state = Some(Box::new(on_call_state));
callbacks.on_call_media_state = Some(Box::new(on_call_media_state));
}
unsafe {
ffi::pjsua2_set_callbacks(
Some(on_reg_state_ffi),
Some(on_incoming_call_ffi),
Some(on_call_state_ffi),
Some(on_call_media_state_ffi),
);
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in set_callbacks: {:?}", e);
}
}
pub fn test_basic() -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_test_basic() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in test_basic: {:?}", e);
-1
}
}
}
pub fn test_object_creation() -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_test_object_creation() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in test_object_creation: {:?}", e);
-1
}
}
}
pub fn test_account_config() -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_test_account_config() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in test_account_config: {:?}", e);
-1
}
}
}
pub fn get_state(&self) -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_test_endpoint_state() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in get_state: {:?}", e);
-1
}
}
}
pub fn get_active_call_count(&self) -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_get_active_call_count() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in get_active_call_count: {:?}", e);
0
}
}
}
pub fn cleanup_calls(&self) -> i32 {
match catch_unwind(|| unsafe { ffi::pjsua2_cleanup_calls() }) {
Ok(result) => result,
Err(e) => {
eprintln!("[pjsipua_win FFI] PANIC in cleanup_calls: {:?}", e);
0
}
}
}
}
impl Drop for Pjsua2Endpoint {
fn drop(&mut self) {
let result = catch_unwind(AssertUnwindSafe(|| {
let mut initialized = INITIALIZED.lock().unwrap();
if *initialized {
unsafe {
ffi::pjsua2_destroy();
}
*initialized = false;
}
}));
if let Err(e) = result {
eprintln!("[pjsipua_win FFI] PANIC in Drop: {:?}", e);
}
}
}
#[derive(Debug, Clone)]
pub struct AudioDevice {
pub id: i32,
pub name: String,
pub is_capture: bool,
pub is_playback: bool,
}
#[derive(Debug, Clone)]
pub struct AccountConfig {
pub id_uri: String,
pub registrar_uri: String,
pub username: String,
pub password: String,
}
impl AccountConfig {
pub fn new(username: &str, domain: &str, password: &str) -> Self {
Self {
id_uri: format!("sip:{}@{}", username, domain),
registrar_uri: format!("sip:{}", domain),
username: username.to_string(),
password: password.to_string(),
}
}
}
pub fn run_diagnostics() -> DiagnosticResults {
DiagnosticResults {
basic_test: Pjsua2Endpoint::test_basic(),
object_creation: Pjsua2Endpoint::test_object_creation(),
account_config: Pjsua2Endpoint::test_account_config(),
}
}
#[derive(Debug)]
pub struct DiagnosticResults {
pub basic_test: i32,
pub object_creation: i32,
pub account_config: i32,
}