1use crate::core::context::output::Output;
2use crate::error::Error::{RtmpCreateStream, RtmpStreamAlreadyExists};
3use crate::flv::flv_buffer::FlvBuffer;
4use crate::flv::flv_tag::FlvTag;
5use crate::rtmp::reactor::{effective_max_connections, Reactor, CHANNEL_HEADROOM};
6use bytes::{BufMut, Bytes};
7use log::{debug, error, info, warn};
8use rml_rtmp::chunk_io::ChunkSerializer;
9use rml_rtmp::messages::{MessagePayload, RtmpMessage};
10use rml_rtmp::rml_amf0::Amf0Value;
11use rml_rtmp::time::RtmpTimestamp;
12use std::collections::HashMap;
13use std::marker::PhantomData;
14use std::net::{Shutdown, TcpListener, TcpStream};
15use std::sync::atomic::{AtomicUsize, Ordering};
16use std::sync::Arc;
17
18#[derive(Clone)]
19pub struct Initialization;
20#[derive(Clone)]
21pub struct Running;
22#[derive(Clone)]
23pub struct Ended;
24
25#[derive(Clone)]
26pub struct EmbedRtmpServer<S> {
27 address: String,
28 bound_addr: Option<std::net::SocketAddr>,
29 status: Arc<AtomicUsize>,
30 stream_keys: dashmap::DashSet<String>,
31 publisher_sender: Option<crossbeam_channel::Sender<(String, crossbeam_channel::Receiver<Vec<u8>>)>>,
33 gop_limit: usize,
34 max_connections: Option<usize>,
35 state: PhantomData<S>,
36}
37
38const STATUS_INIT: usize = 0;
39const STATUS_RUN: usize = 1;
40const STATUS_END: usize = 2;
41
42impl<S: 'static> EmbedRtmpServer<S> {
43 fn into_state<T>(self) -> EmbedRtmpServer<T> {
44 EmbedRtmpServer {
45 address: self.address,
46 bound_addr: self.bound_addr,
47 status: self.status,
48 stream_keys: self.stream_keys,
49 publisher_sender: self.publisher_sender,
50 gop_limit: self.gop_limit,
51 max_connections: self.max_connections,
52 state: Default::default(),
53 }
54 }
55
56 pub fn is_stopped(&self) -> bool {
64 self.status.load(Ordering::Acquire) == STATUS_END
65 }
66}
67
68impl EmbedRtmpServer<Initialization> {
69 pub fn new(address: impl Into<String>) -> EmbedRtmpServer<Initialization> {
81 Self::new_with_gop_limit(address, 1)
82 }
83
84 pub fn new_with_gop_limit(address: impl Into<String>, gop_limit: usize) -> EmbedRtmpServer<Initialization> {
103 Self {
104 address: address.into(),
105 bound_addr: None,
106 status: Arc::new(AtomicUsize::new(STATUS_INIT)),
107 stream_keys: Default::default(),
108 publisher_sender: None,
109 gop_limit,
110 max_connections: None,
111 state: Default::default(),
112 }
113 }
114
115 pub fn set_max_connections(mut self, max_connections: usize) -> Self {
128 self.max_connections = Some(max_connections);
129 self
130 }
131
132 pub fn start(mut self) -> crate::error::Result<EmbedRtmpServer<Running>> {
141 let listener = TcpListener::bind(self.address.clone())
142 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
143
144 let actual_addr = listener.local_addr()
146 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
147 self.bound_addr = Some(actual_addr);
148
149 listener
150 .set_nonblocking(true)
151 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
152
153 self.status.store(STATUS_RUN, Ordering::Release);
154
155 let effective_max = effective_max_connections(self.max_connections);
158 let channel_capacity = effective_max.saturating_add(CHANNEL_HEADROOM);
159 let (stream_sender, stream_receiver) = crossbeam_channel::bounded(channel_capacity);
160 let (publisher_sender, publisher_receiver) = crossbeam_channel::bounded(1024);
161 self.publisher_sender = Some(publisher_sender);
162 let stream_keys = self.stream_keys.clone();
163 let status = self.status.clone();
164 let max_connections = self.max_connections;
165 let result = std::thread::Builder::new()
166 .name("rtmp-server-worker".to_string())
167 .spawn(move || handle_connections(stream_receiver, publisher_receiver, stream_keys, self.gop_limit, max_connections, status));
168 if let Err(e) = result {
169 error!("Thread[rtmp-server-worker] exited with error: {e}");
170 return Err(crate::error::Error::RtmpThreadExited);
171 }
172
173 info!(
174 "Embed rtmp server listening for connections on {} (actual: {}, max_connections: {}).",
175 &self.address, actual_addr, effective_max
176 );
177
178 let status = self.status.clone();
179 let result = std::thread::Builder::new()
180 .name("rtmp-server-io".to_string())
181 .spawn(move || {
182 for stream in listener.incoming() {
183 if status.load(Ordering::Acquire) == STATUS_END {
188 info!("Embed rtmp server stopped.");
189 break;
190 }
191 match stream {
192 Ok(stream) => {
193 match stream_sender.try_send(stream) {
195 Ok(_) => {
196 debug!("New rtmp connection accepted.");
197 }
198 Err(crossbeam_channel::TrySendError::Full(s)) => {
199 let _ = s.shutdown(Shutdown::Both);
201 debug!("Connection rejected: server at capacity (channel full)");
202 }
203 Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
204 error!("Connection channel disconnected");
205 status.store(STATUS_END, Ordering::Release);
206 return;
207 }
208 }
209 }
210 Err(e) => {
211 if e.kind() == std::io::ErrorKind::WouldBlock {
212 std::thread::sleep(std::time::Duration::from_millis(100));
213 } else if is_fd_exhaustion(&e) {
214 warn!("Accept failed, file descriptors exhausted: {e}");
218 std::thread::sleep(std::time::Duration::from_millis(100));
219 } else {
220 debug!("Rtmp connection error: {:?}", e);
221 }
222 }
223 }
224 }
225 });
226 if let Err(e) = result {
227 error!("Thread[rtmp-server-io] exited with error: {e}");
228 return Err(crate::error::Error::RtmpThreadExited);
229 }
230
231 Ok(self.into_state())
232 }
233}
234
235impl EmbedRtmpServer<Running> {
236 pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
253 self.bound_addr
254 }
255
256 pub fn create_rtmp_input(
304 &self,
305 app_name: impl Into<String>,
306 stream_key: impl Into<String>,
307 ) -> crate::error::Result<Output> {
308 let message_sender = self.create_stream_sender(app_name, stream_key)?;
309
310 let mut flv_buffer = FlvBuffer::new();
311 let mut serializer = ChunkSerializer::new();
312 let write_callback: Box<dyn FnMut(&[u8]) -> i32 + Send> = Box::new(move |buf: &[u8]| -> i32 {
313 flv_buffer.write_data(buf);
314 while let Some(mut flv_tag) = flv_buffer.get_flv_tag() {
318 flv_tag.header.stream_id = 1;
319 match serializer.serialize(&flv_tag_to_message_payload(flv_tag), false, true) {
320 Ok(packet) => {
321 if let Err(e) = message_sender.send(packet.bytes) {
322 error!("Failed to send RTMP packet: {:?}", e);
323 return -1;
324 }
325 }
326 Err(e) => {
327 error!("Failed to serialize RTMP message: {:?}", e);
328 return -1;
329 }
330 }
331 }
332 buf.len() as i32
333 });
334
335 let output: Output = write_callback.into();
336
337 Ok(output
338 .set_format("flv")
339 .set_video_codec("h264")
340 .set_audio_codec("aac")
341 .set_format_opt("flvflags", "no_duration_filesize"))
342 }
343
344 pub fn create_stream_sender(
366 &self,
367 app_name: impl Into<String>,
368 stream_key: impl Into<String>,
369 ) -> crate::error::Result<crossbeam_channel::Sender<Vec<u8>>> {
370 let stream_key = stream_key.into();
371 if self.stream_keys.contains(&stream_key) {
372 return Err(RtmpStreamAlreadyExists(stream_key));
373 }
374
375 let (sender, receiver) = crossbeam_channel::bounded(1024);
376
377 let publisher_sender = match self.publisher_sender.as_ref() {
378 Some(sender) => sender,
379 None => {
380 error!("Publisher sender not initialized");
381 return Err(RtmpCreateStream.into());
382 }
383 };
384
385 if let Err(_) = publisher_sender.send((stream_key.clone(), receiver)) {
386 if self.status.load(Ordering::Acquire) != STATUS_END {
387 warn!("Rtmp server worker already exited. Can't create stream sender.");
388 } else {
389 error!("Rtmp Server aborted. Can't create stream sender.");
390 }
391 return Err(RtmpCreateStream.into());
392 }
393
394 let mut serializer = ChunkSerializer::new();
395
396 let mut properties: HashMap<String, Amf0Value> = HashMap::new();
398 properties.insert("app".to_string(), Amf0Value::Utf8String(app_name.into()));
399 let connect_cmd = RtmpMessage::Amf0Command {
400 command_name: "connect".to_string(),
401 transaction_id: 1.0,
402 command_object: Amf0Value::Object(properties),
403 additional_arguments: Vec::new(),
404 }
405 .into_message_payload(RtmpTimestamp { value: 0 }, 0);
406
407 let connect_cmd = match connect_cmd {
408 Ok(cmd) => cmd,
409 Err(e) => {
410 error!("Failed to create connect command: {:?}", e);
411 return Err(RtmpCreateStream.into());
412 }
413 };
414
415 let connect_packet = match serializer.serialize(&connect_cmd, false, true) {
416 Ok(packet) => packet,
417 Err(e) => {
418 error!("Failed to serialize connect command: {:?}", e);
419 return Err(RtmpCreateStream.into());
420 }
421 };
422
423 if let Err(_) = sender.send(connect_packet.bytes) {
424 error!("Can't send connect command to rtmp server.");
425 return Err(RtmpCreateStream.into());
426 }
427
428 let create_stream_cmd = RtmpMessage::Amf0Command {
430 command_name: "createStream".to_string(),
431 transaction_id: 2.0,
432 command_object: Amf0Value::Null,
433 additional_arguments: Vec::new(),
434 }
435 .into_message_payload(RtmpTimestamp { value: 0 }, 1);
436
437 let create_stream_cmd = match create_stream_cmd {
438 Ok(cmd) => cmd,
439 Err(e) => {
440 error!("Failed to create createStream command: {:?}", e);
441 return Err(RtmpCreateStream.into());
442 }
443 };
444
445 let create_stream_packet = match serializer.serialize(&create_stream_cmd, false, true) {
446 Ok(packet) => packet,
447 Err(e) => {
448 error!("Failed to serialize createStream command: {:?}", e);
449 return Err(RtmpCreateStream.into());
450 }
451 };
452
453 if let Err(_) = sender.send(create_stream_packet.bytes) {
454 error!("Can't send createStream command to rtmp server.");
455 return Err(RtmpCreateStream.into());
456 }
457
458 let mut arguments = Vec::new();
460 arguments.push(Amf0Value::Utf8String(stream_key));
461 arguments.push(Amf0Value::Utf8String("live".into()));
462 let publish_cmd = RtmpMessage::Amf0Command {
463 command_name: "publish".to_string(),
464 transaction_id: 3.0,
465 command_object: Amf0Value::Null,
466 additional_arguments: arguments,
467 }
468 .into_message_payload(RtmpTimestamp { value: 0 }, 1);
469
470 let publish_cmd = match publish_cmd {
471 Ok(cmd) => cmd,
472 Err(e) => {
473 error!("Failed to create publish command: {:?}", e);
474 return Err(RtmpCreateStream.into());
475 }
476 };
477
478 let publish_packet = match serializer.serialize(&publish_cmd, false, true) {
479 Ok(packet) => packet,
480 Err(e) => {
481 error!("Failed to serialize publish command: {:?}", e);
482 return Err(RtmpCreateStream.into());
483 }
484 };
485
486 if let Err(_) = sender.send(publish_packet.bytes) {
487 error!("Can't send publish command to rtmp server.");
488 return Err(RtmpCreateStream.into());
489 }
490 Ok(sender)
491 }
492
493 pub fn stop(self) -> EmbedRtmpServer<Ended> {
505 self.status.store(STATUS_END, Ordering::Release);
506 self.into_state()
507 }
508}
509
510fn is_fd_exhaustion(e: &std::io::Error) -> bool {
520 #[cfg(unix)]
521 {
522 matches!(e.raw_os_error(), Some(code) if code == libc::EMFILE || code == libc::ENFILE)
523 }
524 #[cfg(windows)]
525 {
526 e.raw_os_error() == Some(10024)
528 }
529 #[cfg(not(any(unix, windows)))]
530 {
531 let _ = e;
532 false
533 }
534}
535
536fn handle_connections(
537 connection_receiver: crossbeam_channel::Receiver<TcpStream>,
538 publisher_receiver: crossbeam_channel::Receiver<(String, crossbeam_channel::Receiver<Vec<u8>>)>,
539 stream_keys: dashmap::DashSet<String>,
540 gop_limit: usize,
541 max_connections: Option<usize>,
542 status: Arc<AtomicUsize>,
543) {
544 let mut reactor = match Reactor::new(gop_limit, max_connections, stream_keys, status.clone()) {
546 Ok(r) => r,
547 Err(e) => {
548 error!("Failed to create Reactor: {:?}", e);
549 status.store(STATUS_END, Ordering::Release);
550 return;
551 }
552 };
553
554 reactor.run(connection_receiver, publisher_receiver);
556
557 if status.load(Ordering::Acquire) != STATUS_END {
558 error!("Rtmp Server aborted.");
559 }
560}
561
562pub fn flv_tag_to_message_payload(flv_tag: FlvTag) -> MessagePayload {
563 let timestamp = flv_tag.header.timestamp | ((flv_tag.header.timestamp_ext as u32) << 24);
564
565 let type_id = flv_tag.header.tag_type;
566 let message_stream_id = flv_tag.header.stream_id;
567
568 let data = if type_id == 0x12 {
569 wrap_metadata(flv_tag.data)
570 } else {
571 flv_tag.data
572 };
573
574 MessagePayload {
575 timestamp: RtmpTimestamp { value: timestamp },
576 type_id,
577 message_stream_id,
578 data,
579 }
580}
581
582fn wrap_metadata(data: Bytes) -> Bytes {
583 let s = "@setDataFrame";
584
585 let insert_len = 16;
586
587 let mut bytes = bytes::BytesMut::with_capacity(insert_len + data.len());
588
589 bytes.put_u8(0x02);
590 bytes.put_u16(s.len() as u16);
591 bytes.put(s.as_bytes());
592
593 bytes.put(data);
594
595 bytes.freeze()
596}
597
598
599use crate::core::context::ffmpeg_context::FfmpegContext;
604use crate::core::context::input::Input;
605use crate::core::scheduler::ffmpeg_scheduler::{FfmpegScheduler, Running as SchedulerRunning};
606use crate::error::StreamError;
607use std::path::{Path, PathBuf};
608
609pub struct StreamBuilder {
630 address: Option<String>,
631 app_name: Option<String>,
632 stream_key: Option<String>,
633 input_file: Option<PathBuf>,
634 readrate: Option<f32>,
635 gop_limit: Option<usize>,
636 max_connections: Option<usize>,
637}
638
639impl Default for StreamBuilder {
640 fn default() -> Self {
641 Self::new()
642 }
643}
644
645impl StreamBuilder {
646 pub fn new() -> Self {
652 Self {
653 address: None,
654 app_name: None,
655 stream_key: None,
656 input_file: None,
657 readrate: Some(1.0), gop_limit: None,
659 max_connections: None,
660 }
661 }
662
663 pub fn address(mut self, address: impl Into<String>) -> Self {
665 self.address = Some(address.into());
666 self
667 }
668
669 pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
671 self.app_name = Some(app_name.into());
672 self
673 }
674
675 pub fn stream_key(mut self, stream_key: impl Into<String>) -> Self {
677 self.stream_key = Some(stream_key.into());
678 self
679 }
680
681 pub fn input_file(mut self, path: impl AsRef<Path>) -> Self {
683 self.input_file = Some(path.as_ref().to_path_buf());
684 self
685 }
686
687 pub fn readrate(mut self, rate: f32) -> Self {
692 self.readrate = Some(rate);
693 self
694 }
695
696 pub fn gop_limit(mut self, limit: usize) -> Self {
700 self.gop_limit = Some(limit);
701 self
702 }
703
704 pub fn max_connections(mut self, max: usize) -> Self {
706 self.max_connections = Some(max);
707 self
708 }
709
710 pub fn start(self) -> Result<StreamHandle, StreamError> {
734 let address = self
736 .address
737 .ok_or(StreamError::MissingParameter("address"))?;
738 let app_name = self
739 .app_name
740 .ok_or(StreamError::MissingParameter("app_name"))?;
741 let stream_key = self
742 .stream_key
743 .ok_or(StreamError::MissingParameter("stream_key"))?;
744 let input_file = self
745 .input_file
746 .ok_or(StreamError::MissingParameter("input_file"))?;
747
748 if !input_file.is_file() {
750 return Err(StreamError::InputNotFound { path: input_file });
751 }
752
753 let mut server = if let Some(gop_limit) = self.gop_limit {
755 EmbedRtmpServer::new_with_gop_limit(&address, gop_limit)
756 } else {
757 EmbedRtmpServer::new(&address)
758 };
759
760 if let Some(max_conn) = self.max_connections {
761 server = server.set_max_connections(max_conn);
762 }
763
764 let server = server.start().map_err(StreamError::Ffmpeg)?;
766 let server = Arc::new(server);
767
768 let output = server
770 .create_rtmp_input(&app_name, &stream_key)
771 .map_err(StreamError::Ffmpeg)?;
772
773 let input_path = input_file.to_string_lossy().to_string();
775 let mut input = Input::from(input_path);
776 if let Some(rate) = self.readrate {
777 input = input.set_readrate(rate);
778 }
779
780 let scheduler = FfmpegContext::builder()
782 .input(input)
783 .output(output)
784 .build()
785 .map_err(StreamError::Ffmpeg)?
786 .start()
787 .map_err(StreamError::Ffmpeg)?;
788
789 Ok(StreamHandle {
790 _server: server,
791 scheduler: Some(scheduler),
792 })
793 }
794}
795
796pub struct StreamHandle {
815 _server: Arc<EmbedRtmpServer<Running>>,
816 scheduler: Option<FfmpegScheduler<SchedulerRunning>>,
817}
818
819impl StreamHandle {
820 pub fn wait(mut self) -> Result<(), StreamError> {
830 if let Some(scheduler) = self.scheduler.take() {
831 scheduler.wait().map_err(StreamError::Ffmpeg)?;
832 }
833 Ok(())
834 }
835}
836
837impl Drop for StreamHandle {
838 fn drop(&mut self) {
839 if let Some(scheduler) = self.scheduler.take() {
843 let _ = scheduler.wait();
845 }
846 }
847}
848
849impl EmbedRtmpServer<Initialization> {
850 pub fn stream_builder() -> StreamBuilder {
879 StreamBuilder::new()
880 }
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886 use crate::core::context::ffmpeg_context::FfmpegContext;
887 use crate::core::context::input::Input;
888 use crate::core::context::output::Output;
889 use crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler;
890 use ffmpeg_next::time::current;
891 use std::thread::sleep;
892 use std::time::Duration;
893
894 #[test]
895 #[ignore] fn test_concat_stream_loop() {
897 let _ = env_logger::builder()
898 .filter_level(log::LevelFilter::Trace)
899 .is_test(true)
900 .try_init();
901
902 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
903 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
904
905 let output = embed_rtmp_server
906 .create_rtmp_input("my-app", "my-stream")
907 .unwrap();
908
909 let start = current();
910
911 let result = FfmpegContext::builder()
912 .input(Input::from("test.mp4")
913 .set_readrate(1.0)
914 .set_stream_loop(3)
915 )
916 .input(
917 Input::from("test.mp4")
918 .set_readrate(1.0)
919 .set_stream_loop(3)
920 )
921 .input(
922 Input::from("test.mp4")
923 .set_readrate(1.0)
924 .set_stream_loop(3)
925 )
926 .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
927 .output(output)
928 .build()
929 .unwrap()
930 .start()
931 .unwrap()
932 .wait();
933
934 assert!(result.is_ok());
935 info!("elapsed time: {}", current() - start);
936 }
937
938 #[test]
939 #[ignore] fn test_stream_loop() {
941 let _ = env_logger::builder()
942 .filter_level(log::LevelFilter::Trace)
943 .is_test(true)
944 .try_init();
945
946 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
947 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
948
949 let output = embed_rtmp_server
950 .create_rtmp_input("my-app", "my-stream")
951 .unwrap();
952
953 let start = current();
954
955 let result = FfmpegContext::builder()
956 .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(-1))
957 .output(output.set_video_codec("h264_videotoolbox"))
959 .build()
960 .unwrap()
961 .start()
962 .unwrap()
963 .wait();
964
965 assert!(result.is_ok());
966
967 info!("elapsed time: {}", current() - start);
968 }
969
970 #[test]
971 #[ignore] fn test_concat_realtime() {
973 let _ = env_logger::builder()
974 .filter_level(log::LevelFilter::Trace)
975 .is_test(true)
976 .try_init();
977
978 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
979 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
980
981 let output = embed_rtmp_server
982 .create_rtmp_input("my-app", "my-stream")
983 .unwrap();
984
985 let start = current();
986
987 let result = FfmpegContext::builder()
988 .independent_readrate()
989 .input(Input::from("test.mp4").set_readrate(1.0))
990 .input(
991 Input::from("test.mp4")
992 .set_readrate(1.0)
993 )
994 .input(
995 Input::from("test.mp4")
996 .set_readrate(1.0)
997 )
998 .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
999 .output(output)
1000 .build()
1001 .unwrap()
1002 .start()
1003 .unwrap()
1004 .wait();
1005
1006 assert!(result.is_ok());
1007
1008 sleep(Duration::from_secs(1));
1009 info!("elapsed time: {}", current() - start);
1010 }
1011
1012 #[test]
1013 #[ignore] fn test_realtime() {
1015 let _ = env_logger::builder()
1016 .filter_level(log::LevelFilter::Trace)
1017 .is_test(true)
1018 .try_init();
1019
1020 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1021 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1022
1023 let output = embed_rtmp_server
1024 .create_rtmp_input("my-app", "my-stream")
1025 .unwrap();
1026
1027 let start = current();
1028
1029 let result = FfmpegContext::builder()
1030 .input(Input::from("test.mp4").set_readrate(1.0))
1031 .output(output)
1032 .build()
1033 .unwrap()
1034 .start()
1035 .unwrap()
1036 .wait();
1037
1038 assert!(result.is_ok());
1039
1040 info!("elapsed time: {}", current() - start);
1041 }
1042
1043 #[test]
1044 #[ignore] fn test_readrate() {
1046 let _ = env_logger::builder()
1047 .filter_level(log::LevelFilter::Trace)
1048 .is_test(true)
1049 .try_init();
1050
1051 let mut output: Output = "output.flv".into();
1052 output.audio_codec = Some("adpcm_swf".to_string());
1053
1054 let mut input: Input = "test.mp4".into();
1055 input.readrate = Some(1.0);
1056
1057 let context = FfmpegContext::builder()
1058 .input(input)
1059 .output(output)
1060 .build()
1061 .unwrap();
1062
1063 let result = FfmpegScheduler::new(context).start().unwrap().wait();
1064 if let Err(error) = result {
1065 println!("Error: {error}");
1066 }
1067 }
1068
1069 #[test]
1070 #[ignore] fn test_embed_rtmp_server() {
1072 let _ = env_logger::builder()
1073 .filter_level(log::LevelFilter::Trace)
1074 .is_test(true)
1075 .try_init();
1076
1077 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1078 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1079
1080 let output = embed_rtmp_server
1081 .create_rtmp_input("my-app", "my-stream")
1082 .unwrap();
1083 let mut input: Input = "test.mp4".into();
1084 input.readrate = Some(1.0);
1085
1086 let context = FfmpegContext::builder()
1087 .input(input)
1088 .output(output)
1089 .build()
1090 .unwrap();
1091
1092 let result = FfmpegScheduler::new(context).start().unwrap().wait();
1093
1094 assert!(result.is_ok());
1095
1096 sleep(Duration::from_secs(3));
1097 }
1098}