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