#![cfg(feature = "tokio")]
use rtsp_runtime::client::ClientEvent;
use rtsp_runtime::server::ServerEvent;
use rtsp_runtime::{
AsyncRtspClient, AsyncRtspServer, ClientSession, Credentials, SessionState, StatusCode,
Transport, TransportSpec,
};
use tokio::net::{TcpListener, TcpStream};
const URI: &str = "rtsp://127.0.0.1/stream";
fn tcp_interleaved() -> Transport {
Transport::single(TransportSpec::rtp_avp_tcp_interleaved(0, 1))
}
#[tokio::test]
async fn plain_tcp_full_session() {
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 states = Vec::new();
for _ in 0..5 {
let events = srv.next_request().await.unwrap().expect("request");
states.push(srv.state());
assert!(events
.iter()
.any(|e| matches!(e, ServerEvent::RequestAccepted { .. })));
}
states
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
let ev = client.options(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.describe(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.setup(URI, &tcp_interleaved()).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Ready);
assert!(client.session_id().is_some());
let ev = client.play(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Playing);
let ev = client.teardown(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Init);
let server_states = server.await.unwrap();
assert_eq!(
server_states,
vec![
SessionState::Init, SessionState::Init, SessionState::Ready, SessionState::Playing, SessionState::Init, ]
);
}
#[tokio::test]
async fn interleaved_media_over_tcp() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let payload1: Vec<u8> = (0u8..32).collect();
let payload2: Vec<u8> = (100u8..140).collect();
let p1 = payload1.clone();
let p2 = payload2.clone();
let server = tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept(sock);
srv.next_request().await.unwrap().expect("SETUP");
srv.next_request().await.unwrap().expect("PLAY");
srv.send_interleaved(0, &p1).await.unwrap();
srv.send_interleaved(0, &p2).await.unwrap();
srv.stream_mut().flush().await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
});
let mut client = AsyncRtspClient::connect(addr).await.unwrap();
client.setup(URI, &tcp_interleaved()).await.unwrap();
client.play(URI).await.unwrap();
let f1 = client.recv_interleaved().await.unwrap().expect("frame 1");
assert!(matches!(f1, ClientEvent::MediaData { channel: 0, ref data } if *data == payload1));
let f2 = client.recv_interleaved().await.unwrap().expect("frame 2");
assert!(matches!(f2, ClientEvent::MediaData { channel: 0, ref data } if *data == payload2));
server.await.unwrap();
}
#[tokio::test]
async fn digest_auth_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 (mut sock, _) = listener.accept().await.unwrap();
let req1 = read_one_request(&mut sock).await;
assert!(req1.contains("DESCRIBE"));
assert!(
!req1.contains("Authorization:"),
"first request must be unauthenticated"
);
let cseq1 = cseq_of(&req1);
let challenge = "Digest realm=\"IP Camera\", \
nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", qop=\"auth\", algorithm=MD5";
let resp401 = format!(
"RTSP/1.0 401 Unauthorized\r\nCSeq: {cseq1}\r\n\
WWW-Authenticate: {challenge}\r\n\r\n"
);
sock.write_all(resp401.as_bytes()).await.unwrap();
sock.flush().await.unwrap();
let req2 = read_one_request(&mut sock).await;
assert!(req2.contains("DESCRIBE"));
assert!(
req2.contains("Authorization: Digest "),
"retry must carry a Digest Authorization: {req2}"
);
assert!(req2.contains("response="));
assert!(req2.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""));
let cseq2 = cseq_of(&req2);
let ok = format!("RTSP/1.0 200 OK\r\nCSeq: {cseq2}\r\n\r\n");
sock.write_all(ok.as_bytes()).await.unwrap();
sock.flush().await.unwrap();
cseq2
});
let session = ClientSession::new().with_credentials(Credentials::new("admin", "12345"));
let mut client = AsyncRtspClient::connect_with(addr, session).await.unwrap();
let ev = client.describe(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let cseq2 = server.await.unwrap();
assert!(cseq2 > 1, "retry used a fresh CSeq");
}
#[cfg(feature = "tls")]
#[tokio::test]
async fn tls_full_session_over_loopback() {
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
let cert_der = include_bytes!("fixtures/localhost-cert.der").to_vec();
let key_der = include_bytes!("fixtures/localhost-key.der").to_vec();
let server_config = {
let certs = vec![CertificateDer::from(cert_der.clone())];
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der));
rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("server config")
};
let client_config = {
let mut roots = rustls::RootCertStore::empty();
roots
.add(CertificateDer::from(cert_der))
.expect("add self-signed root");
rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth()
};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let mut srv = AsyncRtspServer::accept_tls(tcp, server_config)
.await
.expect("TLS handshake (server)");
let mut states = Vec::new();
for _ in 0..4 {
srv.next_request().await.unwrap().expect("request");
states.push(srv.state());
}
states
});
let mut client = AsyncRtspClient::connect_tls(addr, "localhost", client_config)
.await
.expect("TLS handshake (client)");
let ev = client.options(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let ev = client.setup(URI, &tcp_interleaved()).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Ready);
let ev = client.play(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
assert_eq!(client.state(), SessionState::Playing);
let ev = client.teardown(URI).await.unwrap();
assert!(matches!(ev, ClientEvent::Response { status, .. } if status == StatusCode::Ok));
let states = server.await.unwrap();
assert_eq!(
states,
vec![
SessionState::Init, SessionState::Ready, SessionState::Playing, SessionState::Init, ]
);
}
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn read_one_request(sock: &mut TcpStream) -> String {
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
if let Some(pos) = find_header_end(&buf) {
return String::from_utf8_lossy(&buf[..pos]).to_string();
}
let n = sock.read(&mut chunk).await.unwrap();
assert!(n > 0, "peer closed before a full request");
buf.extend_from_slice(&chunk[..n]);
}
}
fn find_header_end(buf: &[u8]) -> Option<usize> {
buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
}
fn cseq_of(request: &str) -> u32 {
for line in request.lines() {
if let Some(rest) = line.strip_prefix("CSeq:") {
return rest.trim().parse().unwrap();
}
}
panic!("no CSeq in request: {request}");
}