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 if expected != &stream_key {
636 let on_status = Command {
637 name: "onStatus".to_string(),
638 transaction_id: 0.0,
639 arguments: vec![
640 Amf0Value::Null,
641 Amf0Value::Object(vec![
642 ("level".to_string(), Amf0Value::String("error".to_string())),
643 (
644 "code".to_string(),
645 Amf0Value::String("NetStream.Publish.BadName".to_string()),
646 ),
647 (
648 "description".to_string(),
649 Amf0Value::String("Stream key mismatch.".to_string()),
650 ),
651 ]),
652 ],
653 };
654 out.extend_from_slice(&self.writer.write(&self.command_message(msg, &on_status)));
655 return Ok(());
657 }
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 create_stream_replies_result_with_stream_id() {
1049 let mut session = ServerSession::with_defaults();
1050 session.handle_data(&build_c0_c1()).unwrap();
1051 session.handle_data(&build_c2()).unwrap();
1052 session.handle_data(&connect_bytes("live")).unwrap();
1053
1054 let (out, _events) = session.handle_data(&create_stream_bytes()).unwrap();
1055 let commands = decode_commands(&out);
1056 let result = commands
1057 .iter()
1058 .find(|c| c.name == "_result")
1059 .expect("createStream _result reply");
1060 assert_eq!(
1061 result.arguments.get(1),
1062 Some(&Amf0Value::Number(1.0)),
1063 "first allocated stream id must be 1"
1064 );
1065 }
1066
1067 #[test]
1068 fn create_stream_before_connect_is_unexpected_state() {
1069 let mut session = ServerSession::with_defaults();
1070 session.handle_data(&build_c0_c1()).unwrap();
1071 session.handle_data(&build_c2()).unwrap();
1072
1073 let err = session.handle_data(&create_stream_bytes()).unwrap_err();
1074 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1075 }
1076
1077 #[test]
1080 fn publish_reaches_publishing_emits_event_and_stream_begin_plus_onstatus() {
1081 let (_session, out, events) = publish_flow(ServerConfig::default(), "testkey");
1082
1083 assert!(events.contains(&ServerEvent::Publish {
1084 app: "live".to_string(),
1085 stream_key: "testkey".to_string(),
1086 stream_id: 1,
1087 }));
1088
1089 let messages = decode_messages(&out);
1090 let has_stream_begin = messages.iter().any(|m| {
1091 m.message_type_id == msg_type::USER_CONTROL
1092 && matches!(
1093 UserControl::parse(&m.payload),
1094 Ok(UserControl::StreamBegin(1))
1095 )
1096 });
1097 assert!(
1098 has_stream_begin,
1099 "publish reply must include StreamBegin(1)"
1100 );
1101
1102 let commands = decode_commands(&out);
1103 let on_status = commands
1104 .iter()
1105 .find(|c| c.name == "onStatus")
1106 .expect("onStatus reply to publish");
1107 assert_eq!(
1108 onstatus_code(on_status).as_deref(),
1109 Some("NetStream.Publish.Start")
1110 );
1111 }
1112
1113 #[test]
1114 fn publish_before_connect_is_unexpected_state() {
1115 let mut session = ServerSession::with_defaults();
1116 session.handle_data(&build_c0_c1()).unwrap();
1117 session.handle_data(&build_c2()).unwrap();
1118
1119 let err = session
1120 .handle_data(&publish_bytes(1, "testkey"))
1121 .unwrap_err();
1122 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1123 }
1124
1125 #[test]
1126 fn publish_without_create_stream_is_unexpected_state() {
1127 let mut session = ServerSession::with_defaults();
1128 session.handle_data(&build_c0_c1()).unwrap();
1129 session.handle_data(&build_c2()).unwrap();
1130 session.handle_data(&connect_bytes("live")).unwrap();
1131
1132 let err = session
1134 .handle_data(&publish_bytes(1, "testkey"))
1135 .unwrap_err();
1136 assert!(matches!(err, RtmpError::UnexpectedState { .. }));
1137 }
1138
1139 #[test]
1140 fn create_stream_after_closed_is_unexpected_state() {
1141 let mut session = ServerSession::with_defaults();
1142 session.handle_data(&build_c0_c1()).unwrap();
1143 session.handle_data(&build_c2()).unwrap();
1144 session.handle_data(&connect_bytes("live")).unwrap();
1145 session.handle_data(&create_stream_bytes()).unwrap();
1146 session.handle_data(&publish_bytes(1, "testkey")).unwrap();
1147
1148 let delete_stream = command_message(
1149 CLIENT_CSID,
1150 1,
1151 "deleteStream",
1152 4.0,
1153 vec![Amf0Value::Null, Amf0Value::Number(1.0)],
1154 );
1155 let (_out, events) = session
1156 .handle_data(&ChunkWriter::new().write(&delete_stream))
1157 .unwrap();
1158 assert_eq!(events, vec![ServerEvent::Eof]);
1159
1160 let err = session.handle_data(&create_stream_bytes()).unwrap_err();
1161 assert!(
1162 matches!(err, RtmpError::UnexpectedState { .. }),
1163 "createStream after State::Closed must be rejected, not silently re-allowed"
1164 );
1165 }
1166
1167 #[test]
1170 fn audio_and_video_emit_media_first_carries_flv_file_header() {
1171 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1172
1173 let (_out1, events1) = session
1174 .handle_data(&av_bytes(
1175 1,
1176 msg_type::AUDIO,
1177 4,
1178 0,
1179 vec![0xAF, 0x01, 0xDE, 0xAD],
1180 ))
1181 .unwrap();
1182 assert_eq!(events1.len(), 1);
1183 let ServerEvent::Media { flv } = &events1[0] else {
1184 panic!("expected Media event");
1185 };
1186 assert!(
1187 flv.starts_with(b"FLV"),
1188 "the first Media event must carry the FLV file header"
1189 );
1190
1191 let (_out2, events2) = session
1192 .handle_data(&av_bytes(
1193 1,
1194 msg_type::VIDEO,
1195 6,
1196 40,
1197 vec![0x17, 0x01, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF],
1198 ))
1199 .unwrap();
1200 assert_eq!(events2.len(), 1);
1201 let ServerEvent::Media { flv } = &events2[0] else {
1202 panic!("expected Media event");
1203 };
1204 assert!(
1205 !flv.starts_with(b"FLV"),
1206 "only the first Media event carries the file header"
1207 );
1208 }
1209
1210 #[test]
1211 fn concatenated_media_forms_structurally_valid_flv() {
1212 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1213
1214 let mut flv_stream = Vec::new();
1215 let (_out1, events1) = session
1216 .handle_data(&av_bytes(
1217 1,
1218 msg_type::AUDIO,
1219 4,
1220 0,
1221 vec![0xAF, 0x01, 1, 2, 3],
1222 ))
1223 .unwrap();
1224 let (_out2, events2) = session
1225 .handle_data(&av_bytes(
1226 1,
1227 msg_type::VIDEO,
1228 6,
1229 33,
1230 vec![0x17, 0x01, 0, 0, 0, 4, 5, 6],
1231 ))
1232 .unwrap();
1233 for e in events1.into_iter().chain(events2) {
1234 if let ServerEvent::Media { flv } = e {
1235 flv_stream.extend(flv);
1236 }
1237 }
1238
1239 assert_eq!(&flv_stream[0..3], b"FLV");
1241 assert_eq!(flv_stream[3], 1, "FLV version");
1242 assert_eq!(flv_stream[4], 0b0000_0101, "audio+video TypeFlags");
1243 assert_eq!(
1244 u32::from_be_bytes(flv_stream[5..9].try_into().unwrap()),
1245 9,
1246 "DataOffset (header size)"
1247 );
1248 assert_eq!(
1249 u32::from_be_bytes(flv_stream[9..13].try_into().unwrap()),
1250 0,
1251 "PreviousTagSize0"
1252 );
1253
1254 let tag1 = &flv_stream[13..];
1256 assert_eq!(tag1[0], msg_type::AUDIO);
1257 let data_size1 =
1258 (u32::from(tag1[1]) << 16) | (u32::from(tag1[2]) << 8) | u32::from(tag1[3]);
1259 assert_eq!(data_size1, 5);
1260 let tag1_total = FLV_TAG_HEADER_LEN + 5 + FLV_PREV_TAG_SIZE_LEN;
1261 let prev_tag_size1 = u32::from_be_bytes(
1262 flv_stream[13 + tag1_total - 4..13 + tag1_total]
1263 .try_into()
1264 .unwrap(),
1265 );
1266 assert_eq!(prev_tag_size1 as usize, FLV_TAG_HEADER_LEN + 5);
1267
1268 let tag2 = &flv_stream[13 + tag1_total..];
1270 assert_eq!(tag2[0], msg_type::VIDEO);
1271 let data_size2 =
1272 (u32::from(tag2[1]) << 16) | (u32::from(tag2[2]) << 8) | u32::from(tag2[3]);
1273 assert_eq!(data_size2, 8);
1274 assert_eq!(
1275 flv_stream.len(),
1276 13 + tag1_total + FLV_TAG_HEADER_LEN + 8 + FLV_PREV_TAG_SIZE_LEN
1277 );
1278 }
1279
1280 #[test]
1281 fn data_amf0_onmetadata_emits_media_with_script_tag_type() {
1282 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1283
1284 let mut payload = Amf0Value::String("onMetaData".to_string()).to_bytes();
1289 payload.extend(
1290 Amf0Value::Object(vec![
1291 ("width".to_string(), Amf0Value::Number(1920.0)),
1292 ("height".to_string(), Amf0Value::Number(1080.0)),
1293 ])
1294 .to_bytes(),
1295 );
1296
1297 let (_out, events) = session
1298 .handle_data(&av_bytes(1, msg_type::DATA_AMF0, 4, 0, payload))
1299 .unwrap();
1300
1301 assert_eq!(events.len(), 1);
1302 let ServerEvent::Media { flv } = &events[0] else {
1303 panic!("expected Media event");
1304 };
1305 assert!(
1306 flv.starts_with(b"FLV"),
1307 "the first Media event must carry the FLV file header"
1308 );
1309 let tag_type = flv[FLV_HEADER_SIZE as usize + FLV_PREV_TAG_SIZE_LEN];
1310 assert_eq!(
1311 tag_type,
1312 msg_type::DATA_AMF0,
1313 "Data-AMF0 message must produce a script(18) FLV tag"
1314 );
1315 }
1316
1317 #[test]
1318 fn media_before_publishing_is_silently_dropped() {
1319 let mut session = ServerSession::with_defaults();
1320 session.handle_data(&build_c0_c1()).unwrap();
1321 session.handle_data(&build_c2()).unwrap();
1322 session.handle_data(&connect_bytes("live")).unwrap();
1323 session.handle_data(&create_stream_bytes()).unwrap();
1324
1325 let (_out, events) = session
1327 .handle_data(&av_bytes(1, msg_type::AUDIO, 4, 0, vec![0xAF, 0x01]))
1328 .unwrap();
1329 assert!(events.is_empty());
1330 }
1331
1332 #[test]
1335 fn stream_key_mismatch_suppresses_publish_and_media_events() {
1336 let config = ServerConfig {
1337 expected_stream_key: Some("rightkey".to_string()),
1338 ..ServerConfig::default()
1339 };
1340 let (mut session, out, events) = publish_flow(config, "wrongkey");
1341
1342 assert!(
1343 !events
1344 .iter()
1345 .any(|e| matches!(e, ServerEvent::Publish { .. })),
1346 "mismatched stream key must not emit Publish"
1347 );
1348
1349 let commands = decode_commands(&out);
1350 let on_status = commands
1351 .iter()
1352 .find(|c| c.name == "onStatus")
1353 .expect("onStatus reply on mismatch");
1354 assert_eq!(
1355 onstatus_code(on_status).as_deref(),
1356 Some("NetStream.Publish.BadName")
1357 );
1358
1359 let (_out2, events2) = session
1361 .handle_data(&av_bytes(1, msg_type::AUDIO, 4, 0, vec![0xAF, 0x01]))
1362 .unwrap();
1363 assert!(
1364 events2.is_empty(),
1365 "no Media may be emitted after a rejected publish"
1366 );
1367 }
1368
1369 #[test]
1372 fn ack_written_once_window_ack_size_is_crossed() {
1373 let config = ServerConfig {
1374 window_ack_size: 32,
1375 ..ServerConfig::default()
1376 };
1377 let mut session = ServerSession::new(config);
1378 session.handle_data(&build_c0_c1()).unwrap();
1379 session.handle_data(&build_c2()).unwrap();
1380
1381 let (out, _events) = session.handle_data(&connect_bytes("live")).unwrap();
1383 let messages = decode_messages(&out);
1384 let has_ack = messages.iter().any(|m| {
1385 matches!(
1386 ProtocolControl::from_message(m),
1387 Ok(Some(ProtocolControl::Acknowledgement(_)))
1388 )
1389 });
1390 assert!(
1391 has_ack,
1392 "crossing window_ack_size must produce an Acknowledgement"
1393 );
1394 }
1395
1396 #[test]
1397 fn no_ack_below_window_ack_size() {
1398 let config = ServerConfig {
1399 window_ack_size: 10_000_000,
1400 ..ServerConfig::default()
1401 };
1402 let mut session = ServerSession::new(config);
1403 session.handle_data(&build_c0_c1()).unwrap();
1404 let (out, _events) = session.handle_data(&build_c2()).unwrap();
1405 let messages = decode_messages(&out);
1406 assert!(
1407 !messages.iter().any(|m| matches!(
1408 ProtocolControl::from_message(m),
1409 Ok(Some(ProtocolControl::Acknowledgement(_)))
1410 )),
1411 "no Acknowledgement should be due yet"
1412 );
1413 }
1414
1415 #[test]
1418 fn garbage_command_payload_after_handshake_is_error_not_panic() {
1419 let mut session = ServerSession::with_defaults();
1420 session.handle_data(&build_c0_c1()).unwrap();
1421 session.handle_data(&build_c2()).unwrap();
1422
1423 let bogus = Message {
1427 chunk_stream_id: CLIENT_CSID,
1428 timestamp: 0,
1429 message_type_id: msg_type::COMMAND_AMF0,
1430 message_stream_id: 0,
1431 payload: vec![0xFF, 0xFF, 0xFF, 0xFF],
1432 };
1433 let bytes = ChunkWriter::new().write(&bogus);
1434 let err = session.handle_data(&bytes).unwrap_err();
1435 assert!(matches!(
1436 err,
1437 RtmpError::Unsupported { .. }
1438 | RtmpError::Malformed { .. }
1439 | RtmpError::BufferTooShort { .. }
1440 ));
1441 }
1442
1443 #[test]
1444 fn truncated_post_handshake_bytes_do_not_panic() {
1445 let mut session = ServerSession::with_defaults();
1446 session.handle_data(&build_c0_c1()).unwrap();
1447 session.handle_data(&build_c2()).unwrap();
1448
1449 let (out, events) = session.handle_data(&[0x03, 0x01, 0x02]).unwrap();
1451 assert!(out.is_empty());
1452 assert!(events.is_empty());
1453 }
1454
1455 #[test]
1458 fn mutation_check_publish_event_must_echo_actual_stream_key() {
1459 let (_session, _out, events) = publish_flow(ServerConfig::default(), "specific-key-xyz");
1460 let publish_event = events
1461 .iter()
1462 .find_map(|e| match e {
1463 ServerEvent::Publish { stream_key, .. } => Some(stream_key.clone()),
1464 _ => None,
1465 })
1466 .expect("Publish event");
1467 assert_eq!(
1468 publish_event, "specific-key-xyz",
1469 "a hardcoded/ignored stream_key would fail this"
1470 );
1471 }
1472
1473 #[test]
1474 fn mutation_check_flv_file_header_bytes_are_exact() {
1475 let header = flv_file_header();
1476 assert_eq!(
1477 header,
1478 vec![
1479 b'F',
1480 b'L',
1481 b'V', 1, 0b0000_0101, 0,
1485 0,
1486 0,
1487 9, 0,
1489 0,
1490 0,
1491 0, ]
1493 );
1494 }
1495
1496 #[test]
1499 fn client_set_chunk_size_takes_effect_before_next_message_in_same_call() {
1500 let (mut session, _out, _events) = publish_flow(ServerConfig::default(), "testkey");
1509
1510 const NEW_CHUNK_SIZE: u32 = 4096;
1511 let set_chunk_size_bytes =
1512 ChunkWriter::new().write(&ProtocolControl::SetChunkSize(NEW_CHUNK_SIZE).to_message());
1513
1514 let big_payload = vec![0x17u8; 300];
1519 let mut client_writer = ChunkWriter::new();
1520 client_writer.set_chunk_size(NEW_CHUNK_SIZE);
1521 let video_bytes = client_writer.write(&Message {
1522 chunk_stream_id: 6,
1523 timestamp: 0,
1524 message_type_id: msg_type::VIDEO,
1525 message_stream_id: 1,
1526 payload: big_payload.clone(),
1527 });
1528
1529 let mut combined = set_chunk_size_bytes;
1530 combined.extend_from_slice(&video_bytes);
1531
1532 let (_out, events) = session.handle_data(&combined).expect(
1535 "SetChunkSize must take effect before parsing the message that follows it \
1536 in the same handle_data call, not only on a subsequent call",
1537 );
1538
1539 let media = events
1540 .iter()
1541 .find_map(|e| match e {
1542 ServerEvent::Media { flv } => Some(flv.clone()),
1543 _ => None,
1544 })
1545 .expect("the video message must still be parsed into a Media event");
1546 assert!(
1547 media
1548 .windows(big_payload.len())
1549 .any(|w| w == big_payload.as_slice()),
1550 "the video payload must survive intact through the chunk-size change"
1551 );
1552 }
1553
1554 #[test]
1555 fn control_chunk_stream_id_constant_matches_message_module() {
1556 assert_ne!(COMMAND_CHUNK_STREAM_ID, CTRL_CSID);
1559 }
1560
1561 #[test]
1562 fn next_stream_id_saturates_instead_of_overflowing() {
1563 let mut session = ServerSession::with_defaults();
1568 session.handle_data(&build_c0_c1()).unwrap();
1569 session.handle_data(&build_c2()).unwrap();
1570 session.handle_data(&connect_bytes("live")).unwrap();
1571
1572 session.next_stream_id = u32::MAX;
1573 let (out, _events) = session
1574 .handle_data(&create_stream_bytes())
1575 .expect("createStream must not panic when next_stream_id is already u32::MAX");
1576
1577 let commands = decode_commands(&out);
1578 let result = commands
1579 .iter()
1580 .find(|c| c.name == "_result")
1581 .expect("createStream _result reply");
1582 assert_eq!(
1583 result.arguments.get(1),
1584 Some(&Amf0Value::Number(f64::from(u32::MAX))),
1585 "the stream id allocated at the u32::MAX boundary must still be u32::MAX"
1586 );
1587 assert_eq!(
1588 session.next_stream_id,
1589 u32::MAX,
1590 "next_stream_id must saturate at u32::MAX, not wrap to 0"
1591 );
1592 }
1593
1594 #[cfg(feature = "serde")]
1597 #[test]
1598 fn server_config_and_server_event_serde_round_trip() {
1599 let config = ServerConfig::default()
1600 .with_chunk_size(8192)
1601 .with_expected_stream_key(Some("k".to_string()));
1602 let json = serde_json::to_string(&config).expect("serialize ServerConfig");
1603 let back: ServerConfig = serde_json::from_str(&json).expect("deserialize ServerConfig");
1604 assert_eq!(back.chunk_size, config.chunk_size);
1605 assert_eq!(back.expected_stream_key, config.expected_stream_key);
1606
1607 let event = ServerEvent::Publish {
1608 app: "live".to_string(),
1609 stream_key: "testkey".to_string(),
1610 stream_id: 1,
1611 };
1612 let json = serde_json::to_string(&event).expect("serialize ServerEvent");
1613 let back: ServerEvent = serde_json::from_str(&json).expect("deserialize ServerEvent");
1614 assert_eq!(back, event);
1615 }
1616}