この記事は筆者オンリーのAdvent Calendar 202517日目の記事です。
以前の記事 の「INVITE/ACK/BYE 最小実装メモ」の続きで、MVPで返している SDP が実装上どう組み立てられているかを残す。対象は INVITE に対する 200 OK の SDP と オファーの読み取り。
SDP を返す流れ
-
sessionがローカル SDP を組み立て(PCMU/8000 固定)→SessionOut::SipSend200{answer}としてsipへ渡す。src/session/session.rsのbuild_answer_pcmu8kがSdp::pcmu(local_ip, local_port)を返す。 -
sip側のSipCore::handle_session_outがresponse_final_with_sdpを呼び出し、リクエストヘッダを引き継いだ 200 OK + SDP を生成して送信する。
200 OK を作るコード(実装抜粋)
// src/sip/builder.rs:221-
pub fn response_final_with_sdp(
req: &SipRequest,
code: u16,
reason: &str,
contact_ip: &str,
sip_port: u16,
answer: &Sdp,
) -> Option<SipResponse> {
let via = req.header_value("Via")?;
let from = req.header_value("From")?;
let mut to = req.header_value("To")?.to_string();
let call_id = req.header_value("Call-ID")?;
let cseq = req.header_value("CSeq")?;
if !to.to_ascii_lowercase().contains("tag=") {
to = format!("{to};tag=rustbot");
}
let sdp = format!(
concat!(
"v=0\r\n",
"o=rustbot 1 1 IN IP4 {ip}\r\n",
"s=Rust PCMU Bot\r\n",
"c=IN IP4 {ip}\r\n",
"t=0 0\r\n",
"m=audio {rtp} RTP/AVP {pt}\r\n",
"a=rtpmap:{pt} {codec}\r\n",
"a=sendrecv\r\n",
),
ip = answer.ip,
rtp = answer.port,
pt = answer.payload_type,
codec = answer.codec
);
Some(
SipResponseBuilder::new(code, reason)
.header("Via", via)
.header("From", from)
.header("To", to)
.header("Call-ID", call_id)
.header("CSeq", cseq)
.header("Contact", format!("sip:rustbot@{contact_ip}:{sip_port}"))
.body(sdp.as_bytes(), Some("application/sdp"))
.build(),
)
}
SDP の中身(固定値)
-
v=0/o=rustbot 1 1 IN IP4 {ip}/s=Rust PCMU Bot/c=IN IP4 {ip}/t=0 0 -
m=audio {rtp} RTP/AVP {pt}(pt はSdp.payload_type、デフォルト 0) -
a=rtpmap:{pt} {codec}(codecは"PCMU/8000"固定) a=sendrecv
オファーの扱い(受信側)
-
src/sip/mod.rsのparse_offer_sdpで c= の IPv4 と m=audio のポート/pt だけを抜き出し、Sdpに詰める。 - 失敗/欠落時は
Sdp::pcmu("0.0.0.0", 0)にフォールバックする。 - 属性(
a=行)や fmtp/ptime は読まない、pt が無い場合は 0 を仮定。
オファーパースのコード
// src/sip/mod.rs:85-
fn parse_offer_sdp(body: &[u8]) -> Option<Sdp> {
let s = std::str::from_utf8(body).ok()?;
let mut ip = None;
let mut port = None;
let mut pt = None;
for line in s.lines() {
let line = line.trim();
if line.starts_with("c=IN IP4 ") {
let v = line.trim_start_matches("c=IN IP4 ").trim();
ip = Some(v.to_string());
} else if line.starts_with("m=audio ") {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() >= 4 {
port = cols[1].parse::<u16>().ok();
pt = cols[3].parse::<u8>().ok();
}
}
}
Some(Sdp {
ip: ip?,
port: port?,
payload_type: pt.unwrap_or(0),
codec: "PCMU/8000".to_string(),
})
}
ローカル SDP を組むコード(session 側)
// src/session/session.rs:365-
fn build_answer_pcmu8k(&self) -> Sdp {
// PCMU/8000 でローカル SDP を組み立て
Sdp::pcmu(self.media_cfg.local_ip.clone(), self.media_cfg.local_port)
}
SDP 構造体
// src/session/types.rs:4-
#[derive(Clone, Debug)]
pub struct Sdp {
pub ip: String,
pub port: u16,
pub payload_type: u8,
pub codec: String, // e.g. "PCMU/8000"
}
impl Sdp {
pub fn pcmu(ip: impl Into<String>, port: u16) -> Self {
Self {
ip: ip.into(),
port,
payload_type: 0,
codec: "PCMU/8000".to_string(),
}
}
}
現状の割り切り / 次の宿題
- コーデックは PCMU のみ、
a=sendrecv固定。Contactはadvertised_ip:sip_portで組み立て。 - オファーの属性は無視しているため、モノラル/ptime などは受け流し。複数 m 行や ICE/SRTP も未対応。
- 今後の拡張案: 複数コーデック対応、fmtp/ptime の反映、
a=recvonly/sendonlyの尊重、ICE/SRTP/RTCP 周りの項目を追加。