Skip to main content

ez_ffmpeg/rtmp/
embed_rtmp_server.rs

1use crate::core::context::output::Output;
2use crate::error::Error::{
3    RtmpCreateStream, RtmpRegistrationQueueFull, RtmpServerAlreadyStarted, RtmpStreamAlreadyExists,
4};
5use crate::flv::flv_buffer::FlvBuffer;
6use crate::flv::flv_tag::FlvTag;
7use crate::rtmp::poller::{waker_pair, WakeHandle, Waker};
8use crate::rtmp::reactor::{
9    effective_max_connections, EnqueueRefused, PublisherFeed, PublisherRegistration,
10    PublisherSource, Reactor, RegistrationHandoff, RegistrationKillSwitch, StreamKeyClaim,
11    CHANNEL_HEADROOM, PUBLISHER_CHANNEL_CAPACITY,
12};
13use bytes::{BufMut, Bytes};
14use log::{debug, error, info, warn};
15use rml_rtmp::chunk_io::ChunkSerializer;
16use rml_rtmp::messages::{MessagePayload, RtmpMessage};
17use rml_rtmp::rml_amf0::Amf0Value;
18use rml_rtmp::time::RtmpTimestamp;
19use std::collections::HashMap;
20use std::marker::PhantomData;
21use std::net::{Shutdown, TcpListener, TcpStream};
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::sync::{Arc, Mutex, PoisonError};
24use std::thread::JoinHandle;
25
26#[derive(Clone)]
27pub struct Initialization;
28#[derive(Clone)]
29pub struct Running;
30#[derive(Clone)]
31pub struct Ended;
32
33#[derive(Clone)]
34pub struct EmbedRtmpServer<S> {
35    address: String,
36    bound_addr: Option<std::net::SocketAddr>,
37    status: Arc<AtomicUsize>,
38    // Arc-shared with the reactor. create_* claims a key by wrapping it in a
39    // StreamKeyClaim (insert-or-fail, so concurrent creates cannot both win);
40    // the claim rides inside the queued registration and releases itself on
41    // drop unless the reactor accepts the publisher — from then on removal
42    // of the publisher releases the key. DashSet's Clone is a deep copy, so
43    // a non-Arc field cloned into the worker thread would split server and
44    // reactor onto two disjoint sets and disable the duplicate-key check
45    // entirely.
46    stream_keys: Arc<dashmap::DashSet<String>>,
47    // Registrations for the reactor: key claim + publisher source (raw byte
48    // path or media-bypass feed). Lock-shared with the worker rather than a
49    // channel, so the create paths' worker-liveness check and their enqueue
50    // form one critical section (see RegistrationHandoff).
51    registrations: Option<Arc<RegistrationHandoff>>,
52    /// Producer-side wakeup for the reactor (PERF-3), set in `start()`.
53    wake_handle: Option<WakeHandle>,
54    /// Join handles of the two server threads (worker, accept), pushed by
55    /// `start()` and shared by every clone of the family. `stop()` drains
56    /// this registry and joins them — its settlement barrier — holding the
57    /// lock across the joins so a racing `stop()` on a clone blocks on the
58    /// lock and also returns only after teardown finished. The server
59    /// threads themselves never reach this lock: the only way user code
60    /// (which could hold a clone and call `stop()`) runs on them is through
61    /// a user-installed global logger, and `settle_server_threads` bails out
62    /// on a server thread BEFORE touching the registry (see its reentrancy
63    /// notes) — so holding the lock across the joins cannot deadlock.
64    threads: Arc<Mutex<Vec<JoinHandle<()>>>>,
65    /// The `ThreadId`s of the threads whose handles live in `threads`,
66    /// recorded at spawn. `settle_server_threads` probes this list — briefly,
67    /// never across a join — to detect the reentrant case above without
68    /// going anywhere near the registry lock.
69    server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>>,
70    /// WHY the family reached its terminal state — `CAUSE_DELIBERATE` for
71    /// the user-initiated paths (`stop()`, a handle/guard drop), or
72    /// `CAUSE_FATAL` for the failure paths (a contained reactor panic, the
73    /// worker dying). First writer wins: the cause is claimed with a
74    /// compare-exchange from `CAUSE_NONE` by whichever terminal transition
75    /// lands first, so a `stop()` arriving after a crash cannot relabel the
76    /// crash as deliberate. In-process publisher write callbacks read it
77    /// when their feed send fails, to report a deliberate server stop
78    /// calmly instead of as an opaque send error.
79    terminal_cause: Arc<AtomicUsize>,
80    gop_limit: usize,
81    max_connections: Option<usize>,
82    state: PhantomData<S>,
83}
84
85const STATUS_INIT: usize = 0;
86const STATUS_RUN: usize = 1;
87const STATUS_END: usize = 2;
88
89/// `terminal_cause` values. Kept separate from the status flag: the status
90/// answers "is the family stopped" (three states, compared all over the
91/// reactor and accept loops), the cause answers "who stopped it" (write-once,
92/// read only by failure classification). Folding the cause into the status
93/// would turn every `== STATUS_END` comparison into a range check.
94const CAUSE_NONE: usize = 0;
95const CAUSE_DELIBERATE: usize = 1;
96const CAUSE_FATAL: usize = 2;
97
98/// The one funnel for the server's terminal transition: close the
99/// registration intake FIRST, then release-store `STATUS_END`.
100///
101/// The release-store orders the close ahead of any observer's acquire-load
102/// of the status, so a caller that saw `is_stopped() == true` can no longer
103/// enqueue a registration and be told `Ok` when no reactor round will
104/// consume it (the reactor checks the stop flag before draining the queue).
105/// Every path that publishes `STATUS_END` must run through here — a store
106/// over a still-open intake reintroduces exactly that lie. Both steps are
107/// idempotent, so racing terminal paths (a stop signal against a worker
108/// panic, or the accept loop noticing the worker died) may each run this
109/// with no ordering between them.
110/// Every direct caller of this funnel is a FAILURE path (worker death,
111/// reactor panic, start() unwind), so the funnel claims `CAUSE_FATAL` for
112/// them; the deliberate paths all run through `signal_stop`, which claims
113/// `CAUSE_DELIBERATE` BEFORE delegating here, making this claim a no-op.
114/// The compare-exchange makes the cause first-writer-wins under races
115/// between a crash and a late `stop()`.
116fn close_intake_and_publish_end(
117    registrations: &RegistrationHandoff,
118    status: &AtomicUsize,
119    terminal_cause: &AtomicUsize,
120) {
121    let _ = terminal_cause.compare_exchange(
122        CAUSE_NONE,
123        CAUSE_FATAL,
124        Ordering::AcqRel,
125        Ordering::Acquire,
126    );
127    registrations.close();
128    status.store(STATUS_END, Ordering::Release);
129}
130
131/// `stop()`'s settlement barrier: join every server thread in the family's
132/// registry, holding the registry lock across the joins so racing `stop()`
133/// calls on clones serialize behind it and each returns only once teardown
134/// finished. The caller must have signaled the stop first, or the joins
135/// would wait on threads with no reason to exit.
136///
137/// Reentrancy guard — FIRST, before any lock. The server threads do run
138/// user code: every `log` macro they execute dispatches to a user-installed
139/// global logger, which may hold a server clone and call `stop()`. Such a
140/// call must neither block on the registry lock (an outer settlement holds
141/// it while joining the very thread the logger runs on — deadlock) nor
142/// consume handles (its own would be dropped, detaching a thread a later
143/// user-thread `stop()` could still settle). A server thread therefore gets
144/// signal-only semantics: return SILENTLY with the registry untouched —
145/// silently, because this thread may be inside the logger's own log()
146/// frame, above a non-reentrant lock the logger holds, and any log macro
147/// here would re-enter the logger and deadlock on it: the exact failure
148/// this branch exists to prevent. The id list's lock is held only for this
149/// membership probe — never across a join — so the probe itself cannot
150/// deadlock.
151///
152/// The join loop performs no logging and nothing else that can unwind: an
153/// unwind mid-drain would detach the remaining handles and poison the
154/// registry into a state indistinguishable from settled (empty), turning a
155/// later `stop()` into a false barrier. That discipline includes the panic
156/// PAYLOADS of joined threads: a payload is an arbitrary `Box<dyn Any>`,
157/// and settlement never runs its drop glue at all — payloads are moved
158/// aside untouched during the drain and deliberately LEAKED
159/// (`mem::forget`) once the lock is released. Running their destructors
160/// here, however contained, is unwinnable in principle: a payload whose
161/// two field destructors both panic aborts the process inside its own
162/// drop glue before any catch regains control, and a destructor is
163/// equally free to BLOCK forever on a lock the `stop()` caller holds —
164/// either outcome inside a blocking public API is strictly worse than
165/// leaking a few bytes per crashed thread, an already-broken, bounded
166/// path. (Contrast `dispose_panic_payload` in packet_sink, which runs
167/// destructors bounded-chain-contained on paths where a caller is not
168/// blocked behind them.) The report afterwards runs under containment,
169/// and its own panic payload — a logger's — is forgotten the same way, so
170/// nothing can unwind a completed settlement out of `stop()`. Riding over
171/// poisoning is safe under that discipline: a poisoned registry still
172/// holds exactly the handles no settlement consumed, and the drain below
173/// joins them as usual.
174fn settle_server_threads(
175    server_thread_ids: &Mutex<Vec<std::thread::ThreadId>>,
176    threads: &Mutex<Vec<JoinHandle<()>>>,
177) {
178    let current = std::thread::current().id();
179    let is_server_thread = server_thread_ids
180        .lock()
181        .unwrap_or_else(PoisonError::into_inner)
182        .contains(&current);
183    if is_server_thread {
184        // No logging here — not even a warn. See the reentrancy note above:
185        // this frame may sit inside the user logger itself, and dispatching
186        // to it again would deadlock on the logger's own lock.
187        return;
188    }
189
190    let mut panicked: Vec<String> = Vec::new();
191    let mut payloads: Vec<Box<dyn std::any::Any + Send>> = Vec::new();
192    {
193        let mut registry = threads.lock().unwrap_or_else(PoisonError::into_inner);
194        for handle in registry.drain(..) {
195            // Defensive second line for a handle the id list missed (only
196            // reachable from hand-assembled registries in tests): detaching
197            // beats self-joining forever. No logging in this loop — see the
198            // unwind-safety note above.
199            if handle.thread().id() == current {
200                continue;
201            }
202            let name = handle.thread().name().unwrap_or("rtmp-server").to_string();
203            if let Err(payload) = handle.join() {
204                // Move the payload aside WITHOUT dropping it: its
205                // destructor is arbitrary user code that may panic, and no
206                // unwind is allowed while the drain and the registry guard
207                // are live (see the unwind-safety note above).
208                payloads.push(payload);
209                panicked.push(name);
210            }
211        }
212    }
213    // Leak the retained payloads BEFORE any reporting: no drop glue of an
214    // arbitrary payload may run inside a blocking stop() — a two-bomb
215    // payload aborts inside its own drop glue before any catch regains
216    // control, and a destructor can just as well block on a lock the
217    // stop() caller holds. See the unwind-safety note above.
218    for payload in payloads {
219        std::mem::forget(payload);
220    }
221    if !panicked.is_empty() {
222        // The threads died to uncontained panics (e.g. a user-installed
223        // logger panicking outside the reactor's unwind boundary). They are
224        // just as terminated — the barrier holds — so report rather than
225        // rethrow. The report itself runs contained: settlement is already
226        // complete and this is purely informational, so a logger panicking
227        // HERE must not unwind a finished settlement out of stop() — and
228        // whatever payload that panic carries is forgotten like the join
229        // payloads above.
230        let report = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
231            for name in &panicked {
232                error!("Thread[{name}] terminated by an uncontained panic");
233            }
234        }));
235        if let Err(payload) = report {
236            std::mem::forget(payload);
237        }
238    }
239}
240
241/// Unwind backstop for `start()`'s window between the lifecycle claim (the
242/// `compare_exchange` of `STATUS_INIT` to `STATUS_RUN`) and the successful
243/// handoff to the accept thread. Code in that window still logs, and
244/// `error!`/`info!` dispatch to a user-installed logger that can itself
245/// panic; an unwind escaping there would leave the family stuck at
246/// `STATUS_RUN` — every clone reporting a started server that no one can
247/// ever stop, and a worker thread that already spawned polling that status
248/// forever. Dropped while armed, the guard runs the same terminal sequence
249/// as `signal_stop`: the funnel (intake closed first, then `STATUS_END`),
250/// then the reactor wake. All steps are idempotent, so overlapping the
251/// explicit `signal_stop` on `start()`'s error returns is harmless. The
252/// success path disarms the guard once the accept thread owns the listener —
253/// from then on terminal transitions belong to `stop()`, the RAII guards
254/// and the worker's own fatal paths.
255struct StartFailGuard {
256    armed: bool,
257    registrations: Arc<RegistrationHandoff>,
258    status: Arc<AtomicUsize>,
259    wake_handle: Option<WakeHandle>,
260    threads: Arc<Mutex<Vec<JoinHandle<()>>>>,
261    server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>>,
262    terminal_cause: Arc<AtomicUsize>,
263}
264
265impl Drop for StartFailGuard {
266    fn drop(&mut self) {
267        if !self.armed {
268            return;
269        }
270        close_intake_and_publish_end(&self.registrations, &self.status, &self.terminal_cause);
271        if let Some(wake_handle) = &self.wake_handle {
272            wake_handle.wake();
273        }
274        // Settle whatever did spawn before the unwind: the signal above
275        // makes those threads exit promptly, and joining them here keeps
276        // the failed start from leaving live threads (and their registry
277        // handles) behind for a family no `stop()` can ever reach —
278        // `start()` consumed the one lifecycle claim, so no `Running`
279        // value will exist to settle them later. Runs on `start()`'s
280        // caller thread, never a server thread, so the reentrancy bail-out
281        // inside cannot fire.
282        settle_server_threads(&self.server_thread_ids, &self.threads);
283    }
284}
285
286impl<S: 'static> EmbedRtmpServer<S> {
287    fn into_state<T>(self) -> EmbedRtmpServer<T> {
288        EmbedRtmpServer {
289            address: self.address,
290            bound_addr: self.bound_addr,
291            status: self.status,
292            stream_keys: self.stream_keys,
293            registrations: self.registrations,
294            wake_handle: self.wake_handle,
295            threads: self.threads,
296            server_thread_ids: self.server_thread_ids,
297            terminal_cause: self.terminal_cause,
298            gop_limit: self.gop_limit,
299            max_connections: self.max_connections,
300            state: Default::default(),
301        }
302    }
303
304    /// Checks whether the RTMP server has been signaled to stop. This returns
305    /// `true` once [`stop`](EmbedRtmpServer<Running>::stop) has been called
306    /// (or a fatal internal error stopped the server), otherwise `false`.
307    ///
308    /// Note this reports the *signal*, not thread teardown: the worker threads
309    /// observe the flag and exit shortly after (the reactor on its next
310    /// wakeup, the accept thread within its ~100ms accept cycle). The one
311    /// place both coincide is [`stop`](EmbedRtmpServer<Running>::stop), which
312    /// joins the server threads before returning — after a `stop()` call has
313    /// returned, teardown is complete as well (except for `stop()`'s
314    /// reentrant logger edge, documented there, which is signal-only).
315    ///
316    /// # Returns
317    ///
318    /// * `true` if the server has been signaled to stop (and will no longer accept connections).
319    /// * `false` if the server is still running.
320    pub fn is_stopped(&self) -> bool {
321        self.status.load(Ordering::Acquire) == STATUS_END
322    }
323
324    /// Signal the server threads to stop without consuming the server.
325    ///
326    /// Idempotent: re-closing a closed intake, storing `STATUS_END` again and
327    /// re-waking an already-woken reactor are all no-ops. The wake matters —
328    /// without it the reactor notices the flag only on its next poll timeout,
329    /// and a reactor parked in `poll()` would otherwise hold the shutdown for
330    /// up to 100ms. When no wake handle exists (waker_pair creation failed at
331    /// start), that 100ms poll fallback is exactly the degraded path the
332    /// reactor already runs on.
333    ///
334    /// This exists because `EmbedRtmpServer` cannot implement `Drop` itself:
335    /// `into_state()` moves fields out of `self`, which the compiler forbids
336    /// for types with a `Drop` impl (E0509). Shared owners that only hold a
337    /// reference (e.g. [`StreamHandle`]) stop the server through this instead.
338    fn signal_stop(&self) {
339        // Claim the terminal cause BEFORE the funnel, first-writer-wins: if
340        // a fatal transition (worker death, contained panic) already claimed
341        // `CAUSE_FATAL`, this exchange fails and the crash keeps its loud
342        // classification — a late deliberate stop() must not relabel it.
343        // Ordering: the funnel's release-store of `STATUS_END` orders this
344        // claim ahead of any acquire-load of the status, and the reactor
345        // tears publishers down only after observing that status, so the
346        // cause is in place before any teardown a callback could witness.
347        let _ = self.terminal_cause.compare_exchange(
348            CAUSE_NONE,
349            CAUSE_DELIBERATE,
350            Ordering::AcqRel,
351            Ordering::Acquire,
352        );
353        // Close-then-publish through the shared funnel (see
354        // `close_intake_and_publish_end` for the ordering argument).
355        // Registrations already queued keep their owners — the reactor's
356        // remaining rounds, with the worker's kill-switch drain as the
357        // backstop.
358        match &self.registrations {
359            Some(registrations) => {
360                close_intake_and_publish_end(registrations, &self.status, &self.terminal_cause)
361            }
362            // Defensive arm, unreachable today: every caller reaches this
363            // with the handoff installed (`stop()`, the RAII guards and
364            // `start()`'s failure paths all run after `start()` stored
365            // it). Should a signal path for never-started handles ever
366            // appear, it must not use this plain store while a started
367            // sibling's intake exists — route it through that intake and
368            // the funnel instead.
369            None => self.status.store(STATUS_END, Ordering::Release),
370        }
371        if let Some(wake_handle) = &self.wake_handle {
372            wake_handle.wake();
373        }
374    }
375}
376
377impl EmbedRtmpServer<Initialization> {
378    /// Creates a new RTMP server instance that will listen on the specified address
379    /// when [`start`](EmbedRtmpServer<Initialization>::start) is called.
380    ///
381    /// # Parameters
382    ///
383    /// * `address` - A string slice representing the address (host:port) to bind the
384    ///   RTMP server socket.
385    ///
386    /// # Returns
387    ///
388    /// An [`EmbedRtmpServer`] configured to listen on the given address.
389    pub fn new(address: impl Into<String>) -> EmbedRtmpServer<Initialization> {
390        Self::new_with_gop_limit(address, 1)
391    }
392
393    /// Creates a new RTMP server instance that will listen on the specified address,
394    /// with a custom GOP limit.
395    ///
396    /// This method allows specifying the maximum number of GOPs to be cached.
397    /// A GOP (Group of Pictures) represents a sequence of video frames (I, P, B frames)
398    /// used for efficient video decoding and random access. The GOP limit defines
399    /// how many such groups are stored in the cache.
400    ///
401    /// # Parameters
402    ///
403    /// * `address` - A string slice representing the address (host:port) to bind the
404    ///   RTMP server socket.
405    /// * `gop_limit` - The maximum number of GOPs to cache.
406    ///
407    /// # Returns
408    ///
409    /// An [`EmbedRtmpServer`] instance configured to listen on the given address and
410    /// using the specified GOP limit.
411    pub fn new_with_gop_limit(
412        address: impl Into<String>,
413        gop_limit: usize,
414    ) -> EmbedRtmpServer<Initialization> {
415        Self {
416            address: address.into(),
417            bound_addr: None,
418            status: Arc::new(AtomicUsize::new(STATUS_INIT)),
419            stream_keys: Default::default(),
420            registrations: None,
421            wake_handle: None,
422            threads: Default::default(),
423            server_thread_ids: Default::default(),
424            terminal_cause: Default::default(),
425            gop_limit,
426            max_connections: None,
427            state: Default::default(),
428        }
429    }
430
431    /// Sets the maximum number of concurrent connections allowed.
432    ///
433    /// If not set, the limit is auto-detected based on system file descriptor limits
434    /// (default: 10000, capped at 80% of system FD limit).
435    ///
436    /// # Parameters
437    ///
438    /// * `max_connections` - Maximum number of concurrent connections
439    ///
440    /// # Returns
441    ///
442    /// Self for method chaining.
443    pub fn set_max_connections(mut self, max_connections: usize) -> Self {
444        self.max_connections = Some(max_connections);
445        self
446    }
447
448    /// Starts the RTMP server on the configured address, entering a loop that
449    /// accepts incoming client connections. This method spawns background threads
450    /// to handle the connections and publish events.
451    ///
452    /// Clones of one server value share a single lifecycle, and that
453    /// lifecycle can be started only once: the first `start()` wins, and
454    /// every later attempt — a sibling clone while the server runs, or any
455    /// clone after it stopped — fails with
456    /// [`RtmpServerAlreadyStarted`](crate::error::Error::RtmpServerAlreadyStarted),
457    /// refused before any socket is bound: a second start on a fixed port
458    /// reports the lifecycle error, not `AddrInUse` from the winner's port.
459    /// A `start()` that fails before it claims the lifecycle (e.g. the
460    /// bind) leaves it untouched, so a clone may retry; from the claim on,
461    /// a failing `start()` stops the lifecycle for good.
462    ///
463    /// # Returns
464    ///
465    /// * `Ok(())` if the server successfully starts listening.
466    /// * An error variant if the socket could not be bound, this server
467    ///   value's lifecycle was already started once, or other I/O errors
468    ///   occur.
469    pub fn start(mut self) -> crate::error::Result<EmbedRtmpServer<Running>> {
470        // Admission, step one: refuse an already-started (or stopped)
471        // family before touching the address. Binding first would report a
472        // second start() on a fixed port as the winner's port being in use
473        // — Error::IO(AddrInUse) — instead of the typed lifecycle error,
474        // and right after stop() the outcome would flip between the two
475        // depending on how quickly the old accept thread released the
476        // listener. This load is advisory only; the compare_exchange below
477        // is the authoritative claim.
478        if self.status.load(Ordering::Acquire) != STATUS_INIT {
479            return Err(RtmpServerAlreadyStarted);
480        }
481
482        // Admission, step two: the bind. Failures from here up to the
483        // claim below leave the status at STATUS_INIT, so a failed bind
484        // stays retryable through a clone.
485        let listener = TcpListener::bind(self.address.clone())
486            .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
487
488        // Get actual bound address (important for port 0)
489        let actual_addr = listener
490            .local_addr()
491            .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
492        self.bound_addr = Some(actual_addr);
493
494        listener
495            .set_nonblocking(true)
496            .map_err(|e| <std::io::Error as Into<crate::error::Error>>::into(e))?;
497
498        // Calculate effective max and create bounded channel with headroom
499        // This prevents unbounded queue growth when reactor is at capacity
500        let effective_max = effective_max_connections(self.max_connections);
501        let channel_capacity = effective_max.saturating_add(CHANNEL_HEADROOM);
502        let (stream_sender, stream_receiver) = crossbeam_channel::bounded(channel_capacity);
503        // Publisher registrations ride a lock-shared queue, not a channel:
504        // the worker-liveness check and the enqueue must share one critical
505        // section, and the worker's exit drain must be terminal (see
506        // RegistrationHandoff). The reactor picks registrations up on its
507        // normal loop turns (wake or poll timeout), as it did the channel.
508        let registrations = Arc::new(RegistrationHandoff::new());
509        self.registrations = Some(registrations.clone());
510
511        // PERF-3: create the reactor wakeup pair. The Waker (read side) is moved
512        // into the worker thread and registered with the poller; the WakeHandle
513        // (write side) is kept here so create_rtmp_input can signal the reactor
514        // the instant media is queued.
515        // If the wakeup pair cannot be created (e.g. eventfd/loopback exhaustion),
516        // degrade gracefully to the POLL_TIMEOUT_MS fallback rather than failing
517        // server startup: the low-latency wakeup is an optimization, not a
518        // correctness requirement.
519        let (waker, wake_handle) = match waker_pair() {
520            Ok((waker, handle)) => (Some(waker), Some(handle)),
521            Err(e) => {
522                warn!("PERF-3: reactor waker unavailable ({e:?}); falling back to the poll-timeout for in-process media latency");
523                (None, None)
524            }
525        };
526        self.wake_handle = wake_handle;
527
528        // Admission, step three — the single-start gate for the whole clone
529        // family. The status flag (and the key set) is shared by every
530        // clone of this server value, but each start() builds its own
531        // registration intake around it — so a second started server would
532        // leave the family with TWO intakes and one status: stopping either
533        // one closes only its own intake while publishing the shared
534        // STATUS_END, and the other's create paths would keep returning Ok
535        // on handles that report stopped. The CAS makes that state
536        // structurally impossible: a racer that slipped past the advisory
537        // load above is refused right here, before any thread spawns, and
538        // its freshly bound listener drops with the return, releasing the
539        // port. This CAS is also the lifecycle's point of no return —
540        // failures above it leave the status at STATUS_INIT (retryable
541        // through a clone); failures below it are terminal for the family.
542        if self
543            .status
544            .compare_exchange(STATUS_INIT, STATUS_RUN, Ordering::AcqRel, Ordering::Acquire)
545            .is_err()
546        {
547            return Err(RtmpServerAlreadyStarted);
548        }
549
550        // The claim is consumed: from here to the disarm at the bottom,
551        // every exit — the explicit error returns as much as an unwind out
552        // of a log call whose user-installed logger panics — must publish
553        // the terminal state, or the family is stuck reporting a running
554        // server that no one can ever stop. The guard covers the unwinds;
555        // the error paths below also run the funnel explicitly, first
556        // thing, so the terminal state is published before they log.
557        let mut start_fail_guard = StartFailGuard {
558            armed: true,
559            registrations: registrations.clone(),
560            status: self.status.clone(),
561            wake_handle: self.wake_handle.clone(),
562            threads: self.threads.clone(),
563            server_thread_ids: self.server_thread_ids.clone(),
564            terminal_cause: self.terminal_cause.clone(),
565        };
566
567        let status = self.status.clone();
568        let max_connections = self.max_connections;
569        let worker_registrations = registrations.clone();
570        let worker_terminal_cause = self.terminal_cause.clone();
571        let result = std::thread::Builder::new()
572            .name("rtmp-server-worker".to_string())
573            .spawn(move || {
574                handle_connections(
575                    stream_receiver,
576                    worker_registrations,
577                    self.gop_limit,
578                    max_connections,
579                    status,
580                    worker_terminal_cause,
581                    waker,
582                )
583            });
584        match result {
585            // The worker's handle joins the family registry: it is what
586            // stop()'s settlement barrier joins. Registered before the
587            // accept thread spawns, so no exit below can leave a running
588            // thread the registry does not know about. Its ThreadId goes
589            // into the reentrancy list first — both writes happen before
590            // any Running value (and thus any stop()) can exist, so the
591            // two locks never race a settlement.
592            Ok(handle) => {
593                self.server_thread_ids
594                    .lock()
595                    .unwrap_or_else(PoisonError::into_inner)
596                    .push(handle.thread().id());
597                self.threads
598                    .lock()
599                    .unwrap_or_else(PoisonError::into_inner)
600                    .push(handle);
601            }
602            Err(e) => {
603                // Nothing has spawned yet: no worker observes STATUS_RUN, and the
604                // listener is still owned here (moved into the io closure only
605                // below), so it drops on return and releases the port. The
606                // family's one start has been consumed by the gate above,
607                // though, so the status must not stay at STATUS_RUN — every
608                // clone would report a running server forever. Publish the
609                // terminal state BEFORE logging, intake closed first as on
610                // every terminal path: `error!` can run a user-installed
611                // logger that itself panics, and that unwind must not skip
612                // the publication.
613                self.signal_stop();
614                error!("Thread[rtmp-server-worker] exited with error: {e}");
615                return Err(crate::error::Error::RtmpThreadExited);
616            }
617        }
618
619        info!(
620            "Embed rtmp server listening for connections on {} (actual: {}, max_connections: {}).",
621            &self.address, actual_addr, effective_max
622        );
623
624        let status = self.status.clone();
625        // The accept loop owns one terminal transition of its own (the
626        // worker's connection channel disconnecting below) and needs the
627        // intake to run it through the funnel: it takes the handoff Arc
628        // still held from the setup above. That transition is a worker
629        // death — a fatal cause, which the funnel claims.
630        let terminal_cause = self.terminal_cause.clone();
631        let result = std::thread::Builder::new()
632            .name("rtmp-server-io".to_string())
633            .spawn(move || {
634                for stream in listener.incoming() {
635                    // Check the stop flag on every iteration, not only when the
636                    // listener runs dry: under a steady stream of incoming
637                    // connections the WouldBlock branch is never taken and
638                    // stop() would otherwise never terminate this thread.
639                    if status.load(Ordering::Acquire) == STATUS_END {
640                        info!("Embed rtmp server stopped.");
641                        break;
642                    }
643                    match stream {
644                        Ok(stream) => {
645                            // Use try_send to apply backpressure when channel is full
646                            match stream_sender.try_send(stream) {
647                                Ok(_) => {
648                                    debug!("New rtmp connection accepted.");
649                                }
650                                Err(crossbeam_channel::TrySendError::Full(s)) => {
651                                    // Channel full - server at capacity, reject connection immediately
652                                    let _ = s.shutdown(Shutdown::Both);
653                                    debug!(
654                                        "Connection rejected: server at capacity (channel full)"
655                                    );
656                                }
657                                Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
658                                    error!("Connection channel disconnected");
659                                    // The worker died out from under this
660                                    // loop. Its own teardown closes the
661                                    // intake too, but this thread cannot
662                                    // order itself after that: funnel the
663                                    // close ahead of the store here as
664                                    // well, so STATUS_END is never
665                                    // observable over an open intake no
666                                    // matter whose store lands first.
667                                    close_intake_and_publish_end(
668                                        &registrations,
669                                        &status,
670                                        &terminal_cause,
671                                    );
672                                    return;
673                                }
674                            }
675                        }
676                        Err(e) => {
677                            if e.kind() == std::io::ErrorKind::WouldBlock {
678                                std::thread::sleep(std::time::Duration::from_millis(100));
679                            } else if is_fd_exhaustion(&e) {
680                                // Accepting again immediately would fail the same
681                                // way and spin the CPU; back off and let existing
682                                // connections close first.
683                                warn!("Accept failed, file descriptors exhausted: {e}");
684                                std::thread::sleep(std::time::Duration::from_millis(100));
685                            } else {
686                                debug!("Rtmp connection error: {:?}", e);
687                            }
688                        }
689                    }
690                }
691            });
692        match result {
693            Ok(handle) => {
694                self.server_thread_ids
695                    .lock()
696                    .unwrap_or_else(PoisonError::into_inner)
697                    .push(handle.thread().id());
698                self.threads
699                    .lock()
700                    .unwrap_or_else(PoisonError::into_inner)
701                    .push(handle);
702            }
703            Err(e) => {
704                // The worker thread spawned successfully above and is now polling
705                // `status` (still STATUS_RUN); without this it would run forever.
706                // Signal STATUS_END (and wake the reactor) so it exits, and do it
707                // BEFORE logging: `error!` can run a user-installed logger that
708                // itself panics, and that unwind must not strand the already
709                // running worker. The listener was moved into the failed io
710                // closure and drops with it, releasing the port. Then settle
711                // the worker: this failed start() consumed the family's one
712                // lifecycle claim, so no Running value will ever exist to
713                // join it later — its registry handle would otherwise pin a
714                // live thread until the last clone drops. Settling before
715                // the log keeps the usual discipline: a panicking logger
716                // cannot skip it.
717                self.signal_stop();
718                settle_server_threads(&self.server_thread_ids, &self.threads);
719                error!("Thread[rtmp-server-io] exited with error: {e}");
720                return Err(crate::error::Error::RtmpThreadExited);
721            }
722        }
723
724        // Handoff complete: the accept thread owns the listener, and every
725        // terminal transition from here on belongs to stop(), the RAII
726        // guards or the worker's own fatal paths.
727        start_fail_guard.armed = false;
728        Ok(self.into_state())
729    }
730}
731
732/// Handle for feeding raw, pre-packaged RTMP chunk bytes to a stream published
733/// via [`EmbedRtmpServer::<Running>::create_stream_sender`]. Wraps the internal
734/// channel so callers do not depend on the channel implementation.
735///
736/// Cloneable and multi-producer: every clone feeds the same stream, so several
737/// producers can push chunks to one stream concurrently.
738#[derive(Clone)]
739pub struct RtmpStreamSender {
740    inner: crossbeam_channel::Sender<Vec<u8>>,
741    /// Wakes the reactor after each send so a raw publisher's media is
742    /// drained on the next loop turn instead of waiting up to POLL_TIMEOUT_MS
743    /// (~100ms). `None` only for the unit-test constructor (no running reactor).
744    wake_handle: Option<WakeHandle>,
745}
746
747impl RtmpStreamSender {
748    /// Sends one already-RTMP-chunk-packaged byte buffer to the stream.
749    ///
750    /// The underlying channel is bounded, so this **blocks** while the stream's
751    /// queue is full (the server applying backpressure) and returns once space
752    /// frees up. Returns
753    /// [`Error::RtmpStreamClosed`](crate::error::Error::RtmpStreamClosed) if the
754    /// stream has been torn down — its receiver was dropped because the server
755    /// stopped or the stream was removed.
756    pub fn send(&self, chunk: Vec<u8>) -> crate::error::Result<()> {
757        self.inner
758            .send(chunk)
759            .map_err(|_| crate::error::Error::RtmpStreamClosed)?;
760        // Nudge the reactor so process_publishers drains this Raw channel on the
761        // next loop turn rather than after the POLL_TIMEOUT_MS fallback. The
762        // internal Feed path (create_rtmp_input) wakes the same way per packet.
763        if let Some(wake) = &self.wake_handle {
764            wake.wake();
765        }
766        Ok(())
767    }
768}
769
770impl EmbedRtmpServer<Running> {
771    /// Returns the actual bound socket address of the RTMP server.
772    ///
773    /// This is particularly useful when binding to port 0 (random port allocation),
774    /// as it allows you to discover which port the OS assigned.
775    ///
776    /// # Returns
777    ///
778    /// * `Option<std::net::SocketAddr>` - The actual bound address, or `None` if not available.
779    ///
780    /// # Example
781    ///
782    /// ```rust,ignore
783    /// let server = EmbedRtmpServer::new("127.0.0.1:0").start().unwrap();
784    /// let actual_port = server.local_addr().unwrap().port();
785    /// println!("Server listening on port: {}", actual_port);
786    /// ```
787    pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
788        self.bound_addr
789    }
790
791    /// Creates an RTMP "input" endpoint for this server (from the server's perspective),
792    /// returning an [`Output`] that can be used by FFmpeg to push media data.
793    ///
794    /// From the FFmpeg standpoint, the returned [`Output`] is where media content is
795    /// sent (i.e., FFmpeg "outputs" to this RTMP server). After obtaining this [`Output`],
796    /// you can pass it to your FFmpeg job or scheduler to start streaming data into the server.
797    ///
798    /// # Parameters
799    ///
800    /// * `app_name` - The RTMP application name, typically corresponding to the `app` part
801    ///   of an RTMP URL (e.g., `rtmp://host:port/app/stream_key`).
802    /// * `stream_key` - The stream key (or "stream name"). If a stream with the same key
803    ///   already exists, an error will be returned.
804    ///
805    /// # Returns
806    ///
807    /// * [`Output`] - An output object preconfigured for streaming to this RTMP server.
808    ///   This can be passed to the FFmpeg SDK for actual data push.
809    /// * [`crate::error::Error`] - If a stream with the same key already exists, the server
810    ///   is not ready, or an internal error occurs, the corresponding error is returned.
811    ///
812    /// # Example
813    ///
814    /// ```rust,ignore
815    /// # // Assume there are definitions and initializations for FfmpegContext, FfmpegScheduler, etc.
816    ///
817    /// // 1. Create and start the RTMP server
818    /// let mut rtmp_server = EmbedRtmpServer::new("localhost:1935");
819    /// rtmp_server.start().expect("Failed to start RTMP server");
820    ///
821    /// // 2. Create an RTMP "input" with app_name="my-app" and stream_key="my-stream"
822    /// let output = rtmp_server
823    ///     .create_rtmp_input("my-app", "my-stream")
824    ///     .expect("Failed to create RTMP input");
825    ///
826    /// // 3. Prepare the FFmpeg context to push a local file to the newly created `Output`
827    /// let context = FfmpegContext::builder()
828    ///     .input("test.mp4")
829    ///     .output(output)
830    ///     .build()
831    ///     .expect("Failed to build Ffmpeg context");
832    ///
833    /// // 4. Start FFmpeg to push "test.mp4" to the local RTMP server on "my-app/my-stream"
834    /// FfmpegScheduler::new(context)
835    ///     .start()
836    ///     .expect("Failed to start Ffmpeg job");
837    /// ```
838    pub fn create_rtmp_input(
839        &self,
840        app_name: impl Into<String>,
841        stream_key: impl Into<String>,
842    ) -> crate::error::Result<Output> {
843        // PERF-5a serialize-bypass: steady-state audio/video FLV tags are
844        // handed to the scheduler already parsed (PublisherFeed::Media),
845        // skipping the serialize→loopback→re-parse round-trip. Metadata and
846        // control still travel as serialized RTMP chunk bytes on the same FIFO
847        // feed (PublisherFeed::Raw), so the scheduler observes an identical,
848        // in-order sequence to the pure-serialize path. External TCP clients
849        // are unaffected — they never touch this feed.
850        let feed_sender = self.create_bypass_feed_sender(app_name, stream_key)?;
851        // PERF-3: both feed paths hold a WakeHandle — this internal Feed path
852        // and raw `create_stream_sender` users (RtmpStreamSender::send wakes
853        // per chunk). Wake once now so the reactor flushes the queued
854        // connect/createStream/publish handshake immediately instead of
855        // waiting for the first media frame or the 100ms poll fallback —
856        // otherwise stream setup carries avoidable startup latency.
857        let wake_handle = self.wake_handle.clone();
858        if let Some(waker) = &wake_handle {
859            waker.wake();
860        }
861
862        let mut flv_buffer = FlvBuffer::new();
863        let mut serializer = ChunkSerializer::new();
864        // A feed send fails only once the receiver died. Under a deliberate
865        // stop that is the expected end of this publisher — classify it as
866        // such instead of logging the send error a genuine failure gets.
867        // The callback must still fail the write (the FFmpeg job has to
868        // end), so the classification changes the reporting, not the flow.
869        //
870        // Scope: this classifies the SERVER lifecycle, not this feed's own
871        // history. A feed torn down for its own fatal protocol error is
872        // reported loudly at its removal site (the reactor warns when it
873        // rejects the publisher), independent of what this classification
874        // says later — a deliberate stop landing between that removal and
875        // this send failure makes the calm line below true of the server
876        // while the earlier warn still records why the feed itself died.
877        //
878        // The classification is best-effort by construction. First-writer-
879        // wins on `terminal_cause` means a server crash that beat a late
880        // stop() stays CAUSE_FATAL — never relabeled calm. In the other
881        // direction the load below can, in principle, still read a stale
882        // CAUSE_NONE during a deliberate stop: the send-failure observation
883        // travels through the channel's disconnect flag, whose sender-side
884        // fast path is a relaxed read, so it carries no happens-before edge
885        // for this unrelated atomic. A stale read only ever shows the OLDER
886        // value (CAUSE_NONE -> the loud error branch), so the worst case is
887        // the pre-classification behavior for one racing write, never a
888        // server crash reported as deliberate.
889        let terminal_cause = self.terminal_cause.clone();
890        let classify_feed_send_failure = move |what: &str, e: &dyn std::fmt::Debug| {
891            if terminal_cause.load(Ordering::Acquire) == CAUSE_DELIBERATE {
892                info!("The rtmp server was deliberately stopped; ending the in-process publisher (failing its {what} write)");
893            } else {
894                error!("Failed to send in-process {what}: {e:?}");
895            }
896        };
897        let write_callback: Box<dyn FnMut(&[u8]) -> i32 + Send> =
898            Box::new(move |buf: &[u8]| -> i32 {
899                flv_buffer.write_data(buf);
900                // One AVIO write can carry many FLV tags (the muxer hands over
901                // 64KB blocks): drain every complete tag now, or the backlog
902                // grows and the final tags of the stream are never sent.
903                while let Some(mut flv_tag) = flv_buffer.get_flv_tag() {
904                    flv_tag.header.stream_id = 1;
905                    let tag_type = flv_tag.header.tag_type;
906
907                    // 0x08 audio / 0x09 video: bypass the serializer. The
908                    // (timestamp, data) handed over is byte-identical to what
909                    // flv_tag_to_message_payload would build and the RTMP chunk
910                    // round-trip would reconstruct, so the scheduler's sequence-
911                    // header / keyframe-gate / GOP semantics are preserved exactly.
912                    if tag_type == 0x08 || tag_type == 0x09 {
913                        let timestamp = flv_tag.header.timestamp
914                            | ((flv_tag.header.timestamp_ext as u32) << 24);
915                        let feed = PublisherFeed::Media {
916                            tag_type,
917                            timestamp: RtmpTimestamp { value: timestamp },
918                            data: flv_tag.data,
919                        };
920                        if let Err(e) = feed_sender.send(feed) {
921                            classify_feed_send_failure("media tag", &e);
922                            return -1;
923                        }
924                        // PERF-3: wake the reactor for each bypassed media tag, the
925                        // same as the Raw path below — without it the parsed tag
926                        // waits in the feed until the 100ms poll fallback, negating
927                        // the PERF-5a bypass. The token coalesces repeated wakes.
928                        if let Some(waker) = &wake_handle {
929                            waker.wake();
930                        }
931                        continue;
932                    }
933
934                    // 0x12 metadata and anything else keep the serialize path: the
935                    // scheduler consumes the parsed StreamMetadataChanged event,
936                    // which needs the @setDataFrame wrapping + AMF decode.
937                    match serializer.serialize(&flv_tag_to_message_payload(flv_tag), false, true) {
938                        Ok(packet) => {
939                            if let Err(e) = feed_sender.send(PublisherFeed::Raw(packet.bytes)) {
940                                classify_feed_send_failure("RTMP packet", &e);
941                                return -1;
942                            }
943                            // Wake the reactor for each enqueued packet. Unconditional
944                            // (no message_sender.is_empty() gate): the reactor can
945                            // drain the queue and sleep in poll() between an emptiness
946                            // check and this send, so a was_empty gate loses the
947                            // wakeup and the packet stalls until the 100ms poll
948                            // fallback. Per-packet rather than once-after-the-batch:
949                            // the channel is bounded (1024), so a large batch would
950                            // block in send() before a post-batch wake ever ran,
951                            // stranding the reactor. The eventfd/pipe token coalesces
952                            // the wakes into a single reactor drain, so the cost is a
953                            // cheap (already-signaled) syscall.
954                            if let Some(waker) = &wake_handle {
955                                waker.wake();
956                            }
957                        }
958                        Err(e) => {
959                            error!("Failed to serialize RTMP message: {:?}", e);
960                            return -1;
961                        }
962                    }
963                }
964                buf.len() as i32
965            });
966
967        let output: Output = write_callback.into();
968
969        Ok(output
970            .set_format("flv")
971            .set_video_codec("h264")
972            .set_audio_codec("aac")
973            .set_format_opt("flvflags", "no_duration_filesize"))
974    }
975
976    /// Creates a sender channel for an RTMP stream, identified by `app_name` and `stream_key`.
977    /// Call this to publish an in-process stream directly by feeding raw,
978    /// pre-packaged RTMP chunk bytes into the server's handling pipeline.
979    ///
980    /// # Parameters
981    ///
982    /// * `app_name` - The RTMP application name.
983    /// * `stream_key` - The unique name (or key) for this stream. Must not already be in use.
984    ///
985    /// # Returns
986    ///
987    /// * [`RtmpStreamSender`] - A handle whose [`send`](RtmpStreamSender::send) feeds
988    ///   raw RTMP bytes into the server's handling pipeline.
989    /// * [`crate::error::Error`] - If a stream with the same key already exists or other
990    ///   internal issues occur, an error is returned.
991    ///
992    /// # Notes
993    ///
994    /// * This function sets up the initial RTMP "connect" and "publish" commands automatically.
995    /// * If you manually send bytes to the resulting channel, they should already be properly
996    ///   packaged as RTMP chunks. Otherwise, the server might fail to parse them.
997    pub fn create_stream_sender(
998        &self,
999        app_name: impl Into<String>,
1000        stream_key: impl Into<String>,
1001    ) -> crate::error::Result<RtmpStreamSender> {
1002        let stream_key = stream_key.into();
1003        // Claim the key atomically: `claim` inserts-or-fails, so two
1004        // concurrent creates for the same key cannot both pass. A separate
1005        // contains() check would let both pass before either registration
1006        // reached the reactor; both would return Ok and the loser would only
1007        // fail later with an opaque send error.
1008        let Ok(claim) = StreamKeyClaim::claim(self.stream_keys.clone(), stream_key.clone()) else {
1009            return Err(RtmpStreamAlreadyExists(stream_key));
1010        };
1011
1012        let (sender, receiver) = crossbeam_channel::bounded(PUBLISHER_CHANNEL_CAPACITY);
1013        self.register_publisher(claim, PublisherSource::Raw(receiver))?;
1014
1015        // Prime the raw byte channel with the connect / createStream / publish
1016        // handshake the server session expects before any media.
1017        for packet_bytes in build_publish_control(app_name.into(), stream_key)? {
1018            if sender.send(packet_bytes).is_err() {
1019                error!("Can't send publish control command to rtmp server.");
1020                return Err(RtmpCreateStream.into());
1021            }
1022        }
1023        // The registration and its primed handshake ride a queue and a
1024        // channel the poller cannot see. Wake the reactor once so it picks
1025        // them up now instead of on the next 100ms poll fallback — the same
1026        // wake create_rtmp_input performs after priming; RtmpStreamSender
1027        // wakes per send, but nothing else announces this handshake.
1028        if let Some(wake_handle) = &self.wake_handle {
1029            wake_handle.wake();
1030        }
1031        Ok(RtmpStreamSender {
1032            inner: sender,
1033            wake_handle: self.wake_handle.clone(),
1034        })
1035    }
1036
1037    /// Registers an in-process publisher whose steady-state audio/video is
1038    /// delivered already parsed (PERF-5a serialize-bypass). Metadata and
1039    /// control still ride the same feed as serialized RTMP chunk bytes, so the
1040    /// scheduler sees an identical, in-order message sequence to the raw path.
1041    ///
1042    /// Returns the feed sender, primed with the publish handshake.
1043    fn create_bypass_feed_sender(
1044        &self,
1045        app_name: impl Into<String>,
1046        stream_key: impl Into<String>,
1047    ) -> crate::error::Result<crossbeam_channel::Sender<PublisherFeed>> {
1048        let stream_key = stream_key.into();
1049        // Claim the key atomically (insert-or-fail); see create_stream_sender.
1050        let Ok(claim) = StreamKeyClaim::claim(self.stream_keys.clone(), stream_key.clone()) else {
1051            return Err(RtmpStreamAlreadyExists(stream_key));
1052        };
1053
1054        let (sender, receiver) = crossbeam_channel::bounded(PUBLISHER_CHANNEL_CAPACITY);
1055        self.register_publisher(claim, PublisherSource::Feed(receiver))?;
1056
1057        // Prime the feed with the same connect / createStream / publish bytes
1058        // the raw path would send, wrapped as PublisherFeed::Raw.
1059        for packet_bytes in build_publish_control(app_name.into(), stream_key)? {
1060            if sender.send(PublisherFeed::Raw(packet_bytes)).is_err() {
1061                error!("Can't send publish control command to rtmp server.");
1062                return Err(RtmpCreateStream.into());
1063            }
1064        }
1065        Ok(sender)
1066    }
1067
1068    /// Hands a newly registered publisher's receiving end to the reactor.
1069    ///
1070    /// The registration carries the caller's `stream_keys` claim, and the
1071    /// claim is a drop-releasing guard: whichever side ends up holding the
1072    /// registration when it dies releases the key, with no path releasing
1073    /// twice. Concretely:
1074    /// - no registration queue, or the enqueue is refused because the intake
1075    ///   is closed (worker gone, server stopped) or at capacity: the
1076    ///   registration drops here, releasing the claim — the refusal and a
1077    ///   successful enqueue are the same critical section, so the reactor
1078    ///   side can never have seen it;
1079    /// - the reactor consumes the registration: `add_publisher` either
1080    ///   moves the still-armed claim into the accepted publisher's state
1081    ///   (released when that state drops — `remove_publisher`, or reactor
1082    ///   teardown) or drops a refused one;
1083    /// - the worker dies — cleanly, by panic, or before the reactor even
1084    ///   existed — with the registration still queued: the worker's
1085    ///   `RegistrationKillSwitch` drains it, releasing the claim.
1086    fn register_publisher(
1087        &self,
1088        claim: StreamKeyClaim,
1089        source: PublisherSource,
1090    ) -> crate::error::Result<()> {
1091        let registration = PublisherRegistration { claim, source };
1092        let registrations = match self.registrations.as_ref() {
1093            Some(registrations) => registrations,
1094            None => {
1095                error!("Publisher registration queue not initialized");
1096                return Err(RtmpCreateStream.into());
1097            }
1098        };
1099
1100        match registrations.enqueue(registration) {
1101            Ok(()) => Ok(()),
1102            Err(EnqueueRefused::Closed(registration)) => {
1103                // The worker is gone, or the server was signaled to stop:
1104                // nothing will ever consume this registration. Dropping it
1105                // here releases its stream-key claim immediately.
1106                drop(registration);
1107                if self.status.load(Ordering::Acquire) != STATUS_END {
1108                    warn!("Rtmp server worker already exited. Can't create stream sender.");
1109                } else {
1110                    error!("Rtmp Server aborted. Can't create stream sender.");
1111                }
1112                Err(RtmpCreateStream)
1113            }
1114            Err(EnqueueRefused::Full(registration)) => {
1115                // The worker is alive but the reactor has a full queue of
1116                // earlier registrations to pick up. Refusing keeps the
1117                // parked key claims bounded; dropping the refusal reopens
1118                // this key immediately, so the caller may retry once the
1119                // backlog drains.
1120                drop(registration);
1121                warn!("Rtmp registration queue is full. Can't create stream sender.");
1122                Err(RtmpRegistrationQueueFull)
1123            }
1124        }
1125    }
1126
1127    /// Stops the RTMP server: signals the listening and connection-handling
1128    /// threads to terminate, then joins them, so this returns only once
1129    /// teardown has settled — the listener socket is released, every
1130    /// connection is closed, and every publisher is torn down with its
1131    /// stream key freed. A caller can rebind the address or reuse a stream
1132    /// key immediately after this returns, without polling.
1133    ///
1134    /// Blocking is bounded but not hard-real-time: the reactor finishes the
1135    /// loop turn it is in, then drains still-queued tails for roughly its
1136    /// graceful-shutdown window (a few seconds, re-checked between drain
1137    /// passes, and only spent when a peer stopped reading); the accept
1138    /// thread notices within its ~100ms accept cycle. Concurrent stops on
1139    /// clones serialize behind one settlement and each returns only after
1140    /// it.
1141    ///
1142    /// One reentrant edge degrades to signal-only: user code reached
1143    /// through a user-installed global logger runs ON the server threads,
1144    /// and if such code calls `stop()`, joining from there would deadlock —
1145    /// that call silently skips the joins (silently, because logging from
1146    /// inside the logger's own frame could deadlock on the logger's lock),
1147    /// leaves them available to any other caller, and returns with only
1148    /// the signal sent.
1149    ///
1150    /// More generally: the joined threads log through the global logger
1151    /// while they wind down, so blocking in `stop()` while holding ANY
1152    /// resource that logger may acquire is a lock inversion. That has two
1153    /// shapes, neither detectable from this crate:
1154    /// - do not call `stop()` from inside a logger implementation (on any
1155    ///   thread) — a `log()` blocking here while holding the logger's own
1156    ///   internal lock deadlocks against the winding-down threads' log
1157    ///   calls;
1158    /// - do not call `stop()` while holding a lock your logger sink also
1159    ///   takes (e.g. an `Arc<Mutex<_>>` shared between application code
1160    ///   and a custom logger) — the server threads block on that lock in
1161    ///   their shutdown logging while `stop()` blocks joining them.
1162    ///
1163    /// The same discipline applies to joining any thread that logs; the
1164    /// server-thread reentrant shape above is the only one the crate can
1165    /// detect cheaply, and it is guarded.
1166    ///
1167    /// An FFmpeg job still pushing to this server (via
1168    /// [`create_rtmp_input`](Self::create_rtmp_input)) loses its feed and
1169    /// fails its next write; the server classifies that failure — best
1170    /// effort, by the server's terminal cause — as a deliberate stop
1171    /// rather than as an opaque send error. A feed that already died for
1172    /// its own protocol error before the stop keeps the warning it got at
1173    /// removal time. Stop such jobs first if their clean completion
1174    /// matters.
1175    ///
1176    /// # Example
1177    /// ```rust,ignore
1178    /// let server = EmbedRtmpServer::new("localhost:1935");
1179    /// // ... start and handle streaming
1180    /// server.stop();
1181    /// assert!(server.is_stopped());
1182    /// ```
1183    pub fn stop(self) -> EmbedRtmpServer<Ended> {
1184        self.signal_stop();
1185        settle_server_threads(&self.server_thread_ids, &self.threads);
1186        self.into_state()
1187    }
1188}
1189
1190/// Handle connections using optimized Reactor
1191///
1192/// Replaces old multi-threaded handle_connections with single-threaded event-driven model:
1193/// - Uses epoll/kqueue/WSAPoll for IO multiplexing
1194/// - Write queue with backpressure management
1195/// - Strict drain until WouldBlock semantics
1196/// Whether an accept error means the process (EMFILE) or system (ENFILE) ran
1197/// out of file descriptors. `io::ErrorKind` has no stable variant for these,
1198/// so match on the raw OS error code.
1199fn is_fd_exhaustion(e: &std::io::Error) -> bool {
1200    #[cfg(unix)]
1201    {
1202        matches!(e.raw_os_error(), Some(code) if code == libc::EMFILE || code == libc::ENFILE)
1203    }
1204    #[cfg(windows)]
1205    {
1206        // WSAEMFILE: too many open sockets.
1207        e.raw_os_error() == Some(10024)
1208    }
1209    #[cfg(not(any(unix, windows)))]
1210    {
1211        let _ = e;
1212        false
1213    }
1214}
1215
1216/// The single unwind boundary of the rtmp-server-worker thread.
1217///
1218/// The reactor is the only consumer of the connection/publisher channels. An
1219/// uncontained panic — in the reactor's construction just as much as in its
1220/// loop — would kill the worker thread without ever publishing `STATUS_END`:
1221/// `is_stopped()` would stay `false` forever and the accept thread — whose
1222/// loop exits only on the status flag or a disconnected channel send — would
1223/// keep feeding connections nobody drains. Containing the unwind here closes
1224/// the registration intake and publishes the terminal status so the accept
1225/// thread exits on its next ~100ms cycle, and returns normally so the
1226/// caller's `Reactor` drop still runs, closing every accepted connection.
1227///
1228/// Both terminal steps are idempotent: a clean return only reaches
1229/// `STATUS_END` through `signal_stop` (or the worker's own fatal paths, all
1230/// of which close the intake first), and this function touches neither the
1231/// intake nor the status unless it actually caught an unwind.
1232fn contain_reactor_panic(
1233    registrations: &RegistrationHandoff,
1234    status: &AtomicUsize,
1235    terminal_cause: &AtomicUsize,
1236    run_reactor: impl FnOnce(),
1237) {
1238    // AssertUnwindSafe: the closure borrows the caller's reactor slot mutably,
1239    // and `&mut T` is not `UnwindSafe`. That is acceptable here because after
1240    // an unwind the slot's reactor is never used again — the caller only
1241    // drops it.
1242    if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run_reactor)) {
1243        // Publish the terminal state BEFORE logging: `error!` can run a
1244        // user-installed logger that itself panics, and that unwind must not
1245        // skip the store and leave the server half-dead after all. The
1246        // funnel closes the intake ahead of the store, so no create path
1247        // can observe the stopped status and still be told Ok; the worker's
1248        // kill switch re-closes it during its terminal drain — a no-op.
1249        close_intake_and_publish_end(registrations, status, terminal_cause);
1250        let msg = payload
1251            .downcast_ref::<&str>()
1252            .copied()
1253            .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
1254            .unwrap_or("non-string panic payload");
1255        error!("Rtmp reactor panicked ({msg}); the server is stopped and all connections will be closed.");
1256    }
1257}
1258
1259fn handle_connections(
1260    connection_receiver: crossbeam_channel::Receiver<TcpStream>,
1261    registrations: Arc<RegistrationHandoff>,
1262    gop_limit: usize,
1263    max_connections: Option<usize>,
1264    status: Arc<AtomicUsize>,
1265    terminal_cause: Arc<AtomicUsize>,
1266    waker: Option<Waker>,
1267) {
1268    // FIRST statement of the worker, before the fallible reactor
1269    // construction and before any log call: arm the kill switch. However
1270    // this thread ends — `Reactor::new` failing, a user-installed logger
1271    // panicking, `run()` unwinding, or a clean return — the switch's drop
1272    // closes the registration intake and releases the key claims of every
1273    // registration still queued. Declared before `reactor_slot` so it drops
1274    // AFTER the reactor: the drain it performs is the worker's last word,
1275    // with nothing left that could enqueue concurrently forever.
1276    let kill_switch = RegistrationKillSwitch::arm(registrations);
1277    // The reactor is CREATED inside the contained closure so that a panicking
1278    // constructor also publishes `STATUS_END`, but STORED in this outer slot
1279    // so that on a contained panic it is dropped HERE, after the terminal
1280    // status is published — a `Drop` panicking mid-unwind would abort the
1281    // process instead. This drop is also what releases the key claims of
1282    // publishers still accepted when the worker dies: each claim lives in
1283    // its `PublisherState`, torn down with the reactor's publisher slab.
1284    let mut reactor_slot: Option<Reactor> = None;
1285    contain_reactor_panic(kill_switch.handoff(), &status, &terminal_cause, || {
1286        let reactor = match Reactor::new(gop_limit, max_connections, status.clone()) {
1287            Ok(r) => r,
1288            Err(e) => {
1289                // Publish the terminal state BEFORE logging, mirroring the
1290                // panic arm above: `error!` can run a user-installed logger
1291                // that itself panics, and that unwind must not leave the
1292                // worker dead with the status still running and the accept
1293                // loop parked forever. The funnel closes the intake ahead
1294                // of the store; the kill switch re-closes it on the way
1295                // out — a no-op.
1296                close_intake_and_publish_end(kill_switch.handoff(), &status, &terminal_cause);
1297                error!("Failed to create Reactor: {:?}", e);
1298                return;
1299            }
1300        };
1301        reactor_slot
1302            .insert(reactor)
1303            .run(connection_receiver, kill_switch.handoff(), waker)
1304    });
1305
1306    if status.load(Ordering::Acquire) != STATUS_END {
1307        error!("Rtmp Server aborted.");
1308    }
1309}
1310
1311/// Serializes the `connect` / `createStream` / `publish` command sequence an
1312/// in-process publisher sends before any media. Shared by both the raw byte
1313/// path ([`create_stream_sender`](EmbedRtmpServer<Running>::create_stream_sender))
1314/// and the media-bypass path ([`create_rtmp_input`](EmbedRtmpServer<Running>::create_rtmp_input))
1315/// so their control bytes stay identical.
1316pub(crate) fn build_publish_control(
1317    app_name: String,
1318    stream_key: String,
1319) -> crate::error::Result<[Vec<u8>; 3]> {
1320    let mut serializer = ChunkSerializer::new();
1321
1322    // connect
1323    let mut properties: HashMap<String, Amf0Value> = HashMap::new();
1324    properties.insert("app".to_string(), Amf0Value::Utf8String(app_name));
1325    let connect_cmd = RtmpMessage::Amf0Command {
1326        command_name: "connect".to_string(),
1327        transaction_id: 1.0,
1328        command_object: Amf0Value::Object(properties),
1329        additional_arguments: Vec::new(),
1330    }
1331    .into_message_payload(RtmpTimestamp { value: 0 }, 0)
1332    .map_err(|e| {
1333        error!("Failed to create connect command: {:?}", e);
1334        RtmpCreateStream
1335    })?;
1336    let connect_packet = serializer
1337        .serialize(&connect_cmd, false, true)
1338        .map_err(|e| {
1339            error!("Failed to serialize connect command: {:?}", e);
1340            RtmpCreateStream
1341        })?;
1342
1343    // createStream
1344    let create_stream_cmd = RtmpMessage::Amf0Command {
1345        command_name: "createStream".to_string(),
1346        transaction_id: 2.0,
1347        command_object: Amf0Value::Null,
1348        additional_arguments: Vec::new(),
1349    }
1350    .into_message_payload(RtmpTimestamp { value: 0 }, 1)
1351    .map_err(|e| {
1352        error!("Failed to create createStream command: {:?}", e);
1353        RtmpCreateStream
1354    })?;
1355    let create_stream_packet = serializer
1356        .serialize(&create_stream_cmd, false, true)
1357        .map_err(|e| {
1358            error!("Failed to serialize createStream command: {:?}", e);
1359            RtmpCreateStream
1360        })?;
1361
1362    // publish
1363    let arguments = vec![
1364        Amf0Value::Utf8String(stream_key),
1365        Amf0Value::Utf8String("live".into()),
1366    ];
1367    let publish_cmd = RtmpMessage::Amf0Command {
1368        command_name: "publish".to_string(),
1369        transaction_id: 3.0,
1370        command_object: Amf0Value::Null,
1371        additional_arguments: arguments,
1372    }
1373    .into_message_payload(RtmpTimestamp { value: 0 }, 1)
1374    .map_err(|e| {
1375        error!("Failed to create publish command: {:?}", e);
1376        RtmpCreateStream
1377    })?;
1378    let publish_packet = serializer
1379        .serialize(&publish_cmd, false, true)
1380        .map_err(|e| {
1381            error!("Failed to serialize publish command: {:?}", e);
1382            RtmpCreateStream
1383        })?;
1384
1385    Ok([
1386        connect_packet.bytes,
1387        create_stream_packet.bytes,
1388        publish_packet.bytes,
1389    ])
1390}
1391
1392pub(crate) fn flv_tag_to_message_payload(flv_tag: FlvTag) -> MessagePayload {
1393    let timestamp = flv_tag.header.timestamp | ((flv_tag.header.timestamp_ext as u32) << 24);
1394
1395    let type_id = flv_tag.header.tag_type;
1396    let message_stream_id = flv_tag.header.stream_id;
1397
1398    let data = if type_id == 0x12 {
1399        wrap_metadata(flv_tag.data)
1400    } else {
1401        flv_tag.data
1402    };
1403
1404    MessagePayload {
1405        timestamp: RtmpTimestamp { value: timestamp },
1406        type_id,
1407        message_stream_id,
1408        data,
1409    }
1410}
1411
1412fn wrap_metadata(data: Bytes) -> Bytes {
1413    let s = "@setDataFrame";
1414
1415    let insert_len = 16;
1416
1417    let mut bytes = bytes::BytesMut::with_capacity(insert_len + data.len());
1418
1419    bytes.put_u8(0x02);
1420    bytes.put_u16(s.len() as u16);
1421    bytes.put(s.as_bytes());
1422
1423    bytes.put(data);
1424
1425    bytes.freeze()
1426}
1427
1428// ============================================================================
1429// StreamBuilder API - Simplified RTMP streaming interface
1430// ============================================================================
1431
1432use crate::core::context::ffmpeg_context::FfmpegContext;
1433use crate::core::context::input::Input;
1434use crate::core::scheduler::ffmpeg_scheduler::{FfmpegScheduler, Running as SchedulerRunning};
1435use crate::error::StreamError;
1436use std::path::{Path, PathBuf};
1437
1438/// A builder for creating RTMP streaming sessions with a simplified API.
1439///
1440/// This builder provides a fluent interface for configuring and starting
1441/// RTMP streaming without needing to manually manage the server lifecycle.
1442///
1443/// # Example
1444///
1445/// ```rust,ignore
1446/// use ez_ffmpeg::rtmp::embed_rtmp_server::EmbedRtmpServer;
1447///
1448/// let handle = EmbedRtmpServer::stream_builder()
1449///     .address("localhost:1935")
1450///     .app_name("live")
1451///     .stream_key("stream1")
1452///     .input_file("video.mp4")
1453///     // readrate defaults to 1.0 (realtime)
1454///     .start()?;
1455///
1456/// handle.wait()?;
1457/// ```
1458/// RAII guard that stops a started server unless explicitly disarmed.
1459///
1460/// `EmbedRtmpServer` cannot implement `Drop` (`into_state` moves out of `self`,
1461/// which `Drop` forbids), so a partially-built [`StreamHandle`] would otherwise
1462/// leak the running server on any post-start failure: the two worker threads
1463/// keep polling `status` and hold the listener port bound. This guard is armed
1464/// right after `server.start()` and disarmed only once the server is handed to
1465/// a `StreamHandle`; any early return drops it and calls `signal_stop`.
1466struct ServerStopGuard {
1467    server: Option<Arc<EmbedRtmpServer<Running>>>,
1468}
1469
1470impl Drop for ServerStopGuard {
1471    fn drop(&mut self) {
1472        if let Some(server) = &self.server {
1473            server.signal_stop();
1474        }
1475    }
1476}
1477
1478impl ServerStopGuard {
1479    /// Take ownership of the server out of the guard, so its `Drop` becomes a
1480    /// no-op. Called on the success path where a `StreamHandle` assumes the
1481    /// stop responsibility.
1482    fn disarm(mut self) -> Arc<EmbedRtmpServer<Running>> {
1483        self.server
1484            .take()
1485            .expect("ServerStopGuard is armed exactly once before disarm")
1486    }
1487}
1488
1489pub struct StreamBuilder {
1490    address: Option<String>,
1491    app_name: Option<String>,
1492    stream_key: Option<String>,
1493    input_file: Option<PathBuf>,
1494    readrate: Option<f32>,
1495    gop_limit: Option<usize>,
1496    max_connections: Option<usize>,
1497}
1498
1499impl Default for StreamBuilder {
1500    fn default() -> Self {
1501        Self::new()
1502    }
1503}
1504
1505impl StreamBuilder {
1506    /// Creates a new `StreamBuilder` with default settings.
1507    ///
1508    /// By default, `readrate` is set to `1.0` (real-time playback speed),
1509    /// which is equivalent to FFmpeg's `-re` flag. This is the recommended
1510    /// setting for live RTMP streaming scenarios.
1511    pub fn new() -> Self {
1512        Self {
1513            address: None,
1514            app_name: None,
1515            stream_key: None,
1516            input_file: None,
1517            readrate: Some(1.0), // Default to real-time speed for live streaming
1518            gop_limit: None,
1519            max_connections: None,
1520        }
1521    }
1522
1523    /// Sets the address for the RTMP server (e.g., "localhost:1935").
1524    pub fn address(mut self, address: impl Into<String>) -> Self {
1525        self.address = Some(address.into());
1526        self
1527    }
1528
1529    /// Sets the RTMP application name.
1530    pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
1531        self.app_name = Some(app_name.into());
1532        self
1533    }
1534
1535    /// Sets the stream key (publishing name).
1536    pub fn stream_key(mut self, stream_key: impl Into<String>) -> Self {
1537        self.stream_key = Some(stream_key.into());
1538        self
1539    }
1540
1541    /// Sets the input file path to stream.
1542    pub fn input_file(mut self, path: impl AsRef<Path>) -> Self {
1543        self.input_file = Some(path.as_ref().to_path_buf());
1544        self
1545    }
1546
1547    /// Sets the read rate for the input file.
1548    ///
1549    /// A value of 1.0 means realtime playback speed.
1550    /// This is useful for simulating live streaming from a file.
1551    pub fn readrate(mut self, rate: f32) -> Self {
1552        self.readrate = Some(rate);
1553        self
1554    }
1555
1556    /// Sets the GOP (Group of Pictures) limit for the RTMP server.
1557    ///
1558    /// This controls how many GOPs are buffered for new subscribers.
1559    pub fn gop_limit(mut self, limit: usize) -> Self {
1560        self.gop_limit = Some(limit);
1561        self
1562    }
1563
1564    /// Sets the maximum number of connections the server will accept.
1565    pub fn max_connections(mut self, max: usize) -> Self {
1566        self.max_connections = Some(max);
1567        self
1568    }
1569
1570    /// Starts the RTMP streaming session.
1571    ///
1572    /// This method validates all required parameters, starts the RTMP server,
1573    /// and begins streaming the input file.
1574    ///
1575    /// # Required Parameters
1576    ///
1577    /// - `address`: The server address
1578    /// - `app_name`: The RTMP application name
1579    /// - `stream_key`: The stream key (publishing name)
1580    /// - `input_file`: The file to stream
1581    ///
1582    /// # Returns
1583    ///
1584    /// A `StreamHandle` that can be used to wait for completion or manage the stream.
1585    ///
1586    /// # Errors
1587    ///
1588    /// Returns `StreamError` if:
1589    /// - Any required parameter is missing
1590    /// - The input file does not exist
1591    /// - The server fails to start
1592    /// - FFmpeg context creation fails
1593    pub fn start(self) -> Result<StreamHandle, StreamError> {
1594        // Validate required parameters
1595        let address = self
1596            .address
1597            .ok_or(StreamError::MissingParameter("address"))?;
1598        let app_name = self
1599            .app_name
1600            .ok_or(StreamError::MissingParameter("app_name"))?;
1601        let stream_key = self
1602            .stream_key
1603            .ok_or(StreamError::MissingParameter("stream_key"))?;
1604        let input_file = self
1605            .input_file
1606            .ok_or(StreamError::MissingParameter("input_file"))?;
1607
1608        // Validate input file exists and is a file (not a directory)
1609        if !input_file.is_file() {
1610            return Err(StreamError::InputNotFound { path: input_file });
1611        }
1612
1613        // Create and configure the server
1614        let mut server = if let Some(gop_limit) = self.gop_limit {
1615            EmbedRtmpServer::new_with_gop_limit(&address, gop_limit)
1616        } else {
1617            EmbedRtmpServer::new(&address)
1618        };
1619
1620        if let Some(max_conn) = self.max_connections {
1621            server = server.set_max_connections(max_conn);
1622        }
1623
1624        // Start the server, then immediately arm a stop guard. EmbedRtmpServer
1625        // has no Drop, so any `?` failure below (create_rtmp_input, FFmpeg
1626        // build/start) would otherwise drop the Arc without stopping the two
1627        // server threads: they leak forever and the port stays AddrInUse. The
1628        // guard is disarmed only once ownership passes to the StreamHandle,
1629        // whose own Drop then owns the stop.
1630        let server = server.start().map_err(StreamError::Ffmpeg)?;
1631        let guard = ServerStopGuard {
1632            server: Some(Arc::new(server)),
1633        };
1634
1635        // Create the RTMP output (a `?` here drops the guard -> signal_stop).
1636        let output = guard
1637            .server
1638            .as_ref()
1639            .unwrap()
1640            .create_rtmp_input(&app_name, &stream_key)
1641            .map_err(StreamError::Ffmpeg)?;
1642
1643        // Create the input with optional readrate
1644        let input_path = input_file.to_string_lossy().to_string();
1645        let mut input = Input::from(input_path);
1646        if let Some(rate) = self.readrate {
1647            input = input.set_readrate(rate);
1648        }
1649
1650        // Build and start the FFmpeg context (a `?` here drops the guard too).
1651        let scheduler = FfmpegContext::builder()
1652            .input(input)
1653            .output(output)
1654            .build()
1655            .map_err(StreamError::Ffmpeg)?
1656            .start()
1657            .map_err(StreamError::Ffmpeg)?;
1658
1659        // Success: hand the server to the StreamHandle and disarm the guard so
1660        // the handle's Drop (not the guard's) owns the stop from here on.
1661        let server = guard.disarm();
1662        Ok(StreamHandle {
1663            server,
1664            scheduler: Some(scheduler),
1665        })
1666    }
1667}
1668
1669/// A handle to a running RTMP streaming session.
1670///
1671/// This handle manages the lifecycle of both the RTMP server and the FFmpeg
1672/// streaming context. When dropped, it will attempt to clean up resources.
1673///
1674/// # Example
1675///
1676/// ```rust,ignore
1677/// let handle = EmbedRtmpServer::stream_builder()
1678///     .address("localhost:1935")
1679///     .app_name("live")
1680///     .stream_key("stream1")
1681///     .input_file("video.mp4")
1682///     .start()?;
1683///
1684/// // Wait for streaming to complete
1685/// handle.wait()?;
1686/// ```
1687pub struct StreamHandle {
1688    server: Arc<EmbedRtmpServer<Running>>,
1689    scheduler: Option<FfmpegScheduler<SchedulerRunning>>,
1690}
1691
1692impl StreamHandle {
1693    /// Waits for the streaming session to complete.
1694    ///
1695    /// This method blocks until the FFmpeg context finishes processing
1696    /// (e.g., when the input file ends or an error occurs).
1697    ///
1698    /// # Returns
1699    ///
1700    /// Returns `Ok(())` if streaming completed successfully, or an error
1701    /// if something went wrong during streaming.
1702    pub fn wait(mut self) -> Result<(), StreamError> {
1703        if let Some(scheduler) = self.scheduler.take() {
1704            scheduler.wait().map_err(StreamError::Ffmpeg)?;
1705        }
1706        Ok(())
1707    }
1708
1709    /// The actual bound socket address of the underlying RTMP server.
1710    ///
1711    /// Useful when the builder was given `"127.0.0.1:0"`: the OS assigns a real
1712    /// port, and this surfaces it (delegating to
1713    /// [`EmbedRtmpServer::local_addr`]) so tests and callers can observe the
1714    /// port without a bind/drop/rebind race on a pre-probed one.
1715    pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
1716        self.server.local_addr()
1717    }
1718}
1719
1720impl Drop for StreamHandle {
1721    fn drop(&mut self) {
1722        // Wait-then-signal. Waiting first preserves the drain semantics: a
1723        // handle dropped mid-stream still delivers the remaining frames to
1724        // connected watchers before the server goes away — best-effort, not
1725        // absolute: a watcher whose join-replay budget was exhausted keeps
1726        // only its bounded backlog, and stop-time teardown does not re-run
1727        // finished-status delivery for it (cancelling the FFmpeg job instead
1728        // is a separate concern, out of scope here). The explicit stop after
1729        // it is what actually releases the listener port and the worker
1730        // threads — dropping the Arc alone never did: the threads own clones
1731        // of the status flag and keep running (and keep the port bound)
1732        // until the flag flips.
1733        if let Some(scheduler) = self.scheduler.take() {
1734            let _ = scheduler.wait();
1735        }
1736        self.server.signal_stop();
1737    }
1738}
1739
1740impl EmbedRtmpServer<Initialization> {
1741    /// Creates a new `StreamBuilder` for simplified RTMP streaming.
1742    ///
1743    /// This is the recommended entry point for simple streaming scenarios
1744    /// where you want to stream a file to an embedded RTMP server.
1745    ///
1746    /// # Example
1747    ///
1748    /// ```rust,ignore
1749    /// use ez_ffmpeg::rtmp::embed_rtmp_server::EmbedRtmpServer;
1750    ///
1751    /// let handle = EmbedRtmpServer::stream_builder()
1752    ///     .address("localhost:1935")
1753    ///     .app_name("live")
1754    ///     .stream_key("stream1")
1755    ///     .input_file("video.mp4")
1756    ///     .start()?;
1757    ///
1758    /// handle.wait()?;
1759    /// ```
1760    ///
1761    /// For more complex scenarios requiring full control over the server
1762    /// and FFmpeg context, use the traditional API:
1763    ///
1764    /// ```rust,ignore
1765    /// let server = EmbedRtmpServer::new("localhost:1935").start()?;
1766    /// let output = server.create_rtmp_input("app", "stream")?;
1767    /// // ... configure Input and FfmpegContext manually
1768    /// ```
1769    pub fn stream_builder() -> StreamBuilder {
1770        StreamBuilder::new()
1771    }
1772}
1773
1774#[cfg(test)]
1775mod bypass_parity_tests {
1776    //! PERF-5a: the in-process media bypass hands the scheduler a
1777    //! `(timestamp, data)` pair taken directly from the parsed FLV tag. These
1778    //! tests prove that pair is byte-identical to what the pure-serialize path
1779    //! reconstructs (FLV tag -> `flv_tag_to_message_payload` -> RTMP chunk
1780    //! serialize -> deserialize -> `MessagePayload`), so the scheduler observes
1781    //! an identical sequence either way.
1782    use super::*;
1783    use crate::flv::flv_tag::FlvTag;
1784    use crate::flv::flv_tag_header::FlvTagHeader;
1785    use rml_rtmp::chunk_io::ChunkDeserializer;
1786
1787    fn make_tag(tag_type: u8, timestamp: u32, timestamp_ext: u8, data: Vec<u8>) -> FlvTag {
1788        FlvTag {
1789            header: FlvTagHeader {
1790                tag_type,
1791                data_size: data.len() as u32,
1792                timestamp,
1793                timestamp_ext,
1794                stream_id: 1,
1795            },
1796            data: Bytes::from(data),
1797            previous_tag_size: 0,
1798        }
1799    }
1800
1801    /// Assert the bypass `(timestamp, data)` equals the serialize round-trip.
1802    fn assert_parity(tag_type: u8, timestamp: u32, timestamp_ext: u8, data: Vec<u8>) {
1803        let tag = make_tag(tag_type, timestamp, timestamp_ext, data);
1804
1805        // What the bypass path hands to the scheduler, straight from the tag.
1806        let bypass_timestamp = tag.header.timestamp | ((tag.header.timestamp_ext as u32) << 24);
1807        let bypass_data = tag.data.clone();
1808
1809        // What the serialize path reconstructs: payload -> chunk bytes ->
1810        // deserialize -> payload.
1811        let payload = flv_tag_to_message_payload(tag);
1812        let mut serializer = ChunkSerializer::new();
1813        let packet = serializer
1814            .serialize(&payload, false, true)
1815            .expect("serialize");
1816        let mut deserializer = ChunkDeserializer::new();
1817        let round = deserializer
1818            .get_next_message(&packet.bytes)
1819            .expect("deserialize")
1820            .expect("a complete message from the serialized chunks");
1821
1822        assert_eq!(
1823            round.type_id, tag_type,
1824            "tag type parity for {tag_type:#04x}"
1825        );
1826        assert_eq!(
1827            round.timestamp.value, bypass_timestamp,
1828            "timestamp parity for tag {tag_type:#04x}"
1829        );
1830        assert_eq!(
1831            round.data, bypass_data,
1832            "payload parity for tag {tag_type:#04x}"
1833        );
1834    }
1835
1836    #[test]
1837    fn video_sequence_header_round_trips_identically() {
1838        // AVC sequence header (0x17 0x00 ...), timestamp 0.
1839        assert_parity(
1840            0x09,
1841            0,
1842            0,
1843            vec![0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x1f],
1844        );
1845    }
1846
1847    #[test]
1848    fn audio_sequence_header_round_trips_identically() {
1849        // AAC AudioSpecificConfig (0xaf 0x00 ...).
1850        assert_parity(0x08, 0, 0, vec![0xaf, 0x00, 0x12, 0x10]);
1851    }
1852
1853    #[test]
1854    fn large_keyframe_spanning_multiple_chunks_round_trips_identically() {
1855        // A keyframe larger than the default 128-byte chunk size forces the
1856        // serializer to split it into continuation chunks; the deserializer
1857        // must reassemble the exact same bytes.
1858        let mut data = vec![0x17, 0x01, 0x00, 0x00, 0x00];
1859        data.extend((0u16..400).map(|i| (i & 0xff) as u8));
1860        assert_parity(0x09, 0x1234, 0, data);
1861    }
1862
1863    #[test]
1864    fn delta_frame_round_trips_identically() {
1865        assert_parity(0x09, 0x0001_0000, 0, vec![0x27, 0x01, 0x00, 0x11, 0x22]);
1866    }
1867
1868    #[test]
1869    fn extended_timestamp_round_trips_identically() {
1870        // timestamp field saturated (0xFFFFFF) plus an extension byte forces
1871        // the RTMP extended-timestamp encoding; the full 32-bit value must
1872        // survive the round-trip.
1873        assert_parity(0x08, 0x00ff_ffff, 0x01, vec![0xaf, 0x01, 0xAA, 0xBB]);
1874    }
1875
1876    #[test]
1877    fn audio_and_video_tags_never_wrap_their_payload() {
1878        // Unlike 0x12 metadata (which flv_tag_to_message_payload prefixes with
1879        // @setDataFrame), media payloads must pass through untouched — the
1880        // bypass relies on this to skip the serializer entirely.
1881        let audio = make_tag(0x08, 10, 0, vec![0xaf, 0x01, 0x01, 0x02, 0x03]);
1882        let video = make_tag(0x09, 10, 0, vec![0x27, 0x01, 0x09, 0x08, 0x07]);
1883        assert_eq!(
1884            flv_tag_to_message_payload(audio.clone()).data,
1885            audio.data,
1886            "audio payload must not be wrapped"
1887        );
1888        assert_eq!(
1889            flv_tag_to_message_payload(video.clone()).data,
1890            video.data,
1891            "video payload must not be wrapped"
1892        );
1893    }
1894}
1895
1896#[cfg(test)]
1897mod tests {
1898    use super::*;
1899    use crate::core::context::ffmpeg_context::FfmpegContext;
1900    use crate::core::context::input::Input;
1901    use crate::core::context::output::Output;
1902    use crate::core::scheduler::ffmpeg_scheduler::FfmpegScheduler;
1903    use ffmpeg_next::time::current;
1904    use std::sync::atomic::AtomicBool;
1905    use std::thread::sleep;
1906    use std::time::Duration;
1907
1908    /// Poll (up to ~2s) until `addr` can be bound again. The accept thread
1909    /// releases the listener within one ~100ms accept cycle of the stop
1910    /// signal, so a successful rebind proves the stop actually tore the
1911    /// server down rather than merely flipping a flag.
1912    fn wait_for_port_release(addr: std::net::SocketAddr) -> bool {
1913        let deadline = std::time::Instant::now() + Duration::from_secs(2);
1914        loop {
1915            match std::net::TcpListener::bind(addr) {
1916                Ok(_) => return true,
1917                Err(_) if std::time::Instant::now() < deadline => sleep(Duration::from_millis(20)),
1918                Err(_) => return false,
1919            }
1920        }
1921    }
1922
1923    // API-hygiene regression: the stream sender must report a *stream*-scoped
1924    // close (not a server-thread exit) when its consumer is gone, and stay
1925    // multi-producer like the `crossbeam_channel::Sender` it used to be.
1926    #[test]
1927    fn stream_sender_reports_stream_closed_and_clones_share_the_stream() {
1928        let (tx, rx) = crossbeam_channel::bounded::<Vec<u8>>(4);
1929        let sender = RtmpStreamSender {
1930            inner: tx,
1931            wake_handle: None,
1932        };
1933
1934        // A clone feeds the same stream: a chunk pushed through the clone is
1935        // observed on the single receiver (the old handle was `Clone`).
1936        let clone = sender.clone();
1937        clone
1938            .send(b"chunk".to_vec())
1939            .expect("send on a live stream");
1940        assert_eq!(rx.recv().unwrap(), b"chunk".to_vec());
1941
1942        // Dropping the consumer is a stream close, not a server-thread exit:
1943        // the error must be RtmpStreamClosed, never RtmpThreadExited.
1944        drop(rx);
1945        assert_eq!(
1946            sender.send(b"late".to_vec()),
1947            Err(crate::error::Error::RtmpStreamClosed),
1948        );
1949    }
1950
1951    // H7 regression: stop() used to only flip the status flag; the accept
1952    // thread parked on the listener kept the port bound, so a start-stop-start
1953    // cycle on the same address failed with AddrInUse.
1954    #[test]
1955    fn stopped_server_releases_its_port_for_rebind() {
1956        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1957        let addr = server.local_addr().expect("bound address");
1958
1959        // Sanity: while the server runs, the port is genuinely held.
1960        assert!(
1961            std::net::TcpListener::bind(addr).is_err(),
1962            "the running server must hold its port"
1963        );
1964
1965        let stopped = server.stop();
1966        assert!(stopped.is_stopped());
1967        assert!(
1968            wait_for_port_release(addr),
1969            "the port must be rebindable within 2s of stop()"
1970        );
1971    }
1972
1973    // H7: a StreamHandle drop must stop the server it holds. Before the fix
1974    // its Drop only waited on the scheduler and let the Arc'd server leak its
1975    // threads and port forever.
1976    #[test]
1977    fn stream_handle_drop_stops_the_server() {
1978        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
1979        let addr = server.local_addr().expect("bound address");
1980        let server = Arc::new(server);
1981        let observer = server.clone();
1982
1983        let handle = StreamHandle {
1984            server,
1985            scheduler: None,
1986        };
1987        drop(handle);
1988
1989        assert!(
1990            observer.is_stopped(),
1991            "dropping the handle must signal the server to stop"
1992        );
1993        assert!(
1994            wait_for_port_release(addr),
1995            "the port must be rebindable within 2s of the handle drop"
1996        );
1997    }
1998
1999    // F6: the post-start failure path — a StreamBuilder that starts the server
2000    // but then fails to build the FFmpeg job must not leak it. Rather than a
2001    // racy probe/drop/rebind on a fixed port (which a parallel test could steal
2002    // between the drop and the builder's bind), drive the exact RAII path
2003    // directly: start on port 0, read the OS-assigned address, arm the
2004    // ServerStopGuard, then drop it as any post-start `?` would — no
2005    // reserve/drop/rebind window.
2006    #[test]
2007    fn server_stop_guard_drop_releases_the_port() {
2008        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
2009        let addr = server.local_addr().expect("bound address");
2010
2011        // The port is genuinely held while the guard owns the running server.
2012        assert!(
2013            std::net::TcpListener::bind(addr).is_err(),
2014            "the running server must hold its port while the guard is armed"
2015        );
2016
2017        // Arm the guard exactly as StreamBuilder::start does, then simulate the
2018        // post-start failure by dropping it — its Drop must signal_stop.
2019        let guard = ServerStopGuard {
2020            server: Some(Arc::new(server)),
2021        };
2022        drop(guard);
2023
2024        assert!(
2025            wait_for_port_release(addr),
2026            "dropping the armed guard (a post-start failure) must release the port"
2027        );
2028    }
2029
2030    // H8.b regression: the reactor used to receive a `.clone()` of the
2031    // DashSet — a deep copy — so keys it inserted never became visible to the
2032    // duplicate-key check in create_* and RtmpStreamAlreadyExists was dead
2033    // code: two publishers could claim the same stream key.
2034    #[test]
2035    fn duplicate_stream_key_is_rejected_once_registered() {
2036        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
2037        // Keep the Output alive: dropping it drops the feed sender, which
2038        // deregisters the publisher and frees the key again.
2039        let _output = server
2040            .create_rtmp_input("app", "dup-key")
2041            .expect("first create must succeed");
2042
2043        // The key is claimed synchronously inside create_*, before the
2044        // registration ever reaches the reactor thread.
2045        assert!(
2046            server.stream_keys.contains("dup-key"),
2047            "the stream key must be claimed by the time create returns"
2048        );
2049
2050        let second = server.create_rtmp_input("app", "dup-key");
2051        assert!(
2052            matches!(
2053                second,
2054                Err(crate::error::Error::RtmpStreamAlreadyExists(ref key)) if key == "dup-key"
2055            ),
2056            "a second create for a registered key must fail with RtmpStreamAlreadyExists"
2057        );
2058
2059        server.stop();
2060    }
2061
2062    // Claiming a stream key must be atomic with the duplicate check: when two
2063    // threads race create with the same key, exactly one may get Ok and the
2064    // other must fail immediately with the typed RtmpStreamAlreadyExists
2065    // error, not with a later opaque send failure.
2066    #[test]
2067    fn racing_creates_for_same_key_yield_exactly_one_winner() {
2068        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
2069
2070        for round in 0..8 {
2071            let key = format!("race-key-{round}");
2072            let barrier = std::sync::Barrier::new(2);
2073            let (a, b) = std::thread::scope(|s| {
2074                let ta = s.spawn(|| {
2075                    barrier.wait();
2076                    server.create_rtmp_input("app", key.as_str())
2077                });
2078                let tb = s.spawn(|| {
2079                    barrier.wait();
2080                    server.create_rtmp_input("app", key.as_str())
2081                });
2082                (ta.join().expect("thread a"), tb.join().expect("thread b"))
2083            });
2084
2085            let oks = a.is_ok() as usize + b.is_ok() as usize;
2086            assert_eq!(
2087                oks, 1,
2088                "round {round}: exactly one racing create may claim the key, got {oks} Ok"
2089            );
2090            let loser = if a.is_ok() { b } else { a };
2091            assert!(
2092                matches!(
2093                    loser,
2094                    Err(crate::error::Error::RtmpStreamAlreadyExists(ref k)) if k == &key
2095                ),
2096                "round {round}: the losing create must fail with RtmpStreamAlreadyExists"
2097            );
2098        }
2099
2100        server.stop();
2101    }
2102
2103    // H7: signal_stop is idempotent — a second signal (or a stop() after a
2104    // signal) must be a harmless no-op.
2105    #[test]
2106    fn double_stop_signal_is_idempotent() {
2107        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
2108        let addr = server.local_addr().expect("bound address");
2109
2110        server.signal_stop();
2111        assert!(server.is_stopped());
2112        server.signal_stop();
2113        assert!(server.is_stopped());
2114
2115        let ended = server.stop();
2116        assert!(ended.is_stopped());
2117        assert!(wait_for_port_release(addr));
2118    }
2119
2120    // R-A2: stop() is a settlement barrier, not just a signal. When it
2121    // returns, both server threads must have exited: the listener is
2122    // released (a bind succeeds on the FIRST attempt, no retry loop) and
2123    // every in-process publisher torn down with its key claim freed (no
2124    // polling for the reactor to "get around to it").
2125    #[test]
2126    fn stop_settles_the_server_threads_before_returning() {
2127        let server = EmbedRtmpServer::new("127.0.0.1:0").start().expect("start");
2128        let addr = server.local_addr().expect("bound address");
2129        let observer = server.clone();
2130
2131        // A live in-process publisher whose claim only the worker's teardown
2132        // can release. Kept alive across stop(): the release must come from
2133        // the joined reactor, not from this Output dropping.
2134        let _live = server
2135            .create_rtmp_input("app", "settlement-key")
2136            .expect("create while running must succeed");
2137        assert!(observer.stream_keys.contains("settlement-key"));
2138
2139        let ended = server.stop();
2140        assert!(ended.is_stopped());
2141
2142        // No wait_for_port_release here — that helper exists for signal-only
2143        // paths. A settled stop has already torn everything down.
2144        assert!(
2145            !observer.stream_keys.contains("settlement-key"),
2146            "stop() must not return before the worker released the key claims"
2147        );
2148        assert!(
2149            std::net::TcpListener::bind(addr).is_ok(),
2150            "stop() must not return before the accept thread released the listener"
2151        );
2152    }
2153
2154    // The settlement barrier must never deadlock on reentrancy: user code
2155    // reached through the global logger runs ON the server threads and may
2156    // call stop(). Such a call must return promptly WITHOUT touching the
2157    // registry — the handles stay available, so a later stop() from any
2158    // other thread still settles for real.
2159    #[test]
2160    fn settlement_from_a_server_thread_is_signal_only_and_preserves_handles() {
2161        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2162        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2163        let (registered_tx, registered_rx) = std::sync::mpsc::channel::<()>();
2164        let (settled_tx, settled_rx) = std::sync::mpsc::channel::<()>();
2165
2166        let ids_probe = server_thread_ids.clone();
2167        let threads_probe = threads.clone();
2168        let handle = std::thread::Builder::new()
2169            .name("reentrancy-probe".to_string())
2170            .spawn(move || {
2171                // Hold until the spawner registered this thread's id and
2172                // handle, then run the settlement from inside that thread —
2173                // the shape of a logger-held clone calling stop().
2174                registered_rx
2175                    .recv()
2176                    .expect("the registration signal must arrive");
2177                settle_server_threads(&ids_probe, &threads_probe);
2178                settled_tx
2179                    .send(())
2180                    .expect("report that the settlement returned");
2181            })
2182            .expect("spawn the probe thread");
2183
2184        server_thread_ids.lock().unwrap().push(handle.thread().id());
2185        threads.lock().unwrap().push(handle);
2186        registered_tx.send(()).expect("signal the registration");
2187
2188        // Deadline-bounded receive, not synchronization: the reentrant call
2189        // returns at once; a self-join would hang forever and only this
2190        // bound turns that hang into a test failure.
2191        settled_rx
2192            .recv_timeout(Duration::from_secs(5))
2193            .expect("settlement on a server thread must be signal-only, not a deadlock");
2194        assert_eq!(
2195            threads.lock().unwrap().len(),
2196            1,
2197            "the reentrant call must leave the registry untouched"
2198        );
2199
2200        // Any other thread can still run the real settlement afterwards.
2201        settle_server_threads(&server_thread_ids, &threads);
2202        assert!(
2203            threads.lock().unwrap().is_empty(),
2204            "a later settlement from a non-server thread must consume and join the handle"
2205        );
2206    }
2207
2208    // Racing stops on clones must EACH return only after the server threads
2209    // exited: the second caller serializes behind the first's joins on the
2210    // registry lock. Each settler asserts the exit flag the moment its
2211    // settlement returns — an implementation that returned early would trip
2212    // it.
2213    #[test]
2214    fn concurrent_settlements_both_wait_for_the_thread_exit() {
2215        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2216        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2217        let exited = Arc::new(AtomicBool::new(false));
2218        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
2219
2220        let exited_worker = exited.clone();
2221        let worker = std::thread::Builder::new()
2222            .name("held-worker".to_string())
2223            .spawn(move || {
2224                // Parks until released — stands in for a server thread that
2225                // is still settling when the stops arrive.
2226                release_rx.recv().expect("the release signal must arrive");
2227                exited_worker.store(true, Ordering::Release);
2228            })
2229            .expect("spawn the held worker");
2230        server_thread_ids.lock().unwrap().push(worker.thread().id());
2231        threads.lock().unwrap().push(worker);
2232
2233        let settler = |ids: Arc<Mutex<Vec<std::thread::ThreadId>>>,
2234                       threads: Arc<Mutex<Vec<JoinHandle<()>>>>,
2235                       exited: Arc<AtomicBool>| {
2236            std::thread::spawn(move || {
2237                settle_server_threads(&ids, &threads);
2238                assert!(
2239                    exited.load(Ordering::Acquire),
2240                    "settlement returned before the registered thread exited"
2241                );
2242            })
2243        };
2244        let first = settler(
2245            server_thread_ids.clone(),
2246            threads.clone(),
2247            exited.clone(),
2248        );
2249
2250        // Deadline-bounded entry probe, not synchronization: wait until the
2251        // first settler demonstrably holds the registry lock while joining
2252        // the still-parked worker. Only then is the second settler spawned,
2253        // so it provably contends with a settlement in progress — and an
2254        // implementation that never held the lock across the join would
2255        // time this probe out.
2256        let deadline = std::time::Instant::now() + Duration::from_secs(5);
2257        while threads.try_lock().is_ok() {
2258            assert!(
2259                std::time::Instant::now() < deadline,
2260                "the first settler must be inside the settlement (holding the registry lock) within 5s"
2261            );
2262            sleep(Duration::from_millis(1));
2263        }
2264        let second = settler(server_thread_ids, threads, exited);
2265
2266        // One-sided guard on the second settler: while the worker is still
2267        // parked, the first settler holds the registry lock inside its
2268        // join, so a correct second settlement CANNOT return — it is either
2269        // not yet scheduled or blocked on the lock. An early-returning
2270        // implementation finishes here and fails; a correct one can never
2271        // trip this, so the bounded window adds no flake risk (slow
2272        // scheduling only makes the check vacuous, never wrong).
2273        for _ in 0..50 {
2274            assert!(
2275                !second.is_finished(),
2276                "the second settlement returned while the worker was still parked"
2277            );
2278            sleep(Duration::from_millis(1));
2279        }
2280
2281        release_tx.send(()).expect("release the held worker");
2282        first
2283            .join()
2284            .expect("the first settler must observe the exit");
2285        second
2286            .join()
2287            .expect("the second settler must observe the exit");
2288    }
2289
2290    // A poisoned registry is NOT a settled registry: settlement must ride
2291    // over the poison and still join whatever handles remain, or a panic in
2292    // one caller would turn every later stop() into a false barrier.
2293    #[test]
2294    fn settlement_joins_handles_out_of_a_poisoned_registry() {
2295        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2296        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2297        let exited = Arc::new(AtomicBool::new(false));
2298        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
2299
2300        let exited_worker = exited.clone();
2301        let worker = std::thread::spawn(move || {
2302            release_rx.recv().expect("the release signal must arrive");
2303            exited_worker.store(true, Ordering::Release);
2304        });
2305        threads.lock().unwrap().push(worker);
2306
2307        // Poison the registry the way a panicking holder would.
2308        let poisoner = threads.clone();
2309        let _ = std::thread::spawn(move || {
2310            let _guard = poisoner.lock().unwrap();
2311            panic!("poison the registry");
2312        })
2313        .join();
2314        assert!(threads.lock().is_err(), "the registry must be poisoned");
2315
2316        release_tx.send(()).expect("release the worker");
2317        settle_server_threads(&server_thread_ids, &threads);
2318        assert!(
2319            exited.load(Ordering::Acquire),
2320            "settlement must still join the handle held by the poisoned registry"
2321        );
2322        assert!(
2323            threads
2324                .lock()
2325                .unwrap_or_else(PoisonError::into_inner)
2326                .is_empty(),
2327            "the poisoned registry must be drained by the settlement"
2328        );
2329    }
2330
2331    // A registered thread that died to an uncontained panic is just as
2332    // terminated: settlement must report it and return normally instead of
2333    // rethrowing the payload out of stop().
2334    #[test]
2335    fn settlement_contains_a_panicked_thread_join() {
2336        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2337        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2338        threads.lock().unwrap().push(std::thread::spawn(|| {
2339            panic!("uncontained thread panic");
2340        }));
2341
2342        settle_server_threads(&server_thread_ids, &threads);
2343        assert!(
2344            threads.lock().unwrap().is_empty(),
2345            "the panicked thread's handle must still be consumed"
2346        );
2347    }
2348
2349    // A panic PAYLOAD is arbitrary user data whose destructor can itself
2350    // panic (panic_any with a drop bomb). Disposing of it mid-drain would
2351    // unwind with the registry guard live: the remaining handles would be
2352    // detached and the registry poisoned into a state that reads as
2353    // settled. Settlement must join EVERY handle — including those behind
2354    // the bomb — and return normally.
2355    #[test]
2356    fn settlement_contains_a_panicking_join_payload() {
2357        struct PayloadBomb;
2358        impl Drop for PayloadBomb {
2359            fn drop(&mut self) {
2360                panic!("panic payload drop bomb");
2361            }
2362        }
2363
2364        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2365        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2366        let survivor_joined = Arc::new(AtomicBool::new(false));
2367
2368        // The bomb thread is registered FIRST so its payload is handled
2369        // while another handle still sits behind it in the drain.
2370        threads.lock().unwrap().push(std::thread::spawn(|| {
2371            std::panic::panic_any(PayloadBomb);
2372        }));
2373        let survivor_flag = survivor_joined.clone();
2374        threads.lock().unwrap().push(std::thread::spawn(move || {
2375            survivor_flag.store(true, Ordering::Release);
2376        }));
2377
2378        settle_server_threads(&server_thread_ids, &threads);
2379        assert!(
2380            survivor_joined.load(Ordering::Acquire),
2381            "the handle behind the bomb payload must still be joined"
2382        );
2383        assert!(
2384            threads.lock().unwrap().is_empty(),
2385            "settlement must consume every handle despite the bomb payload"
2386        );
2387    }
2388
2389    // Settlement never runs a payload's drop glue at all: a payload whose
2390    // destructor re-panics with another bomb, and even one whose TWO field
2391    // destructors both panic (an abort inside its own drop glue that no
2392    // catch can regain control from), must be leaked untouched — the
2393    // barrier completes, every handle behind the bombs is joined, and
2394    // nothing unwinds or aborts.
2395    #[test]
2396    fn settlement_contains_a_chained_panic_payload() {
2397        struct ChainBomb(u32);
2398        impl Drop for ChainBomb {
2399            fn drop(&mut self) {
2400                if self.0 > 0 {
2401                    std::panic::panic_any(ChainBomb(self.0 - 1));
2402                }
2403            }
2404        }
2405        struct PlainBomb;
2406        impl Drop for PlainBomb {
2407            fn drop(&mut self) {
2408                panic!("plain payload bomb");
2409            }
2410        }
2411        // Two panicking field destructors in one box: dropping this ANYWHERE
2412        // is a guaranteed process abort (panic during panic-unwind).
2413        struct TwoBomb {
2414            _a: PlainBomb,
2415            _b: PlainBomb,
2416        }
2417
2418        let server_thread_ids: Arc<Mutex<Vec<std::thread::ThreadId>>> = Default::default();
2419        let threads: Arc<Mutex<Vec<JoinHandle<()>>>> = Default::default();
2420        let survivor_joined = Arc::new(AtomicBool::new(false));
2421
2422        // Chain bomb, plain bomb, the unabortable two-bomb, then a live
2423        // survivor: none of the payloads may be dropped, and the survivor
2424        // behind all of them must still be joined.
2425        threads.lock().unwrap().push(std::thread::spawn(|| {
2426            std::panic::panic_any(ChainBomb(2));
2427        }));
2428        threads.lock().unwrap().push(std::thread::spawn(|| {
2429            std::panic::panic_any(PlainBomb);
2430        }));
2431        threads.lock().unwrap().push(std::thread::spawn(|| {
2432            std::panic::panic_any(TwoBomb {
2433                _a: PlainBomb,
2434                _b: PlainBomb,
2435            });
2436        }));
2437        let survivor_flag = survivor_joined.clone();
2438        threads.lock().unwrap().push(std::thread::spawn(move || {
2439            survivor_flag.store(true, Ordering::Release);
2440        }));
2441
2442        settle_server_threads(&server_thread_ids, &threads);
2443        assert!(
2444            survivor_joined.load(Ordering::Acquire),
2445            "the survivor behind the bombs must still be joined"
2446        );
2447        assert!(
2448            threads.lock().unwrap().is_empty(),
2449            "settlement must consume every handle despite the bomb payloads"
2450        );
2451    }
2452
2453    // The terminal cause drives the publisher write callbacks' failure
2454    // classification, first-writer-wins: signal_stop (every user-initiated
2455    // path) claims CAUSE_DELIBERATE, the bare terminal funnel (worker
2456    // death, contained reactor panic) claims CAUSE_FATAL, and whichever
2457    // transition lands first keeps the label — in particular, a stop()
2458    // arriving AFTER a crash must not relabel the crash as deliberate.
2459    #[test]
2460    fn terminal_cause_is_first_writer_wins() {
2461        // Deliberate first: the cause is claimed as deliberate and a later
2462        // fatal funnel (the worker noticing the stop and winding down) must
2463        // not overwrite it.
2464        let server = EmbedRtmpServer::<Running> {
2465            address: String::new(),
2466            bound_addr: None,
2467            status: Arc::new(AtomicUsize::new(STATUS_RUN)),
2468            stream_keys: Default::default(),
2469            registrations: Some(Arc::new(RegistrationHandoff::new())),
2470            wake_handle: None,
2471            threads: Default::default(),
2472            server_thread_ids: Default::default(),
2473            terminal_cause: Default::default(),
2474            gop_limit: 1,
2475            max_connections: None,
2476            state: PhantomData,
2477        };
2478        assert_eq!(server.terminal_cause.load(Ordering::Acquire), CAUSE_NONE);
2479        server.signal_stop();
2480        assert!(server.is_stopped());
2481        assert_eq!(
2482            server.terminal_cause.load(Ordering::Acquire),
2483            CAUSE_DELIBERATE,
2484            "signal_stop must claim the deliberate cause"
2485        );
2486        let registrations = server.registrations.clone().expect("handoff installed");
2487        close_intake_and_publish_end(&registrations, &server.status, &server.terminal_cause);
2488        assert_eq!(
2489            server.terminal_cause.load(Ordering::Acquire),
2490            CAUSE_DELIBERATE,
2491            "a fatal funnel after a deliberate stop must not relabel it"
2492        );
2493
2494        // Fatal first: the crash claims the cause, and a LATE stop() on a
2495        // surviving clone must not repaint it as deliberate — the racing
2496        // publisher's failing write keeps its loud classification.
2497        let crashed = EmbedRtmpServer::<Running> {
2498            address: String::new(),
2499            bound_addr: None,
2500            status: Arc::new(AtomicUsize::new(STATUS_RUN)),
2501            stream_keys: Default::default(),
2502            registrations: Some(Arc::new(RegistrationHandoff::new())),
2503            wake_handle: None,
2504            threads: Default::default(),
2505            server_thread_ids: Default::default(),
2506            terminal_cause: Default::default(),
2507            gop_limit: 1,
2508            max_connections: None,
2509            state: PhantomData,
2510        };
2511        let registrations = crashed.registrations.clone().expect("handoff installed");
2512        close_intake_and_publish_end(&registrations, &crashed.status, &crashed.terminal_cause);
2513        assert!(crashed.is_stopped());
2514        assert_eq!(
2515            crashed.terminal_cause.load(Ordering::Acquire),
2516            CAUSE_FATAL,
2517            "a fatal terminal transition must claim the fatal cause"
2518        );
2519        crashed.signal_stop();
2520        assert_eq!(
2521            crashed.terminal_cause.load(Ordering::Acquire),
2522            CAUSE_FATAL,
2523            "a stop() after the crash must not relabel the crash as deliberate"
2524        );
2525    }
2526
2527    // signal_stop must close the registration intake in the same breath as
2528    // publishing the stopped status. Without that, a server clone could
2529    // observe is_stopped() == true, still enqueue a registration, and be
2530    // told Ok even though no reactor round would ever consume it — the kill
2531    // switch would eventually release the claim, but the Ok reported a
2532    // stream that could never exist. No socket is bound here: the create
2533    // path touches only the handoff, the key set and the status flag.
2534    #[test]
2535    fn stop_signal_closes_the_registration_intake() {
2536        let server = EmbedRtmpServer::<Running> {
2537            address: String::new(),
2538            bound_addr: None,
2539            status: Arc::new(AtomicUsize::new(STATUS_RUN)),
2540            stream_keys: Default::default(),
2541            registrations: Some(Arc::new(RegistrationHandoff::new())),
2542            wake_handle: None,
2543            threads: Default::default(),
2544            server_thread_ids: Default::default(),
2545            terminal_cause: Default::default(),
2546            gop_limit: 1,
2547            max_connections: None,
2548            state: PhantomData,
2549        };
2550
2551        let _live = server
2552            .create_rtmp_input("app", "before")
2553            .expect("create while running must succeed");
2554
2555        server.signal_stop();
2556        assert!(server.is_stopped());
2557
2558        let after = server.create_rtmp_input("app", "after");
2559        assert!(
2560            matches!(after, Err(crate::error::Error::RtmpCreateStream)),
2561            "a create issued after the stop signal must fail with the stopped error"
2562        );
2563        assert!(
2564            !server.stream_keys.contains("after"),
2565            "the refused create must release its key claim immediately"
2566        );
2567    }
2568
2569    // The claim-release chain end to end, through the public lifecycle: a
2570    // create claims the key and queues the registration, the running worker
2571    // consumes it and moves the claim into the accepted publisher's state,
2572    // and stop() must unwind that whole chain so the key is claimable again
2573    // afterwards. The server is assembled exactly as start() builds it —
2574    // same shared status, key set and handoff, and the same worker body
2575    // start() spawns (handle_connections) — minus the TCP listener and
2576    // accept thread, so no port is bound and every wait is a bounded poll
2577    // against a deadline.
2578    #[test]
2579    fn stopped_server_releases_accepted_stream_keys() {
2580        let registrations = Arc::new(RegistrationHandoff::new());
2581        let status = Arc::new(AtomicUsize::new(STATUS_RUN));
2582        let server = EmbedRtmpServer::<Running> {
2583            address: String::new(),
2584            bound_addr: None,
2585            status: status.clone(),
2586            stream_keys: Default::default(),
2587            registrations: Some(registrations.clone()),
2588            wake_handle: None,
2589            threads: Default::default(),
2590            server_thread_ids: Default::default(),
2591            terminal_cause: Default::default(),
2592            gop_limit: 1,
2593            max_connections: None,
2594            state: PhantomData,
2595        };
2596
2597        // The accept thread's side of the connection channel, kept open and
2598        // idle for the worker's lifetime — a listener that never accepts.
2599        let (_connection_sender, connection_receiver) =
2600            crossbeam_channel::bounded::<TcpStream>(1);
2601        let worker = {
2602            let status = status.clone();
2603            std::thread::Builder::new()
2604                .name("rtmp-server-worker".to_string())
2605                .spawn(move || {
2606                    handle_connections(
2607                        connection_receiver,
2608                        registrations,
2609                        1,
2610                        None,
2611                        status,
2612                        Default::default(),
2613                        None,
2614                    )
2615                })
2616                .expect("spawn the worker thread")
2617        };
2618
2619        // The create claims the key synchronously and queues the
2620        // registration, primed with the connect / createStream / publish
2621        // handshake, for the worker.
2622        let sender = server
2623            .create_stream_sender("app", "lifecycle-key")
2624            .expect("create on the running server must succeed");
2625
2626        // Wait until the reactor drains that primed handshake (with no wake
2627        // handle it picks the registration up on a ~100ms poll timeout). An
2628        // empty channel means the worker consumed the registration, so the
2629        // claim now lives in the reactor's publisher state — the ownership
2630        // stop() must tear down.
2631        let deadline = std::time::Instant::now() + Duration::from_secs(5);
2632        while !sender.inner.is_empty() {
2633            assert!(
2634                std::time::Instant::now() < deadline,
2635                "the worker must drain the primed publish handshake within 5s"
2636            );
2637            sleep(Duration::from_millis(10));
2638        }
2639
2640        // The accepted publisher holds the key: a duplicate create is
2641        // refused with the typed already-exists error.
2642        assert!(
2643            matches!(
2644                server.create_stream_sender("app", "lifecycle-key"),
2645                Err(crate::error::Error::RtmpStreamAlreadyExists(ref key)) if key == "lifecycle-key"
2646            ),
2647            "the key must stay held while its publisher is accepted and live"
2648        );
2649
2650        // Stop through the public seam; a surviving clone observes the
2651        // aftermath, as any second owner of the server value would.
2652        let observer = server.clone();
2653        assert!(server.stop().is_stopped());
2654
2655        // The worker notices the stop flag on its next poll cycle and
2656        // exits; joining it orders every teardown release before the
2657        // assertions below.
2658        let deadline = std::time::Instant::now() + Duration::from_secs(5);
2659        while !worker.is_finished() {
2660            assert!(
2661                std::time::Instant::now() < deadline,
2662                "the worker must exit within 5s of stop()"
2663            );
2664            sleep(Duration::from_millis(10));
2665        }
2666        worker.join().expect("the worker must exit cleanly");
2667
2668        // The key is claimable again: the second create gets past the
2669        // duplicate-key gate, and what refuses it is the stopped server's
2670        // closed intake — not a lingering claim.
2671        assert!(
2672            matches!(
2673                observer.create_stream_sender("app", "lifecycle-key"),
2674                Err(crate::error::Error::RtmpCreateStream)
2675            ),
2676            "after stop the key must be free; only the closed intake may refuse the create"
2677        );
2678        assert!(
2679            !observer.stream_keys.contains("lifecycle-key"),
2680            "no claim may survive the worker's teardown"
2681        );
2682    }
2683
2684    /// A minimal registration for probing whether an intake accepts or
2685    /// refuses enqueues. Its claim lives in a key set private to the probe.
2686    fn probe_registration(key: &str) -> PublisherRegistration {
2687        let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
2688        PublisherRegistration {
2689            claim: StreamKeyClaim::claim(Arc::new(dashmap::DashSet::new()), key.to_string())
2690                .expect("a fresh key set accepts its first claim"),
2691            source: PublisherSource::Feed(feed_rx),
2692        }
2693    }
2694
2695    // A reactor panic must not leave the server half-dead: the unwind
2696    // boundary closes the registration intake and publishes STATUS_END so
2697    // is_stopped() flips true, creates are refused, and the accept thread's
2698    // per-iteration status check terminates its loop.
2699    #[test]
2700    fn reactor_panic_publishes_terminal_status() {
2701        let registrations = RegistrationHandoff::new();
2702        let status = AtomicUsize::new(STATUS_RUN);
2703        let terminal_cause = AtomicUsize::new(CAUSE_NONE);
2704        contain_reactor_panic(&registrations, &status, &terminal_cause, || {
2705            panic!("injected reactor panic")
2706        });
2707        assert_eq!(status.load(Ordering::Acquire), STATUS_END);
2708        assert_eq!(
2709            terminal_cause.load(Ordering::Acquire),
2710            CAUSE_FATAL,
2711            "a contained panic is a fatal cause"
2712        );
2713        assert!(
2714            matches!(
2715                registrations.enqueue(probe_registration("panicked")),
2716                Err(EnqueueRefused::Closed(_))
2717            ),
2718            "the contained panic must close the intake, not just flip the status"
2719        );
2720    }
2721
2722    // The clean path owns no transition at all: a normally-returning reactor
2723    // reaches STATUS_END only through signal_stop (or the worker's own fatal
2724    // paths), and the containment must force neither STATUS_END onto a live
2725    // status nor a close onto an open intake.
2726    #[test]
2727    fn reactor_clean_return_leaves_status_untouched() {
2728        let registrations = RegistrationHandoff::new();
2729        let status = AtomicUsize::new(STATUS_RUN);
2730        let terminal_cause = AtomicUsize::new(CAUSE_NONE);
2731        contain_reactor_panic(&registrations, &status, &terminal_cause, || {});
2732        assert_eq!(status.load(Ordering::Acquire), STATUS_RUN);
2733        assert_eq!(
2734            terminal_cause.load(Ordering::Acquire),
2735            CAUSE_NONE,
2736            "a clean return must not claim any terminal cause"
2737        );
2738        assert!(
2739            registrations
2740                .enqueue(probe_registration("still-open"))
2741                .is_ok(),
2742            "a clean reactor return must leave the intake open"
2743        );
2744    }
2745
2746    // Fatal worker paths must uphold the same ordering as signal_stop: the
2747    // intake closes BEFORE STATUS_END becomes observable. The reactor checks
2748    // the stop flag before draining registrations, so a create accepted
2749    // after that flag is up would be an Ok for a stream that can never
2750    // exist — the kill switch discards the queued registration without any
2751    // reactor round consuming it. This drives the panic-containment path
2752    // against the real create path, wired to one handoff and status exactly
2753    // as start() builds them.
2754    #[test]
2755    fn reactor_panic_refuses_creates_on_surviving_handles() {
2756        let registrations = Arc::new(RegistrationHandoff::new());
2757        let status = Arc::new(AtomicUsize::new(STATUS_RUN));
2758        let server = EmbedRtmpServer::<Running> {
2759            address: String::new(),
2760            bound_addr: None,
2761            status: status.clone(),
2762            stream_keys: Default::default(),
2763            registrations: Some(registrations.clone()),
2764            wake_handle: None,
2765            threads: Default::default(),
2766            server_thread_ids: Default::default(),
2767            terminal_cause: Default::default(),
2768            gop_limit: 1,
2769            max_connections: None,
2770            state: PhantomData,
2771        };
2772
2773        let _live = server
2774            .create_rtmp_input("app", "before-panic")
2775            .expect("create while running must succeed");
2776
2777        contain_reactor_panic(&registrations, &status, &server.terminal_cause, || {
2778            panic!("injected reactor panic")
2779        });
2780
2781        assert!(
2782            server.is_stopped(),
2783            "the contained panic must stop the server"
2784        );
2785        let refused = server.create_rtmp_input("app", "after-panic").err();
2786        assert!(
2787            matches!(refused, Some(crate::error::Error::RtmpCreateStream)),
2788            "a create observing the panic-stopped server must be refused, got {refused:?}"
2789        );
2790        assert!(
2791            !server.stream_keys.contains("after-panic"),
2792            "the refused create must release its key claim immediately"
2793        );
2794    }
2795
2796    // StartFailGuard is the unwind backstop for start()'s window between
2797    // the lifecycle claim (the INIT->RUN CAS) and the accept-thread
2798    // handoff: a log call in that window can panic in a user-installed
2799    // logger, and the escaping unwind must still publish the terminal
2800    // state — otherwise every clone reports a started server no one can
2801    // ever stop. Dropped by that unwind, the guard must run the same
2802    // funnel as signal_stop: intake closed first, STATUS_END second.
2803    #[test]
2804    fn armed_start_fail_guard_publishes_terminal_state_on_unwind() {
2805        let registrations = Arc::new(RegistrationHandoff::new());
2806        let status = Arc::new(AtomicUsize::new(STATUS_RUN));
2807        let guard = StartFailGuard {
2808            armed: true,
2809            registrations: registrations.clone(),
2810            status: status.clone(),
2811            wake_handle: None,
2812            threads: Default::default(),
2813            server_thread_ids: Default::default(),
2814            terminal_cause: Default::default(),
2815        };
2816
2817        let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
2818            let _guard = guard;
2819            panic!("injected panic between the claim and the handoff");
2820        }));
2821        assert!(unwind.is_err(), "the injected panic must unwind");
2822
2823        assert_eq!(status.load(Ordering::Acquire), STATUS_END);
2824        assert!(
2825            matches!(
2826                registrations.enqueue(probe_registration("after-unwind")),
2827                Err(EnqueueRefused::Closed(_))
2828            ),
2829            "the guard must close the intake, not just flip the status"
2830        );
2831    }
2832
2833    // The success handoff disarms the guard: its drop must then leave the
2834    // running lifecycle alone — no closed intake, no STATUS_END.
2835    #[test]
2836    fn disarmed_start_fail_guard_leaves_the_lifecycle_running() {
2837        let registrations = Arc::new(RegistrationHandoff::new());
2838        let status = Arc::new(AtomicUsize::new(STATUS_RUN));
2839        let mut guard = StartFailGuard {
2840            armed: true,
2841            registrations: registrations.clone(),
2842            status: status.clone(),
2843            wake_handle: None,
2844            threads: Default::default(),
2845            server_thread_ids: Default::default(),
2846            terminal_cause: Default::default(),
2847        };
2848        guard.armed = false;
2849        drop(guard);
2850
2851        assert_eq!(status.load(Ordering::Acquire), STATUS_RUN);
2852        assert!(
2853            registrations
2854                .enqueue(probe_registration("still-open"))
2855                .is_ok(),
2856            "a disarmed guard must leave the intake open"
2857        );
2858    }
2859
2860    /// A hand-assembled `Initialization` sibling of `family`: it shares the
2861    /// lifecycle (status flag) and key set exactly as clone() would, but
2862    /// aims at `address` instead of the address the family was created
2863    /// with, which clone() copies verbatim.
2864    fn family_sibling<S: 'static>(
2865        family: &EmbedRtmpServer<S>,
2866        address: impl Into<String>,
2867    ) -> EmbedRtmpServer<Initialization> {
2868        EmbedRtmpServer::<Initialization> {
2869            address: address.into(),
2870            bound_addr: None,
2871            status: family.status.clone(),
2872            stream_keys: family.stream_keys.clone(),
2873            registrations: None,
2874            wake_handle: None,
2875            threads: Default::default(),
2876            server_thread_ids: Default::default(),
2877            terminal_cause: Default::default(),
2878            gop_limit: 1,
2879            max_connections: None,
2880            state: PhantomData,
2881        }
2882    }
2883
2884    // Initialization clones share one status flag and key set — one
2885    // lifecycle. Each start() would otherwise mint its own registration
2886    // intake around that shared status: stopping one started clone closed
2887    // only its own intake while publishing the shared STATUS_END, so the
2888    // other kept accepting creates on handles that reported stopped. The
2889    // single-start gate makes that state unreachable: exactly one start()
2890    // per family may ever succeed.
2891    #[test]
2892    fn second_start_of_a_cloned_family_is_refused() {
2893        let first = EmbedRtmpServer::new("127.0.0.1:0");
2894        let second = first.clone();
2895        let third = first.clone();
2896
2897        let running = first.start().expect("the family's first start must win");
2898        let addr = running.local_addr().expect("bound address");
2899
2900        // While the winner runs: a sibling clone must not start the family
2901        // again. Its bind would even succeed (another ephemeral port); the
2902        // shared lifecycle refuses it before that bind is ever attempted.
2903        let refused = second.start().err();
2904        assert!(
2905            matches!(refused, Some(crate::error::Error::RtmpServerAlreadyStarted)),
2906            "a second start on a running family must be refused, got {refused:?}"
2907        );
2908        assert!(
2909            !running.is_stopped(),
2910            "a refused start must not perturb the running server"
2911        );
2912
2913        // The fixed-port shape of the same refusal: a sibling aimed at THE
2914        // port the winner holds. The admission check must refuse it before
2915        // any bind attempt — binding first would surface Error::IO
2916        // (AddrInUse) from the winner's port instead of the typed
2917        // lifecycle error.
2918        let refused = family_sibling(&running, addr.to_string()).start().err();
2919        assert!(
2920            matches!(refused, Some(crate::error::Error::RtmpServerAlreadyStarted)),
2921            "a second start on the winner's own port must get the typed refusal, got {refused:?}"
2922        );
2923        assert!(
2924            !running.is_stopped(),
2925            "the fixed-port refusal must not perturb the running server"
2926        );
2927
2928        // After stop, STATUS_END is terminal for the family: restarting
2929        // would flip stopped handles back to not-stopped around an intake
2930        // that is closed for good.
2931        let ended = running.stop();
2932        assert!(ended.is_stopped());
2933        let refused = third.start().err();
2934        assert!(
2935            matches!(refused, Some(crate::error::Error::RtmpServerAlreadyStarted)),
2936            "a start after the family stopped must be refused, got {refused:?}"
2937        );
2938
2939        // The fixed-port shape right after stop(): the refusal must be
2940        // decided by the lifecycle alone — never by a bind whose outcome
2941        // depends on how quickly the stopped accept thread releases the
2942        // listener (typed refusal one moment, AddrInUse the next).
2943        let refused = family_sibling(&ended, addr.to_string()).start().err();
2944        assert!(
2945            matches!(refused, Some(crate::error::Error::RtmpServerAlreadyStarted)),
2946            "a fixed-port start right after stop must get the typed refusal, got {refused:?}"
2947        );
2948    }
2949
2950    // Bind failures are pre-claim: they must surface as the typed IO error
2951    // and leave the family's lifecycle at INIT, retryable through a clone —
2952    // the admission refusal is reserved for families that actually started.
2953    #[test]
2954    fn failed_bind_leaves_the_family_retryable() {
2955        // Hold a port so the family's first bind deterministically fails.
2956        let blocker = std::net::TcpListener::bind("127.0.0.1:0").expect("reserve a port");
2957        let blocked_addr = blocker.local_addr().expect("blocker address");
2958
2959        let first = EmbedRtmpServer::new(blocked_addr.to_string());
2960        let survivor = first.clone();
2961        let failed = first.start().err();
2962        assert!(
2963            matches!(failed, Some(crate::error::Error::IO(_))),
2964            "binding a held port must surface the typed IO error, got {failed:?}"
2965        );
2966
2967        // The failed bind claimed nothing: a sibling of the same family can
2968        // still win its one start, on a bindable address.
2969        let running = family_sibling(&survivor, "127.0.0.1:0")
2970            .start()
2971            .expect("a failed bind must leave the family startable");
2972        assert!(running.stop().is_stopped());
2973    }
2974
2975    #[test]
2976    #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
2977    fn test_concat_stream_loop() {
2978        let _ = env_logger::builder()
2979            .filter_level(log::LevelFilter::Trace)
2980            .is_test(true)
2981            .try_init();
2982
2983        let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
2984        let embed_rtmp_server = embed_rtmp_server.start().unwrap();
2985
2986        let output = embed_rtmp_server
2987            .create_rtmp_input("my-app", "my-stream")
2988            .unwrap();
2989
2990        let start = current();
2991
2992        let result = FfmpegContext::builder()
2993            .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
2994            .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
2995            .input(Input::from("test.mp4").set_readrate(1.0).set_stream_loop(3))
2996            .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
2997            .output(output)
2998            .build()
2999            .unwrap()
3000            .start()
3001            .unwrap()
3002            .wait();
3003
3004        assert!(result.is_ok());
3005        info!("elapsed time: {}", current() - start);
3006    }
3007
3008    #[test]
3009    #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
3010    fn test_stream_loop() {
3011        let _ = env_logger::builder()
3012            .filter_level(log::LevelFilter::Trace)
3013            .is_test(true)
3014            .try_init();
3015
3016        let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
3017        let embed_rtmp_server = embed_rtmp_server.start().unwrap();
3018
3019        let output = embed_rtmp_server
3020            .create_rtmp_input("my-app", "my-stream")
3021            .unwrap();
3022
3023        let start = current();
3024
3025        let result = FfmpegContext::builder()
3026            .input(
3027                Input::from("test.mp4")
3028                    .set_readrate(1.0)
3029                    .set_stream_loop(-1),
3030            )
3031            // .filter_desc("hue=s=0")
3032            .output(output.set_video_codec("h264_videotoolbox"))
3033            .build()
3034            .unwrap()
3035            .start()
3036            .unwrap()
3037            .wait();
3038
3039        assert!(result.is_ok());
3040
3041        info!("elapsed time: {}", current() - start);
3042    }
3043
3044    #[test]
3045    #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
3046    fn test_concat_realtime() {
3047        let _ = env_logger::builder()
3048            .filter_level(log::LevelFilter::Trace)
3049            .is_test(true)
3050            .try_init();
3051
3052        let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
3053        let embed_rtmp_server = embed_rtmp_server.start().unwrap();
3054
3055        let output = embed_rtmp_server
3056            .create_rtmp_input("my-app", "my-stream")
3057            .unwrap();
3058
3059        let start = current();
3060
3061        let result = FfmpegContext::builder()
3062            .independent_readrate()
3063            .input(Input::from("test.mp4").set_readrate(1.0))
3064            .input(Input::from("test.mp4").set_readrate(1.0))
3065            .input(Input::from("test.mp4").set_readrate(1.0))
3066            .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1")
3067            .output(output)
3068            .build()
3069            .unwrap()
3070            .start()
3071            .unwrap()
3072            .wait();
3073
3074        assert!(result.is_ok());
3075
3076        sleep(Duration::from_secs(1));
3077        info!("elapsed time: {}", current() - start);
3078    }
3079
3080    #[test]
3081    #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
3082    fn test_realtime() {
3083        let _ = env_logger::builder()
3084            .filter_level(log::LevelFilter::Trace)
3085            .is_test(true)
3086            .try_init();
3087
3088        let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
3089        let embed_rtmp_server = embed_rtmp_server.start().unwrap();
3090
3091        let output = embed_rtmp_server
3092            .create_rtmp_input("my-app", "my-stream")
3093            .unwrap();
3094
3095        let start = current();
3096
3097        let result = FfmpegContext::builder()
3098            .input(Input::from("test.mp4").set_readrate(1.0))
3099            .output(output)
3100            .build()
3101            .unwrap()
3102            .start()
3103            .unwrap()
3104            .wait();
3105
3106        assert!(result.is_ok());
3107
3108        info!("elapsed time: {}", current() - start);
3109    }
3110
3111    #[test]
3112    #[ignore] // Integration test: requires test.mp4
3113    fn test_readrate() {
3114        let _ = env_logger::builder()
3115            .filter_level(log::LevelFilter::Trace)
3116            .is_test(true)
3117            .try_init();
3118
3119        let mut output: Output = "output.flv".into();
3120        output.audio_codec = Some("adpcm_swf".to_string());
3121
3122        let mut input: Input = "test.mp4".into();
3123        input.readrate = Some(1.0);
3124
3125        let context = FfmpegContext::builder()
3126            .input(input)
3127            .output(output)
3128            .build()
3129            .unwrap();
3130
3131        let result = FfmpegScheduler::new(context).start().unwrap().wait();
3132        if let Err(error) = result {
3133            println!("Error: {error}");
3134        }
3135    }
3136
3137    #[test]
3138    #[ignore] // Integration test: requires exclusive port 1935 and test.mp4
3139    fn test_embed_rtmp_server() {
3140        let _ = env_logger::builder()
3141            .filter_level(log::LevelFilter::Trace)
3142            .is_test(true)
3143            .try_init();
3144
3145        let embed_rtmp_server = EmbedRtmpServer::new("localhost:1935");
3146        let embed_rtmp_server = embed_rtmp_server.start().unwrap();
3147
3148        let output = embed_rtmp_server
3149            .create_rtmp_input("my-app", "my-stream")
3150            .unwrap();
3151        let mut input: Input = "test.mp4".into();
3152        input.readrate = Some(1.0);
3153
3154        let context = FfmpegContext::builder()
3155            .input(input)
3156            .output(output)
3157            .build()
3158            .unwrap();
3159
3160        let result = FfmpegScheduler::new(context).start().unwrap().wait();
3161
3162        assert!(result.is_ok());
3163
3164        sleep(Duration::from_secs(3));
3165    }
3166}