use super::*;
#[test]
fn ping_watcher_builds_a_ping_request_only_for_watching_clients() {
use rml_rtmp::chunk_io::ChunkDeserializer;
use rml_rtmp::messages::{MessagePayload, RtmpMessage, UserControlEventType};
let mut scheduler = RtmpScheduler::new(1);
assert!(scheduler.ping_watcher(9).is_none());
let waiting_connection_id = 5;
let _ = scheduler.bytes_received(waiting_connection_id, &[]);
assert!(scheduler.ping_watcher(waiting_connection_id).is_none());
let publisher_connection_id = 6;
let _ = scheduler.bytes_received(publisher_connection_id, &[]);
let publisher_client_id = *scheduler
.connection_to_client_map
.get(&publisher_connection_id)
.unwrap();
scheduler
.clients
.get_mut(publisher_client_id)
.unwrap()
.current_action = ClientAction::Publishing {
stream_key: Rc::from("ping_stream"),
channel: ChannelHandle {
index: 0,
generation: 0,
},
};
assert!(scheduler.ping_watcher(publisher_connection_id).is_none());
fn drain_messages(deserializer: &mut ChunkDeserializer, bytes: &[u8]) -> Vec<MessagePayload> {
let mut messages = Vec::new();
let mut next = deserializer
.get_next_message(bytes)
.expect("valid chunk bytes");
while let Some(payload) = next {
messages.push(payload);
next = deserializer
.get_next_message(&[])
.expect("valid buffered continuation");
}
messages
}
let watcher_connection_id = 7;
let mut deserializer = ChunkDeserializer::new();
let prior = scheduler
.bytes_received(watcher_connection_id, &[])
.expect("create the watcher client");
let mut results = Vec::new();
scheduler.handle_play_requested(
watcher_connection_id,
1,
"app".to_string(),
"ping_stream".to_string(),
1,
&mut results,
);
for result in prior.into_iter().chain(results) {
if let ServerResult::OutboundPacket {
target_connection_id,
bytes,
..
} = result
{
if target_connection_id == watcher_connection_id {
drain_messages(&mut deserializer, &bytes);
}
}
}
let packet = scheduler
.ping_watcher(watcher_connection_id)
.expect("a watching client must be pingable");
let messages = drain_messages(&mut deserializer, &packet.bytes);
assert_eq!(messages.len(), 1, "the ping packet is one message");
let message = messages
.into_iter()
.next()
.expect("length asserted above")
.to_rtmp_message()
.expect("decode the ping payload");
assert!(
matches!(
message,
RtmpMessage::UserControl {
event_type: UserControlEventType::PingRequest,
..
}
),
"the built packet must be a UserControl PingRequest, got {message:?}"
);
}
#[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 mut server_results = Vec::new();
let result = scheduler.publish_bytes_received(
nonexistent_connection_id,
vec![0x03],
&mut server_results,
);
assert!(result.is_ok());
assert!(server_results.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,
timestamp,
data,
ReceivedDataType::Video,
&mut server_results,
);
assert!(server_results.is_empty());
assert!(!scheduler.channels.contains_key(&stream_key));
}
#[test]
fn watcher_set_capacity_shrinks_after_watcher_churn() {
let mut scheduler = RtmpScheduler::new(1);
let stream_key = "shrink_stream";
let publisher_connection_id = 1;
assert!(scheduler.new_channel(stream_key.to_string(), publisher_connection_id));
let first_watcher = 100;
let watcher_count = 512;
for connection_id in first_watcher..first_watcher + watcher_count {
play(&mut scheduler, connection_id, stream_key);
}
let channel = scheduler.channels.get(stream_key).unwrap();
assert_eq!(channel.watching_client_ids.len(), watcher_count);
let peak_capacity = channel.watching_client_ids.capacity();
assert!(
peak_capacity > WATCHER_SET_SHRINK_MIN_CAPACITY,
"512 watchers must grow the table past the shrink floor, got {peak_capacity}"
);
let survivor_connection_id = first_watcher;
for connection_id in first_watcher + 1..first_watcher + watcher_count {
scheduler.notify_connection_closed(connection_id);
}
let channel = scheduler.channels.get(stream_key).unwrap();
assert_eq!(channel.watching_client_ids.len(), 1);
let decayed_capacity = channel.watching_client_ids.capacity();
assert!(
decayed_capacity < WATCHER_SET_SHRINK_MIN_CAPACITY,
"decayed watcher set must shrink below the floor, got {decayed_capacity}"
);
let results = feed_video(&mut scheduler, stream_key, 0, KEYFRAME);
assert!(
results.iter().any(|result| matches!(
result,
ServerResult::OutboundPacket {
target_connection_id,
..
} if *target_connection_id == survivor_connection_id
)),
"survivor must still receive the keyframe after the shrink"
);
}
#[test]
fn stale_handle_after_republish_is_rejected_without_touching_the_new_channel() {
let mut scheduler = RtmpScheduler::new(10);
assert!(scheduler.new_channel("live".to_string(), 100));
let old_handle = scheduler
.channels
.handle_by_key("live")
.expect("channel exists while published");
scheduler.notify_publisher_closed(100);
assert!(!scheduler.channels.contains_key("live"));
assert!(scheduler.new_channel("live".to_string(), 101));
let new_handle = scheduler
.channels
.handle_by_key("live")
.expect("republished channel exists");
assert_eq!(
old_handle.index, new_handle.index,
"the freed slot must be reused for the ABA scenario to be exercised"
);
assert_ne!(
old_handle.generation, new_handle.generation,
"a republish must never revalidate handles from the previous incarnation"
);
play(&mut scheduler, 2, "live");
let results = feed_media(&mut scheduler, 101, 0x09, 0, KEYFRAME);
assert_eq!(
results.len(),
1,
"the new publisher's keyframe reaches the watcher"
);
let frames_before = scheduler
.channels
.get("live")
.unwrap()
.gops
.current_frames()
.len();
let mut stale_results = Vec::new();
scheduler.distribute_media(
old_handle,
RtmpTimestamp { value: 33 },
Bytes::from_static(DELTA),
ReceivedDataType::Video,
&mut stale_results,
);
assert!(
stale_results.is_empty(),
"a stale handle must distribute nothing"
);
assert_eq!(
scheduler
.channels
.get("live")
.unwrap()
.gops
.current_frames()
.len(),
frames_before,
"a stale handle must not mutate the live channel's GOP cache"
);
let results = feed_media(&mut scheduler, 101, 0x09, 66, DELTA);
assert_eq!(
results.len(),
1,
"the new publisher keeps flowing after the stale attempt"
);
}
#[test]
fn distribute_after_full_teardown_matches_the_missing_key_path() {
let mut scheduler = RtmpScheduler::new(10);
assert!(scheduler.new_channel("live".to_string(), 100));
play(&mut scheduler, 2, "live");
let handle = scheduler
.channels
.handle_by_key("live")
.expect("channel exists while published");
assert_eq!(
feed_media(&mut scheduler, 100, 0x09, 0, KEYFRAME).len(),
1,
"the live channel delivers before teardown"
);
scheduler.notify_publisher_closed(100);
assert!(
scheduler.channels.contains_key("live"),
"the lingering watcher must keep the channel alive"
);
scheduler.notify_connection_closed(2);
assert!(!scheduler.channels.contains_key("live"));
let mut handle_results = Vec::new();
scheduler.distribute_media(
handle,
RtmpTimestamp { value: 33 },
Bytes::from_static(KEYFRAME),
ReceivedDataType::Video,
&mut handle_results,
);
let mut key_results = Vec::new();
scheduler.handle_audio_video_data_received(
"live",
RtmpTimestamp { value: 33 },
Bytes::from_static(KEYFRAME),
ReceivedDataType::Video,
&mut key_results,
);
assert!(
handle_results.is_empty(),
"a handle to a torn-down channel must distribute nothing"
);
assert!(
key_results.is_empty(),
"the removed key must distribute nothing on the string path"
);
assert!(
!scheduler.channels.contains_key("live"),
"neither path may resurrect the channel"
);
}