diff --git a/src/client/mod.rs b/src/client/mod.rs index 932132d..23db1e6 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -497,7 +497,7 @@ fn keepalive_interval(session: &SessionHeader) -> std::time::Duration { /// Options which must be known right as a session is created. /// /// Decisions which can be deferred are in [`SetupOptions`] or [`PlayOptions`] instead. -#[derive(Default)] +#[derive(Default, Clone)] pub struct SessionOptions { creds: Option, user_agent: Option>, @@ -505,6 +505,7 @@ pub struct SessionOptions { teardown: TeardownPolicy, unassigned_channel_data: UnassignedChannelDataPolicy, session_id: SessionIdPolicy, + follow_redirects: bool, } /// Policy for handling data received on unassigned RTSP interleaved channels. @@ -713,6 +714,11 @@ impl SessionOptions { self.session_id = policy; self } + + pub fn follow_redirects(mut self, follow_redirects: bool) -> Self { + self.follow_redirects = follow_redirects; + self + } } /// Per-stream options decided for `SETUP` time, for future expansion. @@ -1352,6 +1358,33 @@ impl RtspConnection { }), }; continue; + } else if resp.status() == rtsp_types::StatusCode::Found + || resp.status() == rtsp_types::StatusCode::MovedPermanently + { + let location = match resp.header(&rtsp_types::headers::LOCATION) { + None => bail!(ErrorInt::RtspResponseError { + conn_ctx: *self.inner.ctx(), + msg_ctx, + method: req.method().clone(), + cseq, + status: resp.status(), + description: "Redirect without Location header".into(), + }), + Some(h) => h, + }; + let location = location.as_str(); + let location = match location.parse() { + Ok(l) => l, + Err(e) => bail!(ErrorInt::RtspResponseError { + conn_ctx: *self.inner.ctx(), + msg_ctx, + method: req.method().clone(), + cseq, + status: resp.status(), + description: format!("Can't parse Location header: {e}"), + }), + }; + bail!(ErrorInt::RtspRedirection(location)); } else if !resp.status().is_success() { bail!(ErrorInt::RtspResponseError { conn_ctx: *self.inner.ctx(), @@ -1487,8 +1520,21 @@ impl Session { /// /// Expects to be called from a tokio runtime. pub async fn describe(url: Url, options: SessionOptions) -> Result { - let conn = RtspConnection::connect(&url).await?; - Self::describe_with_conn(conn, options, url).await + let mut url = url; + loop { + let conn = RtspConnection::connect(&url).await?; + let result = Self::describe_with_conn(conn, options.clone(), url).await; + if !options.follow_redirects { + return result; + } + if let Err(Error(e)) = &result { + if let ErrorInt::RtspRedirection(ref location) = e.as_ref() { + url = location.clone(); + continue; + } + } + return result; + } } async fn describe_with_conn( diff --git a/src/error.rs b/src/error.rs index f503d40..33fbc5e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ use std::{fmt::Display, sync::Arc}; use crate::{ConnectionContext, PacketContext, RtspMessageContext, StreamContext, WallTime}; use bytes::Bytes; use thiserror::Error; +use url::Url; /// An opaque `std::error::Error + Send + Sync + 'static` implementation. /// @@ -49,6 +50,9 @@ pub(crate) enum ErrorInt { #[error("Invalid argument: {0}")] InvalidArgument(String), + #[error("Redirect to: {0}")] + RtspRedirection(Url), + /// Unparseable or unexpected RTSP message. #[error("RTSP framing error: {description}\n\nconn: {conn_ctx}\nmsg: {msg_ctx}")] RtspFramingError {