Skip to main content

librtmp2/
lib.rs

1//! librtmp2 — A Rust port of the librtmp2 RTMP/RTMPS protocol library.
2//!
3//! This is a complete 1:1 Rust port of the C library `librtmp2`.
4//! It provides server, client, handshake, chunk, message, AMF, FLV, and E-RTMP functionality.
5
6#![allow(dead_code)]
7#![allow(unused_variables)]
8#![allow(unused_imports)]
9#![allow(clippy::all)]
10
11pub mod alloc;
12pub mod amf;
13pub mod buffer;
14pub mod bytes;
15pub mod chunk;
16pub mod client;
17pub mod ertmp;
18pub mod flv;
19pub mod handshake;
20pub mod log;
21pub mod message;
22pub mod net;
23pub mod server;
24pub mod session;
25pub mod transport;
26pub mod types;
27
28// Re-exports for convenience
29pub use types::*;
30
31/// Library version string.
32pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION");
33
34/// Get the library version string.
35pub fn version_string() -> &'static str {
36    VERSION_STRING
37}
38
39/// Get the major version number.
40pub fn version_major() -> i32 {
41    i32::try_from(types::VERSION_MAJOR).unwrap_or(0)
42}
43
44/// Get the minor version number.
45pub fn version_minor() -> i32 {
46    i32::try_from(types::VERSION_MINOR).unwrap_or(0)
47}
48
49/// Get the patch version number.
50pub fn version_patch() -> i32 {
51    i32::try_from(types::VERSION_PATCH).unwrap_or(0)
52}
53
54/// Check if TLS support is available.
55pub fn tls_supported() -> bool {
56    transport::tls_available()
57}
58
59/// Get the error string for an error code.
60pub fn error_string(code: ErrorCode) -> &'static str {
61    code.as_str()
62}
63
64fn error_c_string(code: ErrorCode) -> &'static [u8] {
65    match code {
66        ErrorCode::Ok => b"OK\0",
67        ErrorCode::Io => b"I/O error\0",
68        ErrorCode::Timeout => b"Timeout\0",
69        ErrorCode::Protocol => b"Protocol error\0",
70        ErrorCode::Handshake => b"Handshake error\0",
71        ErrorCode::Chunk => b"Chunk error\0",
72        ErrorCode::Amf => b"AMF error\0",
73        ErrorCode::Unsupported => b"Unsupported\0",
74        ErrorCode::Auth => b"Authentication error\0",
75        ErrorCode::Internal => b"Internal error\0",
76    }
77}
78
79/// Map a raw FFI error code integer to `ErrorCode`, falling back to
80/// `Internal` for any value a caller passes that isn't one of ours.
81/// Constructing an `ErrorCode` directly from an arbitrary caller-supplied
82/// value would be undefined behavior, since not every `i32` is a valid
83/// discriminant for the enum — so the FFI boundary must validate the raw
84/// `i32` first.
85fn error_code_from_raw(code: i32) -> ErrorCode {
86    match code {
87        0 => ErrorCode::Ok,
88        -1 => ErrorCode::Io,
89        -2 => ErrorCode::Timeout,
90        -3 => ErrorCode::Protocol,
91        -4 => ErrorCode::Handshake,
92        -5 => ErrorCode::Chunk,
93        -6 => ErrorCode::Amf,
94        -7 => ErrorCode::Unsupported,
95        -8 => ErrorCode::Auth,
96        -9 => ErrorCode::Internal,
97        _ => ErrorCode::Internal,
98    }
99}
100
101#[cfg(test)]
102mod ffi_tests {
103    use super::*;
104    use crate::server::Server;
105    use std::ptr::NonNull;
106
107    #[test]
108    fn server_create_applies_default_max_connections_for_zero_config() {
109        let config = ServerConfig {
110            max_connections: 0,
111            chunk_size: 128,
112            tls_enabled: 0,
113            tls_cert_file: std::ptr::null(),
114            tls_key_file: std::ptr::null(),
115            tls_ca_file: std::ptr::null(),
116            tls_insecure: 0,
117        };
118        let server = NonNull::new(unsafe { lrtmp2_server_create(&config) })
119            .expect("lrtmp2_server_create returned null");
120        unsafe {
121            assert_eq!(
122                server.as_ref().config.max_connections,
123                DEFAULT_FFI_MAX_CONNECTIONS
124            );
125            lrtmp2_server_destroy(server.as_ptr());
126        }
127    }
128
129    #[test]
130    fn server_create_preserves_negative_max_connections_as_unlimited() {
131        let config = ServerConfig {
132            max_connections: -1,
133            chunk_size: 128,
134            tls_enabled: 0,
135            tls_cert_file: std::ptr::null(),
136            tls_key_file: std::ptr::null(),
137            tls_ca_file: std::ptr::null(),
138            tls_insecure: 0,
139        };
140        let server = NonNull::new(unsafe { lrtmp2_server_create(&config) })
141            .expect("lrtmp2_server_create returned null");
142        unsafe {
143            // Only the zero-initialized case gets the default; an explicit
144            // negative value must pass through unchanged since `Server`
145            // already treats `max_connections <= 0` as unlimited.
146            assert_eq!(server.as_ref().config.max_connections, -1);
147            lrtmp2_server_destroy(server.as_ptr());
148        }
149    }
150
151    #[test]
152    fn server_new_preserves_zero_max_connections_for_rust_api() {
153        let config = ServerConfig {
154            max_connections: 0,
155            chunk_size: 128,
156            tls_enabled: 0,
157            tls_cert_file: std::ptr::null(),
158            tls_key_file: std::ptr::null(),
159            tls_ca_file: std::ptr::null(),
160            tls_insecure: 0,
161        };
162        let server = Server::new(config).unwrap();
163        assert_eq!(server.config.max_connections, 0);
164    }
165}
166
167use std::ffi::c_int;
168
169/// Default connection cap applied at the FFI boundary when `max_connections`
170/// is exactly zero — the usual pattern from `calloc`/`{0}` initialization in
171/// C embedders, which would otherwise disable all connection limiting. A
172/// negative value is left as-is: `Server` already treats `max_connections <=
173/// 0` as unlimited, and callers may pass a negative value deliberately to
174/// request that, so only the zero-initialized case needs the default.
175const DEFAULT_FFI_MAX_CONNECTIONS: c_int = 256;
176
177/// Create a server (FFI-compatible).
178#[unsafe(no_mangle)]
179pub unsafe extern "C" fn lrtmp2_server_create(config: *const ServerConfig) -> *mut server::Server {
180    if config.is_null() {
181        return std::ptr::null_mut();
182    }
183    let mut cfg = unsafe { *config };
184    if cfg.max_connections == 0 {
185        cfg.max_connections = DEFAULT_FFI_MAX_CONNECTIONS;
186    }
187    match server::Server::new(cfg) {
188        Ok(s) => Box::into_raw(Box::new(s)),
189        Err(_) => std::ptr::null_mut(),
190    }
191}
192
193/// Destroy a server (FFI-compatible).
194#[unsafe(no_mangle)]
195pub unsafe extern "C" fn lrtmp2_server_destroy(server: *mut server::Server) {
196    if !server.is_null() {
197        drop(unsafe { Box::from_raw(server) });
198    }
199}
200
201/// Start listening (FFI-compatible).
202#[unsafe(no_mangle)]
203pub unsafe extern "C" fn lrtmp2_server_listen(
204    server: *mut server::Server,
205    bind_addr: *const u8,
206) -> i32 {
207    if server.is_null() || bind_addr.is_null() {
208        return ErrorCode::Internal as i32;
209    }
210    let s = unsafe { &mut *server };
211    let addr = unsafe { std::ffi::CStr::from_ptr(bind_addr as *const std::ffi::c_char) };
212    match s.listen(addr.to_str().unwrap_or("")) {
213        Ok(()) => 0,
214        Err(e) => e as i32,
215    }
216}
217
218/// Poll for events (FFI-compatible).
219#[unsafe(no_mangle)]
220pub unsafe extern "C" fn lrtmp2_server_poll(server: *mut server::Server, timeout_ms: i32) -> i32 {
221    if server.is_null() {
222        return ErrorCode::Internal as i32;
223    }
224    let s = unsafe { &mut *server };
225    match s.poll(timeout_ms) {
226        Ok(()) => 0,
227        Err(e) => e as i32,
228    }
229}
230
231/// Stop the server (FFI-compatible).
232#[unsafe(no_mangle)]
233pub unsafe extern "C" fn lrtmp2_server_stop(server: *mut server::Server) {
234    if !server.is_null() {
235        unsafe { (*server).stop() };
236    }
237}
238
239/// Create a client (FFI-compatible). `config` may be NULL to use defaults
240/// (verify `rtmps://` peers against the system trust store). When non-NULL,
241/// `config.tls_ca_file` (a CA bundle to trust instead of the system store)
242/// and `config.tls_insecure` (skip verification entirely, for testing only)
243/// control `rtmps://` verification for this client. Returns NULL if
244/// `tls_ca_file` is non-NULL but not valid UTF-8, rather than silently
245/// falling back to default verification.
246#[unsafe(no_mangle)]
247pub unsafe extern "C" fn lrtmp2_client_create(config: *const ServerConfig) -> *mut client::Client {
248    let mut c = client::Client::new();
249    if !config.is_null() {
250        let cfg = unsafe { &*config };
251        let ca_file = if cfg.tls_ca_file.is_null() {
252            None
253        } else {
254            match unsafe { std::ffi::CStr::from_ptr(cfg.tls_ca_file as *const std::ffi::c_char) }
255                .to_str()
256            {
257                Ok(s) => Some(s.to_string()),
258                Err(_) => return std::ptr::null_mut(),
259            }
260        };
261        c.set_tls_client_config(ca_file, cfg.tls_insecure != 0);
262    }
263    Box::into_raw(Box::new(c))
264}
265
266/// Destroy a client (FFI-compatible).
267#[unsafe(no_mangle)]
268pub unsafe extern "C" fn lrtmp2_client_destroy(c: *mut client::Client) {
269    if !c.is_null() {
270        drop(unsafe { Box::from_raw(c) });
271    }
272}
273
274/// Connect (FFI-compatible).
275#[unsafe(no_mangle)]
276pub unsafe extern "C" fn lrtmp2_client_connect(c: *mut client::Client, url: *const u8) -> i32 {
277    if c.is_null() || url.is_null() {
278        return ErrorCode::Internal as i32;
279    }
280    let client = unsafe { &mut *c };
281    let url_str = unsafe { std::ffi::CStr::from_ptr(url as *const std::ffi::c_char) };
282    match client.connect(url_str.to_str().unwrap_or("")) {
283        Ok(()) => 0,
284        Err(e) => e as i32,
285    }
286}
287
288/// Publish (FFI-compatible).
289#[unsafe(no_mangle)]
290pub unsafe extern "C" fn lrtmp2_client_publish(c: *mut client::Client) -> i32 {
291    if c.is_null() {
292        return ErrorCode::Internal as i32;
293    }
294    match unsafe { (*c).publish() } {
295        Ok(()) => 0,
296        Err(e) => e as i32,
297    }
298}
299
300/// Play (FFI-compatible).
301#[unsafe(no_mangle)]
302pub unsafe extern "C" fn lrtmp2_client_play(c: *mut client::Client) -> i32 {
303    if c.is_null() {
304        return ErrorCode::Internal as i32;
305    }
306    match unsafe { (*c).play() } {
307        Ok(()) => 0,
308        Err(e) => e as i32,
309    }
310}
311
312/// Send a frame (FFI-compatible).
313#[unsafe(no_mangle)]
314pub unsafe extern "C" fn lrtmp2_client_send_frame(
315    c: *mut client::Client,
316    frame: *const Frame,
317) -> i32 {
318    if c.is_null() || frame.is_null() {
319        return ErrorCode::Internal as i32;
320    }
321    let frame_ref = unsafe { &*frame };
322    if unsafe { (*c).state } != client::ClientState::Publishing {
323        return ErrorCode::Protocol as i32;
324    }
325    if frame_ref.size > 0 && frame_ref.data.is_null() {
326        return ErrorCode::Internal as i32;
327    }
328    if frame_ref.size as usize > client::MAX_CLIENT_FRAME_BYTES {
329        return ErrorCode::Protocol as i32;
330    }
331    let payload = if frame_ref.size == 0 || frame_ref.data.is_null() {
332        Vec::new()
333    } else {
334        unsafe {
335            std::slice::from_raw_parts(frame_ref.data, frame_ref.size as usize).to_vec()
336        }
337    };
338    match unsafe {
339        (*c).send_frame_payload(
340            frame_ref.frame_type,
341            frame_ref.timestamp,
342            &payload,
343        )
344    } {
345        Ok(()) => 0,
346        Err(e) => e as i32,
347    }
348}
349
350/// Poll client (FFI-compatible).
351#[unsafe(no_mangle)]
352pub unsafe extern "C" fn lrtmp2_client_poll(c: *mut client::Client, timeout_ms: i32) -> i32 {
353    if c.is_null() {
354        return ErrorCode::Internal as i32;
355    }
356    match unsafe { (*c).poll(timeout_ms) } {
357        Ok(()) => 0,
358        Err(e) => e as i32,
359    }
360}
361
362/// Get connection fd (FFI-compatible).
363#[unsafe(no_mangle)]
364pub unsafe extern "C" fn lrtmp2_conn_get_fd(_conn: *const session::conn::Conn) -> i32 {
365    -1
366}
367
368/// Check TLS support (FFI-compatible).
369#[unsafe(no_mangle)]
370pub extern "C" fn lrtmp2_tls_supported() -> i32 {
371    if tls_supported() {
372        1
373    } else {
374        0
375    }
376}
377
378/// Get version string (FFI-compatible).
379#[unsafe(no_mangle)]
380pub extern "C" fn lrtmp2_version_string() -> *const u8 {
381    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr()
382}
383
384/// Get major version (FFI-compatible).
385#[unsafe(no_mangle)]
386pub extern "C" fn lrtmp2_version_major() -> i32 {
387    version_major()
388}
389
390/// Get minor version (FFI-compatible).
391#[unsafe(no_mangle)]
392pub extern "C" fn lrtmp2_version_minor() -> i32 {
393    version_minor()
394}
395
396/// Get patch version (FFI-compatible).
397#[unsafe(no_mangle)]
398pub extern "C" fn lrtmp2_version_patch() -> i32 {
399    version_patch()
400}
401
402/// Get error string (FFI-compatible).
403///
404/// Takes a raw `i32` rather than `ErrorCode` so that an out-of-range value
405/// from a caller (e.g. a weakly-typed binding forwarding an unrelated int)
406/// cannot construct an invalid enum discriminant, which would be undefined
407/// behavior.
408#[unsafe(no_mangle)]
409pub unsafe extern "C" fn lrtmp2_error_string(code: i32) -> *const u8 {
410    error_c_string(error_code_from_raw(code)).as_ptr()
411}