use std::collections::HashMap;
use std::net::TcpListener;
use std::os::unix::io::{AsRawFd, IntoRawFd};
#[cfg(feature = "tls")]
use std::time::{Duration, Instant};
use crate::chunk::state::DEFAULT_CHUNK_SIZE;
use crate::net;
use crate::session::conn::Conn;
#[cfg(feature = "tls")]
use crate::transport::{PendingTlsAccept, TlsAcceptOutcome};
use crate::transport::{TlsCtx, Transport};
use crate::types::*;
const MAX_STREAM_CACHE_ENTRIES: usize = 1024;
const MAX_STREAM_CACHE_BYTES: usize = 64 * 1024 * 1024;
#[cfg(feature = "tls")]
const MAX_PENDING_TLS_HANDSHAKES: usize = 128;
#[cfg(feature = "tls")]
const TLS_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
const MAX_RECV_BYTES_PER_CONN_PER_POLL: usize = 256 * 1024;
struct StreamCache {
avc_header: Option<Vec<u8>>,
aac_header: Option<Vec<u8>>,
last_keyframe: Option<(u32, Vec<u8>)>,
}
struct ListenerEntry {
tcp: TcpListener,
tls_ctx: Option<TlsCtx>,
}
#[cfg(feature = "tls")]
struct PendingTlsConnection {
handshake: PendingTlsAccept,
remote_addr: String,
deadline: Instant,
}
pub struct Server {
pub config: ServerConfig,
pub resource_limits: ResourceLimits,
pub running: bool,
pub server_fd: i32,
pub connections: Vec<Conn>,
pub on_frame_cb: Option<fn(&Frame)>,
pub on_connect_cb: Option<fn()>,
pub on_publish_cb: Option<fn(conn_id: u64, app: &str, stream_name: &str) -> bool>,
pub on_play_cb: Option<fn(conn_id: u64, app: &str, stream_name: &str) -> bool>,
pub on_media_cb: Option<fn(u64, FrameType, Option<&str>) -> bool>,
pub tls_ctx: Option<TlsCtx>,
listeners: Vec<ListenerEntry>,
next_listener_accept: usize,
#[cfg(feature = "tls")]
pending_tls: Vec<PendingTlsConnection>,
stream_cache: HashMap<(String, String), StreamCache>,
publisher_cache_keys: HashMap<u64, Vec<(String, String)>>,
next_conn_id: u64,
conn_ids_issued: bool,
pub defer_media_relay: bool,
}
impl Server {
pub fn new(config: ServerConfig) -> Result<Self> {
let tls_ctx = if config.tls_enabled != 0 {
if config.tls_cert_file.is_null() || config.tls_key_file.is_null() {
return Err(ErrorCode::Internal);
}
let cert = unsafe {
std::ffi::CStr::from_ptr(config.tls_cert_file as *const std::ffi::c_char)
};
let key =
unsafe { std::ffi::CStr::from_ptr(config.tls_key_file as *const std::ffi::c_char) };
let cert_str = cert.to_str().map_err(|_| ErrorCode::Internal)?;
let key_str = key.to_str().map_err(|_| ErrorCode::Internal)?;
if cert_str.is_empty() || key_str.is_empty() {
return Err(ErrorCode::Internal);
}
Some(TlsCtx::new_server(cert_str, key_str)?)
} else {
None
};
Ok(Self {
config,
resource_limits: ResourceLimits::default(),
running: false,
server_fd: -1,
connections: Vec::new(),
on_frame_cb: None,
on_connect_cb: None,
on_publish_cb: None,
on_play_cb: None,
on_media_cb: None,
tls_ctx,
listeners: Vec::new(),
next_listener_accept: 0,
#[cfg(feature = "tls")]
pending_tls: Vec::new(),
stream_cache: HashMap::new(),
publisher_cache_keys: HashMap::new(),
next_conn_id: 1,
conn_ids_issued: false,
defer_media_relay: false,
})
}
pub fn set_conn_id_base(&mut self, base: u64) {
assert!(base != 0, "conn_id base must be non-zero");
assert!(
base < u64::MAX,
"conn_id base must leave room for at least one later connection ID"
);
assert!(
!self.conn_ids_issued && self.connections.is_empty(),
"set_conn_id_base must be called before accepting any connections"
);
#[cfg(feature = "tls")]
assert!(
self.pending_tls.is_empty(),
"set_conn_id_base must be called before accepting any connections"
);
self.next_conn_id = base;
}
fn resolve_bind_addr(bind_addr: &str) -> Result<String> {
let mut host = String::new();
let mut port = String::new();
net::split_host_port(bind_addr, &mut host, &mut port, "1935")?;
Ok(if host.is_empty() {
format!("0.0.0.0:{port}")
} else if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
})
}
fn bind_listener(&mut self, addr: &str) -> Result<TcpListener> {
let listener = TcpListener::bind(addr).map_err(|_| ErrorCode::Io)?;
listener.set_nonblocking(true).map_err(|_| ErrorCode::Io)?;
if self.server_fd < 0 {
self.server_fd = listener.as_raw_fd();
}
self.running = true;
Ok(listener)
}
pub fn listener_fds(&self) -> Vec<i32> {
self.listeners
.iter()
.map(|listener| listener.tcp.as_raw_fd())
.collect()
}
pub fn listen(&mut self, bind_addr: &str) -> Result<()> {
let addr = Self::resolve_bind_addr(bind_addr)?;
let tcp = self.bind_listener(&addr)?;
self.listeners.push(ListenerEntry {
tcp,
tls_ctx: self.tls_ctx.clone(),
});
Ok(())
}
pub fn listen_tls(&mut self, bind_addr: &str, cert_file: &str, key_file: &str) -> Result<()> {
let ctx = TlsCtx::new_server(cert_file, key_file)?;
let addr = Self::resolve_bind_addr(bind_addr)?;
let tcp = self.bind_listener(&addr)?;
self.listeners.push(ListenerEntry {
tcp,
tls_ctx: Some(ctx),
});
Ok(())
}
pub fn poll(&mut self, timeout_ms: i32) -> Result<()> {
if !self.running {
return Err(ErrorCode::Internal);
}
self.accept_new_connections();
self.process_connections()?;
if timeout_ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(timeout_ms as u64));
}
Ok(())
}
pub fn stop(&mut self) {
self.running = false;
self.listeners.clear();
self.next_listener_accept = 0;
#[cfg(feature = "tls")]
self.pending_tls.clear();
self.server_fd = -1;
}
fn max_connections_reached(&self) -> bool {
self.config.max_connections > 0
&& self.connections.len() >= self.config.max_connections as usize
}
#[cfg(feature = "tls")]
fn tls_handshake_deadline() -> Instant {
Instant::now() + Duration::from_secs(TLS_HANDSHAKE_TIMEOUT_SECS)
}
#[cfg(feature = "tls")]
fn pending_tls_limit_reached(&self) -> bool {
let pending = self.pending_tls.len();
if self.config.max_connections > 0 {
self.connections.len() + pending >= self.config.max_connections as usize
} else {
pending >= MAX_PENDING_TLS_HANDSHAKES
}
}
fn allocate_conn_id(&mut self) -> Option<u64> {
let conn_id = self.next_conn_id;
if conn_id == 0 || conn_id == u64::MAX {
return None;
}
self.next_conn_id = conn_id + 1;
self.conn_ids_issued = true;
Some(conn_id)
}
fn add_connection(&mut self, transport: Transport, remote_addr: String) -> bool {
if self.max_connections_reached() {
return false;
}
let Some(conn_id) = self.allocate_conn_id() else {
return false;
};
let conn_fd = transport.fd();
let mut conn = Conn::new();
conn.chunk_reg.max_reassembly_bytes = self.resource_limits.max_reassembly_bytes;
conn.max_pending_relay_bytes = self.resource_limits.max_pending_relay_bytes;
conn.chunk_size = if self.config.chunk_size > 0 {
self.config.chunk_size as u32
} else {
DEFAULT_CHUNK_SIZE
};
conn.client_fd = conn_fd;
conn.conn_id = conn_id;
conn.remote_addr = remote_addr;
conn.defer_media_relay = self.defer_media_relay;
conn.transport = Some(transport);
conn.on_frame_cb = self.on_frame_cb;
conn.on_media_cb = self.on_media_cb;
conn.on_connect_cb = self.on_connect_cb;
conn.on_publish_cb = self.on_publish_cb;
conn.on_play_cb = self.on_play_cb;
self.connections.push(conn);
true
}
#[cfg(feature = "tls")]
fn progress_pending_tls(&mut self) {
let pending = std::mem::take(&mut self.pending_tls);
let now = Instant::now();
for pending_conn in pending {
if now >= pending_conn.deadline {
continue;
}
if self.max_connections_reached() {
self.pending_tls.push(pending_conn);
continue;
}
match pending_conn.handshake.progress() {
Ok(TlsAcceptOutcome::Complete(transport)) => {
self.add_connection(transport, pending_conn.remote_addr);
}
Ok(TlsAcceptOutcome::WouldBlock(handshake)) => {
if Instant::now() < pending_conn.deadline {
self.pending_tls.push(PendingTlsConnection {
handshake,
remote_addr: pending_conn.remote_addr,
deadline: pending_conn.deadline,
});
}
}
Err(_) => {}
}
}
}
#[cfg(not(feature = "tls"))]
fn progress_pending_tls(&mut self) {}
fn accept_new_connections(&mut self) {
self.progress_pending_tls();
let listener_count = self.listeners.len();
if listener_count == 0 {
self.next_listener_accept = 0;
return;
}
self.next_listener_accept %= listener_count;
loop {
let mut accepted_any = false;
for offset in 0..listener_count {
if self.max_connections_reached() {
return;
}
let i = (self.next_listener_accept + offset) % listener_count;
match self.listeners[i].tcp.accept() {
Ok((stream, addr)) => {
accepted_any = true;
self.next_listener_accept = (i + 1) % listener_count;
let remote_addr = addr.to_string();
let tls_ctx = self.listeners[i].tls_ctx.clone();
if let Some(ctx) = tls_ctx.as_ref() {
#[cfg(feature = "tls")]
{
match ctx.accept_nonblocking(stream.into_raw_fd()) {
Ok(TlsAcceptOutcome::Complete(transport)) => {
self.add_connection(transport, remote_addr);
}
Ok(TlsAcceptOutcome::WouldBlock(handshake)) => {
if !self.pending_tls_limit_reached() {
self.pending_tls.push(PendingTlsConnection {
handshake,
remote_addr,
deadline: Self::tls_handshake_deadline(),
});
}
}
Err(_) => {}
}
}
#[cfg(not(feature = "tls"))]
{
let _ = ctx;
drop(stream);
}
} else {
let _ = stream.set_nonblocking(true);
let transport = Transport::new_plain(stream.into_raw_fd());
self.add_connection(transport, remote_addr);
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(_) => {}
}
}
if !accepted_any {
break;
}
}
}
pub fn process_connections(&mut self) -> Result<()> {
let mut buf = [0u8; 65536];
let mut closed = Vec::new();
self.drain_pending_cache_evictions();
for (i, conn) in self.connections.iter_mut().enumerate() {
let mut bytes_drained = 0usize;
loop {
if bytes_drained >= MAX_RECV_BYTES_PER_CONN_PER_POLL {
break;
}
let Some(transport) = conn.transport.as_mut() else {
closed.push(i);
break;
};
let mut again = 0i32;
let n = transport.recv(&mut buf, &mut again);
if n > 0 {
let chunk_len = n as usize;
if conn.recv(&buf[..chunk_len]).is_err() {
closed.push(i);
break;
}
bytes_drained += chunk_len;
} else if n == 0 {
closed.push(i);
break;
} else if again != 0 {
break;
} else {
closed.push(i);
break;
}
}
}
let abandoned_this_batch = self.drain_pending_cache_evictions();
let relay_frames: Vec<_> = self
.connections
.iter_mut()
.flat_map(|c| c.pending_relay.drain(..))
.collect();
for (i, conn) in self.connections.iter_mut().enumerate() {
if conn.transport.is_none() || !conn.needs_init_frames {
continue;
}
let Some(ref stream) = conn.current_stream else {
continue;
};
if !stream.is_playing || !conn.relay_enabled {
continue;
}
conn.needs_init_frames = false;
let key = (conn.app.clone(), conn.relay_route_key());
if let Some(cache) = self.stream_cache.get(&key) {
let mut send_failed = false;
if let Some(ref hdr) = cache.avc_header.clone() {
send_failed |= conn.send_frame(FrameType::Video, 0, hdr).is_err();
}
if !send_failed {
if let Some(ref hdr) = cache.aac_header.clone() {
send_failed |= conn.send_frame(FrameType::Audio, 0, hdr).is_err();
}
}
if !send_failed {
if let Some((ts, ref kf)) = cache.last_keyframe.clone() {
send_failed |= conn.send_frame(FrameType::Video, ts, kf).is_err();
}
}
if send_failed {
conn.relay_enabled = false;
conn.needs_init_frames = false;
conn.transport = None;
closed.push(i);
}
}
}
for frame in &relay_frames {
let abandon_key = (
frame.app.clone(),
frame.stream_name.clone(),
frame.publisher_conn_id,
);
if !abandoned_this_batch.contains(&abandon_key) {
self.cache_relay_frame(frame);
}
for (i, conn) in self.connections.iter_mut().enumerate() {
let is_player = conn.relay_enabled
&& conn.transport.is_some()
&& conn
.current_stream
.as_ref()
.map(|s| s.is_playing && conn.relay_route_key() == frame.stream_name)
.unwrap_or(false);
if !is_player || conn.app != frame.app {
continue;
}
if conn.send_frame(frame.frame_type, frame.timestamp, &frame.payload).is_err() {
conn.relay_enabled = false;
conn.needs_init_frames = false;
conn.transport = None;
closed.push(i);
}
}
}
for (i, conn) in self.connections.iter_mut().enumerate() {
if conn.transport.is_none() {
closed.push(i);
continue;
}
if conn.maybe_send_ping().is_err() {
closed.push(i);
continue;
}
if conn.flush().is_err() {
closed.push(i);
}
}
closed.sort_unstable();
closed.dedup();
for i in closed.into_iter().rev() {
let conn = &self.connections[i];
if let Some(keys) = self.publisher_cache_keys.remove(&conn.conn_id) {
for key in keys {
let still_owned = self.publisher_cache_keys.values().any(|v| v.contains(&key));
if !still_owned {
self.stream_cache.remove(&key);
}
}
}
self.connections.remove(i);
}
Ok(())
}
fn stream_cache_entry_bytes(cache: &StreamCache) -> usize {
cache.avc_header.as_ref().map(|v| v.len()).unwrap_or(0)
+ cache.aac_header.as_ref().map(|v| v.len()).unwrap_or(0)
+ cache
.last_keyframe
.as_ref()
.map(|(_, v)| v.len())
.unwrap_or(0)
}
fn stream_cache_bytes(&self) -> usize {
self.stream_cache
.values()
.map(Self::stream_cache_entry_bytes)
.sum()
}
fn evict_stream_cache_key(&mut self, key: &(String, String)) {
self.stream_cache.remove(key);
for keys in self.publisher_cache_keys.values_mut() {
keys.retain(|k| k != key);
}
}
fn cache_relay_frame(&mut self, frame: &crate::session::conn::RelayFrame) {
let is_avc_header = frame.frame_type == FrameType::Video
&& frame.payload.len() >= 2
&& frame.payload[0] == 0x17
&& frame.payload[1] == 0x00;
let is_keyframe = frame.frame_type == FrameType::Video
&& frame.payload.len() >= 2
&& frame.payload[0] == 0x17
&& frame.payload[1] == 0x01;
let is_aac_header = frame.frame_type == FrameType::Audio
&& frame.payload.len() >= 2
&& (frame.payload[0] & 0xF0) == 0xA0
&& frame.payload[1] == 0x00;
if !is_avc_header && !is_keyframe && !is_aac_header {
return;
}
let key = (frame.app.clone(), frame.stream_name.clone());
let publisher_keys = self
.publisher_cache_keys
.entry(frame.publisher_conn_id)
.or_default();
if !publisher_keys.iter().any(|k| k == &key) {
publisher_keys.push(key.clone());
}
if self.stream_cache.len() >= MAX_STREAM_CACHE_ENTRIES
&& !self.stream_cache.contains_key(&key)
{
if let Some(evict) = self.stream_cache.keys().find(|k| *k != &key).cloned() {
self.evict_stream_cache_key(&evict);
}
}
let existing_field_len = self
.stream_cache
.get(&key)
.map(|cache| {
if is_avc_header {
cache.avc_header.as_ref().map(|v| v.len()).unwrap_or(0)
} else if is_keyframe {
cache
.last_keyframe
.as_ref()
.map(|(_, v)| v.len())
.unwrap_or(0)
} else {
cache.aac_header.as_ref().map(|v| v.len()).unwrap_or(0)
}
})
.unwrap_or(0);
let incoming_len = frame.payload.len();
let mut projected_total = self.stream_cache_bytes() + incoming_len - existing_field_len;
let max_cache_bytes = self.resource_limits.max_stream_cache_bytes;
if projected_total > max_cache_bytes {
let victims: Vec<_> = self
.stream_cache
.keys()
.filter(|k| *k != &key)
.cloned()
.collect();
for victim in victims {
if projected_total <= max_cache_bytes {
break;
}
if let Some(cache) = self.stream_cache.get(&victim) {
projected_total -= Self::stream_cache_entry_bytes(cache);
}
self.evict_stream_cache_key(&victim);
}
}
if projected_total > max_cache_bytes {
return;
}
let cache = self.stream_cache.entry(key).or_insert(StreamCache {
avc_header: None,
aac_header: None,
last_keyframe: None,
});
if is_avc_header {
cache.avc_header = Some(frame.payload.clone());
} else if is_keyframe {
cache.last_keyframe = Some((frame.timestamp, frame.payload.clone()));
} else if is_aac_header {
cache.aac_header = Some(frame.payload.clone());
}
}
fn drain_pending_cache_evictions(&mut self) -> std::collections::HashSet<(String, String, u64)> {
let mut abandoned = std::collections::HashSet::new();
for conn in &mut self.connections {
for key in conn.pending_cache_evictions.drain(..) {
abandoned.insert((key.0.clone(), key.1.clone(), conn.conn_id));
let owns_key = self
.publisher_cache_keys
.get(&conn.conn_id)
.map(|keys| keys.contains(&key))
.unwrap_or(false);
if !owns_key {
continue;
}
if let Some(keys) = self.publisher_cache_keys.get_mut(&conn.conn_id) {
keys.retain(|k| k != &key);
}
let still_owned = self
.publisher_cache_keys
.values()
.any(|keys| keys.contains(&key));
if !still_owned {
self.stream_cache.remove(&key);
}
}
}
abandoned
}
}
impl Drop for Server {
fn drop(&mut self) {
self.running = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recv_budget_is_at_least_one_socket_read() {
assert!(MAX_RECV_BYTES_PER_CONN_PER_POLL >= 65536);
}
#[test]
fn recv_budget_is_small_enough_for_fairness_across_connections() {
assert!(MAX_RECV_BYTES_PER_CONN_PER_POLL <= 1024 * 1024);
}
}