use crate::rtmp::gop::{FrameData, Gops};
use crate::rtmp::write_queue::QUEUE_WARN_BYTES;
use bytes::Bytes;
use log::{debug, warn};
use rml_rtmp::chunk_io::Packet;
use rml_rtmp::sessions::StreamMetadata;
use rml_rtmp::sessions::{
ServerSession, ServerSessionConfig, ServerSessionError, ServerSessionEvent, ServerSessionResult,
};
use rml_rtmp::time::RtmpTimestamp;
use slab::Slab;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use thiserror::Error;
#[derive(Error, Debug)]
pub(super) enum SchedulerError {
#[error("RTMP session error: {0}")]
Session(#[from] ServerSessionError),
#[error("oversized sequence header: {size} bytes exceeds the {limit}-byte cacheable limit")]
OversizedSequenceHeader { size: usize, limit: usize },
}
const JOIN_REPLAY_HEADROOM: usize = 64 * 1024;
const JOIN_REPLAY_BUDGET_BYTES: usize = QUEUE_WARN_BYTES - JOIN_REPLAY_HEADROOM;
const OUTBOUND_CHUNK_SIZE: usize = 4096;
const MSG_HEADER_MAX: usize = 18;
const CONT_HEADER_MAX: usize = 3;
fn gop_wire_size(payload: usize, frame_count: usize) -> usize {
let per_frame = frame_count.saturating_mul(MSG_HEADER_MAX);
let continuation = payload
.div_ceil(OUTBOUND_CHUNK_SIZE)
.saturating_mul(CONT_HEADER_MAX);
payload
.saturating_add(per_frame)
.saturating_add(continuation)
}
const MAX_CACHEABLE_SEQUENCE_HEADER_BYTES: usize = 256 * 1024;
enum ClientAction {
Waiting,
Publishing(String), Watching { stream_key: String, stream_id: u32 },
}
#[derive(Clone, Copy)]
enum ReceivedDataType {
Audio,
Video,
}
struct Client {
session: ServerSession,
current_action: ClientAction,
connection_id: usize,
has_received_video_keyframe: bool,
}
impl Client {
fn get_active_stream_id(&self) -> Option<u32> {
match self.current_action {
ClientAction::Waiting => None,
ClientAction::Publishing(_) => None,
ClientAction::Watching {
stream_key: _,
stream_id,
} => Some(stream_id),
}
}
}
struct MediaChannel {
publishing_client_id: Option<usize>,
watching_client_ids: HashSet<usize>,
metadata: Option<Rc<StreamMetadata>>,
video_sequence_header: Option<Bytes>,
video_timestamp: RtmpTimestamp,
audio_sequence_header: Option<Bytes>,
audio_timestamp: RtmpTimestamp,
gops: Gops,
}
impl MediaChannel {
fn new(gop_limit: usize) -> MediaChannel {
Self {
publishing_client_id: None,
watching_client_ids: Default::default(),
metadata: None,
video_sequence_header: None,
video_timestamp: RtmpTimestamp { value: 0 },
audio_sequence_header: None,
audio_timestamp: RtmpTimestamp { value: 0 },
gops: Gops::new(gop_limit),
}
}
fn should_remove(&self) -> bool {
self.publishing_client_id.is_none() && self.watching_client_ids.is_empty()
}
}
#[derive(Debug)]
pub(super) enum ServerResult {
DisconnectConnection {
connection_id: usize,
},
OutboundPacket {
target_connection_id: usize,
packet: Packet,
is_keyframe: bool,
is_sequence_header: bool,
is_video: bool,
},
}
pub(super) struct RtmpScheduler {
clients: Slab<Client>,
connection_to_client_map: HashMap<usize, usize>,
publisher_to_client_map: HashMap<usize, usize>,
channels: HashMap<String, MediaChannel>,
gop_limit: usize,
serving_connection_backlog_bytes: usize,
serving_prefix_scan_pos: usize,
serving_prefix_bytes: usize,
}
impl RtmpScheduler {
pub(crate) fn new_channel(
&mut self,
stream_key: String,
publisher_connection_id: usize,
) -> bool {
match self.channels.get(&stream_key) {
None => (),
Some(channel) => match channel.publishing_client_id {
None => (),
Some(_) => {
warn!("Stream key '{}' already being published to", stream_key);
return false;
}
},
}
let config = ServerSessionConfig::new();
let (session, _initial_session_results) = match ServerSession::new(config) {
Ok(results) => results,
Err(e) => {
warn!("Rtmp error creating new server session: {}", e);
return false;
}
};
let client = Client {
session,
connection_id: publisher_connection_id,
current_action: ClientAction::Publishing(stream_key.clone()),
has_received_video_keyframe: false,
};
let client_id = Some(self.clients.insert(client));
self.publisher_to_client_map
.insert(publisher_connection_id, client_id.unwrap());
let channel = self
.channels
.entry(stream_key)
.or_insert_with(|| MediaChannel::new(self.gop_limit));
channel.publishing_client_id = client_id;
true
}
}
impl RtmpScheduler {
pub(super) fn new(gop_limit: usize) -> RtmpScheduler {
RtmpScheduler {
clients: Slab::with_capacity(1024),
connection_to_client_map: HashMap::with_capacity(1024),
publisher_to_client_map: HashMap::with_capacity(32),
channels: HashMap::new(),
gop_limit,
serving_connection_backlog_bytes: 0,
serving_prefix_scan_pos: 0,
serving_prefix_bytes: 0,
}
}
pub fn publish_bytes_received(
&mut self,
publisher_connection_id: usize,
bytes: Vec<u8>,
) -> Result<Vec<ServerResult>, SchedulerError> {
let mut server_results = Vec::new();
if !self
.publisher_to_client_map
.contains_key(&publisher_connection_id)
{
warn!(
"Publishing event for non-existent connection_id: {}",
publisher_connection_id
);
return Ok(server_results);
}
let publisher_results = {
let client_id = self
.publisher_to_client_map
.get(&publisher_connection_id)
.unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
let publisher_results: Vec<ServerSessionResult> =
match client.session.handle_input(&bytes) {
Ok(results) => results,
Err(error) => return Err(error.into()),
};
publisher_results
};
for result in &publisher_results {
if let ServerSessionResult::RaisedEvent(event) = result {
if let Some(err) = oversized_sequence_header_error(event) {
return Err(err);
}
}
}
for result in publisher_results {
match result {
ServerSessionResult::OutboundResponse(_packet) => {
}
ServerSessionResult::RaisedEvent(event) => match event {
ServerSessionEvent::ClientChunkSizeChanged { .. }
| ServerSessionEvent::StreamMetadataChanged { .. }
| ServerSessionEvent::AudioDataReceived { .. }
| ServerSessionEvent::VideoDataReceived { .. }
| ServerSessionEvent::AcknowledgementReceived { .. }
| ServerSessionEvent::PingResponseReceived { .. }
| ServerSessionEvent::PublishStreamFinished { .. } => {
self.handle_raised_event(usize::MAX, event, &mut server_results)?;
}
ServerSessionEvent::ConnectionRequested {
request_id,
app_name: _,
} => {
let client_id = self
.publisher_to_client_map
.get(&publisher_connection_id)
.unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
if let Err(e) = client.session.accept_request(request_id) {
warn!(
"Failed to accept connection request {}: {:?}",
request_id, e
);
}
}
ServerSessionEvent::PublishStreamRequested {
request_id,
app_name: _,
stream_key,
mode: _,
} => {
let client_id = self
.publisher_to_client_map
.get(&publisher_connection_id)
.unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
if let Err(e) = client.session.accept_request(request_id) {
warn!(
"Failed to accept publish request {} for stream '{}': {:?}",
request_id, stream_key, e
);
}
}
_ => {
debug!("Publisher received unexpected event: {:?}", event);
}
},
x => warn!("Server result received: {:?}", x),
}
}
Ok(server_results)
}
pub(super) fn publish_media_received(
&mut self,
publisher_connection_id: usize,
tag_type: u8,
timestamp: RtmpTimestamp,
data: Bytes,
) -> Vec<ServerResult> {
let mut server_results = Vec::new();
let data_type = match tag_type {
0x08 => ReceivedDataType::Audio,
0x09 => ReceivedDataType::Video,
other => {
warn!("In-process media bypass received unexpected FLV tag type {other:#04x}");
return server_results;
}
};
let client_id = match self.publisher_to_client_map.get(&publisher_connection_id) {
Some(client_id) => *client_id,
None => {
warn!(
"In-process media for non-existent publisher connection_id: {}",
publisher_connection_id
);
return server_results;
}
};
let stream_key = match self.clients.get(client_id) {
Some(client) => match &client.current_action {
ClientAction::Publishing(stream_key) => stream_key.clone(),
_ => {
warn!(
"In-process media for a publisher not in the Publishing state: {}",
publisher_connection_id
);
return server_results;
}
},
None => return server_results,
};
self.handle_audio_video_data_received(
stream_key,
timestamp,
data,
data_type,
&mut server_results,
);
server_results
}
#[cfg_attr(not(test), allow(dead_code))]
pub(super) fn bytes_received(
&mut self,
connection_id: usize,
bytes: &[u8],
) -> Result<Vec<ServerResult>, SchedulerError> {
self.bytes_received_with_backlog(connection_id, bytes, 0)
}
pub(super) fn bytes_received_with_backlog(
&mut self,
connection_id: usize,
bytes: &[u8],
connection_backlog_bytes: usize,
) -> Result<Vec<ServerResult>, SchedulerError> {
self.serving_connection_backlog_bytes = connection_backlog_bytes;
self.serving_prefix_scan_pos = 0;
self.serving_prefix_bytes = 0;
let mut server_results = Vec::new();
if !self.connection_to_client_map.contains_key(&connection_id) {
let config = ServerSessionConfig::new();
let (session, initial_session_results) = match ServerSession::new(config) {
Ok(results) => results,
Err(error) => return Err(error.into()),
};
self.handle_session_results(
connection_id,
initial_session_results,
&mut server_results,
);
let client = Client {
session,
connection_id,
current_action: ClientAction::Waiting,
has_received_video_keyframe: false,
};
let client_id = Some(self.clients.insert(client));
self.connection_to_client_map
.insert(connection_id, client_id.unwrap());
}
let client_results: Vec<ServerSessionResult>;
{
let client_id = self.connection_to_client_map.get(&connection_id).unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
client_results = match client.session.handle_input(bytes) {
Ok(results) => results,
Err(error) => return Err(error.into()),
};
}
self.handle_session_results(connection_id, client_results, &mut server_results);
Ok(server_results)
}
pub(super) fn notify_connection_closed(&mut self, connection_id: usize) {
match self.connection_to_client_map.remove(&connection_id) {
None => (),
Some(client_id) => {
let client = self.clients.remove(client_id);
match client.current_action {
ClientAction::Watching {
stream_key,
stream_id: _,
} => self.play_ended(client_id, stream_key),
ClientAction::Waiting => (),
_ => {}
}
}
}
}
pub(super) fn notify_publisher_closed(&mut self, publisher_connection_id: usize) {
match self
.publisher_to_client_map
.remove(&publisher_connection_id)
{
None => (),
Some(client_id) => {
let client = self.clients.remove(client_id);
match client.current_action {
ClientAction::Publishing(stream_key) => self.publishing_ended(stream_key),
_ => {}
}
}
}
}
pub(super) fn abort_publisher_watchers(
&mut self,
publisher_connection_id: usize,
) -> Vec<ServerResult> {
let mut server_results = Vec::new();
let stream_key = match self
.publisher_to_client_map
.get(&publisher_connection_id)
.and_then(|client_id| self.clients.get(*client_id))
{
Some(client) => match &client.current_action {
ClientAction::Publishing(stream_key) => stream_key.clone(),
_ => return server_results,
},
None => return server_results,
};
self.handle_publish_finished(String::new(), stream_key, &mut server_results);
server_results
}
fn handle_session_results(
&mut self,
executed_connection_id: usize,
session_results: Vec<ServerSessionResult>,
server_results: &mut Vec<ServerResult>,
) {
for result in session_results {
match result {
ServerSessionResult::OutboundResponse(packet) => {
server_results.push(ServerResult::OutboundPacket {
target_connection_id: executed_connection_id,
packet,
is_keyframe: false,
is_sequence_header: false,
is_video: false,
})
}
ServerSessionResult::RaisedEvent(event) => {
if let Err(e) =
self.handle_raised_event(executed_connection_id, event, server_results)
{
debug!(
"Rtmp connection {} aborted during event handling: {}",
executed_connection_id, e
);
server_results.push(ServerResult::DisconnectConnection {
connection_id: executed_connection_id,
});
}
}
x => debug!("Server result received: {:?}", x),
}
}
}
fn handle_raised_event(
&mut self,
executed_connection_id: usize,
event: ServerSessionEvent,
server_results: &mut Vec<ServerResult>,
) -> Result<(), SchedulerError> {
if let Some(err) = oversized_sequence_header_error(&event) {
return Err(err);
}
match event {
ServerSessionEvent::ConnectionRequested {
request_id,
app_name,
} => {
self.handle_connection_requested(
executed_connection_id,
request_id,
app_name,
server_results,
);
}
ServerSessionEvent::PublishStreamRequested {
request_id,
app_name,
stream_key,
mode: _,
} => {
self.handle_publish_requested(
executed_connection_id,
request_id,
app_name,
stream_key,
server_results,
);
}
ServerSessionEvent::PublishStreamFinished {
app_name,
stream_key,
} => {
self.handle_publish_finished(app_name, stream_key, server_results);
}
ServerSessionEvent::PlayStreamRequested {
request_id,
app_name,
stream_key,
start_at: _,
duration: _,
reset: _,
stream_id,
} => {
self.handle_play_requested(
executed_connection_id,
request_id,
app_name,
stream_key,
stream_id,
server_results,
);
}
ServerSessionEvent::PlayStreamFinished {
app_name,
stream_key,
} => {
self.handle_play_finished(executed_connection_id, app_name, stream_key);
}
ServerSessionEvent::StreamMetadataChanged {
app_name,
stream_key,
metadata,
} => {
self.handle_metadata_received(app_name, stream_key, metadata, server_results);
}
ServerSessionEvent::VideoDataReceived {
app_name: _,
stream_key,
data,
timestamp,
} => {
self.handle_audio_video_data_received(
stream_key,
timestamp,
data,
ReceivedDataType::Video,
server_results,
);
}
ServerSessionEvent::AudioDataReceived {
app_name: _,
stream_key,
data,
timestamp,
} => {
self.handle_audio_video_data_received(
stream_key,
timestamp,
data,
ReceivedDataType::Audio,
server_results,
);
}
_ => debug!(
"Rtmp event raised by connection {executed_connection_id}: {:?}",
event
),
}
Ok(())
}
fn handle_connection_requested(
&mut self,
requested_connection_id: usize,
request_id: u32,
app_name: String,
server_results: &mut Vec<ServerResult>,
) {
debug!(
"Rtmp connection {requested_connection_id} requested connection to app '{app_name}'"
);
let accept_result;
{
let client_id = self
.connection_to_client_map
.get(&requested_connection_id)
.unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
accept_result = client.session.accept_request(request_id);
}
match accept_result {
Err(error) => {
debug!(
"Rtmp client error occurred accepting connection request: {:?}",
error
);
server_results.push(ServerResult::DisconnectConnection {
connection_id: requested_connection_id,
})
}
Ok(results) => {
self.handle_session_results(requested_connection_id, results, server_results);
}
}
}
fn handle_publish_requested(
&mut self,
requested_connection_id: usize,
_request_id: u32,
_app_name: String,
_stream_key: String,
server_results: &mut Vec<ServerResult>,
) {
warn!("Rtmp publish requested, but socket-based push is not supported.");
server_results.push(ServerResult::DisconnectConnection {
connection_id: requested_connection_id,
});
}
fn handle_publish_finished(
&mut self,
app_name: String,
stream_key: String,
server_results: &mut Vec<ServerResult>,
) {
debug!("Rtmp publish finished on app '{app_name}' and stream key '{stream_key}'");
let channel = match self.channels.get(&stream_key) {
Some(channel) => channel,
None => return,
};
for client_id in &channel.watching_client_ids {
let client = match self.clients.get_mut(*client_id) {
Some(client) => client,
None => continue,
};
let active_stream_id = match client.get_active_stream_id() {
Some(stream_id) => stream_id,
None => continue,
};
match client.session.finish_playing(active_stream_id) {
Ok(packet) => {
server_results.push(ServerResult::OutboundPacket {
target_connection_id: client.connection_id,
packet,
is_keyframe: false,
is_sequence_header: false,
is_video: false,
});
}
Err(error) => {
warn!(
"Error sending stream end to client on connection id {}: {:?}",
client.connection_id, error
);
}
}
server_results.push(ServerResult::DisconnectConnection {
connection_id: client.connection_id,
});
}
}
fn advance_serving_prefix(&mut self, server_results: &[ServerResult], target: usize) -> usize {
while self.serving_prefix_scan_pos < server_results.len() {
if let ServerResult::OutboundPacket {
target_connection_id,
packet,
..
} = &server_results[self.serving_prefix_scan_pos]
{
if *target_connection_id == target {
self.serving_prefix_bytes =
self.serving_prefix_bytes.saturating_add(packet.bytes.len());
}
}
self.serving_prefix_scan_pos += 1;
}
self.serving_prefix_bytes
}
fn handle_play_requested(
&mut self,
requested_connection_id: usize,
request_id: u32,
app_name: String,
stream_key: String,
stream_id: u32,
server_results: &mut Vec<ServerResult>,
) {
debug!("Rtmp play requested on app '{app_name}' and stream key '{stream_key}'");
let previous_watch = {
let client_id = self
.connection_to_client_map
.get(&requested_connection_id)
.unwrap();
let client = self.clients.get(*client_id).unwrap();
match &client.current_action {
ClientAction::Watching {
stream_key: old_stream_key,
..
} if *old_stream_key != stream_key => Some((*client_id, old_stream_key.clone())),
_ => None,
}
};
if let Some((client_id, old_stream_key)) = previous_watch {
self.play_ended(client_id, old_stream_key);
}
let serving_backlog = self.serving_connection_backlog_bytes;
let same_batch_prefix =
self.advance_serving_prefix(server_results.as_slice(), requested_connection_id);
let accept_result;
let mut join_burst = Vec::new();
{
let client_id = self
.connection_to_client_map
.get(&requested_connection_id)
.unwrap();
let client = self.clients.get_mut(*client_id).unwrap();
client.current_action = ClientAction::Watching {
stream_key: stream_key.clone(),
stream_id,
};
client.has_received_video_keyframe = false;
let channel = self
.channels
.entry(stream_key.clone())
.or_insert(MediaChannel::new(self.gop_limit));
channel.watching_client_ids.insert(*client_id);
accept_result = client.session.accept_request(request_id);
if let Ok(ref accept_results) = accept_result {
let accept_prefix_bytes =
join_replay_prefix_bytes(serving_backlog, same_batch_prefix, accept_results);
build_join_burst(
channel,
client,
requested_connection_id,
stream_id,
accept_prefix_bytes,
&mut join_burst,
);
}
}
match accept_result {
Err(error) => {
debug!(
"Rtmp client error occurred accepting playback request: {:?}",
error
);
server_results.push(ServerResult::DisconnectConnection {
connection_id: requested_connection_id,
});
return;
}
Ok(results) => {
self.handle_session_results(requested_connection_id, results, server_results);
server_results.extend(join_burst);
}
}
}
fn handle_play_finished(
&mut self,
finished_connection_id: usize,
app_name: String,
stream_key: String,
) {
debug!("Rtmp play finished on app '{app_name}' and stream key '{stream_key}'");
let client_id = match self.connection_to_client_map.get(&finished_connection_id) {
Some(client_id) => *client_id,
None => return,
};
let client = match self.clients.get_mut(client_id) {
Some(client) => client,
None => return,
};
let is_watching_finished_stream = matches!(
&client.current_action,
ClientAction::Watching {
stream_key: watched_stream_key,
..
} if *watched_stream_key == stream_key
);
if !is_watching_finished_stream {
debug!(
"Rtmp connection {finished_connection_id} finished playing '{stream_key}' \
which it is not currently watching; ignoring"
);
return;
}
client.current_action = ClientAction::Waiting;
client.has_received_video_keyframe = false;
self.play_ended(client_id, stream_key);
}
fn handle_metadata_received(
&mut self,
app_name: String,
stream_key: String,
metadata: StreamMetadata,
server_results: &mut Vec<ServerResult>,
) {
debug!("Rtmp new metadata received for app '{app_name}' and stream key '{stream_key}'");
let channel = match self.channels.get_mut(&stream_key) {
Some(channel) => channel,
None => return,
};
let metadata = Rc::new(metadata);
channel.metadata = Some(metadata.clone());
for client_id in &channel.watching_client_ids {
let client = match self.clients.get_mut(*client_id) {
Some(client) => client,
None => continue,
};
let active_stream_id = match client.get_active_stream_id() {
Some(stream_id) => stream_id,
None => continue,
};
match client.session.send_metadata(active_stream_id, &metadata) {
Ok(packet) => {
server_results.push(ServerResult::OutboundPacket {
target_connection_id: client.connection_id,
packet,
is_keyframe: false,
is_sequence_header: false,
is_video: false,
});
}
Err(error) => {
debug!(
"Rtmp error sending metadata to client on connection id {}: {:?}",
client.connection_id, error
);
server_results.push(ServerResult::DisconnectConnection {
connection_id: client.connection_id,
});
}
}
}
}
fn handle_audio_video_data_received(
&mut self,
stream_key: String,
timestamp: RtmpTimestamp,
data: Bytes,
data_type: ReceivedDataType,
server_results: &mut Vec<ServerResult>,
) {
let channel = match self.channels.get_mut(&stream_key) {
Some(channel) => channel,
None => return,
};
let is_video = matches!(data_type, ReceivedDataType::Video);
let (is_keyframe, is_sequence_header) = if is_video {
(is_video_keyframe(&data), is_video_sequence_header(&data))
} else {
(false, is_audio_sequence_header(&data))
};
match data_type {
ReceivedDataType::Video => {
if is_sequence_header {
if channel
.video_sequence_header
.as_ref()
.is_some_and(|cached| cached != &data)
{
channel.gops.clear();
}
channel.video_sequence_header = Some(data.clone());
channel.video_timestamp = timestamp;
}
channel.gops.save_frame_data(
crate::rtmp::gop::FrameData::Video {
timestamp,
data: data.clone(),
},
is_keyframe,
);
}
ReceivedDataType::Audio => {
if is_sequence_header {
if channel
.audio_sequence_header
.as_ref()
.is_some_and(|cached| cached != &data)
{
channel.gops.clear();
}
channel.audio_sequence_header = Some(data.clone());
channel.audio_timestamp = timestamp;
}
channel.gops.save_frame_data(
crate::rtmp::gop::FrameData::Audio {
timestamp,
data: data.clone(),
},
false,
);
}
}
let channel_is_audio_only = channel
.metadata
.as_ref()
.is_some_and(|m| m.audio_codec_id.is_some() && m.video_codec_id.is_none());
for client_id in &channel.watching_client_ids {
let client = match self.clients.get_mut(*client_id) {
Some(client) => client,
None => continue,
};
let active_stream_id = match &client.current_action {
ClientAction::Watching {
stream_key: watched_stream_key,
stream_id,
} => {
if *watched_stream_key != stream_key {
debug!(
"Rtmp client {} is watching '{}'; skipping frame delivery \
from channel '{}'",
client_id, watched_stream_key, stream_key
);
continue;
}
*stream_id
}
_ => continue,
};
let should_send_to_client = should_send_to_watcher(
data_type,
client.has_received_video_keyframe,
is_keyframe,
is_sequence_header,
channel_is_audio_only,
);
if !should_send_to_client {
continue;
}
let send_result = match data_type {
ReceivedDataType::Audio => {
client
.session
.send_audio_data(active_stream_id, data.clone(), timestamp, true)
}
ReceivedDataType::Video => {
if is_keyframe {
client.has_received_video_keyframe = true;
}
client
.session
.send_video_data(active_stream_id, data.clone(), timestamp, true)
}
};
match send_result {
Ok(packet) => {
server_results.push(ServerResult::OutboundPacket {
target_connection_id: client.connection_id,
packet,
is_keyframe,
is_sequence_header,
is_video,
});
}
Err(error) => {
let data_type_str = if is_video { "video" } else { "audio" };
debug!(
"Rtmp error sending {} data to client on connection id {}: {:?}",
data_type_str, client.connection_id, error
);
server_results.push(ServerResult::DisconnectConnection {
connection_id: client.connection_id,
});
}
}
}
}
fn publishing_ended(&mut self, stream_key: String) {
let should_remove = if let Some(channel) = self.channels.get_mut(&stream_key) {
channel.publishing_client_id = None;
channel.metadata = None;
channel.video_sequence_header = None;
channel.audio_sequence_header = None;
channel.video_timestamp = RtmpTimestamp { value: 0 };
channel.audio_timestamp = RtmpTimestamp { value: 0 };
channel.gops.clear();
channel.should_remove()
} else {
return;
};
if should_remove {
self.channels.remove(&stream_key);
}
}
fn play_ended(&mut self, client_id: usize, stream_key: String) {
let should_remove = if let Some(channel) = self.channels.get_mut(&stream_key) {
channel.watching_client_ids.remove(&client_id);
channel.should_remove()
} else {
return;
};
if should_remove {
self.channels.remove(&stream_key);
}
}
#[cfg(test)]
pub(crate) fn channel_video_sequence_header(&self, stream_key: &str) -> Option<Bytes> {
self.channels
.get(stream_key)
.and_then(|c| c.video_sequence_header.clone())
}
#[cfg(test)]
pub(crate) fn channel_audio_sequence_header(&self, stream_key: &str) -> Option<Bytes> {
self.channels
.get(stream_key)
.and_then(|c| c.audio_sequence_header.clone())
}
#[cfg(test)]
pub(crate) fn channel_frozen_gop_count(&self, stream_key: &str) -> usize {
self.channels
.get(stream_key)
.map(|c| c.gops.frozen_count())
.unwrap_or(0)
}
}
fn should_send_to_watcher(
data_type: ReceivedDataType,
has_received_video_keyframe: bool,
is_keyframe: bool,
is_sequence_header: bool,
channel_is_audio_only: bool,
) -> bool {
match data_type {
ReceivedDataType::Video => has_received_video_keyframe || is_sequence_header || is_keyframe,
ReceivedDataType::Audio => {
has_received_video_keyframe || is_sequence_header || channel_is_audio_only
}
}
}
fn select_replay_start(sizes: &[usize], budget: usize) -> usize {
let mut total: usize = 0;
let mut start = sizes.len();
for (i, &size) in sizes.iter().enumerate().rev() {
match total.checked_add(size) {
Some(sum) if sum <= budget => {
total = sum;
start = i;
}
_ => break,
}
}
start
}
fn join_replay_prefix_bytes(
connection_backlog: usize,
same_batch_prefix: usize,
accept_results: &[ServerSessionResult],
) -> usize {
let accept_packet_bytes = accept_results
.iter()
.fold(0usize, |acc, result| match result {
ServerSessionResult::OutboundResponse(packet) => acc.saturating_add(packet.bytes.len()),
_ => acc,
});
connection_backlog
.saturating_add(same_batch_prefix)
.saturating_add(accept_packet_bytes)
}
fn build_join_burst(
channel: &MediaChannel,
client: &mut Client,
connection_id: usize,
stream_id: u32,
accept_prefix_bytes: usize,
out: &mut Vec<ServerResult>,
) {
let mut prefix_wire_bytes = accept_prefix_bytes;
if let Some(ref metadata) = channel.metadata {
match client.session.send_metadata(stream_id, metadata) {
Ok(packet) => {
prefix_wire_bytes = prefix_wire_bytes.saturating_add(packet.bytes.len());
out.push(ServerResult::OutboundPacket {
target_connection_id: connection_id,
packet,
is_keyframe: false,
is_sequence_header: false,
is_video: false,
});
}
Err(error) => {
debug!(
"Rtmp client error occurred sending existing metadata to new client: {:?}",
error
);
out.push(ServerResult::DisconnectConnection { connection_id });
return;
}
}
}
if let Some(ref data) = channel.video_sequence_header {
match client.session.send_video_data(
stream_id,
data.clone(),
channel.video_timestamp,
false,
) {
Ok(packet) => {
prefix_wire_bytes = prefix_wire_bytes.saturating_add(packet.bytes.len());
out.push(ServerResult::OutboundPacket {
target_connection_id: connection_id,
packet,
is_keyframe: false,
is_sequence_header: true,
is_video: true,
});
}
Err(error) => {
debug!(
"Rtmp client error occurred sending video header to new client: {:?}",
error
);
out.push(ServerResult::DisconnectConnection { connection_id });
return;
}
}
}
if let Some(ref data) = channel.audio_sequence_header {
match client.session.send_audio_data(
stream_id,
data.clone(),
channel.audio_timestamp,
false,
) {
Ok(packet) => {
prefix_wire_bytes = prefix_wire_bytes.saturating_add(packet.bytes.len());
out.push(ServerResult::OutboundPacket {
target_connection_id: connection_id,
packet,
is_keyframe: false,
is_sequence_header: true,
is_video: false,
});
}
Err(error) => {
debug!(
"Rtmp client error occurred sending audio header to new client: {:?}",
error
);
out.push(ServerResult::DisconnectConnection { connection_id });
return;
}
}
}
let budget = JOIN_REPLAY_BUDGET_BYTES.saturating_sub(prefix_wire_bytes);
let frozen: Vec<_> = channel.gops.get_frozen_gops().collect();
let current_frames = channel.gops.current_frames();
let mut sizes: Vec<usize> = frozen
.iter()
.map(|gop| gop_wire_size(gop.byte_size(), gop.frame_count()))
.collect();
sizes.push(gop_wire_size(
channel.gops.current_byte_size(),
current_frames.len(),
));
let start = select_replay_start(&sizes, budget);
let mut replayed_idr = false;
for segment_index in start..sizes.len() {
let frames = if segment_index < frozen.len() {
frozen[segment_index].frames()
} else {
current_frames
};
if !replayed_idr && gop_contains_video_keyframe(frames) {
replayed_idr = true;
client.has_received_video_keyframe = true;
}
for frame_data in frames {
match frame_data {
FrameData::Video { timestamp, data } => {
if !replayed_idr {
continue;
}
let is_keyframe = is_video_keyframe(data);
let is_sequence_header = is_video_sequence_header(data);
match client
.session
.send_video_data(stream_id, data.clone(), *timestamp, false)
{
Ok(packet) => out.push(ServerResult::OutboundPacket {
target_connection_id: connection_id,
packet,
is_keyframe,
is_sequence_header,
is_video: true,
}),
Err(error) => {
debug!(
"Rtmp client error occurred sending video data to new client: {:?}",
error
);
out.push(ServerResult::DisconnectConnection { connection_id });
return;
}
}
}
FrameData::Audio { timestamp, data } => {
let is_sequence_header = is_audio_sequence_header(data);
match client
.session
.send_audio_data(stream_id, data.clone(), *timestamp, false)
{
Ok(packet) => out.push(ServerResult::OutboundPacket {
target_connection_id: connection_id,
packet,
is_keyframe: false,
is_sequence_header,
is_video: false,
}),
Err(error) => {
debug!(
"Rtmp client error occurred sending audio data to new client: {:?}",
error
);
out.push(ServerResult::DisconnectConnection { connection_id });
return;
}
}
}
}
}
}
}
fn gop_contains_video_keyframe(frames: &[FrameData]) -> bool {
frames.iter().any(|f| match f {
FrameData::Video { data, .. } => is_video_keyframe(data),
FrameData::Audio { .. } => false,
})
}
fn is_video_sequence_header(data: &Bytes) -> bool {
data.len() >= 2 && data[0] == 0x17 && data[1] == 0x00
}
fn is_audio_sequence_header(data: &Bytes) -> bool {
data.len() >= 2 && data[0] == 0xaf && data[1] == 0x00
}
fn is_oversized_sequence_header(data: &Bytes, data_type: ReceivedDataType) -> bool {
let is_sequence_header = match data_type {
ReceivedDataType::Video => is_video_sequence_header(data),
ReceivedDataType::Audio => is_audio_sequence_header(data),
};
is_sequence_header && data.len() > MAX_CACHEABLE_SEQUENCE_HEADER_BYTES
}
fn oversized_sequence_header_error(event: &ServerSessionEvent) -> Option<SchedulerError> {
let (data, data_type) = match event {
ServerSessionEvent::VideoDataReceived { data, .. } => (data, ReceivedDataType::Video),
ServerSessionEvent::AudioDataReceived { data, .. } => (data, ReceivedDataType::Audio),
_ => return None,
};
if is_oversized_sequence_header(data, data_type) {
Some(SchedulerError::OversizedSequenceHeader {
size: data.len(),
limit: MAX_CACHEABLE_SEQUENCE_HEADER_BYTES,
})
} else {
None
}
}
fn is_video_keyframe(data: &Bytes) -> bool {
data.len() >= 2 && data[0] == 0x17 && data[1] == 0x01
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_only_stream_delivers_audio_without_a_video_keyframe() {
let channel_is_audio_only = true;
assert!(
should_send_to_watcher(
ReceivedDataType::Audio,
false,
false,
false,
channel_is_audio_only,
),
"audio-only stream must deliver audio frames to a fresh subscriber"
);
}
#[test]
fn av_stream_still_gates_audio_on_a_video_keyframe() {
let channel_is_audio_only = false;
assert!(
!should_send_to_watcher(
ReceivedDataType::Audio,
false,
false,
false,
channel_is_audio_only,
),
"A/V stream must still withhold audio until the client has a keyframe"
);
assert!(should_send_to_watcher(
ReceivedDataType::Audio,
true,
false,
false,
channel_is_audio_only,
));
assert!(should_send_to_watcher(
ReceivedDataType::Audio,
false,
false,
true,
channel_is_audio_only,
));
}
#[test]
fn audio_only_metadata_lets_watcher_receive_audio_before_any_keyframe() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "audio_only".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
let metadata = StreamMetadata {
video_width: None,
video_height: None,
video_codec_id: None,
video_frame_rate: None,
video_bitrate_kbps: None,
audio_codec_id: Some(10), audio_bitrate_kbps: None,
audio_sample_rate: Some(44100),
audio_channels: Some(2),
audio_is_stereo: Some(true),
encoder: None,
};
scheduler.handle_metadata_received(
"app".to_string(),
stream_key.clone(),
metadata,
&mut Vec::new(),
);
let mut server_results = Vec::new();
let audio_data = Bytes::from(vec![0xAF, 0x01, 0xDD, 0xEE]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 50 },
audio_data,
ReceivedDataType::Audio,
&mut server_results,
);
assert_eq!(
server_results.len(),
1,
"audio-only stream (declared via metadata) must deliver audio without a video keyframe"
);
}
#[test]
fn video_gating_is_unchanged() {
for audio_only in [false, true] {
assert!(!should_send_to_watcher(
ReceivedDataType::Video,
false,
false,
false,
audio_only,
));
assert!(should_send_to_watcher(
ReceivedDataType::Video,
false,
true,
false,
audio_only,
));
assert!(should_send_to_watcher(
ReceivedDataType::Video,
true,
false,
false,
audio_only,
));
}
}
#[test]
fn test_new_channel_creation() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let result = scheduler.new_channel(stream_key.clone(), publisher_connection_id);
assert!(result, "First channel creation should succeed");
assert!(scheduler.channels.contains_key(&stream_key));
assert!(scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id));
}
#[test]
fn test_duplicate_channel_rejected() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id_1 = 1;
let publisher_connection_id_2 = 2;
let result1 = scheduler.new_channel(stream_key.clone(), publisher_connection_id_1);
assert!(result1, "First channel creation should succeed");
if let Some(channel) = scheduler.channels.get_mut(&stream_key) {
channel.publishing_client_id = Some(0);
}
let result2 = scheduler.new_channel(stream_key.clone(), publisher_connection_id_2);
assert!(!result2, "Duplicate channel creation should be rejected");
assert!(scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id_1));
assert!(!scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id_2));
}
#[test]
fn test_notify_connection_closed() {
let mut scheduler = RtmpScheduler::new(10);
let connection_id = 1;
let _ = scheduler.bytes_received(connection_id, &[]);
assert!(scheduler
.connection_to_client_map
.contains_key(&connection_id));
let client_id = *scheduler
.connection_to_client_map
.get(&connection_id)
.unwrap();
assert!(scheduler.clients.contains(client_id));
scheduler.notify_connection_closed(connection_id);
assert!(!scheduler
.connection_to_client_map
.contains_key(&connection_id));
assert!(!scheduler.clients.contains(client_id));
}
#[test]
fn test_notify_publisher_closed() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let result = scheduler.new_channel(stream_key.clone(), publisher_connection_id);
assert!(result, "Channel creation should succeed");
assert!(scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id));
let client_id = *scheduler
.publisher_to_client_map
.get(&publisher_connection_id)
.unwrap();
assert!(scheduler.clients.contains(client_id));
scheduler.notify_publisher_closed(publisher_connection_id);
assert!(!scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id));
assert!(!scheduler.clients.contains(client_id));
assert!(
!scheduler.channels.contains_key(&stream_key),
"Empty channel (no publisher, no watchers) should be removed"
);
}
#[test]
fn test_notify_publisher_closed_with_watchers() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let result = scheduler.new_channel(stream_key.clone(), publisher_connection_id);
assert!(result, "Channel creation should succeed");
if let Some(channel) = scheduler.channels.get_mut(&stream_key) {
channel.watching_client_ids.insert(100);
}
scheduler.notify_publisher_closed(publisher_connection_id);
assert!(!scheduler
.publisher_to_client_map
.contains_key(&publisher_connection_id));
assert!(
scheduler.channels.contains_key(&stream_key),
"Channel with watchers should remain after publisher closes"
);
if let Some(channel) = scheduler.channels.get(&stream_key) {
assert_eq!(channel.publishing_client_id, None);
assert!(channel.watching_client_ids.contains(&100));
}
}
#[test]
fn test_publish_bytes_to_nonexistent_connection() {
let mut scheduler = RtmpScheduler::new(10);
let nonexistent_connection_id = 999;
let result = scheduler.publish_bytes_received(nonexistent_connection_id, vec![0x03]);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_bytes_received_creates_session() {
let mut scheduler = RtmpScheduler::new(10);
let connection_id = 1;
assert!(!scheduler
.connection_to_client_map
.contains_key(&connection_id));
let result = scheduler.bytes_received(connection_id, &[0x03]);
assert!(result.is_ok());
assert!(scheduler
.connection_to_client_map
.contains_key(&connection_id));
let client_id = *scheduler
.connection_to_client_map
.get(&connection_id)
.unwrap();
assert!(scheduler.clients.contains(client_id));
let client = scheduler.clients.get(client_id).unwrap();
assert!(matches!(client.current_action, ClientAction::Waiting));
}
#[test]
fn test_handle_play_request_flow() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let connection_id = 1;
let _ = scheduler.bytes_received(connection_id, &[]);
assert!(scheduler
.connection_to_client_map
.contains_key(&connection_id));
let client_id = *scheduler
.connection_to_client_map
.get(&connection_id)
.unwrap();
let client = scheduler.clients.get(client_id).unwrap();
assert!(matches!(client.current_action, ClientAction::Waiting));
let mut server_results = Vec::new();
scheduler.handle_play_requested(
connection_id,
1, "test_app".to_string(),
stream_key.clone(),
1, &mut server_results,
);
let client = scheduler.clients.get(client_id).unwrap();
assert!(matches!(
client.current_action,
ClientAction::Watching { .. }
));
assert!(scheduler.channels.contains_key(&stream_key));
let channel = scheduler.channels.get(&stream_key).unwrap();
assert!(channel.watching_client_ids.contains(&client_id));
}
#[test]
fn test_scheduler_error_propagation() {
let mut scheduler = RtmpScheduler::new(10);
let connection_id = 1;
let _ = scheduler.bytes_received(connection_id, &[]);
let invalid_data = vec![0xFF, 0xFF];
let result = scheduler.bytes_received(connection_id, &invalid_data);
match result {
Ok(_) => {
}
Err(_) => {
}
}
}
#[test]
fn test_invalid_stream_key_warning() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "nonexistent_stream".to_string();
let mut server_results = Vec::new();
let timestamp = RtmpTimestamp { value: 0 };
let data = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
timestamp,
data,
ReceivedDataType::Video,
&mut server_results,
);
assert!(server_results.is_empty());
assert!(!scheduler.channels.contains_key(&stream_key));
}
#[test]
fn test_audio_video_distribution_to_watchers() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
let result = scheduler.new_channel(stream_key.clone(), publisher_connection_id);
assert!(result, "Channel creation should succeed");
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut server_results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1, "test_app".to_string(),
stream_key.clone(),
1, &mut server_results,
);
let channel = scheduler.channels.get(&stream_key).unwrap();
assert_eq!(channel.watching_client_ids.len(), 1);
server_results.clear();
let keyframe_data = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00, 0xAA, 0xBB]);
let timestamp = RtmpTimestamp { value: 1000 };
scheduler.handle_audio_video_data_received(
stream_key.clone(),
timestamp,
keyframe_data,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(server_results.len(), 1, "Watcher should receive keyframe");
match &server_results[0] {
ServerResult::OutboundPacket {
target_connection_id,
is_keyframe,
is_video,
..
} => {
assert_eq!(*target_connection_id, watcher_connection_id);
assert!(*is_keyframe, "Should be marked as keyframe");
assert!(*is_video, "Should be marked as video");
}
_ => panic!("Expected OutboundPacket"),
}
server_results.clear();
let non_keyframe_data = Bytes::from(vec![0x27, 0x01, 0x00, 0x00, 0x00, 0xCC]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 1033 },
non_keyframe_data,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(
server_results.len(),
1,
"Watcher should receive non-keyframe"
);
match &server_results[0] {
ServerResult::OutboundPacket {
is_keyframe,
is_video,
..
} => {
assert!(!*is_keyframe, "Should not be marked as keyframe");
assert!(*is_video, "Should be marked as video");
}
_ => panic!("Expected OutboundPacket"),
}
server_results.clear();
let audio_data = Bytes::from(vec![0xAF, 0x01, 0xDD, 0xEE]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 1040 },
audio_data,
ReceivedDataType::Audio,
&mut server_results,
);
assert_eq!(server_results.len(), 1, "Watcher should receive audio");
match &server_results[0] {
ServerResult::OutboundPacket {
is_video,
is_keyframe,
..
} => {
assert!(!*is_video, "Should be marked as audio");
assert!(!*is_keyframe, "Audio should not be keyframe");
}
_ => panic!("Expected OutboundPacket"),
}
}
#[test]
fn test_multiple_watchers_distribution() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher1_connection_id = 2;
let watcher2_connection_id = 3;
let watcher3_connection_id = 4;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
for (watcher_id, request_id) in [
(watcher1_connection_id, 1u32),
(watcher2_connection_id, 2u32),
(watcher3_connection_id, 3u32),
] {
let _ = scheduler.bytes_received(watcher_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_id,
request_id,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
}
let channel = scheduler.channels.get(&stream_key).unwrap();
assert_eq!(channel.watching_client_ids.len(), 3);
let mut server_results = Vec::new();
let keyframe_data = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 0 },
keyframe_data,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(
server_results.len(),
3,
"All watchers should receive keyframe"
);
let target_ids: HashSet<_> = server_results
.iter()
.filter_map(|r| match r {
ServerResult::OutboundPacket {
target_connection_id,
..
} => Some(*target_connection_id),
_ => None,
})
.collect();
assert!(target_ids.contains(&watcher1_connection_id));
assert!(target_ids.contains(&watcher2_connection_id));
assert!(target_ids.contains(&watcher3_connection_id));
}
#[test]
fn test_watcher_receives_sequence_header_first() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let early_watcher_id = 2;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
let _ = scheduler.bytes_received(early_watcher_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
early_watcher_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
let mut server_results = Vec::new();
let sequence_header = Bytes::from(vec![0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 0 },
sequence_header.clone(),
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(server_results.len(), 1);
match &server_results[0] {
ServerResult::OutboundPacket {
is_sequence_header, ..
} => {
assert!(*is_sequence_header, "Should be marked as sequence header");
}
_ => panic!("Expected OutboundPacket"),
}
let channel = scheduler.channels.get(&stream_key).unwrap();
assert!(channel.video_sequence_header.is_some());
assert_eq!(
channel.video_sequence_header.as_ref().unwrap(),
&sequence_header
);
}
#[test]
fn test_watcher_without_keyframe_skips_non_keyframe() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
let mut server_results = Vec::new();
let non_keyframe = Bytes::from(vec![0x27, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 100 },
non_keyframe,
ReceivedDataType::Video,
&mut server_results,
);
assert!(
server_results.is_empty(),
"Watcher should skip non-keyframe before receiving keyframe"
);
let keyframe = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 200 },
keyframe,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(server_results.len(), 1, "Watcher should receive keyframe");
}
#[test]
fn test_audio_skipped_before_video_keyframe() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
let mut server_results = Vec::new();
let audio_data = Bytes::from(vec![0xAF, 0x01, 0xDD, 0xEE]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 50 },
audio_data.clone(),
ReceivedDataType::Audio,
&mut server_results,
);
assert!(
server_results.is_empty(),
"Watcher should skip audio before video keyframe"
);
let keyframe = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 100 },
keyframe,
ReceivedDataType::Video,
&mut server_results,
);
server_results.clear();
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 150 },
audio_data,
ReceivedDataType::Audio,
&mut server_results,
);
assert_eq!(
server_results.len(),
1,
"Watcher should receive audio after video keyframe"
);
}
#[test]
fn test_audio_sequence_header_sent_before_keyframe() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
let mut server_results = Vec::new();
let audio_seq_header = Bytes::from(vec![0xAF, 0x00, 0x12, 0x10]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 0 },
audio_seq_header.clone(),
ReceivedDataType::Audio,
&mut server_results,
);
assert_eq!(
server_results.len(),
1,
"Audio sequence header should be sent before video keyframe"
);
match &server_results[0] {
ServerResult::OutboundPacket {
is_sequence_header,
is_video,
..
} => {
assert!(*is_sequence_header);
assert!(!*is_video);
}
_ => panic!("Expected OutboundPacket"),
}
let channel = scheduler.channels.get(&stream_key).unwrap();
assert!(channel.audio_sequence_header.is_some());
}
#[test]
fn test_watcher_close_during_stream() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "test_stream".to_string();
let publisher_connection_id = 1;
let watcher1_id = 2;
let watcher2_id = 3;
scheduler.new_channel(stream_key.clone(), publisher_connection_id);
for watcher_id in [watcher1_id, watcher2_id] {
let _ = scheduler.bytes_received(watcher_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut results,
);
}
assert_eq!(
scheduler
.channels
.get(&stream_key)
.unwrap()
.watching_client_ids
.len(),
2
);
let mut server_results = Vec::new();
let keyframe = Bytes::from(vec![0x17, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 0 },
keyframe,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(
server_results.len(),
2,
"Both watchers should receive keyframe"
);
scheduler.notify_connection_closed(watcher1_id);
let channel = scheduler.channels.get(&stream_key).unwrap();
assert_eq!(channel.watching_client_ids.len(), 1);
server_results.clear();
let frame = Bytes::from(vec![0x27, 0x01, 0x00, 0x00, 0x00]);
scheduler.handle_audio_video_data_received(
stream_key.clone(),
RtmpTimestamp { value: 33 },
frame,
ReceivedDataType::Video,
&mut server_results,
);
assert_eq!(
server_results.len(),
1,
"Only remaining watcher should receive frame"
);
match &server_results[0] {
ServerResult::OutboundPacket {
target_connection_id,
..
} => {
assert_eq!(*target_connection_id, watcher2_id);
}
_ => panic!("Expected OutboundPacket"),
}
}
#[test]
fn keyframeless_gop_does_not_flip_the_replay_keyframe_gate() {
let seq_header = FrameData::Video {
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(&[0x17, 0x00, 0x00, 0x00, 0x00]),
};
let pre_roll_audio = FrameData::Audio {
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(&[0xaf, 0x01, 0x21]),
};
assert!(
!gop_contains_video_keyframe(&[seq_header.clone(), pre_roll_audio.clone()]),
"a sequence-header-only GOP must not flip the keyframe gate"
);
let idr = FrameData::Video {
timestamp: RtmpTimestamp { value: 33 },
data: Bytes::from_static(&[0x17, 0x01, 0x00, 0x00, 0x00]),
};
let delta = FrameData::Video {
timestamp: RtmpTimestamp { value: 66 },
data: Bytes::from_static(&[0x27, 0x01, 0x00, 0x00, 0x00]),
};
assert!(
gop_contains_video_keyframe(&[idr, delta.clone(), pre_roll_audio.clone()]),
"a GOP containing a real IDR must flip the keyframe gate"
);
assert!(
!gop_contains_video_keyframe(&[delta.clone(), delta, pre_roll_audio]),
"a delta-only GOP (mid-GOP start) must not flip the keyframe gate"
);
let end_of_seq = FrameData::Video {
timestamp: RtmpTimestamp { value: 99 },
data: Bytes::from_static(&[0x17, 0x02, 0x00, 0x00, 0x00]),
};
assert!(
!gop_contains_video_keyframe(&[end_of_seq]),
"an AVC end-of-sequence tag must not flip the keyframe gate"
);
}
fn play(scheduler: &mut RtmpScheduler, connection_id: usize, stream_key: &str) {
let _ = scheduler.bytes_received(connection_id, &[]);
let mut results = Vec::new();
scheduler.handle_play_requested(
connection_id,
1,
"app".to_string(),
stream_key.to_string(),
1,
&mut results,
);
}
fn feed_video(
scheduler: &mut RtmpScheduler,
stream_key: &str,
timestamp: u32,
data: &'static [u8],
) -> Vec<ServerResult> {
let mut results = Vec::new();
scheduler.handle_audio_video_data_received(
stream_key.to_string(),
RtmpTimestamp { value: timestamp },
Bytes::from_static(data),
ReceivedDataType::Video,
&mut results,
);
results
}
const IDR: &[u8] = &[0x17, 0x01, 0x00, 0x00, 0x00];
const DELTA: &[u8] = &[0x27, 0x01, 0x00, 0x00, 0x00];
#[test]
fn switching_streams_leaves_the_old_channel_and_regates_on_the_new_idr() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_a_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("stream_a".to_string(), publisher_a_conn));
play(&mut scheduler, watcher_conn, "stream_a");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
assert!(scheduler
.channels
.get("stream_a")
.unwrap()
.watching_client_ids
.contains(&client_id));
let results = feed_video(&mut scheduler, "stream_a", 0, IDR);
assert_eq!(results.len(), 1, "watcher should receive A's IDR");
assert!(
scheduler
.clients
.get(client_id)
.unwrap()
.has_received_video_keyframe
);
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_conn,
2,
"app".to_string(),
"stream_b".to_string(),
1,
&mut results,
);
assert!(
!scheduler
.channels
.get("stream_a")
.unwrap()
.watching_client_ids
.contains(&client_id),
"switching to B must remove the client from A's watcher set"
);
assert!(
scheduler
.channels
.get("stream_b")
.unwrap()
.watching_client_ids
.contains(&client_id),
"the client must be a member of B's watcher set"
);
assert!(
!scheduler
.clients
.get(client_id)
.unwrap()
.has_received_video_keyframe,
"a new play request must reset the keyframe gate"
);
let results = feed_video(&mut scheduler, "stream_a", 33, IDR);
assert!(
results.is_empty(),
"A's frames must not reach a client that switched to B"
);
assert!(
!scheduler
.clients
.get(client_id)
.unwrap()
.has_received_video_keyframe,
"A's live IDR must not open the gate of a client watching B"
);
let results = feed_video(&mut scheduler, "stream_b", 40, DELTA);
assert!(
results.is_empty(),
"B's deltas must be withheld until B's IDR"
);
let results = feed_video(&mut scheduler, "stream_b", 66, IDR);
assert_eq!(results.len(), 1, "B's IDR must be delivered");
let results = feed_video(&mut scheduler, "stream_b", 100, DELTA);
assert_eq!(results.len(), 1, "B's deltas flow after B's IDR");
}
#[test]
fn switching_streams_gcs_the_empty_unpublished_old_channel() {
let mut scheduler = RtmpScheduler::new(10);
let watcher_conn = 2;
play(&mut scheduler, watcher_conn, "stream_a");
assert!(scheduler.channels.contains_key("stream_a"));
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_conn,
2,
"app".to_string(),
"stream_b".to_string(),
1,
&mut results,
);
assert!(
!scheduler.channels.contains_key("stream_a"),
"an empty, unpublished channel must be removed on stream switch"
);
assert!(scheduler.channels.contains_key("stream_b"));
}
#[test]
fn replaying_the_same_stream_keeps_the_watcher_membership() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("stream_a".to_string(), publisher_conn));
play(&mut scheduler, watcher_conn, "stream_a");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_conn,
2,
"app".to_string(),
"stream_a".to_string(),
1,
&mut results,
);
assert!(scheduler
.channels
.get("stream_a")
.unwrap()
.watching_client_ids
.contains(&client_id));
}
#[test]
fn play_stream_finished_leaves_the_channel_and_resets_the_play_state() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("stream_a".to_string(), publisher_conn));
play(&mut scheduler, watcher_conn, "stream_a");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
let results = feed_video(&mut scheduler, "stream_a", 0, IDR);
assert_eq!(results.len(), 1, "watcher should receive the IDR");
assert!(
scheduler
.clients
.get(client_id)
.unwrap()
.has_received_video_keyframe
);
let mut results = Vec::new();
let _ = scheduler.handle_raised_event(
watcher_conn,
ServerSessionEvent::PlayStreamFinished {
app_name: "app".to_string(),
stream_key: "stream_a".to_string(),
},
&mut results,
);
let channel = scheduler.channels.get("stream_a").unwrap();
assert!(
!channel.watching_client_ids.contains(&client_id),
"PlayStreamFinished must remove the watcher membership"
);
let client = scheduler.clients.get(client_id).unwrap();
assert!(
matches!(client.current_action, ClientAction::Waiting),
"PlayStreamFinished must reset the action to Waiting"
);
assert!(
!client.has_received_video_keyframe,
"PlayStreamFinished must reset the keyframe gate"
);
let results = feed_video(&mut scheduler, "stream_a", 33, IDR);
assert!(results.is_empty(), "no frames after the play finished");
}
#[test]
fn play_stream_finished_gcs_the_empty_unpublished_channel() {
let mut scheduler = RtmpScheduler::new(10);
let watcher_conn = 2;
play(&mut scheduler, watcher_conn, "stream_a");
assert!(scheduler.channels.contains_key("stream_a"));
let mut results = Vec::new();
let _ = scheduler.handle_raised_event(
watcher_conn,
ServerSessionEvent::PlayStreamFinished {
app_name: "app".to_string(),
stream_key: "stream_a".to_string(),
},
&mut results,
);
assert!(
!scheduler.channels.contains_key("stream_a"),
"an empty, unpublished channel must be removed when the play finishes"
);
}
#[test]
fn play_stream_finished_after_a_same_stream_replay_tears_down_the_current_play() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("stream_a".to_string(), publisher_conn));
play(&mut scheduler, watcher_conn, "stream_a");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_conn,
2,
"app".to_string(),
"stream_a".to_string(),
2,
&mut results,
);
assert!(scheduler
.channels
.get("stream_a")
.unwrap()
.watching_client_ids
.contains(&client_id));
assert!(matches!(
scheduler.clients.get(client_id).unwrap().current_action,
ClientAction::Watching { stream_id: 2, .. }
));
let mut results = Vec::new();
let _ = scheduler.handle_raised_event(
watcher_conn,
ServerSessionEvent::PlayStreamFinished {
app_name: "app".to_string(),
stream_key: "stream_a".to_string(),
},
&mut results,
);
assert!(!scheduler
.channels
.get("stream_a")
.unwrap()
.watching_client_ids
.contains(&client_id));
assert!(matches!(
scheduler.clients.get(client_id).unwrap().current_action,
ClientAction::Waiting
));
assert!(scheduler.channels.contains_key("stream_a"));
}
#[test]
fn play_stream_finished_for_another_stream_is_ignored() {
let mut scheduler = RtmpScheduler::new(10);
let watcher_conn = 2;
play(&mut scheduler, watcher_conn, "stream_b");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
let mut results = Vec::new();
let _ = scheduler.handle_raised_event(
watcher_conn,
ServerSessionEvent::PlayStreamFinished {
app_name: "app".to_string(),
stream_key: "stream_a".to_string(),
},
&mut results,
);
let client = scheduler.clients.get(client_id).unwrap();
assert!(matches!(
client.current_action,
ClientAction::Watching { ref stream_key, .. } if stream_key == "stream_b"
));
assert!(scheduler
.channels
.get("stream_b")
.unwrap()
.watching_client_ids
.contains(&client_id));
}
fn feed_media(
scheduler: &mut RtmpScheduler,
publisher_conn: usize,
tag_type: u8,
timestamp: u32,
data: &'static [u8],
) -> Vec<ServerResult> {
scheduler.publish_media_received(
publisher_conn,
tag_type,
RtmpTimestamp { value: timestamp },
Bytes::from_static(data),
)
}
const VIDEO_SEQ: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
const AUDIO_SEQ: &[u8] = &[0xaf, 0x00, 0x12, 0x10];
const AUDIO_FRAME: &[u8] = &[0xaf, 0x01, 0xDD, 0xEE];
#[test]
fn in_process_bypass_delivers_seq_header_then_gated_idr_then_interleaved() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("live".to_string(), publisher_conn));
play(&mut scheduler, watcher_conn, "live");
let watcher_client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
let results = feed_media(&mut scheduler, publisher_conn, 0x09, 0, VIDEO_SEQ);
assert_eq!(
results.len(),
1,
"video sequence header must reach the watcher"
);
assert!(matches!(
&results[0],
ServerResult::OutboundPacket {
is_sequence_header: true,
is_video: true,
..
}
));
let results = feed_media(&mut scheduler, publisher_conn, 0x08, 0, AUDIO_SEQ);
assert_eq!(
results.len(),
1,
"audio sequence header must reach the watcher"
);
assert!(matches!(
&results[0],
ServerResult::OutboundPacket {
is_sequence_header: true,
is_video: false,
..
}
));
assert!(
feed_media(&mut scheduler, publisher_conn, 0x09, 33, DELTA).is_empty(),
"delta before the IDR must be withheld"
);
assert!(
feed_media(&mut scheduler, publisher_conn, 0x08, 33, AUDIO_FRAME).is_empty(),
"audio before the first IDR must be withheld"
);
assert!(
!scheduler
.clients
.get(watcher_client_id)
.unwrap()
.has_received_video_keyframe
);
let results = feed_media(&mut scheduler, publisher_conn, 0x09, 66, IDR);
assert_eq!(results.len(), 1, "the IDR must be delivered");
assert!(matches!(
&results[0],
ServerResult::OutboundPacket {
is_keyframe: true,
is_video: true,
..
}
));
assert!(
scheduler
.clients
.get(watcher_client_id)
.unwrap()
.has_received_video_keyframe
);
let results = feed_media(&mut scheduler, publisher_conn, 0x08, 70, AUDIO_FRAME);
assert_eq!(results.len(), 1, "audio flows after the IDR");
assert!(matches!(
&results[0],
ServerResult::OutboundPacket {
is_video: false,
is_keyframe: false,
..
}
));
let results = feed_media(&mut scheduler, publisher_conn, 0x09, 99, DELTA);
assert_eq!(results.len(), 1, "delta flows after the IDR");
assert!(matches!(
&results[0],
ServerResult::OutboundPacket {
is_video: true,
is_keyframe: false,
..
}
));
}
#[test]
fn in_process_bypass_populates_seq_headers_and_gop_cache_for_replay() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
assert!(scheduler.new_channel("live".to_string(), publisher_conn));
feed_media(&mut scheduler, publisher_conn, 0x09, 0, VIDEO_SEQ);
feed_media(&mut scheduler, publisher_conn, 0x08, 0, AUDIO_SEQ);
feed_media(&mut scheduler, publisher_conn, 0x09, 33, IDR);
feed_media(&mut scheduler, publisher_conn, 0x08, 34, AUDIO_FRAME);
feed_media(&mut scheduler, publisher_conn, 0x09, 66, DELTA);
feed_media(&mut scheduler, publisher_conn, 0x09, 99, IDR);
let channel = scheduler.channels.get("live").unwrap();
assert_eq!(channel.video_sequence_header.as_deref(), Some(VIDEO_SEQ));
assert_eq!(channel.video_timestamp, RtmpTimestamp { value: 0 });
assert_eq!(channel.audio_sequence_header.as_deref(), Some(AUDIO_SEQ));
assert_eq!(channel.audio_timestamp, RtmpTimestamp { value: 0 });
assert!(
channel.gops.frozen_count() >= 1,
"the completed GOP must be frozen for replay"
);
}
#[test]
fn fanout_skips_watchers_whose_action_points_at_another_stream() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
let watcher_conn = 2;
assert!(scheduler.new_channel("stream_a".to_string(), publisher_conn));
play(&mut scheduler, watcher_conn, "stream_a");
let client_id = *scheduler
.connection_to_client_map
.get(&watcher_conn)
.unwrap();
scheduler.clients.get_mut(client_id).unwrap().current_action = ClientAction::Watching {
stream_key: "stream_b".to_string(),
stream_id: 1,
};
let results = feed_video(&mut scheduler, "stream_a", 0, IDR);
assert!(
results.is_empty(),
"frames must not be delivered through a stale watcher membership"
);
assert!(
!scheduler
.clients
.get(client_id)
.unwrap()
.has_received_video_keyframe,
"a mismatched channel must not open the client's keyframe gate"
);
}
fn make_watching_client(connection_id: usize, stream_key: &str, stream_id: u32) -> Client {
let (session, _) = ServerSession::new(ServerSessionConfig::new()).expect("server session");
Client {
session,
connection_id,
current_action: ClientAction::Watching {
stream_key: stream_key.to_string(),
stream_id,
},
has_received_video_keyframe: false,
}
}
fn video_frame(timestamp: u32, data: Bytes) -> FrameData {
FrameData::Video {
timestamp: RtmpTimestamp { value: timestamp },
data,
}
}
fn audio_frame(timestamp: u32, data: Bytes) -> FrameData {
FrameData::Audio {
timestamp: RtmpTimestamp { value: timestamp },
data,
}
}
fn burst_flags(out: &[ServerResult]) -> Vec<(bool, bool, bool)> {
out.iter()
.map(|result| match result {
ServerResult::OutboundPacket {
is_keyframe,
is_sequence_header,
is_video,
..
} => (*is_keyframe, *is_sequence_header, *is_video),
other => panic!("expected OutboundPacket, got {:?}", other),
})
.collect()
}
fn packet_contains(result: &ServerResult, needle: &[u8]) -> bool {
match result {
ServerResult::OutboundPacket { packet, .. } => {
packet.bytes.windows(needle.len()).any(|w| w == needle)
}
_ => false,
}
}
fn serialized_burst_len(out: &[ServerResult]) -> usize {
out.iter()
.map(|result| match result {
ServerResult::OutboundPacket { packet, .. } => packet.bytes.len(),
_ => 0,
})
.sum()
}
#[test]
fn join_burst_flags_every_packet_like_the_live_path() {
let mut channel = MediaChannel::new(10);
channel.metadata = Some(Rc::new(StreamMetadata {
video_width: None,
video_height: None,
video_codec_id: Some(7), video_frame_rate: None,
video_bitrate_kbps: None,
audio_codec_id: Some(10), audio_bitrate_kbps: None,
audio_sample_rate: None,
audio_channels: None,
audio_is_stereo: None,
encoder: None,
}));
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
channel
.gops
.save_frame_data(video_frame(0, Bytes::from_static(IDR)), true);
channel
.gops
.save_frame_data(audio_frame(10, Bytes::from_static(AUDIO_FRAME)), false);
channel
.gops
.save_frame_data(video_frame(33, Bytes::from_static(DELTA)), false);
channel
.gops
.save_frame_data(video_frame(66, Bytes::from_static(IDR)), true);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
for result in &out {
if let ServerResult::OutboundPacket {
target_connection_id,
..
} = result
{
assert_eq!(*target_connection_id, 7);
}
}
assert_eq!(
burst_flags(&out),
vec![
(false, false, false), (false, true, true), (false, true, false), (true, false, true), (false, false, false), (false, false, true), (true, false, true), ],
"every replayed packet must carry the flags the live path computes"
);
assert!(
client.has_received_video_keyframe,
"replaying a real IDR must open the keyframe gate"
);
}
#[test]
fn join_burst_trims_whole_oldest_gops_to_the_byte_budget() {
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
let make_idr = |marker: u8| {
let mut data = vec![0u8; 400 * 1024];
data[0] = 0x17;
data[1] = 0x01;
data[2] = marker;
Bytes::from(data)
};
for (i, marker) in [1u8, 2, 3].into_iter().enumerate() {
channel
.gops
.save_frame_data(video_frame(i as u32 * 100, make_idr(marker)), true);
}
channel
.gops
.save_frame_data(video_frame(300, Bytes::from_static(IDR)), true);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert_eq!(out.len(), 5, "the burst must trim the oldest GOP entirely");
assert_eq!(
burst_flags(&out)[..2],
[(false, true, true), (false, true, false)],
"sequence headers are sent outside the budget trim"
);
assert!(
out.iter().all(|p| !packet_contains(p, &[0x17, 0x01, 1])),
"no fragment of the trimmed GOP1 may be replayed"
);
assert!(
packet_contains(&out[2], &[0x17, 0x01, 2]),
"the replay must start at GOP2's IDR"
);
assert!(packet_contains(&out[3], &[0x17, 0x01, 3]));
assert!(client.has_received_video_keyframe);
}
#[test]
fn join_burst_with_only_oversized_gops_sends_headers_only() {
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
let huge = {
let mut data = vec![0u8; JOIN_REPLAY_BUDGET_BYTES + 1];
data[0] = 0x17;
data[1] = 0x01;
Bytes::from(data)
};
channel
.gops
.save_frame_data(video_frame(0, huge.clone()), true);
channel.gops.save_frame_data(video_frame(100, huge), true);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert_eq!(
burst_flags(&out),
vec![(false, true, true), (false, true, false)],
"an oversized cache must degrade to sequence headers only"
);
assert!(
!client.has_received_video_keyframe,
"no IDR was replayed, so live deltas must stay gated until a live IDR"
);
}
#[test]
fn join_burst_includes_the_open_current_gop_as_the_last_segment() {
const CURRENT_IDR: &[u8] = &[0x17, 0x01, 0xB2, 0x00, 0x00];
const CURRENT_DELTA: &[u8] = &[0x27, 0x01, 0xB3, 0x00, 0x00];
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel
.gops
.save_frame_data(video_frame(0, Bytes::from_static(IDR)), true);
channel
.gops
.save_frame_data(video_frame(33, Bytes::from_static(DELTA)), false);
channel
.gops
.save_frame_data(video_frame(66, Bytes::from_static(CURRENT_IDR)), true);
channel
.gops
.save_frame_data(video_frame(99, Bytes::from_static(CURRENT_DELTA)), false);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert_eq!(
out.len(),
5,
"the open GOP must be replayed after the frozen ones"
);
assert!(
packet_contains(&out[3], CURRENT_IDR),
"the open GOP's IDR must be replayed — live deltas reference it"
);
assert!(packet_contains(&out[4], CURRENT_DELTA));
assert_eq!(
burst_flags(&out)[3..],
[(true, false, true), (false, false, true)]
);
}
#[test]
fn keyframeless_current_gop_replays_audio_only_and_keeps_the_gate_closed() {
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
channel
.gops
.save_frame_data(video_frame(0, Bytes::from_static(DELTA)), false);
channel
.gops
.save_frame_data(audio_frame(10, Bytes::from_static(AUDIO_FRAME)), false);
channel
.gops
.save_frame_data(video_frame(33, Bytes::from_static(DELTA)), false);
channel
.gops
.save_frame_data(audio_frame(43, Bytes::from_static(AUDIO_FRAME)), false);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert_eq!(
burst_flags(&out),
vec![
(false, true, true), (false, true, false), (false, false, false), (false, false, false), ],
"undecodable pre-IDR deltas must be skipped while audio still flows"
);
assert!(
!client.has_received_video_keyframe,
"a keyframeless replay must not open the gate"
);
}
#[test]
fn select_replay_start_picks_the_longest_fitting_suffix() {
let cases: &[(&[usize], usize, usize)] = &[
(&[], 100, 0), (&[10], 100, 0), (&[100], 100, 0), (&[101], 100, 1), (&[100, 200, 300], 600, 0),
(&[100, 200, 300], 599, 1),
(&[100, 200, 300], 500, 1),
(&[100, 200, 300], 499, 2),
(&[100, 200, 300], 300, 2),
(&[100, 200, 300], 299, 3),
(&[0, 0, 0], 0, 0), (&[usize::MAX, 100], usize::MAX, 1),
(&[usize::MAX - 50, 100], usize::MAX, 1),
];
for &(sizes, budget, expected) in cases {
assert_eq!(
select_replay_start(sizes, budget),
expected,
"sizes={sizes:?} budget={budget}"
);
}
}
#[test]
fn gop_wire_size_adds_per_frame_and_continuation_framing() {
assert_eq!(gop_wire_size(0, 0), 0);
assert_eq!(
gop_wire_size(100, 1),
100 + MSG_HEADER_MAX + CONT_HEADER_MAX
);
assert_eq!(
gop_wire_size(1000, 100),
1000 + 100 * MSG_HEADER_MAX + CONT_HEADER_MAX
);
let payload = OUTBOUND_CHUNK_SIZE * 3 + 1;
assert_eq!(
gop_wire_size(payload, 1),
payload + MSG_HEADER_MAX + 4 * CONT_HEADER_MAX
);
}
#[test]
fn join_burst_oversized_metadata_trims_gops_below_the_warning_threshold() {
const GOP_MARKER: &[u8] = &[0x17, 0x01, 0xC1];
let warn = crate::rtmp::write_queue::QUEUE_WARN_BYTES;
let mut channel = MediaChannel::new(10);
channel.metadata = Some(Rc::new(StreamMetadata {
video_width: None,
video_height: None,
video_codec_id: Some(7),
video_frame_rate: None,
video_bitrate_kbps: None,
audio_codec_id: None,
audio_bitrate_kbps: None,
audio_sample_rate: None,
audio_channels: None,
audio_is_stereo: None,
encoder: Some("x".repeat(60_000)),
}));
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
let mut idr = vec![0u8; 950 * 1024];
idr[0] = 0x17;
idr[1] = 0x01;
idr[2] = 0xC1;
channel
.gops
.save_frame_data(video_frame(0, Bytes::from(idr)), true);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert!(
!out.iter().any(|p| packet_contains(p, GOP_MARKER)),
"the oversized-metadata prefix must trim the GOP that the raw budget would have kept"
);
assert!(
serialized_burst_len(&out) <= warn,
"the real serialized burst must stay within the Warning threshold ({} <= {})",
serialized_burst_len(&out),
warn
);
}
#[test]
fn join_burst_many_small_frames_framing_trims_older_gops() {
const OLD_IDR: &[u8] = &[0x17, 0x01, 0xD1];
const NEW_IDR: &[u8] = &[0x17, 0x01, 0xD2];
let warn = crate::rtmp::write_queue::QUEUE_WARN_BYTES;
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
channel.audio_sequence_header = Some(Bytes::from_static(AUDIO_SEQ));
let idr = |marker: u8| {
let mut d = vec![0u8; 150];
d[0] = 0x17;
d[1] = 0x01;
d[2] = marker;
Bytes::from(d)
};
let delta = || {
let mut d = vec![0u8; 150];
d[0] = 0x27;
d[1] = 0x01;
Bytes::from(d)
};
channel
.gops
.save_frame_data(video_frame(0, idr(0xD1)), true);
for i in 0..2999u32 {
channel
.gops
.save_frame_data(video_frame(i + 1, delta()), false);
}
channel
.gops
.save_frame_data(video_frame(3000, idr(0xD2)), true);
for i in 0..2999u32 {
channel
.gops
.save_frame_data(video_frame(3001 + i, delta()), false);
}
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert!(
out.iter().any(|p| packet_contains(p, NEW_IDR)),
"the newest GOP must be replayed"
);
assert!(
!out.iter().any(|p| packet_contains(p, OLD_IDR)),
"framing must trim the older GOP even though its payload alone fits"
);
assert!(
serialized_burst_len(&out) <= warn,
"the real serialized burst must stay within the Warning threshold"
);
}
#[test]
fn join_burst_oversized_sequence_header_replays_zero_gops() {
const GOP_MARKER: &[u8] = &[0x17, 0x01, 0xE1];
let mut channel = MediaChannel::new(10);
let mut header = vec![0u8; 2 * 1024 * 1024];
header[0] = 0x17;
header[1] = 0x00;
channel.video_sequence_header = Some(Bytes::from(header));
let mut idr = vec![0u8; 4096];
idr[0] = 0x17;
idr[1] = 0x01;
idr[2] = 0xE1;
channel
.gops
.save_frame_data(video_frame(0, Bytes::from(idr)), true);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert!(
!out.iter().any(|p| packet_contains(p, GOP_MARKER)),
"a prefix that exhausts the budget must replay zero GOPs"
);
assert!(
!client.has_received_video_keyframe,
"no IDR was replayed, so the keyframe gate must stay closed"
);
}
#[test]
fn join_burst_counts_the_accept_prefix_against_the_gop_budget() {
const GOP_MARKER: &[u8] = &[0x17, 0x01, 0xF1];
let warn = crate::rtmp::write_queue::QUEUE_WARN_BYTES;
let make_channel = || {
let mut channel = MediaChannel::new(10);
channel.video_sequence_header = Some(Bytes::from_static(VIDEO_SEQ));
let mut idr = vec![0u8; 950 * 1024];
idr[0] = 0x17;
idr[1] = 0x01;
idr[2] = 0xF1;
channel
.gops
.save_frame_data(video_frame(0, Bytes::from(idr)), true);
channel
};
let channel = make_channel();
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, 0, &mut out);
assert!(
out.iter().any(|p| packet_contains(p, GOP_MARKER)),
"the GOP fits the budget when no accept prefix is charged"
);
let channel = make_channel();
let accept_prefix = 64 * 1024;
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(&channel, &mut client, 7, 1, accept_prefix, &mut out);
assert!(
!out.iter().any(|p| packet_contains(p, GOP_MARKER)),
"the accept prefix must trim the GOP the raw budget would have kept"
);
assert!(
accept_prefix + serialized_burst_len(&out) <= warn,
"accept prefix + burst must stay within the Warning threshold ({} + {} <= {})",
accept_prefix,
serialized_burst_len(&out),
warn
);
}
#[test]
fn join_replay_prefix_sums_backlog_prefix_and_accept_bytes() {
let accept = vec![ServerSessionResult::OutboundResponse(Packet {
bytes: vec![0u8; 40],
can_be_dropped: false,
})];
assert_eq!(join_replay_prefix_bytes(500, 350, &accept), 500 + 350 + 40);
assert_eq!(join_replay_prefix_bytes(0, 350, &[]), 350);
assert_eq!(join_replay_prefix_bytes(0, 0, &[]), 0);
assert_eq!(
join_replay_prefix_bytes(usize::MAX, 350, &accept),
usize::MAX
);
}
#[test]
fn serving_prefix_scan_is_incremental_and_targeted() {
let mut scheduler = RtmpScheduler::new(10);
let target = 7usize;
let other = 9usize;
let outbound = |conn: usize, n: usize| ServerResult::OutboundPacket {
target_connection_id: conn,
packet: Packet {
bytes: vec![0u8; n],
can_be_dropped: false,
},
is_keyframe: false,
is_sequence_header: false,
is_video: false,
};
let mut results = vec![
outbound(target, 100),
outbound(other, 999),
outbound(target, 50),
];
assert_eq!(scheduler.advance_serving_prefix(&results, target), 150);
assert_eq!(
scheduler.serving_prefix_scan_pos, 3,
"all three entries consumed exactly once"
);
results.push(outbound(target, 200));
assert_eq!(scheduler.advance_serving_prefix(&results, target), 350);
assert_eq!(
scheduler.serving_prefix_scan_pos, 4,
"only the newly-appended entry is consumed"
);
assert_eq!(scheduler.advance_serving_prefix(&results, target), 350);
assert_eq!(scheduler.serving_prefix_scan_pos, 4);
}
#[test]
fn abort_publisher_watchers_disconnects_every_watcher() {
let mut scheduler = RtmpScheduler::new(10);
let stream_key = "live".to_string();
let publisher_connection_id = 1;
let watcher_connection_id = 2;
assert!(scheduler.new_channel(stream_key.clone(), publisher_connection_id));
let _ = scheduler.bytes_received(watcher_connection_id, &[]);
let mut sink = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
stream_key.clone(),
1,
&mut sink,
);
assert_eq!(
scheduler
.channels
.get(&stream_key)
.unwrap()
.watching_client_ids
.len(),
1,
"watcher must be registered before the abort"
);
let results = scheduler.abort_publisher_watchers(publisher_connection_id);
assert!(
results.iter().any(|r| matches!(
r,
ServerResult::DisconnectConnection { connection_id }
if *connection_id == watcher_connection_id
)),
"the aborted publisher's watcher must receive a Disconnect, not be orphaned"
);
}
#[test]
fn oversized_sequence_header_size_gate_predicate() {
let over = |first: u8, second: u8| {
let mut d = vec![0u8; MAX_CACHEABLE_SEQUENCE_HEADER_BYTES + 1];
d[0] = first;
d[1] = second;
Bytes::from(d)
};
assert!(!is_oversized_sequence_header(
&Bytes::from_static(VIDEO_SEQ),
ReceivedDataType::Video
));
assert!(!is_oversized_sequence_header(
&Bytes::from_static(AUDIO_SEQ),
ReceivedDataType::Audio
));
assert!(is_oversized_sequence_header(
&over(0x17, 0x00),
ReceivedDataType::Video
));
assert!(is_oversized_sequence_header(
&over(0xaf, 0x00),
ReceivedDataType::Audio
));
assert!(!is_oversized_sequence_header(
&over(0x17, 0x01),
ReceivedDataType::Video
));
let mut at_cap = vec![0u8; MAX_CACHEABLE_SEQUENCE_HEADER_BYTES];
at_cap[0] = 0x17;
at_cap[1] = 0x00;
assert!(!is_oversized_sequence_header(
&Bytes::from(at_cap),
ReceivedDataType::Video
));
}
#[test]
fn oversized_sequence_header_error_flags_only_oversized_media() {
let media = |video: bool, first: u8, second: u8, len: usize| {
let mut d = vec![0u8; len];
if len >= 2 {
d[0] = first;
d[1] = second;
}
let data = Bytes::from(d);
let timestamp = RtmpTimestamp { value: 0 };
if video {
ServerSessionEvent::VideoDataReceived {
app_name: "a".to_string(),
stream_key: "k".to_string(),
data,
timestamp,
}
} else {
ServerSessionEvent::AudioDataReceived {
app_name: "a".to_string(),
stream_key: "k".to_string(),
data,
timestamp,
}
}
};
let over = MAX_CACHEABLE_SEQUENCE_HEADER_BYTES + 1;
assert!(oversized_sequence_header_error(&media(true, 0x17, 0x00, over)).is_some());
assert!(oversized_sequence_header_error(&media(false, 0xaf, 0x00, over)).is_some());
assert!(oversized_sequence_header_error(&media(true, 0x17, 0x01, over)).is_none());
assert!(oversized_sequence_header_error(&media(true, 0x17, 0x00, 64)).is_none());
assert!(
oversized_sequence_header_error(&ServerSessionEvent::PublishStreamFinished {
app_name: "a".to_string(),
stream_key: "k".to_string(),
})
.is_none()
);
}
#[test]
fn oversized_sequence_header_rejected_at_ingest_not_cached() {
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
assert!(scheduler.new_channel("live".to_string(), publisher_conn));
let mut header = vec![0u8; MAX_CACHEABLE_SEQUENCE_HEADER_BYTES + 1];
header[0] = 0x17;
header[1] = 0x00;
let mut results = Vec::new();
let outcome = scheduler.handle_raised_event(
usize::MAX,
ServerSessionEvent::VideoDataReceived {
app_name: "app".to_string(),
stream_key: "live".to_string(),
data: Bytes::from(header),
timestamp: RtmpTimestamp { value: 1 },
},
&mut results,
);
assert!(
matches!(outcome, Err(SchedulerError::OversizedSequenceHeader { .. })),
"an over-cap sequence header must abort the publisher at ingest"
);
assert!(
results.is_empty(),
"a rejected header must not be cached or fanned out"
);
assert!(
scheduler.channel_video_sequence_header("live").is_none(),
"an over-cap sequence header must never enter the cache"
);
assert_eq!(
scheduler.channel_frozen_gop_count("live"),
0,
"an over-cap header must not be saved into the GOP cache either"
);
}
#[test]
fn codec_config_change_clears_stale_gops_from_the_replay() {
const VIDEO_SEQ_A: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
const VIDEO_SEQ_B: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x2a];
const IDR_A1: &[u8] = &[0x17, 0x01, 0xa1, 0x00, 0x00];
const IDR_A2: &[u8] = &[0x17, 0x01, 0xa2, 0x00, 0x00];
const IDR_B: &[u8] = &[0x17, 0x01, 0xb1, 0x00, 0x00];
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
assert!(scheduler.new_channel("live".to_string(), publisher_conn));
feed_media(&mut scheduler, publisher_conn, 0x09, 0, VIDEO_SEQ_A);
feed_media(&mut scheduler, publisher_conn, 0x09, 33, IDR_A1);
feed_media(&mut scheduler, publisher_conn, 0x09, 66, IDR_A2);
assert!(
scheduler.channels.get("live").unwrap().gops.frozen_count() >= 1,
"a GOP-A must be frozen before the config change"
);
feed_media(&mut scheduler, publisher_conn, 0x09, 99, VIDEO_SEQ_B);
feed_media(&mut scheduler, publisher_conn, 0x09, 132, IDR_B);
let channel = scheduler.channels.get("live").unwrap();
assert_eq!(
channel.video_sequence_header.as_deref(),
Some(VIDEO_SEQ_B),
"the latest header must be cached"
);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(channel, &mut client, 7, 1, 0, &mut out);
assert!(
out.iter().any(|p| packet_contains(p, IDR_B)),
"the new-config IDR must be replayable to a joiner"
);
assert!(
!out.iter().any(|p| packet_contains(p, &[0x17, 0x01, 0xa1])),
"the old-config IDR A1 must have been cleared on the header change"
);
assert!(
!out.iter().any(|p| packet_contains(p, &[0x17, 0x01, 0xa2])),
"the old-config IDR A2 must have been cleared on the header change"
);
}
#[test]
fn identical_sequence_header_resend_keeps_the_gop_cache() {
const IDR_1: &[u8] = &[0x17, 0x01, 0x51, 0x00, 0x00];
const IDR_2: &[u8] = &[0x17, 0x01, 0x52, 0x00, 0x00];
let mut scheduler = RtmpScheduler::new(10);
let publisher_conn = 100;
assert!(scheduler.new_channel("live".to_string(), publisher_conn));
feed_media(&mut scheduler, publisher_conn, 0x09, 0, VIDEO_SEQ);
feed_media(&mut scheduler, publisher_conn, 0x09, 33, IDR_1);
feed_media(&mut scheduler, publisher_conn, 0x09, 66, IDR_2);
let frozen_before = scheduler.channels.get("live").unwrap().gops.frozen_count();
assert!(frozen_before >= 1);
feed_media(&mut scheduler, publisher_conn, 0x09, 99, VIDEO_SEQ);
let channel = scheduler.channels.get("live").unwrap();
assert_eq!(
channel.gops.frozen_count(),
frozen_before,
"an identical header resend must not clear the GOP cache"
);
let mut client = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(channel, &mut client, 7, 1, 0, &mut out);
assert!(
out.iter().any(|p| packet_contains(p, &[0x17, 0x01, 0x51])),
"the cached IDR must survive an identical header resend"
);
}
#[test]
fn publisher_end_resets_channel_so_same_key_reuse_replays_only_the_new_session() {
const IDR_A: &[u8] = &[0x17, 0x01, 0xa1, 0x00, 0x00];
const IDR_B: &[u8] = &[0x17, 0x01, 0xb1, 0x00, 0x00];
let mut scheduler = RtmpScheduler::new(10);
let publisher_a = 100;
let publisher_b = 101;
let lingering_watcher = 2;
assert!(scheduler.new_channel("live".to_string(), publisher_a));
play(&mut scheduler, lingering_watcher, "live");
feed_media(&mut scheduler, publisher_a, 0x09, 0, VIDEO_SEQ);
feed_media(&mut scheduler, publisher_a, 0x09, 33, IDR_A);
assert_eq!(
scheduler.channel_video_sequence_header("live").as_deref(),
Some(VIDEO_SEQ),
"publisher A's header must be cached before it ends"
);
scheduler.notify_publisher_closed(publisher_a);
assert!(
scheduler.channels.contains_key("live"),
"the lingering watcher must keep the channel alive"
);
assert!(
scheduler.channel_video_sequence_header("live").is_none(),
"publisher-end must clear the cached sequence header"
);
assert!(scheduler.new_channel("live".to_string(), publisher_b));
feed_media(&mut scheduler, publisher_b, 0x09, 0, VIDEO_SEQ);
feed_media(&mut scheduler, publisher_b, 0x09, 33, IDR_B);
let channel = scheduler.channels.get("live").unwrap();
let mut joiner = make_watching_client(7, "live", 1);
let mut out = Vec::new();
build_join_burst(channel, &mut joiner, 7, 1, 0, &mut out);
assert!(
out.iter().any(|p| packet_contains(p, IDR_B)),
"the new session's IDR must be replayable"
);
assert!(
!out.iter().any(|p| packet_contains(p, IDR_A)),
"the previous session's IDR must never leak into the new session"
);
}
}