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