1use broadcast_common::Parse;
64
65use crate::RtmpError;
66use crate::amf0::{Amf0Value, Command};
67use crate::chunk::{ChunkAssembler, ChunkWriter, Message};
68use crate::handshake::Handshake;
69use crate::message::{LimitType, ProtocolControl, UserControl, msg_type};
70
71type Result<T> = core::result::Result<T, RtmpError>;
72
73pub const DEFAULT_CHUNK_SIZE: u32 = 4096;
79pub const DEFAULT_WINDOW_ACK_SIZE: u32 = 2_500_000;
82pub const DEFAULT_PEER_BANDWIDTH: u32 = 2_500_000;
84
85const FIRST_STREAM_ID: u32 = 1;
89
90const COMMAND_CHUNK_STREAM_ID: u32 = 3;
94
95const FMS_VERSION: &str = "FMS/3,0,1,123";
101const CAPABILITIES: f64 = 31.0;
104
105const FLV_SIGNATURE: [u8; 3] = *b"FLV";
109const FLV_VERSION: u8 = 1;
111const FLV_TYPE_FLAGS_AUDIO_VIDEO: u8 = 0b0000_0101;
115const FLV_HEADER_SIZE: u32 = 9;
117const FLV_TAG_HEADER_LEN: usize = 11;
120const FLV_PREV_TAG_SIZE_LEN: usize = 4;
123const FLV_MAX_DATA_SIZE: usize = 0x00FF_FFFF;
125
126#[non_exhaustive]
132#[derive(Debug, Clone)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct ServerConfig {
135 pub chunk_size: u32,
137 pub window_ack_size: u32,
141 pub peer_bandwidth: u32,
143 pub expected_stream_key: Option<String>,
148}
149
150impl Default for ServerConfig {
151 fn default() -> Self {
152 Self {
153 chunk_size: DEFAULT_CHUNK_SIZE,
154 window_ack_size: DEFAULT_WINDOW_ACK_SIZE,
155 peer_bandwidth: DEFAULT_PEER_BANDWIDTH,
156 expected_stream_key: None,
157 }
158 }
159}
160
161impl ServerConfig {
162 #[must_use]
167 pub fn with_expected_stream_key(mut self, expected_stream_key: Option<String>) -> Self {
168 self.expected_stream_key = expected_stream_key;
169 self
170 }
171
172 #[must_use]
174 pub fn with_chunk_size(mut self, chunk_size: u32) -> Self {
175 self.chunk_size = chunk_size;
176 self
177 }
178
179 #[must_use]
181 pub fn with_window_ack_size(mut self, window_ack_size: u32) -> Self {
182 self.window_ack_size = window_ack_size;
183 self
184 }
185
186 #[must_use]
188 pub fn with_peer_bandwidth(mut self, peer_bandwidth: u32) -> Self {
189 self.peer_bandwidth = peer_bandwidth;
190 self
191 }
192}
193
194#[non_exhaustive]
196#[derive(Debug, Clone, PartialEq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198pub enum ServerEvent {
199 Connected {
201 app: String,
203 },
204 Publish {
207 app: String,
209 stream_key: String,
211 stream_id: u32,
214 },
215 Media {
221 flv: Vec<u8>,
224 },
225 Eof,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232enum State {
233 Init,
235 Connected,
238 Publishing,
241 Closed,
243}
244
245#[derive(Debug)]
250pub struct ServerSession {
251 config: ServerConfig,
252 handshake: Handshake,
253 handshake_buf: Vec<u8>,
257 assembler: ChunkAssembler,
258 writer: ChunkWriter,
259 state: State,
260 app: Option<String>,
261 next_stream_id: u32,
262 created_stream_id: Option<u32>,
267 ack_threshold: u32,
271 bytes_received: u64,
273 bytes_acked: u64,
275 flv_header_sent: bool,
278}
279
280impl ServerSession {
281 #[must_use]
283 pub fn new(config: ServerConfig) -> Self {
284 let ack_threshold = config.window_ack_size;
285 Self {
286 config,
287 handshake: Handshake::new(),
288 handshake_buf: Vec::new(),
289 assembler: ChunkAssembler::new(),
290 writer: ChunkWriter::new(),
291 state: State::Init,
292 app: None,
293 next_stream_id: FIRST_STREAM_ID,
294 created_stream_id: None,
295 ack_threshold,
296 bytes_received: 0,
297 bytes_acked: 0,
298 flv_header_sent: false,
299 }
300 }
301
302 #[must_use]
304 pub fn with_defaults() -> Self {
305 Self::new(ServerConfig::default())
306 }
307
308 pub fn handle_data(&mut self, input: &[u8]) -> Result<(Vec<u8>, Vec<ServerEvent>)> {
322 let mut out = Vec::new();
323 let mut events = Vec::new();
324
325 let chunk_input = match self.drive_handshake(input, &mut out)? {
326 Some(bytes) => bytes,
327 None => return Ok((out, events)),
328 };
329
330 self.bytes_received = self.bytes_received.saturating_add(chunk_input.len() as u64);
331
332 self.assembler.feed(&chunk_input);
343 while let Some(msg) = self.assembler.next_message()? {
344 self.dispatch_message(&msg, &mut out, &mut events)?;
345 }
346
347 self.maybe_ack(&mut out);
348
349 Ok((out, events))
350 }
351
352 fn drive_handshake(&mut self, input: &[u8], out: &mut Vec<u8>) -> Result<Option<Vec<u8>>> {
358 if self.handshake.is_done() {
359 return Ok(Some(input.to_vec()));
360 }
361
362 self.handshake_buf.extend_from_slice(input);
363 loop {
364 match self.handshake.read(&self.handshake_buf) {
365 Ok((reply, consumed, done)) => {
366 out.extend_from_slice(&reply);
367 self.handshake_buf.drain(..consumed);
368 if done {
369 break;
370 }
371 }
372 Err(RtmpError::BufferTooShort { .. }) => break,
373 Err(e) => return Err(e),
374 }
375 }
376
377 if self.handshake.is_done() {
378 Ok(Some(core::mem::take(&mut self.handshake_buf)))
379 } else {
380 Ok(None)
381 }
382 }
383
384 fn maybe_ack(&mut self, out: &mut Vec<u8>) {
388 let threshold = u64::from(self.ack_threshold.max(1));
389 if self.bytes_received.saturating_sub(self.bytes_acked) >= threshold {
390 self.bytes_acked = self.bytes_received;
391 let seq = self.bytes_received as u32;
392 let ack_msg = ProtocolControl::Acknowledgement(seq).to_message();
393 out.extend_from_slice(&self.writer.write(&ack_msg));
394 }
395 }
396
397 fn dispatch_message(
399 &mut self,
400 msg: &Message,
401 out: &mut Vec<u8>,
402 events: &mut Vec<ServerEvent>,
403 ) -> Result<()> {
404 if let Some(pc) = ProtocolControl::from_message(msg)? {
405 self.handle_protocol_control(pc);
406 return Ok(());
407 }
408
409 match msg.message_type_id {
410 msg_type::USER_CONTROL => {
411 let _ = UserControl::parse(&msg.payload);
416 Ok(())
417 }
418 msg_type::COMMAND_AMF0 => {
419 let command = Command::parse(&msg.payload)?;
420 self.handle_command(&command, msg, out, events)
421 }
422 msg_type::AUDIO | msg_type::VIDEO | msg_type::DATA_AMF0 => {
423 self.emit_media_if_publishing(msg.message_type_id, msg, events)
424 }
425 _ => Ok(()),
430 }
431 }
432
433 fn handle_protocol_control(&mut self, pc: ProtocolControl) {
437 match pc {
438 ProtocolControl::SetChunkSize(n) => self.assembler.set_chunk_size(n),
439 ProtocolControl::WindowAckSize(w) => self.ack_threshold = w,
440 ProtocolControl::SetPeerBandwidth {
441 ack_window_size, ..
442 } => self.ack_threshold = ack_window_size,
443 ProtocolControl::Abort { .. } | ProtocolControl::Acknowledgement(_) => {}
444 }
445 }
446
447 fn handle_command(
449 &mut self,
450 command: &Command,
451 msg: &Message,
452 out: &mut Vec<u8>,
453 events: &mut Vec<ServerEvent>,
454 ) -> Result<()> {
455 match command.name.as_str() {
456 "connect" => self.handle_connect(command, msg, out, events),
457 "releaseStream" | "FCPublish" => {
458 self.reply_result(command, msg, vec![Amf0Value::Undefined], out);
459 Ok(())
460 }
461 "createStream" => self.handle_create_stream(command, msg, out),
462 "publish" => self.handle_publish(command, msg, out, events),
463 "deleteStream" | "FCUnpublish" => {
464 self.state = State::Closed;
465 events.push(ServerEvent::Eof);
466 Ok(())
467 }
468 _ => Ok(()),
471 }
472 }
473
474 fn handle_connect(
486 &mut self,
487 command: &Command,
488 msg: &Message,
489 out: &mut Vec<u8>,
490 events: &mut Vec<ServerEvent>,
491 ) -> Result<()> {
492 if self.state == State::Closed {
493 return Err(RtmpError::UnexpectedState {
494 what: "connect received after the session was closed",
495 });
496 }
497
498 let Some(Amf0Value::Object(pairs)) = command.arguments.first() else {
499 return Err(RtmpError::Malformed {
500 what: "connect command object / app",
501 });
502 };
503 let Some(app) = pairs.iter().find_map(|(k, v)| {
504 if k == "app" {
505 match v {
506 Amf0Value::String(s) => Some(s.clone()),
507 _ => None,
508 }
509 } else {
510 None
511 }
512 }) else {
513 return Err(RtmpError::Malformed {
514 what: "connect command object / app",
515 });
516 };
517
518 self.app = Some(app.clone());
519 self.state = State::Connected;
520
521 let window_ack = ProtocolControl::WindowAckSize(self.config.window_ack_size).to_message();
522 out.extend_from_slice(&self.writer.write(&window_ack));
523
524 let peer_bandwidth = ProtocolControl::SetPeerBandwidth {
525 ack_window_size: self.config.peer_bandwidth,
526 limit_type: LimitType::Dynamic,
527 }
528 .to_message();
529 out.extend_from_slice(&self.writer.write(&peer_bandwidth));
530
531 let set_chunk_size = ProtocolControl::SetChunkSize(self.config.chunk_size).to_message();
532 out.extend_from_slice(&self.writer.write(&set_chunk_size));
533 self.writer.set_chunk_size(self.config.chunk_size);
534
535 let result = Command {
536 name: "_result".to_string(),
537 transaction_id: command.transaction_id,
538 arguments: vec![
539 Amf0Value::Object(vec![
540 (
541 "fmsVer".to_string(),
542 Amf0Value::String(FMS_VERSION.to_string()),
543 ),
544 ("capabilities".to_string(), Amf0Value::Number(CAPABILITIES)),
545 ]),
546 Amf0Value::Object(vec![
547 ("level".to_string(), Amf0Value::String("status".to_string())),
548 (
549 "code".to_string(),
550 Amf0Value::String("NetConnection.Connect.Success".to_string()),
551 ),
552 (
553 "description".to_string(),
554 Amf0Value::String("Connection succeeded.".to_string()),
555 ),
556 ]),
557 ],
558 };
559 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &result)));
560
561 events.push(ServerEvent::Connected { app });
562 Ok(())
563 }
564
565 fn handle_create_stream(
573 &mut self,
574 command: &Command,
575 msg: &Message,
576 out: &mut Vec<u8>,
577 ) -> Result<()> {
578 if self.state != State::Connected {
579 return Err(RtmpError::UnexpectedState {
580 what: "createStream received before a successful connect (or after the session was closed)",
581 });
582 }
583
584 let stream_id = self.next_stream_id;
585 self.next_stream_id = self.next_stream_id.saturating_add(1);
586 self.created_stream_id = Some(stream_id);
587
588 let result = Command {
589 name: "_result".to_string(),
590 transaction_id: command.transaction_id,
591 arguments: vec![Amf0Value::Null, Amf0Value::Number(f64::from(stream_id))],
592 };
593 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &result)));
594 Ok(())
595 }
596
597 fn handle_publish(
608 &mut self,
609 command: &Command,
610 msg: &Message,
611 out: &mut Vec<u8>,
612 events: &mut Vec<ServerEvent>,
613 ) -> Result<()> {
614 if self.state != State::Connected || self.created_stream_id.is_none() {
615 return Err(RtmpError::UnexpectedState {
616 what: "publish received before a successful connect+createStream (or after the session was closed)",
617 });
618 }
619 let app = self
620 .app
621 .clone()
622 .expect("state == Connected implies app was captured by connect");
623
624 let stream_key = match command.arguments.get(1) {
625 Some(Amf0Value::String(s)) => s.clone(),
626 _ => {
627 return Err(RtmpError::Malformed {
628 what: "publish command missing its publishing-name (string) argument",
629 });
630 }
631 };
632 let stream_id = msg.message_stream_id;
633
634 if let Some(expected) = &self.config.expected_stream_key
635 && expected != &stream_key
636 {
637 let on_status = Command {
638 name: "onStatus".to_string(),
639 transaction_id: 0.0,
640 arguments: vec![
641 Amf0Value::Null,
642 Amf0Value::Object(vec![
643 ("level".to_string(), Amf0Value::String("error".to_string())),
644 (
645 "code".to_string(),
646 Amf0Value::String("NetStream.Publish.BadName".to_string()),
647 ),
648 (
649 "description".to_string(),
650 Amf0Value::String("Stream key mismatch.".to_string()),
651 ),
652 ]),
653 ],
654 };
655 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &on_status)));
656 return Ok(());
658 }
659
660 let stream_begin = UserControl::StreamBegin(stream_id).to_message();
661 out.extend_from_slice(&self.writer.write(&stream_begin));
662
663 let on_status = Command {
664 name: "onStatus".to_string(),
665 transaction_id: 0.0,
666 arguments: vec![
667 Amf0Value::Null,
668 Amf0Value::Object(vec![
669 ("level".to_string(), Amf0Value::String("status".to_string())),
670 (
671 "code".to_string(),
672 Amf0Value::String("NetStream.Publish.Start".to_string()),
673 ),
674 (
675 "description".to_string(),
676 Amf0Value::String(format!("{stream_key} is now published.")),
677 ),
678 ]),
679 ],
680 };
681 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &on_status)));
682
683 self.state = State::Publishing;
684 events.push(ServerEvent::Publish {
685 app,
686 stream_key,
687 stream_id,
688 });
689 Ok(())
690 }
691
692 fn reply_result(
696 &mut self,
697 command: &Command,
698 msg: &Message,
699 arguments: Vec<Amf0Value>,
700 out: &mut Vec<u8>,
701 ) {
702 let result = Command {
703 name: "_result".to_string(),
704 transaction_id: command.transaction_id,
705 arguments,
706 };
707 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &result)));
708 }
709
710 fn command_message(&self, request: &Message, command: &Command) -> Message {
714 Message {
715 chunk_stream_id: COMMAND_CHUNK_STREAM_ID,
716 timestamp: 0,
717 message_type_id: msg_type::COMMAND_AMF0,
718 message_stream_id: request.message_stream_id,
719 payload: command.to_body(),
720 }
721 }
722
723 fn emit_media_if_publishing(
728 &mut self,
729 tag_type: u8,
730 msg: &Message,
731 events: &mut Vec<ServerEvent>,
732 ) -> Result<()> {
733 if self.state != State::Publishing {
734 return Ok(());
735 }
736
737 let mut flv = if self.flv_header_sent {
738 Vec::new()
739 } else {
740 self.flv_header_sent = true;
741 flv_file_header()
742 };
743 flv.extend(flv_tag(tag_type, msg.timestamp, &msg.payload)?);
744 events.push(ServerEvent::Media { flv });
745 Ok(())
746 }
747}
748
749fn flv_file_header() -> Vec<u8> {
752 let mut v = Vec::with_capacity(FLV_HEADER_SIZE as usize + FLV_PREV_TAG_SIZE_LEN);
753 v.extend_from_slice(&FLV_SIGNATURE);
754 v.push(FLV_VERSION);
755 v.push(FLV_TYPE_FLAGS_AUDIO_VIDEO);
756 v.extend_from_slice(&FLV_HEADER_SIZE.to_be_bytes());
757 v.extend_from_slice(&0u32.to_be_bytes());
758 v
759}
760
761fn flv_tag(tag_type: u8, timestamp: u32, payload: &[u8]) -> Result<Vec<u8>> {
768 if payload.len() > FLV_MAX_DATA_SIZE {
769 return Err(RtmpError::Unsupported {
770 what: "flv tag payload exceeds the 24-bit DataSize field",
771 });
772 }
773 let data_size = payload.len() as u32;
774
775 let mut v = Vec::with_capacity(FLV_TAG_HEADER_LEN + payload.len() + FLV_PREV_TAG_SIZE_LEN);
776 v.push(tag_type);
777 v.push((data_size >> 16) as u8);
778 v.push((data_size >> 8) as u8);
779 v.push(data_size as u8);
780 v.push((timestamp >> 16) as u8);
781 v.push((timestamp >> 8) as u8);
782 v.push(timestamp as u8);
783 v.push((timestamp >> 24) as u8);
784 v.extend_from_slice(&[0, 0, 0]); v.extend_from_slice(payload);
786 let prev_tag_size = (FLV_TAG_HEADER_LEN + payload.len()) as u32;
787 v.extend_from_slice(&prev_tag_size.to_be_bytes());
788 Ok(v)
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794 use crate::handshake::{HANDSHAKE_PACKET_LEN, RTMP_VERSION};
795 use crate::message::CONTROL_CHUNK_STREAM_ID as CTRL_CSID;
796 use broadcast_common::Serialize;
797
798 const CLIENT_CSID: u32 = 3;
801
802 fn build_c0_c1() -> Vec<u8> {
803 let mut v = vec![0u8; 1 + HANDSHAKE_PACKET_LEN];
804 v[0] = RTMP_VERSION;
805 v
806 }
807
808 fn build_c2() -> Vec<u8> {
809 vec![0u8; HANDSHAKE_PACKET_LEN]
810 }
811
812 fn command_message(
813 csid: u32,
814 stream_id: u32,
815 name: &str,
816 txn: f64,
817 args: Vec<Amf0Value>,
818 ) -> Message {
819 let body = Command {
820 name: name.to_string(),
821 transaction_id: txn,
822 arguments: args,
823 }
824 .to_body();
825 Message {
826 chunk_stream_id: csid,
827 timestamp: 0,
828 message_type_id: msg_type::COMMAND_AMF0,
829 message_stream_id: stream_id,
830 payload: body,
831 }
832 }
833
834 fn connect_bytes(app: &str) -> Vec<u8> {
835 let args = vec![Amf0Value::Object(vec![
836 ("app".to_string(), Amf0Value::String(app.to_string())),
837 (
838 "type".to_string(),
839 Amf0Value::String("nonprivate".to_string()),
840 ),
841 ])];
842 let msg = command_message(CLIENT_CSID, 0, "connect", 1.0, args);
843 ChunkWriter::new().write(&msg)
844 }
845
846 fn connect_bytes_no_args() -> Vec<u8> {
847 let msg = command_message(CLIENT_CSID, 0, "connect", 1.0, vec![]);
848 ChunkWriter::new().write(&msg)
849 }
850
851 fn create_stream_bytes() -> Vec<u8> {
852 let msg = command_message(CLIENT_CSID, 0, "createStream", 2.0, vec![Amf0Value::Null]);
853 ChunkWriter::new().write(&msg)
854 }
855
856 fn publish_bytes(stream_id: u32, stream_key: &str) -> Vec<u8> {
857 let args = vec![
858 Amf0Value::Null,
859 Amf0Value::String(stream_key.to_string()),
860 Amf0Value::String("live".to_string()),
861 ];
862 let msg = command_message(CLIENT_CSID, stream_id, "publish", 3.0, args);
863 ChunkWriter::new().write(&msg)
864 }
865
866 fn av_bytes(
867 stream_id: u32,
868 message_type_id: u8,
869 csid: u32,
870 timestamp: u32,
871 payload: Vec<u8>,
872 ) -> Vec<u8> {
873 let msg = Message {
874 chunk_stream_id: csid,
875 timestamp,
876 message_type_id,
877 message_stream_id: stream_id,
878 payload,
879 };
880 ChunkWriter::new().write(&msg)
881 }
882
883 fn decode_messages(bytes: &[u8]) -> Vec<Message> {
892 let mut assembler = ChunkAssembler::new();
893 assembler.set_chunk_size(65536);
894 assembler.push(bytes).expect("well-formed reply stream")
895 }
896
897 fn decode_commands(bytes: &[u8]) -> Vec<Command> {
898 decode_messages(bytes)
899 .iter()
900 .filter(|m| m.message_type_id == msg_type::COMMAND_AMF0)
901 .map(|m| Command::parse(&m.payload).expect("well-formed command reply"))
902 .collect()
903 }
904
905 fn onstatus_code(cmd: &Command) -> Option<String> {
906 cmd.arguments.iter().find_map(|v| match v {
907 Amf0Value::Object(pairs) => pairs.iter().find_map(|(k, v)| {
908 if k == "code" {
909 match v {
910 Amf0Value::String(s) => Some(s.clone()),
911 _ => None,
912 }
913 } else {
914 None
915 }
916 }),
917 _ => None,
918 })
919 }
920
921 fn publish_flow(
924 config: ServerConfig,
925 stream_key: &str,
926 ) -> (ServerSession, Vec<u8>, Vec<ServerEvent>) {
927 let mut session = ServerSession::new(config);
928 let mut all_out = Vec::new();
933 let mut all_events = Vec::new();
934
935 session.handle_data(&build_c0_c1()).unwrap();
936 session.handle_data(&build_c2()).unwrap();
937
938 let (out, events) = session.handle_data(&connect_bytes("live")).unwrap();
939 all_out.extend(out);
940 all_events.extend(events);
941
942 let (out, events) = session.handle_data(&create_stream_bytes()).unwrap();
943 all_out.extend(out);
944 all_events.extend(events);
945
946 let (out, events) = session.handle_data(&publish_bytes(1, stream_key)).unwrap();
948 all_out.extend(out);
949 all_events.extend(events);
950
951 (session, all_out, all_events)
952 }
953
954 #[test]
957 fn handshake_completes_and_reply_contains_s0_s1_s2() {
958 let mut session = ServerSession::with_defaults();
959 let (out1, events1) = session.handle_data(&build_c0_c1()).unwrap();
960 assert_eq!(
961 out1.len(),
962 1 + HANDSHAKE_PACKET_LEN + HANDSHAKE_PACKET_LEN,
963 "S0+S1+S2 must be a single 3073-byte reply"
964 );
965 assert!(events1.is_empty());
966
967 let (out2, events2) = session.handle_data(&build_c2()).unwrap();
968 assert!(out2.is_empty(), "C2 receipt produces no reply bytes itself");
969 assert!(events2.is_empty());
970 }
971
972 #[test]
973 fn handshake_split_across_calls_still_completes() {
974 let mut session = ServerSession::with_defaults();
975 let c0c1 = build_c0_c1();
976 let (out1, _) = session.handle_data(&c0c1[..500]).unwrap();
977 assert!(out1.is_empty(), "partial C0+C1 produces no reply yet");
978 let (out2, _) = session.handle_data(&c0c1[500..]).unwrap();
979 assert_eq!(out2.len(), 1 + 2 * HANDSHAKE_PACKET_LEN);
980 let (_out3, _) = session.handle_data(&build_c2()).unwrap();
981 }
982
983 #[test]
984 fn c2_pipelined_with_connect_chunk_in_one_call_still_parses_connect() {
985 let mut session = ServerSession::with_defaults();
992 session.handle_data(&build_c0_c1()).unwrap();
993
994 let mut pipelined = build_c2();
995 pipelined.extend_from_slice(&connect_bytes("live"));
996
997 let (_out, events) = session.handle_data(&pipelined).unwrap();
998 assert_eq!(
999 events,
1000 vec![ServerEvent::Connected {
1001 app: "live".to_string()
1002 }],
1003 "C2 pipelined with the connect chunk in one handle_data call must \
1004 still yield Connected from that call (leftover bytes must not be dropped)"
1005 );
1006 }
1007
1008 #[test]
1011 fn connect_emits_connected_event_and_result_reply() {
1012 let mut session = ServerSession::with_defaults();
1013 session.handle_data(&build_c0_c1()).unwrap();
1014 session.handle_data(&build_c2()).unwrap();
1015
1016 let (out, events) = session.handle_data(&connect_bytes("live")).unwrap();
1017 assert_eq!(
1018 events,
1019 vec![ServerEvent::Connected {
1020 app: "live".to_string()
1021 }]
1022 );
1023
1024 let commands = decode_commands(&out);
1025 assert!(
1026 commands.iter().any(|c| c.name == "_result"),
1027 "connect reply must contain a _result command"
1028 );
1029 }
1030
1031 #[test]
1032 fn connect_without_command_object_is_malformed() {
1033 let mut session = ServerSession::with_defaults();
1034 session.handle_data(&build_c0_c1()).unwrap();
1035 session.handle_data(&build_c2()).unwrap();
1036
1037 let err = session.handle_data(&connect_bytes_no_args()).unwrap_err();
1039 assert!(
1040 matches!(err, RtmpError::Malformed { .. }),
1041 "connect with no command-object argument must error, not default app to \"\""
1042 );
1043 }
1044
1045 #[test]
1048 fn connect_with_object_missing_app_key_is_malformed() {
1049 let mut session = ServerSession::with_defaults();
1050 session.handle_data(&build_c0_c1()).unwrap();
1051 session.handle_data(&build_c2()).unwrap();
1052
1053 let args = vec![Amf0Value::Object(vec![(
1055 "type".to_string(),
1056 Amf0Value::String("nonprivate".to_string()),
1057 )])];
1058 let msg = command_message(CLIENT_CSID, 0, "connect", 1.0, args);
1059 let bytes = ChunkWriter::new().write(&msg);
1060
1061 let err = session.handle_data(&bytes).unwrap_err();
1062 assert!(
1063 matches!(err, RtmpError::Malformed { .. }),
1064 "connect with object missing 'app' key must error"
1065 );
1066 }
1067
1068 #[test]
1071 fn connect_with_app_as_number_is_malformed() {
1072 let mut session = ServerSession::with_defaults();
1073 session.handle_data(&build_c0_c1()).unwrap();
1074 session.handle_data(&build_c2()).unwrap();
1075
1076 let args = vec![Amf0Value::Object(vec![
1078 ("app".to_string(), Amf0Value::Number(123.0)),
1079 (
1080 "type".to_string(),
1081 Amf0Value::String("nonprivate".to_string()),
1082 ),
1083 ])];
1084 let msg = command_message(CLIENT_CSID, 0, "connect", 1.0, args);
1085 let bytes = ChunkWriter::new().write(&msg);
1086
1087 let err = session.handle_data(&bytes).unwrap_err();
1088 assert!(
1089 matches!(err, RtmpError::Malformed { .. }),
1090 "connect with app as Number must error, not accept numeric value"
1091 );
1092 }
1093
1094 #[test]
1097 fn create_stream_replies_result_with_stream_id() {
1098 let mut session = ServerSession::with_defaults();
1099 session.handle_data(&build_c0_c1()).unwrap();
1100 session.handle_data(&build_c2()).unwrap();
1101 session.handle_data(&connect_bytes("live")).unwrap();
1102
1103 let (out, _events) = session.handle_data(&create_stream_bytes()).unwrap();
1104 let commands = decode_commands(&out);
1105 let result = commands
1106 .iter()
1107 .find(|c| c.name == "_result")
1108 .expect("createStream _result reply");
1109 assert_eq!(
1110 result.arguments.get(1),
1111 Some(&Amf0Value::Number(1.0)),
1112 "first allocated stream id must be 1"
1113 );
1114 }
1115
1116 #[test]
1117 fn create_stream_before_connect_is_unexpected_state() {
1118 let mut session = ServerSession::with_defaults();
1119 session.handle_data(&build_c0_c1()).unwrap();
1120 session.handle_data(&build_c2()).unwrap();
1121
1122 let err = session.handle_data(&create_stream_bytes()).unwrap_err();
1123 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1124 }
1125
1126 #[test]
1129 fn publish_reaches_publishing_emits_event_and_stream_begin_plus_onstatus() {
1130 let (_session, out, events) = publish_flow(ServerConfig::default(), "testkey");
1131
1132 assert!(events.contains(&ServerEvent::Publish {
1133 app: "live".to_string(),
1134 stream_key: "testkey".to_string(),
1135 stream_id: 1,
1136 }));
1137
1138 let messages = decode_messages(&out);
1139 let has_stream_begin = messages.iter().any(|m| {
1140 m.message_type_id == msg_type::USER_CONTROL
1141 && matches!(
1142 UserControl::parse(&m.payload),
1143 Ok(UserControl::StreamBegin(1))
1144 )
1145 });
1146 assert!(
1147 has_stream_begin,
1148 "publish reply must include StreamBegin(1)"
1149 );
1150
1151 let commands = decode_commands(&out);
1152 let on_status = commands
1153 .iter()
1154 .find(|c| c.name == "onStatus")
1155 .expect("onStatus reply to publish");
1156 assert_eq!(
1157 onstatus_code(on_status).as_deref(),
1158 Some("NetStream.Publish.Start")
1159 );
1160 }
1161
1162 #[test]
1163 fn publish_before_connect_is_unexpected_state() {
1164 let mut session = ServerSession::with_defaults();
1165 session.handle_data(&build_c0_c1()).unwrap();
1166 session.handle_data(&build_c2()).unwrap();
1167
1168 let err = session
1169 .handle_data(&publish_bytes(1, "testkey"))
1170 .unwrap_err();
1171 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1172 }
1173
1174 #[test]
1175 fn publish_without_create_stream_is_unexpected_state() {
1176 let mut session = ServerSession::with_defaults();
1177 session.handle_data(&build_c0_c1()).unwrap();
1178 session.handle_data(&build_c2()).unwrap();
1179 session.handle_data(&connect_bytes("live")).unwrap();
1180
1181 let err = session
1183 .handle_data(&publish_bytes(1, "testkey"))
1184 .unwrap_err();
1185 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1186 }
1187
1188 #[test]
1189 fn create_stream_after_closed_is_unexpected_state() {
1190 let mut session = ServerSession::with_defaults();
1191 session.handle_data(&build_c0_c1()).unwrap();
1192 session.handle_data(&build_c2()).unwrap();
1193 session.handle_data(&connect_bytes("live")).unwrap();
1194 session.handle_data(&create_stream_bytes()).unwrap();
1195 session.handle_data(&publish_bytes(1, "testkey")).unwrap();
1196
1197 let delete_stream = command_message(
1198 CLIENT_CSID,
1199 1,
1200 "deleteStream",
1201 4.0,
1202 vec![Amf0Value::Null, Amf0Value::Number(1.0)],
1203 );
1204 let (_out, events) = session
1205 .handle_data(&ChunkWriter::new().write(&delete_stream))
1206 .unwrap();
1207 assert_eq!(events, vec![ServerEvent::Eof]);
1208
1209 let err = session.handle_data(&create_stream_bytes()).unwrap_err();
1210 assert!(
1211 matches!(err, RtmpError::UnexpectedState { .. }),
1212 "createStream after State::Closed must be rejected, not silently re-allowed"
1213 );
1214 }
1215
1216 #[test]
1219 fn audio_and_video_emit_media_first_carries_flv_file_header() {
1220 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1221
1222 let (_out1, events1) = session
1223 .handle_data(&av_bytes(
1224 1,
1225 msg_type::AUDIO,
1226 4,
1227 0,
1228 vec![0xAF, 0x01, 0xDE, 0xAD],
1229 ))
1230 .unwrap();
1231 assert_eq!(events1.len(), 1);
1232 let ServerEvent::Media { flv } = &events1[0] else {
1233 panic!("expected Media event");
1234 };
1235 assert!(
1236 flv.starts_with(b"FLV"),
1237 "the first Media event must carry the FLV file header"
1238 );
1239
1240 let (_out2, events2) = session
1241 .handle_data(&av_bytes(
1242 1,
1243 msg_type::VIDEO,
1244 6,
1245 40,
1246 vec![0x17, 0x01, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF],
1247 ))
1248 .unwrap();
1249 assert_eq!(events2.len(), 1);
1250 let ServerEvent::Media { flv } = &events2[0] else {
1251 panic!("expected Media event");
1252 };
1253 assert!(
1254 !flv.starts_with(b"FLV"),
1255 "only the first Media event carries the file header"
1256 );
1257 }
1258
1259 #[test]
1260 fn concatenated_media_forms_structurally_valid_flv() {
1261 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1262
1263 let mut flv_stream = Vec::new();
1264 let (_out1, events1) = session
1265 .handle_data(&av_bytes(
1266 1,
1267 msg_type::AUDIO,
1268 4,
1269 0,
1270 vec![0xAF, 0x01, 1, 2, 3],
1271 ))
1272 .unwrap();
1273 let (_out2, events2) = session
1274 .handle_data(&av_bytes(
1275 1,
1276 msg_type::VIDEO,
1277 6,
1278 33,
1279 vec![0x17, 0x01, 0, 0, 0, 4, 5, 6],
1280 ))
1281 .unwrap();
1282 for e in events1.into_iter().chain(events2) {
1283 if let ServerEvent::Media { flv } = e {
1284 flv_stream.extend(flv);
1285 }
1286 }
1287
1288 assert_eq!(&flv_stream[0..3], b"FLV");
1290 assert_eq!(flv_stream[3], 1, "FLV version");
1291 assert_eq!(flv_stream[4], 0b0000_0101, "audio+video TypeFlags");
1292 assert_eq!(
1293 u32::from_be_bytes(flv_stream[5..9].try_into().unwrap()),
1294 9,
1295 "DataOffset (header size)"
1296 );
1297 assert_eq!(
1298 u32::from_be_bytes(flv_stream[9..13].try_into().unwrap()),
1299 0,
1300 "PreviousTagSize0"
1301 );
1302
1303 let tag1 = &flv_stream[13..];
1305 assert_eq!(tag1[0], msg_type::AUDIO);
1306 let data_size1 =
1307 (u32::from(tag1[1]) << 16) | (u32::from(tag1[2]) << 8) | u32::from(tag1[3]);
1308 assert_eq!(data_size1, 5);
1309 let tag1_total = FLV_TAG_HEADER_LEN + 5 + FLV_PREV_TAG_SIZE_LEN;
1310 let prev_tag_size1 = u32::from_be_bytes(
1311 flv_stream[13 + tag1_total - 4..13 + tag1_total]
1312 .try_into()
1313 .unwrap(),
1314 );
1315 assert_eq!(prev_tag_size1 as usize, FLV_TAG_HEADER_LEN + 5);
1316
1317 let tag2 = &flv_stream[13 + tag1_total..];
1319 assert_eq!(tag2[0], msg_type::VIDEO);
1320 let data_size2 =
1321 (u32::from(tag2[1]) << 16) | (u32::from(tag2[2]) << 8) | u32::from(tag2[3]);
1322 assert_eq!(data_size2, 8);
1323 assert_eq!(
1324 flv_stream.len(),
1325 13 + tag1_total + FLV_TAG_HEADER_LEN + 8 + FLV_PREV_TAG_SIZE_LEN
1326 );
1327 }
1328
1329 #[test]
1330 fn data_amf0_onmetadata_emits_media_with_script_tag_type() {
1331 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1332
1333 let mut payload = Amf0Value::String("onMetaData".to_string()).to_bytes();
1338 payload.extend(
1339 Amf0Value::Object(vec![
1340 ("width".to_string(), Amf0Value::Number(1920.0)),
1341 ("height".to_string(), Amf0Value::Number(1080.0)),
1342 ])
1343 .to_bytes(),
1344 );
1345
1346 let (_out, events) = session
1347 .handle_data(&av_bytes(1, msg_type::DATA_AMF0, 4, 0, payload))
1348 .unwrap();
1349
1350 assert_eq!(events.len(), 1);
1351 let ServerEvent::Media { flv } = &events[0] else {
1352 panic!("expected Media event");
1353 };
1354 assert!(
1355 flv.starts_with(b"FLV"),
1356 "the first Media event must carry the FLV file header"
1357 );
1358 let tag_type = flv[FLV_HEADER_SIZE as usize + FLV_PREV_TAG_SIZE_LEN];
1359 assert_eq!(
1360 tag_type,
1361 msg_type::DATA_AMF0,
1362 "Data-AMF0 message must produce a script(18) FLV tag"
1363 );
1364 }
1365
1366 #[test]
1367 fn media_before_publishing_is_silently_dropped() {
1368 let mut session = ServerSession::with_defaults();
1369 session.handle_data(&build_c0_c1()).unwrap();
1370 session.handle_data(&build_c2()).unwrap();
1371 session.handle_data(&connect_bytes("live")).unwrap();
1372 session.handle_data(&create_stream_bytes()).unwrap();
1373
1374 let (_out, events) = session
1376 .handle_data(&av_bytes(1, msg_type::AUDIO, 4, 0, vec![0xAF, 0x01]))
1377 .unwrap();
1378 assert!(events.is_empty());
1379 }
1380
1381 #[test]
1384 fn stream_key_mismatch_suppresses_publish_and_media_events() {
1385 let config = ServerConfig {
1386 expected_stream_key: Some("rightkey".to_string()),
1387 ..ServerConfig::default()
1388 };
1389 let (mut session, out, events) = publish_flow(config, "wrongkey");
1390
1391 assert!(
1392 !events
1393 .iter()
1394 .any(|e| matches!(e, ServerEvent::Publish { .. })),
1395 "mismatched stream key must not emit Publish"
1396 );
1397
1398 let commands = decode_commands(&out);
1399 let on_status = commands
1400 .iter()
1401 .find(|c| c.name == "onStatus")
1402 .expect("onStatus reply on mismatch");
1403 assert_eq!(
1404 onstatus_code(on_status).as_deref(),
1405 Some("NetStream.Publish.BadName")
1406 );
1407
1408 let (_out2, events2) = session
1410 .handle_data(&av_bytes(1, msg_type::AUDIO, 4, 0, vec![0xAF, 0x01]))
1411 .unwrap();
1412 assert!(
1413 events2.is_empty(),
1414 "no Media may be emitted after a rejected publish"
1415 );
1416 }
1417
1418 #[test]
1421 fn ack_written_once_window_ack_size_is_crossed() {
1422 let config = ServerConfig {
1423 window_ack_size: 32,
1424 ..ServerConfig::default()
1425 };
1426 let mut session = ServerSession::new(config);
1427 session.handle_data(&build_c0_c1()).unwrap();
1428 session.handle_data(&build_c2()).unwrap();
1429
1430 let (out, _events) = session.handle_data(&connect_bytes("live")).unwrap();
1432 let messages = decode_messages(&out);
1433 let has_ack = messages.iter().any(|m| {
1434 matches!(
1435 ProtocolControl::from_message(m),
1436 Ok(Some(ProtocolControl::Acknowledgement(_)))
1437 )
1438 });
1439 assert!(
1440 has_ack,
1441 "crossing window_ack_size must produce an Acknowledgement"
1442 );
1443 }
1444
1445 #[test]
1446 fn no_ack_below_window_ack_size() {
1447 let config = ServerConfig {
1448 window_ack_size: 10_000_000,
1449 ..ServerConfig::default()
1450 };
1451 let mut session = ServerSession::new(config);
1452 session.handle_data(&build_c0_c1()).unwrap();
1453 let (out, _events) = session.handle_data(&build_c2()).unwrap();
1454 let messages = decode_messages(&out);
1455 assert!(
1456 !messages.iter().any(|m| matches!(
1457 ProtocolControl::from_message(m),
1458 Ok(Some(ProtocolControl::Acknowledgement(_)))
1459 )),
1460 "no Acknowledgement should be due yet"
1461 );
1462 }
1463
1464 #[test]
1467 fn garbage_command_payload_after_handshake_is_error_not_panic() {
1468 let mut session = ServerSession::with_defaults();
1469 session.handle_data(&build_c0_c1()).unwrap();
1470 session.handle_data(&build_c2()).unwrap();
1471
1472 let bogus = Message {
1476 chunk_stream_id: CLIENT_CSID,
1477 timestamp: 0,
1478 message_type_id: msg_type::COMMAND_AMF0,
1479 message_stream_id: 0,
1480 payload: vec![0xFF, 0xFF, 0xFF, 0xFF],
1481 };
1482 let bytes = ChunkWriter::new().write(&bogus);
1483 let err = session.handle_data(&bytes).unwrap_err();
1484 assert!(matches!(
1485 err,
1486 RtmpError::Unsupported { .. }
1487 | RtmpError::Malformed { .. }
1488 | RtmpError::BufferTooShort { .. }
1489 ));
1490 }
1491
1492 #[test]
1493 fn truncated_post_handshake_bytes_do_not_panic() {
1494 let mut session = ServerSession::with_defaults();
1495 session.handle_data(&build_c0_c1()).unwrap();
1496 session.handle_data(&build_c2()).unwrap();
1497
1498 let (out, events) = session.handle_data(&[0x03, 0x01, 0x02]).unwrap();
1500 assert!(out.is_empty());
1501 assert!(events.is_empty());
1502 }
1503
1504 #[test]
1507 fn mutation_check_publish_event_must_echo_actual_stream_key() {
1508 let (_session, _out, events) = publish_flow(ServerConfig::default(), "specific-key-xyz");
1509 let publish_event = events
1510 .iter()
1511 .find_map(|e| match e {
1512 ServerEvent::Publish { stream_key, .. } => Some(stream_key.clone()),
1513 _ => None,
1514 })
1515 .expect("Publish event");
1516 assert_eq!(
1517 publish_event, "specific-key-xyz",
1518 "a hardcoded/ignored stream_key would fail this"
1519 );
1520 }
1521
1522 #[test]
1523 fn mutation_check_flv_file_header_bytes_are_exact() {
1524 let header = flv_file_header();
1525 assert_eq!(
1526 header,
1527 vec![
1528 b'F',
1529 b'L',
1530 b'V', 1, 0b0000_0101, 0,
1534 0,
1535 0,
1536 9, 0,
1538 0,
1539 0,
1540 0, ]
1542 );
1543 }
1544
1545 #[test]
1548 fn client_set_chunk_size_takes_effect_before_next_message_in_same_call() {
1549 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1558
1559 const NEW_CHUNK_SIZE: u32 = 4096;
1560 let set_chunk_size_bytes =
1561 ChunkWriter::new().write(&ProtocolControl::SetChunkSize(NEW_CHUNK_SIZE).to_message());
1562
1563 let big_payload = vec![0x17u8; 300];
1568 let mut client_writer = ChunkWriter::new();
1569 client_writer.set_chunk_size(NEW_CHUNK_SIZE);
1570 let video_bytes = client_writer.write(&Message {
1571 chunk_stream_id: 6,
1572 timestamp: 0,
1573 message_type_id: msg_type::VIDEO,
1574 message_stream_id: 1,
1575 payload: big_payload.clone(),
1576 });
1577
1578 let mut combined = set_chunk_size_bytes;
1579 combined.extend_from_slice(&video_bytes);
1580
1581 let (_out, events) = session.handle_data(&combined).expect(
1584 "SetChunkSize must take effect before parsing the message that follows it \
1585 in the same handle_data call, not only on a subsequent call",
1586 );
1587
1588 let media = events
1589 .iter()
1590 .find_map(|e| match e {
1591 ServerEvent::Media { flv } => Some(flv.clone()),
1592 _ => None,
1593 })
1594 .expect("the video message must still be parsed into a Media event");
1595 assert!(
1596 media
1597 .windows(big_payload.len())
1598 .any(|w| w == big_payload.as_slice()),
1599 "the video payload must survive intact through the chunk-size change"
1600 );
1601 }
1602
1603 #[test]
1604 fn control_chunk_stream_id_constant_matches_message_module() {
1605 assert_ne!(COMMAND_CHUNK_STREAM_ID, CTRL_CSID);
1608 }
1609
1610 #[test]
1611 fn next_stream_id_saturates_instead_of_overflowing() {
1612 let mut session = ServerSession::with_defaults();
1617 session.handle_data(&build_c0_c1()).unwrap();
1618 session.handle_data(&build_c2()).unwrap();
1619 session.handle_data(&connect_bytes("live")).unwrap();
1620
1621 session.next_stream_id = u32::MAX;
1622 let (out, _events) = session
1623 .handle_data(&create_stream_bytes())
1624 .expect("createStream must not panic when next_stream_id is already u32::MAX");
1625
1626 let commands = decode_commands(&out);
1627 let result = commands
1628 .iter()
1629 .find(|c| c.name == "_result")
1630 .expect("createStream _result reply");
1631 assert_eq!(
1632 result.arguments.get(1),
1633 Some(&Amf0Value::Number(f64::from(u32::MAX))),
1634 "the stream id allocated at the u32::MAX boundary must still be u32::MAX"
1635 );
1636 assert_eq!(
1637 session.next_stream_id,
1638 u32::MAX,
1639 "next_stream_id must saturate at u32::MAX, not wrap to 0"
1640 );
1641 }
1642
1643 #[cfg(feature = "serde")]
1646 #[test]
1647 fn server_config_and_server_event_serde_round_trip() {
1648 let config = ServerConfig::default()
1649 .with_chunk_size(8192)
1650 .with_expected_stream_key(Some("k".to_string()));
1651 let json = serde_json::to_string(&config).expect("serialize ServerConfig");
1652 let back: ServerConfig = serde_json::from_str(&json).expect("deserialize ServerConfig");
1653 assert_eq!(back.chunk_size, config.chunk_size);
1654 assert_eq!(back.expected_stream_key, config.expected_stream_key);
1655
1656 let event = ServerEvent::Publish {
1657 app: "live".to_string(),
1658 stream_key: "testkey".to_string(),
1659 stream_id: 1,
1660 };
1661 let json = serde_json::to_string(&event).expect("serialize ServerEvent");
1662 let back: ServerEvent = serde_json::from_str(&json).expect("deserialize ServerEvent");
1663 assert_eq!(back, event);
1664 }
1665}