use std::time::{Duration, Instant};
use rsiprtp::dialog::DialogId;
use rsiprtp::sdp::parser::SessionDescription;
use rsiprtp::session::{
CallId, CallManager, Dialog, ManagerConfig, OutboundRequest, OutboundRequestKind,
};
use rsiprtp::sip::{Method, SipMessage, SipRequest, SipResponse};
fn build_manager() -> CallManager {
let mut config = ManagerConfig::default();
config.call_config.session_expires = Duration::from_secs(1800);
config.call_config.min_se = Duration::from_secs(90);
CallManager::new(config)
}
#[allow(dead_code)]
async fn outbound_call_choreography(
manager: &mut CallManager,
call_id: &CallId,
) -> Result<(), Box<dyn std::error::Error>> {
let offer_headers = manager.invite_offer_headers();
println!(
"[A] outbound INVITE attaches Supported: {:?}, Allow: {:?}",
offer_headers.supported_tags,
offer_headers
.allow_methods
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
);
if let Some((secs, refresher)) = offer_headers.session_expires {
println!(
"[A] Session-Expires: {};refresher={:?}, Min-SE: {}",
secs,
refresher,
offer_headers.min_se.unwrap()
);
}
if false {
async fn recv_msg() -> Result<SipMessage, Box<dyn std::error::Error>> {
unreachable!("documentation-only stub")
}
async fn send_to_peer(_bytes: bytes::Bytes) -> Result<(), Box<dyn std::error::Error>> {
unreachable!("documentation-only stub")
}
async fn dispatch_outbound(
_outbound: OutboundRequest,
) -> Result<(), Box<dyn std::error::Error>> {
unreachable!("documentation-only stub")
}
let dialog: Dialog = build_dialog_for_doc();
let dialog_id: DialogId = DialogId::new(
"doc-call".to_string(),
"alice-tag".to_string(),
"bob-tag".to_string(),
);
let answer_sdp: SessionDescription = SessionDescription::parse(
"v=0\r\no=- 1 1 IN IP4 0.0.0.0\r\ns=-\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\n\
m=audio 0 RTP/AVP 0\r\n",
)?;
let local_contact = "sip:me@host:port";
let far_future = Instant::now() + Duration::from_secs(86_400);
loop {
let next = manager.next_deadline().unwrap_or(far_future);
tokio::select! {
msg = recv_msg() => {
match msg? {
SipMessage::Response(resp) if resp.is_provisional() => {
if resp
.require()
.map(|r| r.0.iter().any(|t| t == "100rel"))
.unwrap_or(false)
{
manager.handle_provisional_response(
call_id, &resp, None, local_contact,
);
let _ = manager.handle_provisional_reliable(call_id, &resp);
}
}
SipMessage::Response(resp) if resp.status_code() == 200 => {
manager.handle_invite_success(
call_id,
dialog.clone(),
&answer_sdp,
Some(&resp),
Instant::now(),
);
}
SipMessage::Response(resp) if resp.status_code() == 422 => {
manager.handle_invite_failure(call_id, 422);
}
SipMessage::Request(req) if req.method() == Method::Update => {
if let Some(resp) = manager.handle_inbound_update(
&dialog_id, &req, Instant::now(),
) {
send_to_peer(resp.to_bytes()).await?;
}
}
_ => { }
}
}
_ = tokio::time::sleep_until(next.into()) => {}
}
manager.tick(Instant::now());
for outbound in manager.drain_outbound_requests() {
dispatch_outbound(outbound).await?;
}
}
}
Ok(())
}
#[allow(dead_code)]
fn dispatch_outbound_request(_outbound: OutboundRequest) {
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== rsiprtp session-timers / PRACK / UPDATE choreography ===");
let mut manager = build_manager();
println!("manager configured: session_expires=1800s, min_se=90s");
let _offer_headers = manager.invite_offer_headers();
let call_id = manager.create_call("sip:bob@carrier.example.com".to_string());
println!("[A] created outbound call {}", call_id);
println!(
"[A] next_deadline (no Established calls yet) = {:?}",
manager.next_deadline()
);
let _ = outbound_call_choreography(&mut manager, &call_id).await;
let example_request: SipRequest = SipRequest::builder()
.method(Method::Update)
.uri("sip:bob@carrier.example.com")
.via("10.0.0.1", 5060, "UDP", "z9hG4bKexample")
.from("sip:alice@example.com", "alice-tag")
.to("sip:bob@carrier.example.com")
.to_tag("bob-tag")
.call_id("st-example@example.com")
.cseq(2)
.build()?;
let example = OutboundRequest {
call_id: call_id.clone(),
request: example_request,
kind: OutboundRequestKind::SessionTimerUpdate,
};
println!(
"[A] example outbound request: kind={:?}, method={}",
example.kind,
example.request.method()
);
dispatch_outbound_request(example);
println!(
"\n[B] inbound side: evaluate_inbound_invite_session_timer + accept_session_timer + populate_uas_dialog_routing"
);
println!(" handle_inbound_update returns the 200 OK to send for peer-driven refresh");
Ok(())
}
#[allow(dead_code)]
fn build_dialog_for_doc() -> Dialog {
Dialog::new_uac(
"doc-call".to_string(),
"alice-tag".to_string(),
"bob-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
)
}
#[allow(dead_code)]
fn build_response_for_doc() -> Option<SipResponse> {
let req: SipRequest = SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@example.com")
.via("10.0.0.1", 5060, "UDP", "z9hG4bKdoc")
.from("sip:alice@example.com", "alice-tag")
.to("sip:bob@example.com")
.call_id("doc")
.cseq(1)
.build()
.ok()?;
SipResponse::builder()
.status(200, "OK")
.from_request(&req)
.to_tag("bob-tag")
.session_expires(1800, Some(rsiprtp::sip::Refresher::Uac))
.build()
.ok()
}
#[allow(dead_code)]
fn _doc_anchors(sdp: &SessionDescription) -> usize {
sdp.media.len()
}