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 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** — offer/answer for Opus (RFC 7587, preferred) and G.711
25//!   (PCMU + PCMA) with round-trip parsing ([`build_sdp_offer`],
26//!   [`parse_sdp`]); answers and mid-call re-offers pin the negotiated
27//!   codec ([`CodecMenu`]). Negotiation only — encode/decode stays with
28//!   the consumer.
29//! - **RTP** — header parser ([`RtpHeader`]), a debug-friendly receive loop
30//!   ([`receive_rtp`]), and a codec-agnostic send loop ([`send_loop`]).
31//! - **TLS** — SIP over TLS ([`Transport::Tls`]), gated behind the `tls`
32//!   cargo feature (off by default). Certificate verification against the
33//!   account's SIP domain, optional SHA-256 pinning ([`TlsPolicy`]), and a
34//!   typed error surface ([`CertFailure`], [`UntrustedCertificate`]).
35//!   `docs.rs` builds with all features, so this surface is always visible
36//!   here even when a consumer has not enabled it; see the crate README's
37//!   TLS section for the feature flag and the `Unsupported` error a
38//!   consumer gets if they select [`Transport::Tls`] without it.
39//!
40//! Explicitly out of scope (push these to the consuming application): audio
41//! device I/O, codec encode/decode, jitter buffering, recording; account
42//! persistence; call orchestration / AI pipeline / business logic.
43//!
44//! # Quick start: register against a SIP server
45//!
46//! ```no_run
47//! use tokio_util::sync::CancellationToken;
48//! use wavekat_sip::{Registrar, SipAccount, SipEndpoint, TlsPolicy, Transport};
49//!
50//! # async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
51//! let account = SipAccount {
52//!     display_name: "Office".into(),
53//!     username: "1001".into(),
54//!     password: "secret".into(),
55//!     domain: "sip.example.com".into(),
56//!     auth_username: None,
57//!     server: None,
58//!     port: None,
59//!     transport: Transport::Udp,
60//!     tls_policy: TlsPolicy::default(),
61//! };
62//!
63//! let cancel = CancellationToken::new();
64//! let endpoint = SipEndpoint::new(&account, cancel.clone()).await?;
65//!
66//! // Expires: 60s, re-register every 50s.
67//! let registrar = Registrar::new(account, endpoint, cancel, 60, 50)?;
68//! registrar.register().await?;
69//! registrar.keepalive_loop().await;
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! # Placing an outbound call
75//!
76//! ```no_run
77//! use std::sync::Arc;
78//! use wavekat_sip::{Caller, SipAccount, SipEndpoint};
79//!
80//! # async fn run(account: SipAccount, endpoint: Arc<SipEndpoint>)
81//! #     -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
82//! let caller = Caller::new(account, endpoint);
83//! let target: wavekat_sip::re_exports::Uri = "sip:bob@example.com".try_into()?;
84//! let mut call = caller.dial(target).await?;
85//!
86//! // Wire RTP to your audio / AI pipeline using call.rtp_socket and
87//! // call.remote_media. Hang up locally with:
88//! call.hangup().await?;
89//! # Ok(())
90//! # }
91//! ```
92//!
93//! # Answering inbound calls
94//!
95//! ```no_run
96//! use std::sync::Arc;
97//! use wavekat_sip::SipEndpoint;
98//!
99//! # async fn run(endpoint: Arc<SipEndpoint>)
100//! #     -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
101//! while let Some(incoming) = endpoint.next_incoming_call().await {
102//!     // Inspect incoming.remote_media, then accept (or reject):
103//!     let _call = incoming.accept().await?;
104//! }
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! # Building SDP and parsing the answer
110//!
111//! ```
112//! use std::net::{IpAddr, Ipv4Addr};
113//! use wavekat_sip::{build_sdp_offer, parse_sdp, AudioCodec};
114//!
115//! let local_ip: IpAddr = Ipv4Addr::new(192, 168, 1, 50).into();
116//! let offer = build_sdp_offer(local_ip, 20000); // Opus preferred, G.711 fallback
117//!
118//! let answer = offer.clone(); // simulate a loopback answer
119//! let media = parse_sdp(&answer).expect("valid SDP");
120//! assert_eq!(media.port, 20000);
121//! assert_eq!(media.codec, Some(AudioCodec::Opus { payload_type: 111 }));
122//! ```
123//!
124//! # Reading RTP headers off the wire
125//!
126//! ```
127//! use wavekat_sip::RtpHeader;
128//!
129//! let packet = [
130//!     0x80, 0x00, 0x04, 0xD2, // V=2, PT=0 (PCMU), seq=1234
131//!     0x00, 0x00, 0x16, 0x2E, // timestamp
132//!     0xDE, 0xAD, 0xBE, 0xEF, // SSRC
133//! ];
134//! let header = RtpHeader::parse(&packet).unwrap();
135//! assert_eq!(header.payload_type, 0);
136//! assert_eq!(header.sequence, 1234);
137//! assert_eq!(header.header_len(), 12);
138//! ```
139//!
140//! # Module map
141//!
142//! | Module          | Purpose                                                        |
143//! |-----------------|----------------------------------------------------------------|
144//! | [`account`]     | Runtime [`SipAccount`] + [`Transport`] enum.                   |
145//! | [`endpoint`]    | [`SipEndpoint`] — bound transport, engine, inbound-call routing.|
146//! | [`registrar`]   | REGISTER + digest auth + keepalive re-registration.            |
147//! | [`resolve`]     | RFC 3263 (subset) server location: SRV + A/AAAA fallback.      |
148//! | [`callee`]      | [`IncomingCall`] — inbound INVITE accept/reject.               |
149//! | [`caller`]      | [`Caller`] outbound dial + the [`Call`] handle.                |
150//! | [`sdp`]         | Opus + G.711 offer/answer build + parse (negotiation only).    |
151//! | [`rtp`]         | RTP header parser, debug receive loop, codec-agnostic send loop. |
152//!
153//! # Stability
154//!
155//! Pre-1.0. The public API may still shift between minor versions.
156//!
157//! # License
158//!
159//! Licensed under Apache 2.0. Copyright 2026 WaveKat.
160
161#![cfg_attr(docsrs, feature(doc_cfg))]
162
163pub mod account;
164pub mod callee;
165pub mod caller;
166pub mod dtmf_info;
167pub mod endpoint;
168pub mod inbound;
169pub mod refer;
170pub mod registrar;
171pub mod resolve;
172pub mod rtp;
173pub mod sdp;
174pub mod session_timer;
175// Internal clean-room SIP engine (see `docs/08-own-sip-stack.md`). Entirely
176// `pub(crate)`: it never appears in this crate's public API.
177pub(crate) mod stack;
178pub mod tls_error;
179
180pub use account::{SipAccount, TlsPolicy, Transport};
181pub use callee::IncomingCall;
182pub use caller::{Call, CallSession, Caller, InboundRequests};
183pub use dtmf_info::{build_info_body, content_type_header, InfoOutcome};
184pub use endpoint::SipEndpoint;
185pub use inbound::InboundRequest;
186pub use refer::{
187    is_final_sipfrag, parse_sipfrag_status, refer_to_header, refer_to_with_replaces, DialogTriplet,
188};
189pub use registrar::{Registrar, RegistrarDiagnostics};
190pub use resolve::{order_candidates, resolve_sip_server, SrvRecord};
191pub use rtp::dtmf::{
192    build_event_payload, build_rtp_dtmf_packet, send_dtmf_burst, DtmfBurstConfig, DtmfDigit,
193    DEFAULT_VOLUME_DBM0,
194};
195pub use rtp::dtmf_recv::{parse_event_payload, DtmfEvent, DtmfEventPayload, DtmfReceiver};
196pub use rtp::{receive_rtp, send_loop, RtpHeader, RtpSendConfig};
197pub use sdp::{
198    build_sdp_offer, build_sdp_with, parse_sdp, select_codec, select_dtmf, AudioCodec, CodecMenu,
199    DtmfSpec, MediaDirection, RemoteMedia, DTMF_DEFAULT_PT, DTMF_WIDEBAND_DEFAULT_PT,
200    OPUS_DEFAULT_PT, OPUS_RTP_CLOCK_RATE,
201};
202pub use session_timer::{
203    min_se_in, negotiate_uac, negotiate_uas, require_timer_header, session_expires_in,
204    session_timer_loop, supported_timer_header, supports_timer, Refresher, SessionDialogOps,
205    SessionExpires, SessionTimer, SessionTimerOutcome, UasSessionTimer,
206    DEFAULT_SESSION_EXPIRES_SECS, MIN_SESSION_EXPIRES_SECS,
207};
208pub use tls_error::{untrusted_certificate, CertFailure, UntrustedCertificate};
209
210/// Re-exports of the [`rsip`] message types that appear in our public API.
211/// Pinning them here lets consumers depend only on `wavekat-sip`.
212pub mod re_exports {
213    pub use rsip::{Header, Headers, Method, StatusCode, Uri};
214}
215
216/// Short git hash this crate was built from, or `"unknown"` if unavailable.
217pub const GIT_HASH: &str = env!("WAVEKAT_SIP_GIT_HASH");