mod entry;
mod events;
mod fanout;
mod join;
#[cfg(test)]
mod tests;
use crate::flv::flv_tag_body::{is_audio_sequence_header, is_video_sequence_header};
use crate::rtmp::gop::Gops;
use bytes::Bytes;
use rml_rtmp::chunk_io::{ChunkSerializationError, ChunkSerializer, Packet};
use rml_rtmp::messages::{MessageSerializationError, RtmpMessage};
use rml_rtmp::sessions::StreamMetadata;
use rml_rtmp::sessions::{ServerSession, ServerSessionError, ServerSessionEvent};
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 OUTBOUND_CHUNK_SIZE: usize = 4096;
const MAX_CACHEABLE_SEQUENCE_HEADER_BYTES: usize = 256 * 1024;
enum ClientAction {
Waiting,
Publishing(Rc<str>),
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,
fanout_serializer: ChunkSerializer,
}
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),
fanout_serializer: new_media_serializer(),
}
}
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,
bytes: Bytes,
can_be_dropped: bool,
is_keyframe: bool,
is_sequence_header: bool,
is_video: bool,
},
}
impl ServerResult {
fn outbound(
target_connection_id: usize,
packet: Packet,
is_keyframe: bool,
is_sequence_header: bool,
is_video: bool,
) -> ServerResult {
ServerResult::OutboundPacket {
target_connection_id,
bytes: Bytes::from(packet.bytes),
can_be_dropped: packet.can_be_dropped,
is_keyframe,
is_sequence_header,
is_video,
}
}
}
fn new_media_serializer() -> ChunkSerializer {
let mut serializer = ChunkSerializer::new();
serializer
.set_max_chunk_size(OUTBOUND_CHUNK_SIZE as u32, RtmpTimestamp { value: 0 })
.expect("4096 is a valid outbound chunk size");
serializer
}
#[derive(Debug)]
enum MediaSerializeError {
Message(#[allow(dead_code)] MessageSerializationError),
Chunk(#[allow(dead_code)] ChunkSerializationError),
}
fn serialize_media(
serializer: &mut ChunkSerializer,
data_type: ReceivedDataType,
stream_id: u32,
data: Bytes,
timestamp: RtmpTimestamp,
can_be_dropped: bool,
) -> Result<Packet, MediaSerializeError> {
let message = match data_type {
ReceivedDataType::Audio => RtmpMessage::AudioData { data },
ReceivedDataType::Video => RtmpMessage::VideoData { data },
};
let payload = message
.into_message_payload(timestamp, stream_id)
.map_err(MediaSerializeError::Message)?;
serializer
.serialize(&payload, false, can_be_dropped)
.map_err(MediaSerializeError::Chunk)
}
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,
}
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
}
}