Skip to main content

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}