libjuice_rs/agent/
mod.rs

1//! ICE Agent.
2
3pub mod handler;
4
5use std::ffi::{CStr, CString};
6use std::marker::PhantomData;
7use std::net::IpAddr;
8use std::os::raw::{c_char, c_int, c_void};
9use std::ptr;
10use std::sync::Mutex;
11
12pub use handler::Handler;
13use libjuice_sys as sys;
14
15use crate::error::Error;
16use crate::log::ensure_logging;
17use crate::Result;
18
19/// Convert c function retcode to result
20fn raw_retcode_to_result(retcode: c_int) -> Result<()> {
21    match retcode {
22        0 => Ok(()),
23        sys::JUICE_ERR_INVALID => Err(Error::InvalidArgument),
24        sys::JUICE_ERR_FAILED => Err(Error::Failed),
25        sys::JUICE_ERR_NOT_AVAIL => Err(Error::NotAvailable),
26        _ => unreachable!(),
27    }
28}
29
30/// Agent builder.
31pub struct Builder {
32    stun_server: Option<StunServer>,
33    port_range: Option<(u16, u16)>,
34    bind_address: Option<CString>,
35    turn_servers: Vec<TurnServer>,
36    handler: Handler,
37}
38
39impl Builder {
40    /// Create new builder with given handler
41    fn new(handler: Handler) -> Self {
42        Builder {
43            stun_server: None,
44            port_range: None,
45            bind_address: None,
46            turn_servers: vec![],
47            handler,
48        }
49    }
50
51    /// Set alternative stun server (default is "stun.l.google.com:19302")
52    pub fn with_stun(mut self, host: String, port: u16) -> Self {
53        self.stun_server = Some(StunServer::new(host, port).unwrap());
54        self
55    }
56
57    /// Set port range
58    pub fn with_port_range(mut self, begin: u16, end: u16) -> Self {
59        self.port_range = Some((begin, end));
60        self
61    }
62
63    /// Bind to specific address
64    pub fn with_bind_address(mut self, addr: &IpAddr) -> Self {
65        self.bind_address = Some(CString::new(addr.to_string()).unwrap()); // can't fail
66        self
67    }
68
69    /// Add TURN server
70    pub fn add_turn_server<T>(mut self, host: T, port: u16, user: T, pass: T) -> Result<Self>
71    where
72        T: Into<Vec<u8>>,
73    {
74        let server = TurnServer {
75            host: CString::new(host).map_err(|_| Error::InvalidArgument)?,
76            port,
77            username: CString::new(user).map_err(|_| Error::InvalidArgument)?,
78            password: CString::new(pass).map_err(|_| Error::InvalidArgument)?,
79        };
80        self.turn_servers.push(server);
81
82        Ok(self)
83    }
84
85    /// Build agent
86    pub fn build(self) -> crate::Result<Agent> {
87        ensure_logging();
88
89        let mut holder = Box::new(Holder {
90            agent: ptr::null_mut(),
91            handler: Mutex::new(self.handler),
92            _marker: PhantomData::default(),
93        });
94
95        // [0..0] == no range
96        let port_range = self.port_range.unwrap_or((0, 0));
97        // default is google
98        let stun_server = self.stun_server.unwrap_or_default();
99        let bind_address = self
100            .bind_address
101            .as_ref()
102            .map(|v| v.as_ptr())
103            .unwrap_or(ptr::null());
104
105        let servers = self
106            .turn_servers
107            .iter()
108            .map(|turn| sys::juice_turn_server {
109                host: turn.host.as_ptr(),
110                port: turn.port,
111                username: turn.username.as_ptr(),
112                password: turn.password.as_ptr(),
113            })
114            .collect::<Vec<_>>();
115
116        let turn_servers = if servers.is_empty() {
117            (ptr::null(), 0)
118        } else {
119            (servers.as_ptr(), servers.len() as _)
120        };
121
122        let config = &sys::juice_config {
123            stun_server_host: stun_server.0.as_ptr(),
124            stun_server_port: stun_server.1,
125            turn_servers: turn_servers.0 as _,
126            turn_servers_count: turn_servers.1,
127            bind_address,
128            local_port_range_begin: port_range.0,
129            local_port_range_end: port_range.1,
130            cb_state_changed: Some(on_state_changed),
131            cb_candidate: Some(on_candidate),
132            cb_gathering_done: Some(on_gathering_done),
133            cb_recv: Some(on_recv),
134            user_ptr: holder.as_mut() as *mut Holder as _,
135        };
136
137        let ptr = unsafe { sys::juice_create(config as _) };
138        if ptr.is_null() {
139            Err(Error::Failed)
140        } else {
141            holder.agent = ptr;
142            Ok(Agent { holder })
143        }
144    }
145}
146
147/// ICE agent.
148pub struct Agent {
149    holder: Box<Holder>,
150}
151
152impl Agent {
153    /// Create agent builder
154    pub fn builder(h: Handler) -> Builder {
155        Builder::new(h)
156    }
157
158    /// Get ICE state
159    pub fn get_state(&self) -> State {
160        unsafe {
161            sys::juice_get_state(self.holder.agent)
162                .try_into()
163                .expect("failed to convert state")
164        }
165    }
166
167    /// Get local sdp
168    pub fn get_local_description(&self) -> crate::Result<String> {
169        let mut buf = vec![0; sys::JUICE_MAX_SDP_STRING_LEN as _];
170        let res = unsafe {
171            let res = sys::juice_get_local_description(
172                self.holder.agent,
173                buf.as_mut_ptr(),
174                buf.len() as _,
175            );
176            let _ = raw_retcode_to_result(res)?;
177            let s = CStr::from_ptr(buf.as_mut_ptr());
178            String::from_utf8_lossy(s.to_bytes())
179        };
180        Ok(res.to_string())
181    }
182
183    /// Start ICE candidates gathering
184    pub fn gather_candidates(&self) -> crate::Result<()> {
185        let ret = unsafe { sys::juice_gather_candidates(self.holder.agent) };
186        raw_retcode_to_result(ret)
187    }
188
189    /// Set remote description
190    pub fn set_remote_description(&self, sdp: String) -> crate::Result<()> {
191        let s = CString::new(sdp).map_err(|_| Error::InvalidArgument)?;
192        let ret = unsafe { sys::juice_set_remote_description(self.holder.agent, s.as_ptr()) };
193        raw_retcode_to_result(ret)
194    }
195
196    /// Add remote candidate
197    pub fn add_remote_candidate(&self, sdp: String) -> crate::Result<()> {
198        let s = CString::new(sdp).map_err(|_| Error::InvalidArgument)?;
199        let ret = unsafe { sys::juice_add_remote_candidate(self.holder.agent, s.as_ptr()) };
200        raw_retcode_to_result(ret)
201    }
202
203    /// Signal remote candidates exhausted
204    pub fn set_remote_gathering_done(&self) -> crate::Result<()> {
205        let ret = unsafe { sys::juice_set_remote_gathering_done(self.holder.agent) };
206        raw_retcode_to_result(ret)
207    }
208
209    /// Send packet to remote endpoint
210    pub fn send(&self, data: &[u8]) -> crate::Result<()> {
211        let ret =
212            unsafe { sys::juice_send(self.holder.agent, data.as_ptr() as _, data.len() as _) };
213        raw_retcode_to_result(ret)
214    }
215
216    /// Get selected candidates pair (local,remote)
217    pub fn get_selected_candidates(&self) -> crate::Result<(String, String)> {
218        let mut local = vec![0; sys::JUICE_MAX_SDP_STRING_LEN as _];
219        let mut remote = vec![0; sys::JUICE_MAX_SDP_STRING_LEN as _];
220        let ret = unsafe {
221            let res = sys::juice_get_selected_candidates(
222                self.holder.agent,
223                local.as_mut_ptr() as _,
224                local.len() as _,
225                remote.as_mut_ptr() as _,
226                remote.len() as _,
227            );
228            let _ = raw_retcode_to_result(res)?;
229            let l = CStr::from_ptr(local.as_mut_ptr());
230            let r = CStr::from_ptr(remote.as_mut_ptr());
231            (
232                String::from_utf8_lossy(l.to_bytes()).to_string(),
233                String::from_utf8_lossy(r.to_bytes()).to_string(),
234            )
235        };
236        Ok(ret)
237    }
238
239    pub fn get_selected_addresses(&self) -> crate::Result<(String, String)> {
240        let mut local = vec![0; sys::JUICE_MAX_SDP_STRING_LEN as _];
241        let mut remote = vec![0; sys::JUICE_MAX_SDP_STRING_LEN as _];
242        let ret = unsafe {
243            let res = sys::juice_get_selected_addresses(
244                self.holder.agent,
245                local.as_mut_ptr() as _,
246                local.len() as _,
247                remote.as_mut_ptr() as _,
248                remote.len() as _,
249            );
250            let _ = raw_retcode_to_result(res)?;
251            let l = CStr::from_ptr(local.as_mut_ptr());
252            let r = CStr::from_ptr(remote.as_mut_ptr());
253            (
254                String::from_utf8_lossy(l.to_bytes()).to_string(),
255                String::from_utf8_lossy(r.to_bytes()).to_string(),
256            )
257        };
258        Ok(ret)
259    }
260}
261
262pub(crate) struct Holder {
263    agent: *mut sys::juice_agent_t,
264    handler: Mutex<Handler>,
265    _marker: PhantomData<(sys::juice_agent, std::marker::PhantomPinned)>,
266}
267
268impl Drop for Holder {
269    fn drop(&mut self) {
270        unsafe { sys::juice_destroy(self.agent) }
271    }
272}
273
274// SAFETY: All juice calls protected by mutex internally and can be invoked from any thread
275unsafe impl Sync for Holder {}
276
277unsafe impl Send for Holder {}
278
279impl Holder {
280    pub(crate) fn on_state_changed(&self, state: State) {
281        let mut h = self.handler.lock().unwrap();
282        h.on_state_changed(state)
283    }
284
285    pub(crate) fn on_candidate(&self, candidate: String) {
286        let mut h = self.handler.lock().unwrap();
287        h.on_candidate(candidate)
288    }
289
290    pub(crate) fn on_gathering_done(&self) {
291        let mut h = self.handler.lock().unwrap();
292        h.on_gathering_done()
293    }
294
295    pub(crate) fn on_recv(&self, packet: &[u8]) {
296        let mut h = self.handler.lock().unwrap();
297        h.on_recv(packet)
298    }
299}
300
301#[derive(Debug, Copy, Clone, PartialEq)]
302pub enum State {
303    Disconnected,
304    Gathering,
305    Connecting,
306    Connected,
307    Completed,
308    Failed,
309}
310
311impl TryFrom<sys::juice_state> for State {
312    type Error = ();
313
314    fn try_from(value: sys::juice_state) -> std::result::Result<Self, Self::Error> {
315        Ok(match value {
316            sys::juice_state_JUICE_STATE_DISCONNECTED => State::Disconnected,
317            sys::juice_state_JUICE_STATE_GATHERING => State::Gathering,
318            sys::juice_state_JUICE_STATE_CONNECTING => State::Connecting,
319            sys::juice_state_JUICE_STATE_CONNECTED => State::Connected,
320            sys::juice_state_JUICE_STATE_COMPLETED => State::Completed,
321            sys::juice_state_JUICE_STATE_FAILED => State::Failed,
322            _ => return Err(()),
323        })
324    }
325}
326
327/// Stun server (host:port)
328struct StunServer(CString, u16);
329
330impl Default for StunServer {
331    fn default() -> Self {
332        Self(CString::new("stun.l.google.com").unwrap(), 19302)
333    }
334}
335
336impl StunServer {
337    /// Construct from host and port value
338    fn new<T: Into<Vec<u8>>>(host: T, port: u16) -> Result<Self> {
339        Ok(Self(
340            CString::new(host).map_err(|_| Error::InvalidArgument)?,
341            port,
342        ))
343    }
344}
345
346/// Turn server
347struct TurnServer {
348    pub host: CString,
349    pub username: CString,
350    pub password: CString,
351    pub port: u16,
352}
353
354unsafe extern "C" fn on_state_changed(
355    _: *mut sys::juice_agent_t,
356    state: sys::juice_state_t,
357    user_ptr: *mut c_void,
358) {
359    let agent: &Holder = &*(user_ptr as *const _);
360
361    if let Err(e) = state.try_into().map(|s| agent.on_state_changed(s)) {
362        log::error!("failed to map state {:?}", e)
363    }
364}
365
366unsafe extern "C" fn on_candidate(
367    _: *mut sys::juice_agent_t,
368    sdp: *const c_char,
369    user_ptr: *mut c_void,
370) {
371    let agent: &Holder = &*(user_ptr as *const _);
372    let candidate = {
373        let s = CStr::from_ptr(sdp);
374        String::from_utf8_lossy(s.to_bytes())
375    };
376    agent.on_candidate(candidate.to_string())
377}
378
379unsafe extern "C" fn on_gathering_done(_: *mut sys::juice_agent_t, user_ptr: *mut c_void) {
380    let agent: &Holder = &*(user_ptr as *const _);
381    agent.on_gathering_done()
382}
383
384unsafe extern "C" fn on_recv(
385    _: *mut sys::juice_agent_t,
386    data: *const c_char,
387    len: sys::size_t,
388    user_ptr: *mut c_void,
389) {
390    let agent: &Holder = &*(user_ptr as *const _);
391    let packet = core::slice::from_raw_parts(data as _, len as _);
392    agent.on_recv(packet)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::Handler;
399    use std::sync::{Arc, Barrier};
400
401    #[test]
402    fn build() {
403        crate::test_util::logger_init();
404
405        let handler = Handler::default();
406        let agent = Agent::builder(handler).build().unwrap();
407
408        assert_eq!(agent.get_state(), State::Disconnected);
409        log::debug!(
410            "local description \n\"{}\"",
411            agent.get_local_description().unwrap()
412        );
413    }
414
415    #[test]
416    fn gather() {
417        crate::test_util::logger_init();
418
419        let gathering_barrier = Arc::new(Barrier::new(2));
420
421        let handler = Handler::default()
422            .state_handler(|state| log::debug!("State changed to: {:?}", state))
423            .gathering_done_handler({
424                let barrier = gathering_barrier.clone();
425                move || {
426                    log::debug!("Gathering finished");
427                    barrier.wait();
428                }
429            })
430            .candidate_handler(|candidate| log::debug!("Local candidate: \"{}\"", candidate));
431
432        let agent = Agent::builder(handler).build().unwrap();
433
434        assert_eq!(agent.get_state(), State::Disconnected);
435        log::debug!(
436            "local description \n\"{}\"",
437            agent.get_local_description().unwrap()
438        );
439
440        agent.gather_candidates().unwrap();
441        assert_eq!(agent.get_state(), State::Gathering);
442
443        let _ = gathering_barrier.wait();
444
445        log::debug!(
446            "local description \n\"{}\"",
447            agent.get_local_description().unwrap()
448        );
449    }
450}