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 owns the wire-level concerns — SIP
5//! registration, dialogs, SDP offer/answer, and RTP framing — on a from-scratch
6//! engine (no external SIP stack), while staying out of audio device I/O, codec
7//! work, and call orchestration so it remains light and embeddable.
8//!
9//! The SIP transaction/dialog/transport engine is built in-house (see the
10//! `stack` plan in `docs/08-own-sip-stack.md`); only the [`rsip`] crate is used,
11//! for SIP message types. The engine is an internal detail — consumers depend on
12//! `wavekat-sip` alone.
13//!
14//! [`rsip`]: https://crates.io/crates/rsip
15//!
16//! # Scope
17//!
18//! What this crate covers:
19//!
20//! - **SIP signaling** — REGISTER with digest auth and keepalive
21//! re-registration ([`Registrar`]), a bound endpoint with inbound-call
22//! routing ([`SipEndpoint`]), outbound calls ([`Caller`]), and inbound calls
23//! ([`IncomingCall`]).
24//! - **SDP** — minimal G.711 (PCMU + PCMA) offer/answer with round-trip
25//! parsing ([`build_sdp`], [`parse_sdp`]).
26//! - **RTP** — header parser ([`RtpHeader`]), a debug-friendly receive loop
27//! ([`receive_rtp`]), and a codec-agnostic send loop ([`send_loop`]).
28//!
29//! Explicitly out of scope (push these to the consuming application): audio
30//! device I/O, codec encode/decode, jitter buffering, recording; account
31//! persistence; call orchestration / AI pipeline / business logic.
32//!
33//! # Quick start: register against a SIP server
34//!
35//! ```no_run
36//! use tokio_util::sync::CancellationToken;
37//! use wavekat_sip::{Registrar, SipAccount, SipEndpoint, Transport};
38//!
39//! # async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
40//! let account = SipAccount {
41//! display_name: "Office".into(),
42//! username: "1001".into(),
43//! password: "secret".into(),
44//! domain: "sip.example.com".into(),
45//! auth_username: None,
46//! server: None,
47//! port: None,
48//! transport: Transport::Udp,
49//! };
50//!
51//! let cancel = CancellationToken::new();
52//! let endpoint = SipEndpoint::new(&account, cancel.clone()).await?;
53//!
54//! // Expires: 60s, re-register every 50s.
55//! let registrar = Registrar::new(account, endpoint, cancel, 60, 50)?;
56//! registrar.register().await?;
57//! registrar.keepalive_loop().await;
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! # Placing an outbound call
63//!
64//! ```no_run
65//! use std::sync::Arc;
66//! use wavekat_sip::{Caller, SipAccount, SipEndpoint};
67//!
68//! # async fn run(account: SipAccount, endpoint: Arc<SipEndpoint>)
69//! # -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
70//! let caller = Caller::new(account, endpoint);
71//! let target: wavekat_sip::re_exports::Uri = "sip:bob@example.com".try_into()?;
72//! let mut call = caller.dial(target).await?;
73//!
74//! // Wire RTP to your audio / AI pipeline using call.rtp_socket and
75//! // call.remote_media. Hang up locally with:
76//! call.hangup().await?;
77//! # Ok(())
78//! # }
79//! ```
80//!
81//! # Answering inbound calls
82//!
83//! ```no_run
84//! use std::sync::Arc;
85//! use wavekat_sip::SipEndpoint;
86//!
87//! # async fn run(endpoint: Arc<SipEndpoint>)
88//! # -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
89//! while let Some(incoming) = endpoint.next_incoming_call().await {
90//! // Inspect incoming.remote_media, then accept (or reject):
91//! let _call = incoming.accept().await?;
92//! }
93//! # Ok(())
94//! # }
95//! ```
96//!
97//! # Building SDP and parsing the answer
98//!
99//! ```
100//! use std::net::{IpAddr, Ipv4Addr};
101//! use wavekat_sip::{build_sdp, parse_sdp};
102//!
103//! let local_ip: IpAddr = Ipv4Addr::new(192, 168, 1, 50).into();
104//! let offer = build_sdp(local_ip, 20000);
105//!
106//! let answer = offer.clone(); // simulate a loopback answer
107//! let media = parse_sdp(&answer).expect("valid SDP");
108//! assert_eq!(media.port, 20000);
109//! assert_eq!(media.payload_type, 0); // PCMU
110//! ```
111//!
112//! # Reading RTP headers off the wire
113//!
114//! ```
115//! use wavekat_sip::RtpHeader;
116//!
117//! let packet = [
118//! 0x80, 0x00, 0x04, 0xD2, // V=2, PT=0 (PCMU), seq=1234
119//! 0x00, 0x00, 0x16, 0x2E, // timestamp
120//! 0xDE, 0xAD, 0xBE, 0xEF, // SSRC
121//! ];
122//! let header = RtpHeader::parse(&packet).unwrap();
123//! assert_eq!(header.payload_type, 0);
124//! assert_eq!(header.sequence, 1234);
125//! assert_eq!(header.header_len(), 12);
126//! ```
127//!
128//! # Module map
129//!
130//! | Module | Purpose |
131//! |-----------------|----------------------------------------------------------------|
132//! | [`account`] | Runtime [`SipAccount`] + [`Transport`] enum. |
133//! | [`endpoint`] | [`SipEndpoint`] — bound transport, engine, inbound-call routing.|
134//! | [`registrar`] | REGISTER + digest auth + keepalive re-registration. |
135//! | [`resolve`] | RFC 3263 (subset) server location: SRV + A/AAAA fallback. |
136//! | [`callee`] | [`IncomingCall`] — inbound INVITE accept/reject. |
137//! | [`caller`] | [`Caller`] outbound dial + the [`Call`] handle. |
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.
144//!
145//! # License
146//!
147//! Licensed under Apache 2.0. Copyright 2026 WaveKat.
148
149#![cfg_attr(docsrs, feature(doc_cfg))]
150
151pub mod account;
152pub mod callee;
153pub mod caller;
154pub mod dtmf_info;
155pub mod endpoint;
156pub mod inbound;
157pub mod refer;
158pub mod registrar;
159pub mod resolve;
160pub mod rtp;
161pub mod sdp;
162pub mod session_timer;
163// Internal clean-room SIP engine (see `docs/08-own-sip-stack.md`). Entirely
164// `pub(crate)`: it never appears in this crate's public API.
165pub(crate) mod stack;
166
167pub use account::{SipAccount, Transport};
168pub use callee::IncomingCall;
169pub use caller::{Call, CallSession, Caller, InboundRequests};
170pub use dtmf_info::{build_info_body, content_type_header, InfoOutcome};
171pub use endpoint::SipEndpoint;
172pub use inbound::InboundRequest;
173pub use refer::{
174 is_final_sipfrag, parse_sipfrag_status, refer_to_header, refer_to_with_replaces, DialogTriplet,
175};
176pub use registrar::{Registrar, RegistrarDiagnostics};
177pub use resolve::{order_candidates, resolve_sip_server, SrvRecord};
178pub use rtp::dtmf::{
179 build_event_payload, build_rtp_dtmf_packet, send_dtmf_burst, DtmfBurstConfig, DtmfDigit,
180 DEFAULT_VOLUME_DBM0,
181};
182pub use rtp::dtmf_recv::{parse_event_payload, DtmfEvent, DtmfEventPayload, DtmfReceiver};
183pub use rtp::{receive_rtp, send_loop, RtpHeader, RtpSendConfig};
184pub use sdp::{build_sdp, build_sdp_with, parse_sdp, MediaDirection, RemoteMedia, DTMF_DEFAULT_PT};
185pub use session_timer::{
186 min_se_in, negotiate_uac, negotiate_uas, require_timer_header, session_expires_in,
187 session_timer_loop, supported_timer_header, supports_timer, Refresher, SessionDialogOps,
188 SessionExpires, SessionTimer, SessionTimerOutcome, UasSessionTimer,
189 DEFAULT_SESSION_EXPIRES_SECS, MIN_SESSION_EXPIRES_SECS,
190};
191
192/// Re-exports of the [`rsip`] message types that appear in our public API.
193/// Pinning them here lets consumers depend only on `wavekat-sip`.
194pub mod re_exports {
195 pub use rsip::{Header, Headers, Method, StatusCode, Uri};
196}
197
198/// Short git hash this crate was built from, or `"unknown"` if unavailable.
199pub const GIT_HASH: &str = env!("WAVEKAT_SIP_GIT_HASH");