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(¤t);
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 ®istrations,
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(®istrations, &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(®istrations, &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(®istrations, &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(®istrations, &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(®istrations, &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(®istrations, &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}