Skip to main content

wavekat_sip/
lib.rs

1//! SIP signaling and RTP transport for voice pipelines.
2//!
3//! `wavekat-sip` is a small, focused toolkit for building softphones, voice
4//! bots, and recording bridges in Rust. It wraps [`rsipstack`] and owns the
5//! wire-level concerns — SIP registration, dialogs, SDP offer/answer, and
6//! RTP framing — while staying out of audio device I/O, codec work, and
7//! call orchestration so it remains light and embeddable.
8//!
9//! [`rsipstack`]: https://crates.io/crates/rsipstack
10//!
11//! # Scope
12//!
13//! What this crate covers:
14//!
15//! - **SIP signaling** — REGISTER with digest auth and keepalive
16//!   re-registration ([`Registrar`]), shared transport + dialog layer
17//!   ([`SipEndpoint`]).
18//! - **SDP** — minimal G.711 (PCMU + PCMA) offer/answer with round-trip
19//!   parsing ([`build_sdp`], [`parse_sdp`]).
20//! - **RTP** — header parser ([`RtpHeader`]), a debug-friendly receive
21//!   loop ([`receive_rtp`]) suitable for transcription, recording, or
22//!   smoke-testing inbound media, and a codec-agnostic send loop
23//!   ([`send_loop`]) that packetizes consumer-supplied payloads onto
24//!   the wire.
25//!
26//! Explicitly out of scope (push these to the consuming application):
27//!
28//! - Audio device I/O (e.g. `cpal`), codec encode/decode, jitter buffering,
29//!   recording, WAV writing.
30//! - Account persistence (TOML files, system keychain).
31//! - Call orchestration, AI pipeline, business logic.
32//!
33//! Inbound INVITE handling lives in [`Callee`]; outbound INVITEs are
34//! placed via [`Caller`].
35//!
36//! # Quick start: register against a SIP server
37//!
38//! ```no_run
39//! use std::sync::Arc;
40//! use tokio_util::sync::CancellationToken;
41//! use wavekat_sip::{Registrar, SipAccount, SipEndpoint, Transport};
42//!
43//! # async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
44//! let account = SipAccount {
45//!     display_name: "Office".into(),
46//!     username: "1001".into(),
47//!     password: "secret".into(),
48//!     domain: "sip.example.com".into(),
49//!     auth_username: None,
50//!     server: None,
51//!     port: None,
52//!     transport: Transport::Udp,
53//! };
54//!
55//! let cancel = CancellationToken::new();
56//! let (endpoint, _incoming) = SipEndpoint::new(&account, cancel.clone()).await?;
57//! let endpoint = Arc::new(endpoint);
58//!
59//! // Expires: 60s, re-register every 50s.
60//! let registrar = Registrar::new(account, endpoint, cancel, 60, 50)?;
61//! registrar.register().await?;
62//! registrar.keepalive_loop().await;
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! The `_incoming` stream returned by [`SipEndpoint::new`] yields inbound
68//! transactions (INVITE, OPTIONS, …). For INVITEs, hand the transaction to
69//! [`Callee::accept_transaction`] or [`Callee::reject_transaction`].
70//!
71//! # Placing an outbound call
72//!
73//! ```no_run
74//! use std::sync::Arc;
75//! use wavekat_sip::{Caller, SipAccount, SipEndpoint};
76//!
77//! # async fn run(account: SipAccount, endpoint: Arc<SipEndpoint>)
78//! #     -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
79//! let caller = Caller::new(account, endpoint);
80//! let target: wavekat_sip::re_exports::Uri = "sip:bob@example.com".try_into()?;
81//! let mut pending = caller.dial(target).await?;
82//!
83//! // Pump pending.state_rx to render "Dialing…" / "Ringing…" in the UI.
84//! // When you see DialogState::Confirmed, promote to AcceptedDial:
85//! let accepted = pending.on_confirmed().await?;
86//!
87//! // Wire RTP to your audio / AI pipeline using accepted.rtp_socket
88//! // and accepted.remote_media. Hang up locally with:
89//! accepted.dialog.bye().await?;
90//! # Ok(())
91//! # }
92//! ```
93//!
94//! # Building SDP and parsing the answer
95//!
96//! ```
97//! use std::net::{IpAddr, Ipv4Addr};
98//! use wavekat_sip::{build_sdp, parse_sdp};
99//!
100//! let local_ip: IpAddr = Ipv4Addr::new(192, 168, 1, 50).into();
101//! let offer = build_sdp(local_ip, 20000);
102//!
103//! // ... send `offer` in an INVITE, receive an SDP answer ...
104//! let answer = offer.clone(); // simulate a loopback answer
105//! let media = parse_sdp(&answer).expect("valid SDP");
106//! assert_eq!(media.port, 20000);
107//! assert_eq!(media.payload_type, 0); // PCMU
108//! ```
109//!
110//! # Reading RTP headers off the wire
111//!
112//! For real receive loops use [`receive_rtp`]; to inspect individual
113//! packets, parse directly:
114//!
115//! ```
116//! use wavekat_sip::RtpHeader;
117//!
118//! let packet = [
119//!     0x80, 0x00, 0x04, 0xD2, // V=2, PT=0 (PCMU), seq=1234
120//!     0x00, 0x00, 0x16, 0x2E, // timestamp
121//!     0xDE, 0xAD, 0xBE, 0xEF, // SSRC
122//! ];
123//! let header = RtpHeader::parse(&packet).unwrap();
124//! assert_eq!(header.payload_type, 0);
125//! assert_eq!(header.sequence, 1234);
126//! assert_eq!(header.header_len(), 12);
127//! ```
128//!
129//! # Module map
130//!
131//! | Module          | Purpose                                                        |
132//! |-----------------|----------------------------------------------------------------|
133//! | [`account`]     | Runtime [`SipAccount`] + [`Transport`] enum.                   |
134//! | [`endpoint`]    | [`SipEndpoint`] — bound transport, dialog layer, RX stream.    |
135//! | [`registrar`]   | REGISTER + digest auth + keepalive re-registration.            |
136//! | [`callee`]      | Inbound INVITE accept/reject helper.                           |
137//! | [`caller`]      | Outbound INVITE / dial-cancel helper.                          |
138//! | [`sdp`]         | Minimal G.711 offer/answer build + parse.                      |
139//! | [`rtp`]         | RTP header parser, debug receive loop, codec-agnostic send loop. |
140//!
141//! # Stability
142//!
143//! Pre-1.0. The public API may still shift between minor versions. Pin
144//! an exact version if you need stability today.
145//!
146//! # License
147//!
148//! Licensed under Apache 2.0. Copyright 2026 WaveKat.
149
150#![cfg_attr(docsrs, feature(doc_cfg))]
151
152pub mod account;
153pub mod callee;
154pub mod caller;
155pub mod endpoint;
156pub mod registrar;
157pub mod rtp;
158pub mod sdp;
159
160pub use account::{SipAccount, Transport};
161pub use callee::{AcceptedCall, Callee, PendingCall};
162pub use caller::{AcceptedDial, Caller, PendingDial};
163pub use endpoint::{DispatchOutcome, SipEndpoint};
164pub use registrar::{Registrar, RegistrarDiagnostics};
165pub use rtp::{receive_rtp, send_loop, RtpHeader, RtpSendConfig};
166pub use sdp::{build_sdp, parse_sdp, RemoteMedia};
167
168/// Re-exports of upstream types that appear in our public API. Pinning
169/// them here lets consumers depend only on `wavekat-sip` without taking
170/// a direct dep on `rsip` / `rsipstack`.
171pub mod re_exports {
172    pub use rsip::{Header, Method, StatusCode, Uri};
173    pub use rsipstack::dialog::dialog::{DialogState, DialogStateReceiver, TerminatedReason};
174    pub use rsipstack::dialog::DialogId;
175    pub use rsipstack::transaction::transaction::Transaction;
176    pub use rsipstack::transport::SipAddr;
177}
178
179/// Short git hash this crate was built from, or `"unknown"` if unavailable.
180pub const GIT_HASH: &str = env!("WAVEKAT_SIP_GIT_HASH");