Skip to main content

rtsp_runtime/
state.rs

1//! RTSP session state machine — RFC 2326 Appendix A.
2//!
3//! Implements the client (§A.1) and server (§A.2) transition tables exactly as
4//! transcribed in [`docs/state-machines.md`](../docs/state-machines.md). State
5//! is tracked per session object (stream URL + session id); this module models
6//! a single object's state.
7//!
8//! The state-neutral methods `OPTIONS`, `DESCRIBE`, `ANNOUNCE`, `GET_PARAMETER`,
9//! and `SET_PARAMETER` are permitted in every state and never change it
10//! (RFC 2326 Appendix A intro). The state-affecting methods are `SETUP`,
11//! `PLAY`, `PAUSE`, `TEARDOWN`, `RECORD`, and `REDIRECT`.
12
13use crate::Method;
14use crate::error::{Error, Result};
15
16/// The lifecycle state of an RTSP session object (RFC 2326 §A.1 / §A.2).
17#[non_exhaustive]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub enum SessionState {
21    /// Initial state; no successful SETUP has been completed. A client in `Init`
22    /// that has sent a SETUP is still tracked as `Init` until the 2xx arrives.
23    #[default]
24    Init,
25    /// A SETUP succeeded (or a PAUSE returned from Playing/Recording). Ready to
26    /// PLAY or RECORD.
27    Ready,
28    /// A PLAY succeeded; media is being delivered.
29    Playing,
30    /// A RECORD succeeded; media is being recorded.
31    Recording,
32}
33
34impl SessionState {
35    /// The RFC 2326 label for this state.
36    pub fn name(&self) -> &'static str {
37        match self {
38            SessionState::Init => "Init",
39            SessionState::Ready => "Ready",
40            SessionState::Playing => "Playing",
41            SessionState::Recording => "Recording",
42        }
43    }
44}
45
46impl core::fmt::Display for SessionState {
47    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        f.write_str(self.name())
49    }
50}
51
52/// Returns `true` for the methods that do not affect session state and are
53/// therefore permitted in every state (RFC 2326 Appendix A intro).
54pub fn is_state_neutral(method: &Method) -> bool {
55    matches!(
56        method,
57        Method::Options
58            | Method::Describe
59            | Method::Announce
60            | Method::GetParameter
61            | Method::SetParameter
62    )
63}
64
65/// Computes the next state for the **client** after receiving a `2xx` success
66/// response to `method` sent in `current` state, per the RFC 2326 §A.1 client
67/// state table.
68///
69/// Returns `Err(MethodNotValidInState)` if the method is not listed for the
70/// current state (a client MUST NOT issue such a request). State-neutral methods
71/// return the unchanged current state.
72// NOTE: client_next_state + server_next_state are two hand-written matches
73// mirroring RFC 2326 Appendix A.1/A.2 verbatim. A new SessionState variant
74// (the enum is #[non_exhaustive]) must update BOTH; the wildcard arm returns
75// MethodNotValidInState, so an unhandled combination fails safe, not silently.
76pub fn client_next_state(current: SessionState, method: &Method) -> Result<SessionState> {
77    if is_state_neutral(method) {
78        return Ok(current);
79    }
80    let next = match (current, method) {
81        // Init
82        (SessionState::Init, Method::Setup) => SessionState::Ready,
83        (SessionState::Init, Method::Teardown) => SessionState::Init,
84        // Ready
85        (SessionState::Ready, Method::Play) => SessionState::Playing,
86        (SessionState::Ready, Method::Record) => SessionState::Recording,
87        (SessionState::Ready, Method::Teardown) => SessionState::Init,
88        (SessionState::Ready, Method::Setup) => SessionState::Ready,
89        // Playing
90        (SessionState::Playing, Method::Pause) => SessionState::Ready,
91        (SessionState::Playing, Method::Teardown) => SessionState::Init,
92        (SessionState::Playing, Method::Play) => SessionState::Playing,
93        (SessionState::Playing, Method::Setup) => SessionState::Playing, // changed transport
94        // Recording
95        (SessionState::Recording, Method::Pause) => SessionState::Ready,
96        (SessionState::Recording, Method::Teardown) => SessionState::Init,
97        (SessionState::Recording, Method::Record) => SessionState::Recording,
98        (SessionState::Recording, Method::Setup) => SessionState::Recording, // changed transport
99        _ => {
100            return Err(Error::MethodNotValidInState {
101                method: method.clone(),
102                state: current,
103            });
104        }
105    };
106    Ok(next)
107}
108
109/// Computes the next state for the **server** after sending a `2xx` success
110/// response to a received `method` in `current` state, per the RFC 2326 §A.2
111/// server state table.
112///
113/// Returns `Err(MethodNotValidInState)` if the method is not listed for the
114/// current state; the server maps that error to a `455` response.
115/// State-neutral methods return the unchanged current state.
116pub fn server_next_state(current: SessionState, method: &Method) -> Result<SessionState> {
117    if is_state_neutral(method) {
118        return Ok(current);
119    }
120    let next = match (current, method) {
121        // Init
122        (SessionState::Init, Method::Setup) => SessionState::Ready,
123        (SessionState::Init, Method::Teardown) => SessionState::Init,
124        // Ready
125        (SessionState::Ready, Method::Play) => SessionState::Playing,
126        (SessionState::Ready, Method::Setup) => SessionState::Ready,
127        (SessionState::Ready, Method::Teardown) => SessionState::Init,
128        (SessionState::Ready, Method::Record) => SessionState::Recording,
129        // Playing
130        (SessionState::Playing, Method::Play) => SessionState::Playing,
131        (SessionState::Playing, Method::Pause) => SessionState::Ready,
132        (SessionState::Playing, Method::Teardown) => SessionState::Init,
133        (SessionState::Playing, Method::Setup) => SessionState::Playing,
134        // Recording
135        (SessionState::Recording, Method::Record) => SessionState::Recording,
136        (SessionState::Recording, Method::Pause) => SessionState::Ready,
137        (SessionState::Recording, Method::Teardown) => SessionState::Init,
138        (SessionState::Recording, Method::Setup) => SessionState::Recording,
139        _ => {
140            return Err(Error::MethodNotValidInState {
141                method: method.clone(),
142                state: current,
143            });
144        }
145    };
146    Ok(next)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn client_setup_from_init_is_ready() {
155        assert_eq!(
156            client_next_state(SessionState::Init, &Method::Setup).unwrap(),
157            SessionState::Ready
158        );
159    }
160
161    #[test]
162    fn client_play_from_init_bites() {
163        assert!(client_next_state(SessionState::Init, &Method::Play).is_err());
164    }
165
166    #[test]
167    fn client_pause_from_ready_bites() {
168        assert!(client_next_state(SessionState::Ready, &Method::Pause).is_err());
169    }
170
171    #[test]
172    fn client_teardown_any_to_init() {
173        for s in [
174            SessionState::Ready,
175            SessionState::Playing,
176            SessionState::Recording,
177        ] {
178            assert_eq!(
179                client_next_state(s, &Method::Teardown).unwrap(),
180                SessionState::Init
181            );
182        }
183    }
184
185    #[test]
186    fn state_neutral_methods_never_change_state() {
187        for m in [
188            Method::Options,
189            Method::Describe,
190            Method::Announce,
191            Method::GetParameter,
192            Method::SetParameter,
193        ] {
194            for s in [
195                SessionState::Init,
196                SessionState::Ready,
197                SessionState::Playing,
198            ] {
199                assert_eq!(client_next_state(s, &m).unwrap(), s);
200                assert_eq!(server_next_state(s, &m).unwrap(), s);
201            }
202        }
203    }
204
205    #[test]
206    fn server_play_from_init_bites() {
207        assert!(server_next_state(SessionState::Init, &Method::Play).is_err());
208    }
209}