Skip to main content

rtmp_runtime/
io.rs

1//! Async `tokio` socket adapter driving the sans-IO ingest server session
2//! over a real `tokio::net::TcpStream` — the Layer-2 adapter (see
3//! [`docs/rtmp.md`](../docs/rtmp.md) § Layer-2 adapter; mirrors the
4//! `rtsp_runtime::io` tokio adapter shape in this same workspace).
5//!
6//! [`ServerSession`] never touches a socket: it turns inbound bytes into
7//! `(reply bytes, [`ServerEvent`]s)`. This module is the thin layer that
8//! actually owns a [`TcpStream`], reads whatever bytes are available, feeds
9//! them to [`ServerSession::handle_data`], writes the reply bytes back, and
10//! returns the events — no business logic beyond that plumbing.
11//!
12//! [`ServerSession::handle_data`] buffers partial handshake/chunk input
13//! internally (see its doc comment), so unlike an RTSP or HTTP adapter this
14//! one does not need to detect message boundaries itself: any chunk size,
15//! split anywhere, is fine to feed straight through.
16
17use std::io;
18use std::net::SocketAddr;
19
20use tokio::io::{AsyncReadExt, AsyncWriteExt};
21use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
22
23use crate::server::{ServerConfig, ServerEvent, ServerSession};
24
25/// Size of one socket read chunk. Reads are handed to
26/// [`ServerSession::handle_data`] as soon as they arrive, so this only bounds
27/// a single `read` syscall, never a message.
28const READ_CHUNK: usize = 8192;
29
30/// Maps an [`RtmpError`](crate::RtmpError) from the sans-IO session into an
31/// [`io::Error`] so callers only deal with one error type at this layer.
32fn io_err(e: crate::RtmpError) -> io::Error {
33    io::Error::new(io::ErrorKind::InvalidData, e)
34}
35
36/// A `tokio::net::TcpListener` that accepts inbound RTMP publishers and hands
37/// back a driven [`RtmpConnection`] per connection.
38#[derive(Debug)]
39pub struct AsyncRtmpServer {
40    listener: TcpListener,
41    config: ServerConfig,
42}
43
44impl AsyncRtmpServer {
45    /// Binds a listen address (e.g. `"0.0.0.0:1935"`, the IANA-assigned RTMP
46    /// port). `config` is cloned into a fresh [`ServerSession`] for each
47    /// accepted connection.
48    pub async fn bind<A: ToSocketAddrs>(addr: A, config: ServerConfig) -> io::Result<Self> {
49        let listener = TcpListener::bind(addr).await?;
50        Ok(Self { listener, config })
51    }
52
53    /// Accepts the next inbound connection and wraps it with a fresh
54    /// [`ServerSession`] built from this server's [`ServerConfig`].
55    pub async fn accept(&self) -> io::Result<RtmpConnection> {
56        let (stream, _peer) = self.listener.accept().await?;
57        Ok(RtmpConnection::new(
58            stream,
59            ServerSession::new(self.config.clone()),
60        ))
61    }
62
63    /// The address this server is actually bound to (useful for `":0"`
64    /// ephemeral-port binds).
65    pub fn local_addr(&self) -> io::Result<SocketAddr> {
66        self.listener.local_addr()
67    }
68}
69
70/// One accepted RTMP connection: a [`TcpStream`] driving a [`ServerSession`].
71///
72/// [`next_events`](Self::next_events) is the whole surface: read a chunk,
73/// drive the session, write the reply, return the events.
74#[derive(Debug)]
75pub struct RtmpConnection {
76    stream: TcpStream,
77    session: ServerSession,
78    /// Once set, the connection is done (clean EOF, `ServerEvent::Eof`, or a
79    /// prior `RtmpError`) and further calls to `next_events` return `None`
80    /// without touching the socket again.
81    closed: bool,
82}
83
84impl RtmpConnection {
85    fn new(stream: TcpStream, session: ServerSession) -> Self {
86        Self {
87            stream,
88            session,
89            closed: false,
90        }
91    }
92
93    /// The remote address of the connected publisher.
94    pub fn peer_addr(&self) -> io::Result<SocketAddr> {
95        self.stream.peer_addr()
96    }
97
98    /// Reads one chunk from the socket, drives [`ServerSession::handle_data`],
99    /// writes the reply bytes back (all of them), and returns the resulting
100    /// events.
101    ///
102    /// Returns `Ok(None)` when the connection is finished: the peer closed
103    /// the socket (clean EOF), or the most recent batch of events included
104    /// [`ServerEvent::Eof`] (so the caller sees that final batch once, then
105    /// `None` on the next call). Once a call returns `None`, every
106    /// subsequent call also returns `None` without reading the socket again.
107    ///
108    /// # Errors
109    /// An [`io::Error`] from the underlying socket read/write, or a mapped
110    /// [`RtmpError`](crate::RtmpError) (kind [`io::ErrorKind::InvalidData`])
111    /// from [`ServerSession::handle_data`]. On an `RtmpError` the session is
112    /// unrecoverable (per `handle_data`'s own doc): this connection is torn
113    /// down immediately — marked closed and not driven further, even if the
114    /// caller keeps calling `next_events`.
115    pub async fn next_events(&mut self) -> io::Result<Option<Vec<ServerEvent>>> {
116        if self.closed {
117            return Ok(None);
118        }
119
120        let mut chunk = [0u8; READ_CHUNK];
121        let n = self.stream.read(&mut chunk).await?;
122        if n == 0 {
123            self.closed = true;
124            return Ok(None);
125        }
126
127        let (reply, events) = match self.session.handle_data(&chunk[..n]) {
128            Ok(v) => v,
129            Err(e) => {
130                // Unrecoverable: tear down rather than keep driving a session
131                // whose internal state may have partially advanced.
132                self.closed = true;
133                return Err(io_err(e));
134            }
135        };
136
137        if !reply.is_empty() {
138            self.stream.write_all(&reply).await?;
139        }
140
141        if events.iter().any(|e| matches!(e, ServerEvent::Eof)) {
142            self.closed = true;
143        }
144
145        Ok(Some(events))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    const FIXTURE: &str = concat!(
154        env!("CARGO_MANIFEST_DIR"),
155        "/tests/fixtures/obs-publish.bin"
156    );
157
158    /// Replays the real captured `ffmpeg` publish (T8's `obs-publish.bin`)
159    /// over an actual loopback TCP socket, driving `AsyncRtmpServer`/
160    /// `RtmpConnection` end to end: a spawned client task writes the fixture
161    /// bytes and drains the server's replies, while the server side accepts
162    /// the connection and loops `next_events` until the connection closes.
163    ///
164    /// This is the online counterpart of `tests/ingest_fixture.rs`'s offline
165    /// replay — it proves the tokio adapter (not just the sans-IO session)
166    /// actually drives a real publish to `Connected` -> `Publish` -> `Media`.
167    #[tokio::test]
168    async fn loopback_replay_of_real_publish_reaches_connected_publish_media() {
169        let fixture = std::fs::read(FIXTURE).expect("read tests/fixtures/obs-publish.bin");
170
171        let server = AsyncRtmpServer::bind("127.0.0.1:0", ServerConfig::default())
172            .await
173            .expect("bind ephemeral loopback port");
174        let addr = server.local_addr().expect("local_addr");
175
176        // Client task: play back the captured publisher's raw bytes, and
177        // drain whatever the server writes back (so the server's writes
178        // never block on an unread socket buffer), counting the bytes
179        // received — this is what proves `next_events` actually wrote the
180        // session's reply bytes to the socket, not just decoded events.
181        let client = tokio::spawn(async move {
182            let mut stream = TcpStream::connect(addr).await.expect("connect loopback");
183            stream
184                .write_all(&fixture)
185                .await
186                .expect("write fixture bytes");
187            let mut sink = [0u8; READ_CHUNK];
188            let mut replied_bytes = 0usize;
189            loop {
190                match stream.read(&mut sink).await {
191                    Ok(0) | Err(_) => break,
192                    Ok(n) => replied_bytes += n,
193                }
194            }
195            replied_bytes
196        });
197
198        let mut conn = server.accept().await.expect("accept the client connection");
199        let mut events = Vec::new();
200        while let Some(batch) = conn
201            .next_events()
202            .await
203            .expect("next_events must not error")
204        {
205            events.extend(batch);
206        }
207        // Close the server-side socket so the client's drain loop observes
208        // EOF and the spawned task actually finishes.
209        drop(conn);
210        let replied_bytes = client.await.expect("client task must not panic");
211
212        // The handshake alone (S0 + S1 + S2, §5.2) is 1 + 1536 + 1536 = 3073
213        // bytes; `connect`/`createStream`/`publish` each add a reply on top.
214        // If `next_events` failed to write the reply bytes back, the client
215        // would observe a bare EOF and this would be 0.
216        const HANDSHAKE_REPLY_LEN: usize = 1 + 1536 + 1536;
217        assert!(
218            replied_bytes >= HANDSHAKE_REPLY_LEN,
219            "next_events must write the session's reply bytes back to the socket \
220             (expected at least the {HANDSHAKE_REPLY_LEN}-byte S0+S1+S2 handshake reply), \
221             got {replied_bytes} bytes"
222        );
223
224        assert!(
225            events
226                .iter()
227                .any(|e| matches!(e, ServerEvent::Connected { app } if app == "live")),
228            "must emit Connected{{app: \"live\"}} over the real socket; got {events:?}"
229        );
230        assert!(
231            events.iter().any(
232                |e| matches!(e, ServerEvent::Publish { stream_key, .. } if stream_key == "testkey")
233            ),
234            "must emit Publish{{stream_key: \"testkey\", ..}} over the real socket; got {events:?}"
235        );
236        let media_count = events
237            .iter()
238            .filter(|e| matches!(e, ServerEvent::Media { .. }))
239            .count();
240        assert!(
241            media_count >= 1,
242            "must emit at least one Media event over the real socket, got {media_count}"
243        );
244    }
245}