pub mod ffi;
use std::ffi::{CString, CStr};
use std::sync::{Arc, Mutex};
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(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,
}
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) {
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_registration_state {
handler(acc_id, is_registered != 0);
}
}
}
extern "C" fn on_incoming_call_ffi(acc_id: i32, call_id: i32, remote_uri: *const i8) {
let uri = unsafe {
CStr::from_ptr(remote_uri).to_string_lossy().to_string()
};
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_incoming_call {
handler(acc_id, call_id, uri);
}
}
}
extern "C" fn on_call_state_ffi(call_id: i32, state: i32) {
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_call_state {
handler(call_id, CallState::from(state));
}
}
}
extern "C" fn on_call_media_state_ffi(call_id: i32, state: i32) {
if let Ok(callbacks) = CALLBACKS.lock() {
if let Some(ref handler) = callbacks.on_call_media_state {
handler(call_id, state != 0);
}
}
}
pub struct Pjsua2Endpoint {
_private: (),
}
impl Pjsua2Endpoint {
pub fn init(log_level: i32) -> Result<Self> {
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));
}
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),
);
*initialized = true;
Ok(Self { _private: () })
}
}
pub fn create_account(&self, config: &AccountConfig) -> Result<i32> {
self.create_account_with_callbacks(config, |_| {}, |_, _| {})
}
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,
{
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[..])?;
if let Ok(mut callbacks) = CALLBACKS.lock() {
callbacks.on_registration_state = Some(Box::new(move |_acc_id, is_reg| {
on_reg_state(is_reg);
}));
callbacks.on_incoming_call = Some(Box::new(move |_acc_id, call_id, uri| {
on_incoming_call(call_id, uri);
}));
}
unsafe {
let account_id = ffi::pjsua2_create_account(
id_uri.as_ptr(),
registrar_uri.as_ptr(),
username.as_ptr(),
password.as_ptr(),
None, None,
);
if account_id < 0 {
Err(Pjsua2Error::AccountCreation(account_id))
} else {
Ok(account_id)
}
}
}
pub fn make_call(&self, account_id: i32, uri: &str) -> Result<i32> {
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<()> {
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<()> {
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 get_audio_devices(&self) -> Result<Vec<AudioDevice>> {
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<()> {
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,
{
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));
}
}
pub fn test_basic() -> i32 {
unsafe { ffi::pjsua2_test_basic() }
}
pub fn test_object_creation() -> i32 {
unsafe { ffi::pjsua2_test_object_creation() }
}
pub fn test_account_config() -> i32 {
unsafe { ffi::pjsua2_test_account_config() }
}
pub fn get_state(&self) -> i32 {
unsafe { ffi::pjsua2_test_endpoint_state() }
}
}
impl Drop for Pjsua2Endpoint {
fn drop(&mut self) {
let mut initialized = INITIALIZED.lock().unwrap();
if *initialized {
unsafe {
ffi::pjsua2_destroy();
}
*initialized = false;
}
}
}
#[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,
}