#![cfg(feature = "tokio")]
use std::time::Duration;
use rtsp_runtime::client::ClientEvent;
use rtsp_runtime::server::ServerEvent;
use rtsp_runtime::{
AsyncRtspClient, AsyncRtspServer, ClientSession, RtspTimeouts, StatusCode, Transport,
TransportSpec,
};
use tokio::io::duplex;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
const URI: &str = "rtsp://127.0.0.1/live";
fn tcp_interleaved() -> Transport {
Transport::single(TransportSpec::rtp_avp_tcp_interleaved(0, 1))
}
const SDP: &str = "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=video 0 RTP/AVP 96\r\na=rtpmap:96 H264/90000\r\n";
#[tokio::test]
async fn announce_then_record_round_trips_over_loopback() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
let mut methods = Vec::new();
for _ in 0..3 {
let events = srv.next_request().await.unwrap().expect("request");
for e in events {
if let ServerEvent::RequestAccepted { method, .. } = e {
methods.push(method);
}
}
}
methods
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let ev = client.announce(URI, SDP).await.unwrap();
assert!(
matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok),
"{ev:?}"
);
let ev = client.setup(URI, &tcp_interleaved()).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { .. }), "{ev:?}");
let ev = client.record(URI).await.unwrap();
assert!(
matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok),
"{ev:?}"
);
assert_eq!(
client.state(),
rtsp_runtime::SessionState::Recording,
"a pusher that has RECORDed must be in the Recording state"
);
let methods = server.await.unwrap();
assert_eq!(methods.len(), 3, "server saw {methods:?}");
}
#[tokio::test]
async fn a_client_interleaved_send_arrives_as_a_frame_at_the_server() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
loop {
let events = srv.next_request().await.unwrap()?;
for e in events {
if let ServerEvent::MediaData { channel, data } = e {
return Some((channel, data));
}
}
}
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
client.send_interleaved(0, b"media-payload").await.unwrap();
let got = tokio::time::timeout(Duration::from_secs(5), server)
.await
.expect("server must receive the interleaved frame")
.unwrap()
.expect("a MediaData event");
assert_eq!(got.0, 0, "channel");
assert_eq!(got.1, b"media-payload".to_vec());
}
#[tokio::test(start_paused = true)]
async fn a_stalled_interleaved_send_times_out_at_the_write_bound() {
let (client_io, _server_io) = duplex(8);
let timeouts = RtspTimeouts::default().with_write(Duration::from_secs(1));
let mut client =
AsyncRtspClient::with_stream_timeouts(client_io, ClientSession::new(), timeouts);
let err = tokio::time::timeout(
Duration::from_secs(5),
client.send_interleaved(0, &[0u8; 64]),
)
.await
.expect("the stalled send must fail at the write bound, not hang")
.expect_err("the stalled send must fail at the write bound");
assert!(
matches!(err, rtsp_runtime::Error::Timeout { what: "write" }),
"got {err:?}"
);
}
#[tokio::test]
async fn announce_carries_the_sdp_body_and_content_type_to_the_server() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 1024];
let head_end = loop {
let n = sock.read(&mut tmp).await.unwrap();
assert!(n > 0, "peer closed before a full request");
buf.extend_from_slice(&tmp[..n]);
if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
break pos + 4;
}
};
let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
let clen = head
.lines()
.find_map(|l| {
let (k, v) = l.split_once(':')?;
k.eq_ignore_ascii_case("content-length")
.then(|| v.trim().parse::<usize>().ok())?
})
.unwrap_or(0);
while buf.len() < head_end + clen {
let n = sock.read(&mut tmp).await.unwrap();
assert!(n > 0, "peer closed before the body");
buf.extend_from_slice(&tmp[..n]);
}
sock.write_all(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\n\r\n")
.await
.unwrap();
buf
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let ev = client.announce(URI, SDP).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { .. }), "{ev:?}");
let text = String::from_utf8_lossy(&server.await.unwrap()).to_string();
assert!(
text.starts_with("ANNOUNCE "),
"first request must be ANNOUNCE: {text}"
);
assert!(
text.to_ascii_lowercase()
.contains("content-type: application/sdp"),
"ANNOUNCE must declare application/sdp: {text}"
);
assert!(
text.contains(SDP.trim_end_matches("\r\n")),
"ANNOUNCE body must carry the SDP: {text}"
);
}
#[tokio::test(start_paused = true)]
async fn a_send_only_pusher_still_emits_a_get_parameter_keepalive() {
let (client_io, mut peer) = duplex(64 * 1024);
let mut client = AsyncRtspClient::with_stream_timeouts(
client_io,
ClientSession::new(),
RtspTimeouts::default(),
);
let resp = tokio::join!(client.announce(URI, SDP), answer_next(&mut peer, None));
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
let transport = tcp_interleaved();
let resp = tokio::join!(
client.setup(URI, &transport),
answer_next(&mut peer, Some("Session: 12345678;timeout=2"))
);
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
let resp = tokio::join!(client.record(URI), answer_next(&mut peer, None));
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
client.send_interleaved(0, b"frame").await.unwrap();
let frame = read_one_interleaved(&mut peer).await;
assert_eq!(frame, b"frame", "the pusher's frame must reach the peer");
tokio::time::advance(Duration::from_secs(5)).await;
client.send_interleaved(0, b"frame").await.unwrap();
let (head, _) = tokio::time::timeout(Duration::from_secs(60), read_one_request(&mut peer))
.await
.expect("the pusher must emit its keepalive instead of blocking the peer");
assert!(
head.starts_with("GET_PARAMETER"),
"a send-only pusher must emit a GET_PARAMETER keepalive; got: {head}"
);
let cseq = header_of(&head, "cseq");
peer.write_all(
format!("RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nSession: 12345678\r\n\r\n").as_bytes(),
)
.await
.unwrap();
let frame = read_one_interleaved(&mut peer).await;
assert_eq!(
frame, b"frame",
"the post-keepalive frame must reach the peer"
);
}
async fn read_one_interleaved(peer: &mut tokio::io::DuplexStream) -> Vec<u8> {
let mut hdr = [0u8; 4];
peer.read_exact(&mut hdr).await.unwrap();
assert_eq!(hdr[0], b'$', "interleaved frame must start with $");
let len = usize::from(u16::from_be_bytes([hdr[2], hdr[3]]));
let mut payload = vec![0u8; len];
peer.read_exact(&mut payload).await.unwrap();
payload
}
#[tokio::test(start_paused = true)]
async fn a_454_keepalive_response_is_surfaced_not_discarded() {
let (client_io, mut peer) = duplex(64 * 1024);
let mut client = AsyncRtspClient::with_stream_timeouts(
client_io,
ClientSession::new(),
RtspTimeouts::default(),
);
let resp = tokio::join!(client.announce(URI, SDP), answer_next(&mut peer, None));
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
let transport = tcp_interleaved();
let resp = tokio::join!(
client.setup(URI, &transport),
answer_next(&mut peer, Some("Session: 12345678;timeout=2"))
);
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
let resp = tokio::join!(client.record(URI), answer_next(&mut peer, None));
assert!(matches!(resp.0.unwrap(), ClientEvent::Response { .. }));
tokio::time::advance(Duration::from_secs(5)).await;
client.poll_keepalive().await.unwrap();
let cseq = read_cseq_of(&mut peer, "GET_PARAMETER").await;
peer.write_all(
format!("RTSP/1.0 454 Session Not Found\r\nCSeq: {cseq}\r\nSession: 12345678\r\n\r\n")
.as_bytes(),
)
.await
.unwrap();
let err = client
.send_interleaved(0, b"frame")
.await
.expect_err("a drained 454 must surface, not be discarded");
assert!(
matches!(
err,
rtsp_runtime::Error::SessionNotFound {
method: rtsp_runtime::Method::GetParameter
}
),
"a 454 to the keepalive must surface as SessionNotFound(GetParameter), got {err:?}"
);
}
async fn answer_next(peer: &mut tokio::io::DuplexStream, extra_headers: Option<&str>) {
let (head, _body) = read_one_request(peer).await;
let cseq = header_of(&head, "cseq");
let extra = extra_headers
.map(|h| format!("{h}\r\n"))
.unwrap_or_default();
peer.write_all(format!("RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n{extra}\r\n").as_bytes())
.await
.unwrap();
}
async fn read_cseq_of(peer: &mut tokio::io::DuplexStream, method: &str) -> String {
let (head, _body) = read_one_request(peer).await;
assert!(
head.starts_with(method),
"expected a {method} request, got: {head}"
);
header_of(&head, "cseq")
}
async fn read_one_request(peer: &mut tokio::io::DuplexStream) -> (String, Vec<u8>) {
const BLANK: [u8; 4] = [0x0D, 0x0A, 0x0D, 0x0A];
let mut buf: Vec<u8> = Vec::new();
let mut byte = [0u8; 1];
let head_end = loop {
let n = peer.read(&mut byte).await.unwrap();
assert!(n > 0, "peer closed before a full request");
buf.push(byte[0]);
if buf.len() >= 4 && buf[buf.len() - 4..] == BLANK {
break buf.len();
}
};
let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
let clen = header_of(&head, "content-length")
.parse::<usize>()
.unwrap_or(0);
let mut body = vec![0u8; clen];
if clen > 0 {
peer.read_exact(&mut body).await.unwrap();
}
(head, body)
}
fn header_of(head: &str, name: &str) -> String {
head.lines()
.find_map(|l| {
let (k, v) = l.split_once(':')?;
k.eq_ignore_ascii_case(name).then(|| v.trim().to_string())
})
.unwrap_or_default()
}
#[tokio::test]
async fn a_flooding_peer_does_not_starve_the_send() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let http_server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let mut frame = vec![b'$', 0u8, 0u8, 30u8];
frame.extend(std::iter::repeat_n(0xABu8, 30));
loop {
if sock.write_all(&frame).await.is_err() {
return;
}
}
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let sent =
tokio::time::timeout(Duration::from_secs(5), client.send_interleaved(0, b"ping")).await;
assert!(
sent.is_ok(),
"send_interleaved must return even while the peer floods interleaved frames"
);
http_server.abort();
}