ez_ffmpeg/rtmp/embed_rtmp_server.rs
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::poller::{waker_pair, WakeHandle, Waker};
6use crate::rtmp::reactor::{
7 effective_max_connections, PublisherFeed, PublisherSource, Reactor, CHANNEL_HEADROOM,
8 PUBLISHER_CHANNEL_CAPACITY,
9};
10use bytes::{BufMut, Bytes};
11use log::{debug, error, info, warn};
12use rml_rtmp::chunk_io::ChunkSerializer;
13use rml_rtmp::messages::{MessagePayload, RtmpMessage};
14use rml_rtmp::rml_amf0::Amf0Value;
15use rml_rtmp::time::RtmpTimestamp;
16use std::collections::HashMap;
17use std::marker::PhantomData;
18use std::net::{Shutdown, TcpListener, TcpStream};
19use std::sync::atomic::{AtomicUsize, Ordering};
20use std::sync::Arc;
21
22#[derive(Clone)]
23pub struct Initialization;
24#[derive(Clone)]
25pub struct Running;
26#[derive(Clone)]
27pub struct Ended;
28
29#[derive(Clone)]
30pub struct EmbedRtmpServer<S> {
31 address: String,
32 bound_addr: Option<std::net::SocketAddr>,
33 status: Arc<AtomicUsize>,
34 // Arc-shared with the reactor: the duplicate-key check in create_* reads
35 // the keys the reactor inserts. DashSet's Clone is a deep copy, so a
36 // non-Arc field cloned into the worker thread would split server and
37 // reactor onto two disjoint sets and disable the check entirely.
38 stream_keys: Arc<dashmap::DashSet<String>>,
39 // stream_key -> publisher source (raw byte path or media-bypass feed)
40 publisher_sender: Option<crossbeam_channel::Sender<(String, PublisherSource)>>,
41 /// Producer-side wakeup for the reactor (PERF-3), set in `start()`.
42 wake_handle: Option<WakeHandle>,
43 gop_limit: usize,
44 max_connections: Option<usize>,
45 state: PhantomData<S>,
46}
47
48const STATUS_INIT: usize = 0;
49const STATUS_RUN: usize = 1;
50const STATUS_END: usize = 2;
51
52impl<S: 'static> EmbedRtmpServer<S> {
53 fn into_state<T>(self) -> EmbedRtmpServer<T> {
54 EmbedRtmpServer {
55 address: self.address,
56 bound_addr: self.bound_addr,
57 status: self.status,
58 stream_keys: self.stream_keys,
59 publisher_sender: self.publisher_sender,
60 wake_handle: self.wake_handle,
61 gop_limit: self.gop_limit,
62 max_connections: self.max_connections,
63 state: Default::default(),
64 }
65 }
66
67 /// Checks whether the RTMP server has been signaled to stop. This returns
68 /// `true` once [`stop`](EmbedRtmpServer<Running>::stop) has been called
69 /// (or a fatal internal error stopped the server), otherwise `false`.
70 ///
71 /// Note this reports the *signal*, not thread teardown: the worker threads
72 /// observe the flag and exit shortly after (the reactor on its next
73 /// wakeup, the accept thread within its ~100ms accept cycle).
74 ///
75 /// # Returns
76 ///
77 /// * `true` if the server has been signaled to stop (and will no longer accept connections).
78 /// * `false` if the server is still running.
79 pub fn is_stopped(&self) -> bool {
80 self.status.load(Ordering::Acquire) == STATUS_END
81 }
82
83 /// Signal the server threads to stop without consuming the server.
84 ///
85 /// Idempotent: storing `STATUS_END` again and re-waking an already-woken
86 /// reactor are both no-ops. The wake matters — without it the reactor
87 /// notices the flag only on its next poll timeout, and a reactor parked
88 /// in `poll()` would otherwise hold the shutdown for up to 100ms. When no
89 /// wake handle exists (waker_pair creation failed at start), that 100ms
90 /// poll fallback is exactly the degraded path the reactor already runs on.
91 ///
92 /// This exists because `EmbedRtmpServer` cannot implement `Drop` itself:
93 /// `into_state()` moves fields out of `self`, which the compiler forbids
94 /// for types with a `Drop` impl (E0509). Shared owners that only hold a
95 /// reference (e.g. [`StreamHandle`]) stop the server through this instead.
96 fn signal_stop(&self) {
97 self.status.store(STATUS_END, Ordering::Release);
98 if let Some(wake_handle) = &self.wake_handle {
99 wake_handle.wake();
100 }
101 }
102}
103
104impl EmbedRtmpServer<Initialization> {
105 /// Creates a new RTMP server instance that will listen on the specified address
106 /// when [`start`](EmbedRtmpServer<Initialization>::start) is called.
107 ///
108 /// # Parameters
109 ///
110 /// * `address` - A string slice representing the address (host:port) to bind the
111 /// RTMP server socket.
112 ///
113 /// # Returns
114 ///
115 /// An [`EmbedRtmpServer`] configured to listen on the given address.
116 pub fn new(address: impl Into<String>) -> EmbedRtmpServer<Initialization> {
117 Self::new_with_gop_limit(address, 1)
118 }
119
120 /// Creates a new RTMP server instance that will listen on the specified address,
121 /// with a custom GOP limit.
122 ///
123 /// This method allows specifying the maximum number of GOPs to be cached.
124 /// A GOP (Group of Pictures) represents a sequence of video frames (I, P, B frames)
125 /// used for efficient video decoding and random access. The GOP limit defines
126 /// how many such groups are stored in the cache.
127 ///
128 /// # Parameters
129 ///
130 /// * `address` - A string slice representing the address (host:port) to bind the
131 /// RTMP server socket.
132 /// * `gop_limit` - The maximum number of GOPs to cache.
133 ///
134 /// # Returns
135 ///
136 /// An [`EmbedRtmpServer`] instance configured to listen on the given address and
137 /// using the specified GOP limit.
138 pub fn new_with_gop_limit(
139 address: impl Into<String>,
140 gop_limit: usize,
141 ) -> EmbedRtmpServer<Initialization> {
142 Self {
143 address: address.into(),
144 bound_addr: None,
145 status: Arc::new(AtomicUsize::new(STATUS_INIT)),
146 stream_keys: Default::default(),
147 publisher_sender: None,
148 wake_handle: None,
149 gop_limit,
150 max_connections: None,
151 state: Default::default(),
152 }
153 }
154
155 /// Sets the maximum number of concurrent connections allowed.
156 ///
157 /// If not set, the limit is auto-detected based on system file descriptor limits
158 /// (default: 10000, capped at 80% of system FD limit).
159 ///
160 /// # Parameters
161 ///
162 /// * `max_connections` - Maximum number of concurrent connections
163 ///
164 /// # Returns
165 ///
166 /// Self for method chaining.
167 pub fn set_max_connections(mut self, max_connections: usize) -> Self {
168 self.max_connections = Some(max_connections);
169 self
170 }
171
172 /// Starts the RTMP server on the configured address, entering a loop that
173 /// accepts incoming client connections. This method spawns background threads
174 /// to handle the connections and publish events.
175 ///
176 /// # Returns
177 ///
178 /// * `Ok(())` if the server successfully starts listening.
179 /// * An error variant if the socket could not be bound or other I/O errors occur.
180 pub fn start(mut self) -> crate::error::Result<EmbedRtmpServer<Running>> {
181 let listener = TcpListener::bind(self.address.clone())
182 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
183
184 // Get actual bound address (important for port 0)
185 let actual_addr = listener
186 .local_addr()
187 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
188 self.bound_addr = Some(actual_addr);
189
190 listener
191 .set_nonblocking(true)
192 .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
193
194 self.status.store(STATUS_RUN, Ordering::Release);
195
196 // Calculate effective max and create bounded channel with headroom
197 // This prevents unbounded queue growth when reactor is at capacity
198 let effective_max = effective_max_connections(self.max_connections);
199 let channel_capacity = effective_max.saturating_add(CHANNEL_HEADROOM);
200 let (stream_sender, stream_receiver) = crossbeam_channel::bounded(channel_capacity);
201 let (publisher_sender, publisher_receiver) = crossbeam_channel::bounded(1024);
202 self.publisher_sender = Some(publisher_sender);
203
204 // PERF-3: create the reactor wakeup pair. The Waker (read side) is moved
205 // into the worker thread and registered with the poller; the WakeHandle
206 // (write side) is kept here so create_rtmp_input can signal the reactor
207 // the instant media is queued.
208 // If the wakeup pair cannot be created (e.g. eventfd/loopback exhaustion),
209 // degrade gracefully to the POLL_TIMEOUT_MS fallback rather than failing
210 // server startup: the low-latency wakeup is an optimization, not a
211 // correctness requirement.
212 let (waker, wake_handle) = match waker_pair() {
213 Ok((waker, handle)) => (Some(waker), Some(handle)),
214 Err(e) => {
215 warn!("PERF-3: reactor waker unavailable ({e:?}); falling back to the poll-timeout for in-process media latency");
216 (None, None)
217 }
218 };
219 self.wake_handle = wake_handle;
220
221 let stream_keys = self.stream_keys.clone();
222 let status = self.status.clone();
223 let max_connections = self.max_connections;
224 let result = std::thread::Builder::new()
225 .name("rtmp-server-worker".to_string())
226 .spawn(move || {
227 handle_connections(
228 stream_receiver,
229 publisher_receiver,
230 stream_keys,
231 self.gop_limit,
232 max_connections,
233 status,
234 waker,
235 )
236 });
237 if let Err(e) = result {
238 error!("Thread[rtmp-server-worker] exited with error: {e}");
239 // Nothing has spawned yet: no worker observes STATUS_RUN, and the
240 // listener is still owned here (moved into the io closure only
241 // below), so it drops on return and releases the port. No stop
242 // signal is needed.
243 return Err(crate::error::Error::RtmpThreadExited);
244 }
245
246 info!(
247 "Embed rtmp server listening for connections on {} (actual: {}, max_connections: {}).",
248 &self.address, actual_addr, effective_max
249 );
250
251 let status = self.status.clone();
252 let result = std::thread::Builder::new()
253 .name("rtmp-server-io".to_string())
254 .spawn(move || {
255 for stream in listener.incoming() {
256 // Check the stop flag on every iteration, not only when the
257 // listener runs dry: under a steady stream of incoming
258 // connections the WouldBlock branch is never taken and
259 // stop() would otherwise never terminate this thread.
260 if status.load(Ordering::Acquire) == STATUS_END {
261 info!("Embed rtmp server stopped.");
262 break;
263 }
264 match stream {
265 Ok(stream) => {
266 // Use try_send to apply backpressure when channel is full
267 match stream_sender.try_send(stream) {
268 Ok(_) => {
269 debug!("New rtmp connection accepted.");
270 }
271 Err(crossbeam_channel::TrySendError::Full(s)) => {
272 // Channel full - server at capacity, reject connection immediately
273 let _ = s.shutdown(Shutdown::Both);
274 debug!(
275 "Connection rejected: server at capacity (channel full)"
276 );
277 }
278 Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
279 error!("Connection channel disconnected");
280 status.store(STATUS_END, Ordering::Release);
281 return;
282 }
283 }
284 }
285 Err(e) => {
286 if e.kind() == std::io::ErrorKind::WouldBlock {
287 std::thread::sleep(std::time::Duration::from_millis(100));
288 } else if is_fd_exhaustion(&e) {
289 // Accepting again immediately would fail the same
290 // way and spin the CPU; back off and let existing
291 // connections close first.
292 warn!("Accept failed, file descriptors exhausted: {e}");
293 std::thread::sleep(std::time::Duration::from_millis(100));
294 } else {
295 debug!("Rtmp connection error: {:?}", e);
296 }
297 }
298 }
299 }
300 });
301 if let Err(e) = result {
302 error!("Thread[rtmp-server-io] exited with error: {e}");
303 // The worker thread spawned successfully above and is now polling
304 // `status` (still STATUS_RUN); without this it would run forever and
305 // keep the port bound. Signal STATUS_END (and wake the reactor) so
306 // it exits. The listener was moved into the failed io closure and
307 // drops with it, releasing the port.
308 self.signal_stop();
309 return Err(crate::error::Error::RtmpThreadExited);
310 }
311
312 Ok(self.into_state())
313 }
314}
315
316/// Handle for feeding raw, pre-packaged RTMP chunk bytes to a stream published
317/// via [`EmbedRtmpServer::<Running>::create_stream_sender`]. Wraps the internal
318/// channel so callers do not depend on the channel implementation.
319///
320/// Cloneable and multi-producer: every clone feeds the same stream, so several
321/// producers can push chunks to one stream concurrently.
322#[derive(Clone)]
323pub struct RtmpStreamSender {
324 inner: crossbeam_channel::Sender<Vec<u8>>,
325 /// Wakes the reactor after each send so a raw publisher's media is
326 /// drained on the next loop turn instead of waiting up to POLL_TIMEOUT_MS
327 /// (~100ms). `None` only for the unit-test constructor (no running reactor).
328 wake_handle: Option<WakeHandle>,
329}
330
331impl RtmpStreamSender {
332 /// Sends one already-RTMP-chunk-packaged byte buffer to the stream.
333 ///
334 /// The underlying channel is bounded, so this **blocks** while the stream's
335 /// queue is full (the server applying backpressure) and returns once space
336 /// frees up. Returns
337 /// [`Error::RtmpStreamClosed`](crate::error::Error::RtmpStreamClosed) if the
338 /// stream has been torn down — its receiver was dropped because the server
339 /// stopped or the stream was removed.
340 pub fn send(&self, chunk: Vec<u8>) -> crate::error::Result<()> {
341 self.inner
342 .send(chunk)
343 .map_err(|_| crate::error::Error::RtmpStreamClosed)?;
344 // Nudge the reactor so process_publishers drains this Raw channel on the
345 // next loop turn rather than after the POLL_TIMEOUT_MS fallback. The
346 // internal Feed path (create_rtmp_input) wakes the same way per packet.
347 if let Some(wake) = &self.wake_handle {
348 wake.wake();
349 }
350 Ok(())
351 }
352}
353
354impl EmbedRtmpServer<Running> {
355 /// Returns the actual bound socket address of the RTMP server.
356 ///
357 /// This is particularly useful when binding to port 0 (random port allocation),
358 /// as it allows you to discover which port the OS assigned.
359 ///
360 /// # Returns
361 ///
362 /// * `Option<std::net::SocketAddr>` - The actual bound address, or `None` if not available.
363 ///
364 /// # Example
365 ///
366 /// ```rust,ignore
367 /// let server = EmbedRtmpServer::new("127.0.0.1:0").start().unwrap();
368 /// let actual_port = server.local_addr().unwrap().port();
369 /// println!("Server listening on port: {}", actual_port);
370 /// ```
371 pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
372 self.bound_addr
373 }
374
375 /// Creates an RTMP "input" endpoint for this server (from the server's perspective),
376 /// returning an [`Output`] that can be used by FFmpeg to push media data.
377 ///
378 /// From the FFmpeg standpoint, the returned [`Output`] is where media content is
379 /// sent (i.e., FFmpeg "outputs" to this RTMP server). After obtaining this [`Output`],
380 /// you can pass it to your FFmpeg job or scheduler to start streaming data into the server.
381 ///
382 /// # Parameters
383 ///
384 /// * `app_name` - The RTMP application name, typically corresponding to the `app` part
385 /// of an RTMP URL (e.g., `rtmp://host:port/app/stream_key`).
386 /// * `stream_key` - The stream key (or "stream name"). If a stream with the same key
387 /// already exists, an error will be returned.
388 ///
389 /// # Returns
390 ///
391 /// * [`Output`] - An output object preconfigured for streaming to this RTMP server.
392 /// This can be passed to the FFmpeg SDK for actual data push.
393 /// * [`crate::error::Error`] - If a stream with the same key already exists, the server
394 /// is not ready, or an internal error occurs, the corresponding error is returned.
395 ///
396 /// # Example
397 ///
398 /// ```rust,ignore
399 /// # // Assume there are definitions and initializations for FfmpegContext, FfmpegScheduler, etc.
400 ///
401 /// // 1. Create and start the RTMP server
402 /// let mut rtmp_server = EmbedRtmpServer::new("localhost:1935");
403 /// rtmp_server.start().expect("Failed to start RTMP server");
404 ///
405 /// // 2. Create an RTMP "input" with app_name="my-app" and stream_key="my-stream"
406 /// let output = rtmp_server
407 /// .create_rtmp_input("my-app", "my-stream")
408 /// .expect("Failed to create RTMP input");
409 ///
410 /// // 3. Prepare the FFmpeg context to push a local file to the newly created `Output`
411 /// let context = FfmpegContext::builder()
412 /// .input("test.mp4")
413 /// .output(output)
414 /// .build()
415 /// .expect("Failed to build Ffmpeg context");
416 ///
417 /// // 4. Start FFmpeg to push "test.mp4" to the local RTMP server on "my-app/my-stream"
418 /// FfmpegScheduler::new(context)
419 /// .start()
420 /// .expect("Failed to start Ffmpeg job");
421 /// ```
422 pub fn create_rtmp_input(
423 &self,
424 app_name: impl Into<String>,
425 stream_key: impl Into<String>,
426 ) -> crate::error::Result<Output> {
427 // PERF-5a serialize-bypass: steady-state audio/video FLV tags are
428 // handed to the scheduler already parsed (PublisherFeed::Media),
429 // skipping the serialize→loopback→re-parse round-trip. Metadata and
430 // control still travel as serialized RTMP chunk bytes on the same FIFO
431 // feed (PublisherFeed::Raw), so the scheduler observes an identical,
432 // in-order sequence to the pure-serialize path. External TCP clients
433 // are unaffected — they never touch this feed.
434 let feed_sender = self.create_bypass_feed_sender(app_name, stream_key)?;
435 // PERF-3: both feed paths hold a WakeHandle — this internal Feed path
436 // and raw `create_stream_sender` users (RtmpStreamSender::send wakes
437 // per chunk). Wake once now so the reactor flushes the queued
438 // connect/createStream/publish handshake immediately instead of
439 // waiting for the first media frame or the 100ms poll fallback —
440 // otherwise stream setup carries avoidable startup latency.
441 let wake_handle = self.wake_handle.clone();
442 if let Some(waker) = &wake_handle {
443 waker.wake();
444 }
445
446 let mut flv_buffer = FlvBuffer::new();
447 let mut serializer = ChunkSerializer::new();
448 let write_callback: Box<dyn FnMut(&[u8]) -> i32 + Send> =
449 Box::new(move |buf: &[u8]| -> i32 {
450 flv_buffer.write_data(buf);
451 // One AVIO write can carry many FLV tags (the muxer hands over
452 // 64KB blocks): drain every complete tag now, or the backlog
453 // grows and the final tags of the stream are never sent.
454 while let Some(mut flv_tag) = flv_buffer.get_flv_tag() {
455 flv_tag.header.stream_id = 1;
456 let tag_type = flv_tag.header.tag_type;
457
458 // 0x08 audio / 0x09 video: bypass the serializer. The
459 // (timestamp, data) handed over is byte-identical to what
460 // flv_tag_to_message_payload would build and the RTMP chunk
461 // round-trip would reconstruct, so the scheduler's sequence-
462 // header / keyframe-gate / GOP semantics are preserved exactly.
463 if tag_type == 0x08 || tag_type == 0x09 {
464 let timestamp = flv_tag.header.timestamp
465 | ((flv_tag.header.timestamp_ext as u32) << 24);
466 let feed = PublisherFeed::Media {
467 tag_type,
468 timestamp: RtmpTimestamp { value: timestamp },
469 data: flv_tag.data,
470 };
471 if let Err(e) = feed_sender.send(feed) {
472 error!("Failed to send in-process media tag: {:?}", e);
473 return -1;
474 }
475 // PERF-3: wake the reactor for each bypassed media tag, the
476 // same as the Raw path below — without it the parsed tag
477 // waits in the feed until the 100ms poll fallback, negating
478 // the PERF-5a bypass. The token coalesces repeated wakes.
479 if let Some(waker) = &wake_handle {
480 waker.wake();
481 }
482 continue;
483 }
484
485 // 0x12 metadata and anything else keep the serialize path: the
486 // scheduler consumes the parsed StreamMetadataChanged event,
487 // which needs the @setDataFrame wrapping + AMF decode.
488 match serializer.serialize(&flv_tag_to_message_payload(flv_tag), false, true) {
489 Ok(packet) => {
490 if let Err(e) = feed_sender.send(PublisherFeed::Raw(packet.bytes)) {
491 error!("Failed to send RTMP packet: {:?}", e);
492 return -1;
493 }
494 // Wake the reactor for each enqueued packet. Unconditional
495 // (no message_sender.is_empty() gate): the reactor can
496 // drain the queue and sleep in poll() between an emptiness
497 // check and this send, so a was_empty gate loses the
498 // wakeup and the packet stalls until the 100ms poll
499 // fallback. Per-packet rather than once-after-the-batch:
500 // the channel is bounded (1024), so a large batch would
501 // block in send() before a post-batch wake ever ran,
502 // stranding the reactor. The eventfd/pipe token coalesces
503 // the wakes into a single reactor drain, so the cost is a
504 // cheap (already-signaled) syscall.
505 if let Some(waker) = &wake_handle {
506 waker.wake();
507 }
508 }
509 Err(e) => {
510 error!("Failed to serialize RTMP message: {:?}", e);
511 return -1;
512 }
513 }
514 }
515 buf.len() as i32
516 });
517
518 let output: Output = write_callback.into();
519
520 Ok(output
521 .set_format("flv")
522 .set_video_codec("h264")
523 .set_audio_codec("aac")
524 .set_format_opt("flvflags", "no_duration_filesize"))
525 }
526
527 /// Creates a sender channel for an RTMP stream, identified by `app_name` and `stream_key`.
528 /// Call this to publish an in-process stream directly by feeding raw,
529 /// pre-packaged RTMP chunk bytes into the server's handling pipeline.
530 ///
531 /// # Parameters
532 ///
533 /// * `app_name` - The RTMP application name.
534 /// * `stream_key` - The unique name (or key) for this stream. Must not already be in use.
535 ///
536 /// # Returns
537 ///
538 /// * [`RtmpStreamSender`] - A handle whose [`send`](RtmpStreamSender::send) feeds
539 /// raw RTMP bytes into the server's handling pipeline.
540 /// * [`crate::error::Error`] - If a stream with the same key already exists or other
541 /// internal issues occur, an error is returned.
542 ///
543 /// # Notes
544 ///
545 /// * This function sets up the initial RTMP "connect" and "publish" commands automatically.
546 /// * If you manually send bytes to the resulting channel, they should already be properly
547 /// packaged as RTMP chunks. Otherwise, the server might fail to parse them.
548 pub fn create_stream_sender(
549 &self,
550 app_name: impl Into<String>,
551 stream_key: impl Into<String>,
552 ) -> crate::error::Result<RtmpStreamSender> {
553 let stream_key = stream_key.into();
554 if self.stream_keys.contains(&stream_key) {
555 return Err(RtmpStreamAlreadyExists(stream_key));
556 }
557
558 let (sender, receiver) = crossbeam_channel::bounded(PUBLISHER_CHANNEL_CAPACITY);
559 self.register_publisher(stream_key.clone(), PublisherSource::Raw(receiver))?;
560
561 // Prime the raw byte channel with the connect / createStream / publish
562 // handshake the server session expects before any media.
563 for packet_bytes in build_publish_control(app_name.into(), stream_key)? {
564 if sender.send(packet_bytes).is_err() {
565 error!("Can't send publish control command to rtmp server.");
566 return Err(RtmpCreateStream.into());
567 }
568 }
569 Ok(RtmpStreamSender {
570 inner: sender,
571 wake_handle: self.wake_handle.clone(),
572 })
573 }
574
575 /// Registers an in-process publisher whose steady-state audio/video is
576 /// delivered already parsed (PERF-5a serialize-bypass). Metadata and
577 /// control still ride the same feed as serialized RTMP chunk bytes, so the
578 /// scheduler sees an identical, in-order message sequence to the raw path.
579 ///
580 /// Returns the feed sender, primed with the publish handshake.
581 fn create_bypass_feed_sender(
582 &self,
583 app_name: impl Into<String>,
584 stream_key: impl Into<String>,
585 ) -> crate::error::Result<crossbeam_channel::Sender<PublisherFeed>> {
586 let stream_key = stream_key.into();
587 if self.stream_keys.contains(&stream_key) {
588 return Err(RtmpStreamAlreadyExists(stream_key));
589 }
590
591 let (sender, receiver) = crossbeam_channel::bounded(PUBLISHER_CHANNEL_CAPACITY);
592 self.register_publisher(stream_key.clone(), PublisherSource::Feed(receiver))?;
593
594 // Prime the feed with the same connect / createStream / publish bytes
595 // the raw path would send, wrapped as PublisherFeed::Raw.
596 for packet_bytes in build_publish_control(app_name.into(), stream_key)? {
597 if sender.send(PublisherFeed::Raw(packet_bytes)).is_err() {
598 error!("Can't send publish control command to rtmp server.");
599 return Err(RtmpCreateStream.into());
600 }
601 }
602 Ok(sender)
603 }
604
605 /// Hands a newly registered publisher's receiving end to the reactor.
606 fn register_publisher(
607 &self,
608 stream_key: String,
609 source: PublisherSource,
610 ) -> crate::error::Result<()> {
611 let publisher_sender = match self.publisher_sender.as_ref() {
612 Some(sender) => sender,
613 None => {
614 error!("Publisher sender not initialized");
615 return Err(RtmpCreateStream.into());
616 }
617 };
618
619 if publisher_sender.send((stream_key, source)).is_err() {
620 if self.status.load(Ordering::Acquire) != STATUS_END {
621 warn!("Rtmp server worker already exited. Can't create stream sender.");
622 } else {
623 error!("Rtmp Server aborted. Can't create stream sender.");
624 }
625 return Err(RtmpCreateStream.into());
626 }
627 Ok(())
628 }
629
630 /// Stops the RTMP server by signaling the listening and connection-handling threads
631 /// to terminate. Once called, new incoming connections will be ignored, and existing
632 /// threads will exit gracefully.
633 ///
634 /// # Example
635 /// ```rust,ignore
636 /// let server = EmbedRtmpServer::new("localhost:1935");
637 /// // ... start and handle streaming
638 /// server.stop();
639 /// assert!(server.is_stopped());
640 /// ```
641 pub fn stop(self) -> EmbedRtmpServer<Ended> {
642 self.signal_stop();
643 self.into_state()
644 }
645}
646
647/// Handle connections using optimized Reactor
648///
649/// Replaces old multi-threaded handle_connections with single-threaded event-driven model:
650/// - Uses epoll/kqueue/WSAPoll for IO multiplexing
651/// - Write queue with backpressure management
652/// - Strict drain until WouldBlock semantics
653/// Whether an accept error means the process (EMFILE) or system (ENFILE) ran
654/// out of file descriptors. `io::ErrorKind` has no stable variant for these,
655/// so match on the raw OS error code.
656fn is_fd_exhaustion(e: &std::io::Error) -> bool {
657 #[cfg(unix)]
658 {
659 matches!(e.raw_os_error(), Some(code) if code == libc::EMFILE || code == libc::ENFILE)
660 }
661 #[cfg(windows)]
662 {
663 // WSAEMFILE: too many open sockets.
664 e.raw_os_error() == Some(10024)
665 }
666 #[cfg(not(any(unix, windows)))]
667 {
668 let _ = e;
669 false
670 }
671}
672
673fn handle_connections(
674 connection_receiver: crossbeam_channel::Receiver<TcpStream>,
675 publisher_receiver: crossbeam_channel::Receiver<(String, PublisherSource)>,
676 stream_keys: Arc<dashmap::DashSet<String>>,
677 gop_limit: usize,
678 max_connections: Option<usize>,
679 status: Arc<AtomicUsize>,
680 waker: Option<Waker>,
681) {
682 // Create Reactor
683 let mut reactor = match Reactor::new(gop_limit, max_connections, stream_keys, status.clone()) {
684 Ok(r) => r,
685 Err(e) => {
686 error!("Failed to create Reactor: {:?}", e);
687 status.store(STATUS_END, Ordering::Release);
688 return;
689 }
690 };
691
692 // Run Reactor main loop
693 reactor.run(connection_receiver, publisher_receiver, waker);
694
695 if status.load(Ordering::Acquire) != STATUS_END {
696 error!("Rtmp Server aborted.");
697 }
698}
699
700/// Serializes the `connect` / `createStream` / `publish` command sequence an
701/// in-process publisher sends before any media. Shared by both the raw byte
702/// path ([`create_stream_sender`](EmbedRtmpServer<Running>::create_stream_sender))
703/// and the media-bypass path ([`create_rtmp_input`](EmbedRtmpServer<Running>::create_rtmp_input))
704/// so their control bytes stay identical.
705pub(crate) fn build_publish_control(
706 app_name: String,
707 stream_key: String,
708) -> crate::error::Result<[Vec<u8>; 3]> {
709 let mut serializer = ChunkSerializer::new();
710
711 // connect
712 let mut properties: HashMap<String, Amf0Value> = HashMap::new();
713 properties.insert("app".to_string(), Amf0Value::Utf8String(app_name));
714 let connect_cmd = RtmpMessage::Amf0Command {
715 command_name: "connect".to_string(),
716 transaction_id: 1.0,
717 command_object: Amf0Value::Object(properties),
718 additional_arguments: Vec::new(),
719 }
720 .into_message_payload(RtmpTimestamp { value: 0 }, 0)
721 .map_err(|e| {
722 error!("Failed to create connect command: {:?}", e);
723 RtmpCreateStream
724 })?;
725 let connect_packet = serializer
726 .serialize(&connect_cmd, false, true)
727 .map_err(|e| {
728 error!("Failed to serialize connect command: {:?}", e);
729 RtmpCreateStream
730 })?;
731
732 // createStream
733 let create_stream_cmd = RtmpMessage::Amf0Command {
734 command_name: "createStream".to_string(),
735 transaction_id: 2.0,
736 command_object: Amf0Value::Null,
737 additional_arguments: Vec::new(),
738 }
739 .into_message_payload(RtmpTimestamp { value: 0 }, 1)
740 .map_err(|e| {
741 error!("Failed to create createStream command: {:?}", e);
742 RtmpCreateStream
743 })?;
744 let create_stream_packet = serializer
745 .serialize(&create_stream_cmd, false, true)
746 .map_err(|e| {
747 error!("Failed to serialize createStream command: {:?}", e);
748 RtmpCreateStream
749 })?;
750
751 // publish
752 let arguments = vec![
753 Amf0Value::Utf8String(stream_key),
754 Amf0Value::Utf8String("live".into()),
755 ];
756 let publish_cmd = RtmpMessage::Amf0Command {
757 command_name: "publish".to_string(),
758 transaction_id: 3.0,
759 command_object: Amf0Value::Null,
760 additional_arguments: arguments,
761 }
762 .into_message_payload(RtmpTimestamp { value: 0 }, 1)
763 .map_err(|e| {
764 error!("Failed to create publish command: {:?}", e);
765 RtmpCreateStream
766 })?;
767 let publish_packet = serializer
768 .serialize(&publish_cmd, false, true)
769 .map_err(|e| {
770 error!("Failed to serialize publish command: {:?}", e);
771 RtmpCreateStream
772 })?;
773
774 Ok([
775 connect_packet.bytes,
776 create_stream_packet.bytes,
777 publish_packet.bytes,
778 ])
779}
780
781pub(crate) fn flv_tag_to_message_payload(flv_tag: FlvTag) -> MessagePayload {
782 let timestamp = flv_tag.header.timestamp | ((flv_tag.header.timestamp_ext as u32) << 24);
783
784 let type_id = flv_tag.header.tag_type;
785 let message_stream_id = flv_tag.header.stream_id;
786
787 let data = if type_id == 0x12 {
788 wrap_metadata(flv_tag.data)
789 } else {
790 flv_tag.data
791 };
792
793 MessagePayload {
794 timestamp: RtmpTimestamp { value: timestamp },
795 type_id,
796 message_stream_id,
797 data,
798 }
799}
800
801fn wrap_metadata(data: Bytes) -> Bytes {
802 let s = "@setDataFrame";
803
804 let insert_len = 16;
805
806 let mut bytes = bytes::BytesMut::with_capacity(insert_len + data.len());
807
808 bytes.put_u8(0x02);
809 bytes.put_u16(s.len() as u16);
810 bytes.put(s.as_bytes());
811
812 bytes.put(data);
813
814 bytes.freeze()
815}
816
817// ============================================================================
818// StreamBuilder API - Simplified RTMP streaming interface
819// ============================================================================
820
821use crate::core::context::ffmpeg_context::FfmpegContext;
822use crate::core::context::input::Input;
823use crate::core::scheduler::ffmpeg_scheduler::{FfmpegScheduler, Running as SchedulerRunning};
824use crate::error::StreamError;
825use std::path::{Path, PathBuf};
826
827/// A builder for creating RTMP streaming sessions with a simplified API.
828///
829/// This builder provides a fluent interface for configuring and starting
830/// RTMP streaming without needing to manually manage the server lifecycle.
831///
832/// # Example
833///
834/// ```rust,ignore
835/// use ez_ffmpeg::rtmp::embed_rtmp_server::EmbedRtmpServer;
836///
837/// let handle = EmbedRtmpServer::stream_builder()
838/// .address("localhost:1935")
839/// .app_name("live")
840/// .stream_key("stream1")
841/// .input_file("video.mp4")
842/// // readrate defaults to 1.0 (realtime)
843/// .start()?;
844///
845/// handle.wait()?;
846/// ```
847/// RAII guard that stops a started server unless explicitly disarmed.
848///
849/// `EmbedRtmpServer` cannot implement `Drop` (`into_state` moves out of `self`,
850/// which `Drop` forbids), so a partially-built [`StreamHandle`] would otherwise
851/// leak the running server on any post-start failure: the two worker threads
852/// keep polling `status` and hold the listener port bound. This guard is armed
853/// right after `server.start()` and disarmed only once the server is handed to
854/// a `StreamHandle`; any early return drops it and calls `signal_stop`.
855struct ServerStopGuard {
856 server: Option<Arc<EmbedRtmpServer<Running>>>,
857}
858
859impl Drop for ServerStopGuard {
860 fn drop(&mut self) {
861 if let Some(server) = &self.server {
862 server.signal_stop();
863 }
864 }
865}
866
867impl ServerStopGuard {
868 /// Take ownership of the server out of the guard, so its `Drop` becomes a
869 /// no-op. Called on the success path where a `StreamHandle` assumes the
870 /// stop responsibility.
871 fn disarm(mut self) -> Arc<EmbedRtmpServer<Running>> {
872 self.server
873 .take()
874 .expect("ServerStopGuard is armed exactly once before disarm")
875 }
876}
877
878pub struct StreamBuilder {
879 address: Option<String>,
880 app_name: Option<String>,
881 stream_key: Option<String>,
882 input_file: Option<PathBuf>,
883 readrate: Option<f32>,
884 gop_limit: Option<usize>,
885 max_connections: Option<usize>,
886}
887
888impl Default for StreamBuilder {
889 fn default() -> Self {
890 Self::new()
891 }
892}
893
894impl StreamBuilder {
895 /// Creates a new `StreamBuilder` with default settings.
896 ///
897 /// By default, `readrate` is set to `1.0` (real-time playback speed),
898 /// which is equivalent to FFmpeg's `-re` flag. This is the recommended
899 /// setting for live RTMP streaming scenarios.
900 pub fn new() -> Self {
901 Self {
902 address: None,
903 app_name: None,
904 stream_key: None,
905 input_file: None,
906 readrate: Some(1.0), // Default to real-time speed for live streaming
907 gop_limit: None,
908 max_connections: None,
909 }
910 }
911
912 /// Sets the address for the RTMP server (e.g., "localhost:1935").
913 pub fn address(mut self, address: impl Into<String>) -> Self {
914 self.address = Some(address.into());
915 self
916 }
917
918 /// Sets the RTMP application name.
919 pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
920 self.app_name = Some(app_name.into());
921 self
922 }
923
924 /// Sets the stream key (publishing name).
925 pub fn stream_key(mut self, stream_key: impl Into<String>) -> Self {
926 self.stream_key = Some(stream_key.into());
927 self
928 }
929
930 /// Sets the input file path to stream.
931 pub fn input_file(mut self, path: impl AsRef<Path>) -> Self {
932 self.input_file = Some(path.as_ref().to_path_buf());
933 self
934 }
935
936 /// Sets the read rate for the input file.
937 ///
938 /// A value of 1.0 means realtime playback speed.
939 /// This is useful for simulating live streaming from a file.
940 pub fn readrate(mut self, rate: f32) -> Self {
941 self.readrate = Some(rate);
942 self
943 }
944
945 /// Sets the GOP (Group of Pictures) limit for the RTMP server.
946 ///
947 /// This controls how many GOPs are buffered for new subscribers.
948 pub fn gop_limit(mut self, limit: usize) -> Self {
949 self.gop_limit = Some(limit);
950 self
951 }
952
953 /// Sets the maximum number of connections the server will accept.
954 pub fn max_connections(mut self, max: usize) -> Self {
955 self.max_connections = Some(max);
956 self
957 }
958
959 /// Starts the RTMP streaming session.
960 ///
961 /// This method validates all required parameters, starts the RTMP server,
962 /// and begins streaming the input file.
963 ///
964 /// # Required Parameters
965 ///
966 /// - `address`: The server address
967 /// - `app_name`: The RTMP application name
968 /// - `stream_key`: The stream key (publishing name)
969 /// - `input_file`: The file to stream
970 ///
971 /// # Returns
972 ///
973 /// A `StreamHandle` that can be used to wait for completion or manage the stream.
974 ///
975 /// # Errors
976 ///
977 /// Returns `StreamError` if:
978 /// - Any required parameter is missing
979 /// - The input file does not exist
980 /// - The server fails to start
981 /// - FFmpeg context creation fails
982 pub fn start(self) -> Result<StreamHandle, StreamError> {
983 // Validate required parameters
984 let address = self
985 .address
986 .ok_or(StreamError::MissingParameter("address"))?;
987 let app_name = self
988 .app_name
989 .ok_or(StreamError::MissingParameter("app_name"))?;
990 let stream_key = self
991 .stream_key
992 .ok_or(StreamError::MissingParameter("stream_key"))?;
993 let input_file = self
994 .input_file
995 .ok_or(StreamError::MissingParameter("input_file"))?;
996
997 // Validate input file exists and is a file (not a directory)
998 if !input_file.is_file() {
999 return Err(StreamError::InputNotFound { path: input_file });
1000 }
1001
1002 // Create and configure the server
1003 let mut server = if let Some(gop_limit) = self.gop_limit {
1004 EmbedRtmpServer::new_with_gop_limit(&address, gop_limit)
1005 } else {
1006 EmbedRtmpServer::new(&address)
1007 };
1008
1009 if let Some(max_conn) = self.max_connections {
1010 server = server.set_max_connections(max_conn);
1011 }
1012
1013 // Start the server, then immediately arm a stop guard. EmbedRtmpServer
1014 // has no Drop, so any `?` failure below (create_rtmp_input, FFmpeg
1015 // build/start) would otherwise drop the Arc without stopping the two
1016 // server threads: they leak forever and the port stays AddrInUse. The
1017 // guard is disarmed only once ownership passes to the StreamHandle,
1018 // whose own Drop then owns the stop.
1019 let server = server.start().map_err(StreamError::Ffmpeg)?;
1020 let guard = ServerStopGuard {
1021 server: Some(Arc::new(server)),
1022 };
1023
1024 // Create the RTMP output (a `?` here drops the guard -> signal_stop).
1025 let output = guard
1026 .server
1027 .as_ref()
1028 .unwrap()
1029 .create_rtmp_input(&app_name, &stream_key)
1030 .map_err(StreamError::Ffmpeg)?;
1031
1032 // Create the input with optional readrate
1033 let input_path = input_file.to_string_lossy().to_string();
1034 let mut input = Input::from(input_path);
1035 if let Some(rate) = self.readrate {
1036 input = input.set_readrate(rate);
1037 }
1038
1039 // Build and start the FFmpeg context (a `?` here drops the guard too).
1040 let scheduler = FfmpegContext::builder()
1041 .input(input)
1042 .output(output)
1043 .build()
1044 .map_err(StreamError::Ffmpeg)?
1045 .start()
1046 .map_err(StreamError::Ffmpeg)?;
1047
1048 // Success: hand the server to the StreamHandle and disarm the guard so
1049 // the handle's Drop (not the guard's) owns the stop from here on.
1050 let server = guard.disarm();
1051 Ok(StreamHandle {
1052 server,
1053 scheduler: Some(scheduler),
1054 })
1055 }
1056}
1057
1058/// A handle to a running RTMP streaming session.
1059///
1060/// This handle manages the lifecycle of both the RTMP server and the FFmpeg
1061/// streaming context. When dropped, it will attempt to clean up resources.
1062///
1063/// # Example
1064///
1065/// ```rust,ignore
1066/// let handle = EmbedRtmpServer::stream_builder()
1067/// .address("localhost:1935")
1068/// .app_name("live")
1069/// .stream_key("stream1")
1070/// .input_file("video.mp4")
1071/// .start()?;
1072///
1073/// // Wait for streaming to complete
1074/// handle.wait()?;
1075/// ```
1076pub struct StreamHandle {
1077 server: Arc<EmbedRtmpServer<Running>>,
1078 scheduler: Option<FfmpegScheduler<SchedulerRunning>>,
1079}
1080
1081impl StreamHandle {
1082 /// Waits for the streaming session to complete.
1083 ///
1084 /// This method blocks until the FFmpeg context finishes processing
1085 /// (e.g., when the input file ends or an error occurs).
1086 ///
1087 /// # Returns
1088 ///
1089 /// Returns `Ok(())` if streaming completed successfully, or an error
1090 /// if something went wrong during streaming.
1091 pub fn wait(mut self) -> Result<(), StreamError> {
1092 if let Some(scheduler) = self.scheduler.take() {
1093 scheduler.wait().map_err(StreamError::Ffmpeg)?;
1094 }
1095 Ok(())
1096 }
1097
1098 /// The actual bound socket address of the underlying RTMP server.
1099 ///
1100 /// Useful when the builder was given `"127.0.0.1:0"`: the OS assigns a real
1101 /// port, and this surfaces it (delegating to
1102 /// [`EmbedRtmpServer::local_addr`]) so tests and callers can observe the
1103 /// port without a bind/drop/rebind race on a pre-probed one.
1104 pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
1105 self.server.local_addr()
1106 }
1107}
1108
1109impl Drop for StreamHandle {
1110 fn drop(&mut self) {
1111 // Wait-then-signal. Waiting first preserves the drain semantics: a
1112 // handle dropped mid-stream still delivers the remaining frames to
1113 // connected watchers before the server goes away — best-effort, not
1114 // absolute: a watcher whose join-replay budget was exhausted keeps
1115 // only its bounded backlog, and stop-time teardown does not re-run
1116 // finished-status delivery for it (cancelling the FFmpeg job instead
1117 // is a separate concern, out of scope here). The explicit stop after
1118 // it is what actually releases the listener port and the worker
1119 // threads — dropping the Arc alone never did: the threads own clones
1120 // of the status flag and keep running (and keep the port bound)
1121 // until the flag flips.
1122 if let Some(scheduler) = self.scheduler.take() {
1123 let _ = scheduler.wait();
1124 }
1125 self.server.signal_stop();
1126 }
1127}
1128
1129impl EmbedRtmpServer<Initialization> {
1130 /// Creates a new `StreamBuilder` for simplified RTMP streaming.
1131 ///
1132 /// This is the recommended entry point for simple streaming scenarios
1133 /// where you want to stream a file to an embedded RTMP server.
1134 ///
1135 /// # Example
1136 ///
1137 /// ```rust,ignore
1138 /// use ez_ffmpeg::rtmp::embed_rtmp_server::EmbedRtmpServer;
1139 ///
1140 /// let handle = EmbedRtmpServer::stream_builder()
1141 /// .address("localhost:1935")
1142 /// .app_name("live")
1143 /// .stream_key("stream1")
1144 /// .input_file("video.mp4")
1145 /// .start()?;
1146 ///
1147 /// handle.wait()?;
1148 /// ```
1149 ///
1150 /// For more complex scenarios requiring full control over the server
1151 /// and FFmpeg context, use the traditional API:
1152 ///
1153 /// ```rust,ignore
1154 /// let server = EmbedRtmpServer::new("localhost:1935").start()?;
1155 /// let output = server.create_rtmp_input("app", "stream")?;
1156 /// // ... configure Input and FfmpegContext manually
1157 /// ```
1158 pub fn stream_builder() -> StreamBuilder {
1159 StreamBuilder::new()
1160 }
1161}
1162
1163#[cfg(test)]
1164mod bypass_parity_tests {
1165 //! PERF-5a: the in-process media bypass hands the scheduler a
1166 //! `(timestamp, data)` pair taken directly from the parsed FLV tag. These
1167 //! tests prove that pair is byte-identical to what the pure-serialize path
1168 //! reconstructs (FLV tag -> `flv_tag_to_message_payload` -> RTMP chunk
1169 //! serialize -> deserialize -> `MessagePayload`), so the scheduler observes
1170 //! an identical sequence either way.
1171 use super::*;
1172 use crate::flv::flv_tag::FlvTag;
1173 use crate::flv::flv_tag_header::FlvTagHeader;
1174 use rml_rtmp::chunk_io::ChunkDeserializer;
1175
1176 fn make_tag(tag_type: u8, timestamp: u32, timestamp_ext: u8, data: Vec<u8>) -> FlvTag {
1177 FlvTag {
1178 header: FlvTagHeader {
1179 tag_type,
1180 data_size: data.len() as u32,
1181 timestamp,
1182 timestamp_ext,
1183 stream_id: 1,
1184 },
1185 data: Bytes::from(data),
1186 previous_tag_size: 0,
1187 }
1188 }
1189
1190 /// Assert the bypass `(timestamp, data)` equals the serialize round-trip.
1191 fn assert_parity(tag_type: u8, timestamp: u32, timestamp_ext: u8, data: Vec<u8>) {
1192 let tag = make_tag(tag_type, timestamp, timestamp_ext, data);
1193
1194 // What the bypass path hands to the scheduler, straight from the tag.
1195 let bypass_timestamp = tag.header.timestamp | ((tag.header.timestamp_ext as u32) << 24);
1196 let bypass_data = tag.data.clone();
1197
1198 // What the serialize path reconstructs: payload -> chunk bytes ->
1199 // deserialize -> payload.
1200 let payload = flv_tag_to_message_payload(tag);
1201 let mut serializer = ChunkSerializer::new();
1202 let packet = serializer
1203 .serialize(&payload, false, true)
1204 .expect("serialize");
1205 let mut deserializer = ChunkDeserializer::new();
1206 let round = deserializer
1207 .get_next_message(&packet.bytes)
1208 .expect("deserialize")
1209 .expect("a complete message from the serialized chunks");
1210
1211 assert_eq!(
1212 round.type_id, tag_type,
1213 "tag type parity for {tag_type:#04x}"
1214 );
1215 assert_eq!(
1216 round.timestamp.value, bypass_timestamp,
1217 "timestamp parity for tag {tag_type:#04x}"
1218 );
1219 assert_eq!(
1220 round.data, bypass_data,
1221 "payload parity for tag {tag_type:#04x}"
1222 );
1223 }
1224
1225 #[test]
1226 fn video_sequence_header_round_trips_identically() {
1227 // AVC sequence header (0x17 0x00 ...), timestamp 0.
1228 assert_parity(
1229 0x09,
1230 0,
1231 0,
1232 vec![0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x1f],
1233 );
1234 }
1235
1236 #[test]
1237 fn audio_sequence_header_round_trips_identically() {
1238 // AAC AudioSpecificConfig (0xaf 0x00 ...).
1239 assert_parity(0x08, 0, 0, vec![0xaf, 0x00, 0x12, 0x10]);
1240 }
1241
1242 #[test]
1243 fn large_idr_spanning_multiple_chunks_round_trips_identically() {
1244 // A keyframe larger than the default 128-byte chunk size forces the
1245 // serializer to split it into continuation chunks; the deserializer
1246 // must reassemble the exact same bytes.
1247 let mut data = vec![0x17, 0x01, 0x00, 0x00, 0x00];
1248 data.extend((0u16..400).map(|i| (i & 0xff) as u8));
1249 assert_parity(0x09, 0x1234, 0, data);
1250 }
1251
1252 #[test]
1253 fn delta_frame_round_trips_identically() {
1254 assert_parity(0x09, 0x0001_0000, 0, vec![0x27, 0x01, 0x00, 0x11, 0x22]);
1255 }
1256
1257 #[test]
1258 fn extended_timestamp_round_trips_identically() {
1259 // timestamp field saturated (0xFFFFFF) plus an extension byte forces
1260 // the RTMP extended-timestamp encoding; the full 32-bit value must
1261 // survive the round-trip.
1262 assert_parity(0x08, 0x00ff_ffff, 0x01, vec![0xaf, 0x01, 0xAA, 0xBB]);
1263 }
1264
1265 #[test]
1266 fn audio_and_video_tags_never_wrap_their_payload() {
1267 // Unlike 0x12 metadata (which flv_tag_to_message_payload prefixes with
1268 // @setDataFrame), media payloads must pass through untouched — the
1269 // bypass relies on this to skip the serializer entirely.
1270 let audio = make_tag(0x08, 10, 0, vec![0xaf, 0x01, 0x01, 0x02, 0x03]);
1271 let video = make_tag(0x09, 10, 0, vec![0x27, 0x01, 0x09, 0x08, 0x07]);
1272 assert_eq!(
1273 flv_tag_to_message_payload(audio.clone()).data,
1274 audio.data,
1275 "audio payload must not be wrapped"
1276 );
1277 assert_eq!(
1278 flv_tag_to_message_payload(video.clone()).data,
1279 video.data,
1280 "video payload must not be wrapped"
1281 );
1282 }
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287 use super::*;
1288 use crate::core::context::ffmpeg_context::FfmpegContext;
1289 use crate::core::context::input::Input;
1290 use crate::core::context::output::Output;
1291 use crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler;
1292 use ffmpeg_next::time::current;
1293 use std::thread::sleep;
1294 use std::time::Duration;
1295
1296 /// Poll (up to ~2s) until `addr` can be bound again. The accept thread
1297 /// releases the listener within one ~100ms accept cycle of the stop
1298 /// signal, so a successful rebind proves the stop actually tore the
1299 /// server down rather than merely flipping a flag.
1300 fn wait_for_port_release(addr: std::net::SocketAddr) -> bool {
1301 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1302 loop {
1303 match std::net::TcpListener::bind(addr) {
1304 Ok(_) => return true,
1305 Err(_) if std::time::Instant::now() < deadline => sleep(Duration::from_millis(20)),
1306 Err(_) => return false,
1307 }
1308 }
1309 }
1310
1311 // API-hygiene regression: the stream sender must report a *stream*-scoped
1312 // close (not a server-thread exit) when its consumer is gone, and stay
1313 // multi-producer like the `crossbeam_channel::Sender` it used to be.
1314 #[test]
1315 fn stream_sender_reports_stream_closed_and_clones_share_the_stream() {
1316 let (tx, rx) = crossbeam_channel::bounded::<Vec<u8>>(4);
1317 let sender = RtmpStreamSender {
1318 inner: tx,
1319 wake_handle: None,
1320 };
1321
1322 // A clone feeds the same stream: a chunk pushed through the clone is
1323 // observed on the single receiver (the old handle was `Clone`).
1324 let clone = sender.clone();
1325 clone
1326 .send(b"chunk".to_vec())
1327 .expect("send on a live stream");
1328 assert_eq!(rx.recv().unwrap(), b"chunk".to_vec());
1329
1330 // Dropping the consumer is a stream close, not a server-thread exit:
1331 // the error must be RtmpStreamClosed, never RtmpThreadExited.
1332 drop(rx);
1333 assert_eq!(
1334 sender.send(b"late".to_vec()),
1335 Err(crate::error::Error::RtmpStreamClosed),
1336 );
1337 }
1338
1339 // H7 regression: stop() used to only flip the status flag; the accept
1340 // thread parked on the listener kept the port bound, so a start-stop-start
1341 // cycle on the same address failed with AddrInUse.
1342 #[test]
1343 fn stopped_server_releases_its_port_for_rebind() {
1344 let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1345 let addr = server.local_addr().expect("bound address");
1346
1347 // Sanity: while the server runs, the port is genuinely held.
1348 assert!(
1349 std::net::TcpListener::bind(addr).is_err(),
1350 "the running server must hold its port"
1351 );
1352
1353 let stopped = server.stop();
1354 assert!(stopped.is_stopped());
1355 assert!(
1356 wait_for_port_release(addr),
1357 "the port must be rebindable within 2s of stop()"
1358 );
1359 }
1360
1361 // H7: a StreamHandle drop must stop the server it holds. Before the fix
1362 // its Drop only waited on the scheduler and let the Arc'd server leak its
1363 // threads and port forever.
1364 #[test]
1365 fn stream_handle_drop_stops_the_server() {
1366 let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1367 let addr = server.local_addr().expect("bound address");
1368 let server = Arc::new(server);
1369 let observer = server.clone();
1370
1371 let handle = StreamHandle {
1372 server,
1373 scheduler: None,
1374 };
1375 drop(handle);
1376
1377 assert!(
1378 observer.is_stopped(),
1379 "dropping the handle must signal the server to stop"
1380 );
1381 assert!(
1382 wait_for_port_release(addr),
1383 "the port must be rebindable within 2s of the handle drop"
1384 );
1385 }
1386
1387 // F6: the post-start failure path — a StreamBuilder that starts the server
1388 // but then fails to build the FFmpeg job must not leak it. Rather than a
1389 // racy probe/drop/rebind on a fixed port (which a parallel test could steal
1390 // between the drop and the builder's bind), drive the exact RAII path
1391 // directly: start on port 0, read the OS-assigned address, arm the
1392 // ServerStopGuard, then drop it as any post-start `?` would — no
1393 // reserve/drop/rebind window.
1394 #[test]
1395 fn server_stop_guard_drop_releases_the_port() {
1396 let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1397 let addr = server.local_addr().expect("bound address");
1398
1399 // The port is genuinely held while the guard owns the running server.
1400 assert!(
1401 std::net::TcpListener::bind(addr).is_err(),
1402 "the running server must hold its port while the guard is armed"
1403 );
1404
1405 // Arm the guard exactly as StreamBuilder::start does, then simulate the
1406 // post-start failure by dropping it — its Drop must signal_stop.
1407 let guard = ServerStopGuard {
1408 server: Some(Arc::new(server)),
1409 };
1410 drop(guard);
1411
1412 assert!(
1413 wait_for_port_release(addr),
1414 "dropping the armed guard (a post-start failure) must release the port"
1415 );
1416 }
1417
1418 // H8.b regression: the reactor used to receive a `.clone()` of the
1419 // DashSet — a deep copy — so keys it inserted never became visible to the
1420 // duplicate-key check in create_* and RtmpStreamAlreadyExists was dead
1421 // code: two publishers could claim the same stream key.
1422 #[test]
1423 fn duplicate_stream_key_is_rejected_once_registered() {
1424 let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1425 // Keep the Output alive: dropping it drops the feed sender, which
1426 // deregisters the publisher and frees the key again.
1427 let _output = server
1428 .create_rtmp_input("app", "dup-key")
1429 .expect("first create must succeed");
1430
1431 // Registration travels over a channel to the reactor thread; poll
1432 // until the shared key set reflects it.
1433 let deadline = std::time::Instant::now() + Duration::from_secs(1);
1434 while !server.stream_keys.contains("dup-key") {
1435 assert!(
1436 std::time::Instant::now() < deadline,
1437 "the stream key must appear in the shared set within 1s"
1438 );
1439 sleep(Duration::from_millis(10));
1440 }
1441
1442 let second = server.create_rtmp_input("app", "dup-key");
1443 assert!(
1444 matches!(
1445 second,
1446 Err(crate::error::Error::RtmpStreamAlreadyExists(ref key)) if key == "dup-key"
1447 ),
1448 "a second create for a registered key must fail with RtmpStreamAlreadyExists"
1449 );
1450
1451 server.stop();
1452 }
1453
1454 // H7: signal_stop is idempotent — a second signal (or a stop() after a
1455 // signal) must be a harmless no-op.
1456 #[test]
1457 fn double_stop_signal_is_idempotent() {
1458 let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1459 let addr = server.local_addr().expect("bound address");
1460
1461 server.signal_stop();
1462 assert!(server.is_stopped());
1463 server.signal_stop();
1464 assert!(server.is_stopped());
1465
1466 let ended = server.stop();
1467 assert!(ended.is_stopped());
1468 assert!(wait_for_port_release(addr));
1469 }
1470
1471 #[test]
1472 #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
1473 fn test_concat_stream_loop() {
1474 let _ = env_logger::builder()
1475 .filter_level(log::LevelFilter::Trace)
1476 .is_test(true)
1477 .try_init();
1478
1479 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1480 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1481
1482 let output = embed_rtmp_server
1483 .create_rtmp_input("my-app", "my-stream")
1484 .unwrap();
1485
1486 let start = current();
1487
1488 let result = FfmpegContext::builder()
1489 .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
1490 .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
1491 .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
1492 .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
1493 .output(output)
1494 .build()
1495 .unwrap()
1496 .start()
1497 .unwrap()
1498 .wait();
1499
1500 assert!(result.is_ok());
1501 info!("elapsed time: {}", current() - start);
1502 }
1503
1504 #[test]
1505 #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
1506 fn test_stream_loop() {
1507 let _ = env_logger::builder()
1508 .filter_level(log::LevelFilter::Trace)
1509 .is_test(true)
1510 .try_init();
1511
1512 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1513 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1514
1515 let output = embed_rtmp_server
1516 .create_rtmp_input("my-app", "my-stream")
1517 .unwrap();
1518
1519 let start = current();
1520
1521 let result = FfmpegContext::builder()
1522 .input(
1523 Input::from("test.mp4")
1524 .set_readrate(1.0)
1525 .set_stream_loop(-1),
1526 )
1527 // .filter_desc("hue=s=0")
1528 .output(output.set_video_codec("h264_videotoolbox"))
1529 .build()
1530 .unwrap()
1531 .start()
1532 .unwrap()
1533 .wait();
1534
1535 assert!(result.is_ok());
1536
1537 info!("elapsed time: {}", current() - start);
1538 }
1539
1540 #[test]
1541 #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
1542 fn test_concat_realtime() {
1543 let _ = env_logger::builder()
1544 .filter_level(log::LevelFilter::Trace)
1545 .is_test(true)
1546 .try_init();
1547
1548 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1549 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1550
1551 let output = embed_rtmp_server
1552 .create_rtmp_input("my-app", "my-stream")
1553 .unwrap();
1554
1555 let start = current();
1556
1557 let result = FfmpegContext::builder()
1558 .independent_readrate()
1559 .input(Input::from("test.mp4").set_readrate(1.0))
1560 .input(Input::from("test.mp4").set_readrate(1.0))
1561 .input(Input::from("test.mp4").set_readrate(1.0))
1562 .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
1563 .output(output)
1564 .build()
1565 .unwrap()
1566 .start()
1567 .unwrap()
1568 .wait();
1569
1570 assert!(result.is_ok());
1571
1572 sleep(Duration::from_secs(1));
1573 info!("elapsed time: {}", current() - start);
1574 }
1575
1576 #[test]
1577 #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
1578 fn test_realtime() {
1579 let _ = env_logger::builder()
1580 .filter_level(log::LevelFilter::Trace)
1581 .is_test(true)
1582 .try_init();
1583
1584 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1585 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1586
1587 let output = embed_rtmp_server
1588 .create_rtmp_input("my-app", "my-stream")
1589 .unwrap();
1590
1591 let start = current();
1592
1593 let result = FfmpegContext::builder()
1594 .input(Input::from("test.mp4").set_readrate(1.0))
1595 .output(output)
1596 .build()
1597 .unwrap()
1598 .start()
1599 .unwrap()
1600 .wait();
1601
1602 assert!(result.is_ok());
1603
1604 info!("elapsed time: {}", current() - start);
1605 }
1606
1607 #[test]
1608 #[ignore] // Integration test: requires test.mp4
1609 fn test_readrate() {
1610 let _ = env_logger::builder()
1611 .filter_level(log::LevelFilter::Trace)
1612 .is_test(true)
1613 .try_init();
1614
1615 let mut output: Output = "output.flv".into();
1616 output.audio_codec = Some("adpcm_swf".to_string());
1617
1618 let mut input: Input = "test.mp4".into();
1619 input.readrate = Some(1.0);
1620
1621 let context = FfmpegContext::builder()
1622 .input(input)
1623 .output(output)
1624 .build()
1625 .unwrap();
1626
1627 let result = FfmpegScheduler::new(context).start().unwrap().wait();
1628 if let Err(error) = result {
1629 println!("Error: {error}");
1630 }
1631 }
1632
1633 #[test]
1634 #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
1635 fn test_embed_rtmp_server() {
1636 let _ = env_logger::builder()
1637 .filter_level(log::LevelFilter::Trace)
1638 .is_test(true)
1639 .try_init();
1640
1641 let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
1642 let embed_rtmp_server = embed_rtmp_server.start().unwrap();
1643
1644 let output = embed_rtmp_server
1645 .create_rtmp_input("my-app", "my-stream")
1646 .unwrap();
1647 let mut input: Input = "test.mp4".into();
1648 input.readrate = Some(1.0);
1649
1650 let context = FfmpegContext::builder()
1651 .input(input)
1652 .output(output)
1653 .build()
1654 .unwrap();
1655
1656 let result = FfmpegScheduler::new(context).start().unwrap().wait();
1657
1658 assert!(result.is_ok());
1659
1660 sleep(Duration::from_secs(3));
1661 }
1662}