Skip to main content

ez_ffmpeg/core/packet_sink/
mod.rs

1//! Encoded-packet output: consume encoder packets directly, without a muxer.
2//!
3//! A packet sink is the fourth quadrant of the crate's I/O matrix (decoded
4//! frames out = frame export, PCM out = sample export, frames in =
5//! [`VideoWriter`](crate::VideoWriter), **encoded packets out = packet sink**).
6//! Instead of muxing packets into container bytes, the job hands each encoded
7//! packet to a consumer, normalized for WebCodecs-style use.
8//!
9//! # Experimental
10//!
11//! This module is **experimental**: introduced in 0.15, its API surface may
12//! still be reshaped in minor releases while it settles; within a minor
13//! line, patch releases will not break it. Correctness defects (ordering
14//! violations, duplicated terminal callbacks, malformed configuration
15//! records) are **not** waived by this banner — they are release blockers.
16//!
17//! # Strict tier (v1)
18//!
19//! The construction paths on [`PacketSink`](crate::packet_sink::PacketSink)
20//! build a **strict-tier** sink —
21//! [`PacketView`](crate::packet_sink::PacketView),
22//! [`PacketStreamInfo`](crate::packet_sink::PacketStreamInfo) and the
23//! callback bundle are the strict-tier contract, aligned with WebCodecs
24//! `"avc"` / AAC consumption:
25//!
26//! * **H.264 video** is delivered as avcC-configured, 4-byte length-prefixed,
27//!   access-unit-complete packets. The verified wrappers are `libx264`,
28//!   `h264_nvenc`, `h264_videotoolbox`, and `libopenh264`. Strict-tier
29//!   delivery requires `pts >= dts`; an explicit `bf` or `max_b_frames`
30//!   other than integer `"0"` is rejected at build time for
31//!   `h264_videotoolbox` (`PacketSinkError::BFramesUnsupported`). Other
32//!   admitted wrappers keep runtime timestamp enforcement. `max_b_frames` is a
33//!   policy-recognized admission key, **not** an FFmpeg `AVOption` alias of
34//!   `bf`: a leftover `max_b_frames=0` does not set the encoder's `bf` and
35//!   does not guarantee `dts == pts`. Options are not
36//!   rewritten. Unset B-frame keys are admitted (the wrapper's FFmpeg
37//!   default applies). Admission does not guarantee that the linked FFmpeg
38//!   build contains an encoder or that required hardware is available — it
39//!   only lifts the build-time name rejection and the explicit B-frame
40//!   check. The delivery contract assumes one packet == one access unit, a
41//!   property the Trusted path does not check per packet and therefore
42//!   establishes per encoder wrapper (audited against the FFmpeg versions
43//!   CI pins, with a hardware acceptance line where encoding needs
44//!   hardware). Any other video encoder fails the build with a typed error.
45//!   `libopenh264` needs `--enable-libopenh264`; that is an LGPL-compatible
46//!   **copyright** combination, not an H.264 patent grant.
47//! * **AAC audio** is delivered as raw AAC frames; the stream configuration
48//!   carries the AudioSpecificConfig.
49//! * Anything else (subtitles, data streams, stream copy, bitstream filters)
50//!   is rejected up front with a typed
51//!   [`PacketSinkError`](crate::packet_sink::PacketSinkError).
52//!
53//! Future tiers (generic passthrough, HEVC, Annex-B) will introduce their own
54//! construction paths and view/config types, and a per-packet access-unit
55//! verifier may eventually widen video admission beyond the registry;
56//! everything here is `#[non_exhaustive]` so that growth is additive.
57//!
58//! # Callback order
59//!
60//! All callbacks run **serially on the one delivery (mux worker) thread** —
61//! never concurrently, never reentrantly — in this order:
62//!
63//! 1. `on_stream_info` — at most once, after every encoder finalized its
64//!    parameters and **before any packet**. The video configuration is
65//!    already a valid avcC record here.
66//! 2. `on_packet` — zero or more times.
67//! 3. `on_end` **or** `on_delivery_error` — at most one of them, at most
68//!    once:
69//!    * `on_end` fires only when every output stream reached a recognized
70//!      terminal state (natural encoder EOF, or configured truncation such as
71//!      `set_recording_time_us` / `set_shortest`), everything was delivered,
72//!      and the whole job settled without an error: the delivery thread
73//!      first waits for every other job worker to finish (including
74//!      container outputs' teardown), then decides on one fresh
75//!      status/result read — the linearization point. Sibling packet-sink
76//!      workers are the one exception to that wait: they are only
77//!      guaranteed settled by then (errors recorded, encoders joined,
78//!      contexts freed) — their terminal callbacks and capture drops may
79//!      still be running concurrently. An `abort()` that lands after the
80//!      status read is indistinguishable from one after the callback.
81//!    * `on_delivery_error` fires when delivery stopped because of a
82//!      strict-tier violation or a failing callback, or when the job failed
83//!      elsewhere — whether that failure landed after this sink delivered
84//!      everything or truncated its delivery. Cancellation is silent only
85//!      when it interrupts delivery: a `stop()` that lands after this sink
86//!      fully drained still delivers `on_end`. Cancellation also takes
87//!      precedence over a failure it races with: a sink that observes the
88//!      published termination — `stop()`, `abort()`, or dropping the
89//!      running scheduler (its guard publishes the same status) — and
90//!      cancels its delivery cooperatively before a sibling's error is
91//!      recorded stays silent — no `on_delivery_error`. The late error is
92//!      still recorded first-error-wins as the job result, and the `stop()`
93//!      call that drove the race returns it once every worker has settled;
94//!      after `abort()`, which returns nothing, or a drop, which discards
95//!      the result with the scheduler, it goes unobserved. When the failure
96//!      was recorded OUTSIDE this sink's delivery path, an optional
97//!      observer — the builder callback
98//!      [`PacketSinkBuilder::on_job_failed`](crate::packet_sink::PacketSinkBuilder::on_job_failed)
99//!      or the handler override
100//!      [`PacketSinkHandler::on_job_failed`](crate::packet_sink::PacketSinkHandler::on_job_failed)
101//!      — receives a structured
102//!      [`JobFailureSummary`](crate::packet_sink::JobFailureSummary)
103//!      immediately before that synthesized `JobFailed` dispatch.
104//!
105//! # Timestamp and ordering
106//!
107//! Timestamps are per-stream: within one stream, dts is strictly increasing
108//! and `pts >= dts`. **No cross-stream interleaving order is promised** —
109//! audio and video packets arrive in worker order, and a consumer must route
110//! by [`PacketView::stream_index`](crate::packet_sink::PacketView::stream_index)
111//! rather than assume global ordering. All streams share one time origin (see
112//! [`PacketView::applied_offset`](crate::packet_sink::PacketView::applied_offset)). A
113//! packet that violates the strict contract (including a mid-stream
114//! configuration change) fails the job typed and is **never delivered**.
115//!
116//! # Failure and panic
117//!
118//! The scheduler result returned by `wait()`/`stop()` is **authoritative**;
119//! terminal callbacks are a convenience with deliberately narrower coverage.
120//! In these cases **no terminal sink callback fires at all**:
121//!
122//! * initial configuration failure (missing/malformed extradata, whitelist
123//!   violations) — the job fails before any callback runs;
124//! * cancellation (`stop()` with packets still in flight, `abort()`);
125//! * a panicking DELIVERY callback (`on_stream_info`, `on_packet`) — the job
126//!   fails with a worker-panic error and no further sink callback is
127//!   invoked. The consumer's captures are still destroyed at the defined
128//!   teardown point, each callback box under its own containment, so a
129//!   panicking capture destructor cannot escalate the delivery-phase panic
130//!   into a process abort.
131//!
132//! Single carve-out — the post-settlement region: once the job has settled
133//! and the terminal decision is made, everything that remains on the
134//! delivery thread is user code (the terminal callback itself, then the
135//! destruction of the consumer's captures at the defined teardown point).
136//! A panic ANYWHERE in that region — `on_end`, `on_delivery_error`, or a
137//! capture's `Drop` — is caught, logged at error level, and does NOT change
138//! the already-settled job result (a delivered or decided `on_end` still
139//! yields `wait() == Ok`, and a failing job keeps its original error).
140//!
141//! That containment is **per callback box** (per handler box for
142//! [`PacketSinkHandler`](crate::packet_sink::PacketSinkHandler)), and once
143//! the stream configuration has been collected the same per-box boundary
144//! guards capture teardown along the whole delivery path — including a
145//! delivery-phase unwind. A panic
146//! thrown by a callback, or by ONE
147//! destructor — a captured value's, a stashed error source's, or a
148//! `panic_any` payload's — is contained, and the crate keeps every such
149//! unwind single: each box is destroyed under its own catch, and the
150//! stashed delivery error stays in the worker's custody while
151//! `on_delivery_error` borrows it. The boundary is Rust's own unwind
152//! semantics: when one capture's destructor panics, the remaining captures
153//! OF THAT SAME BOX are dropped by the unwind itself — an erased box
154//! destroys its captures as one indivisible drop-glue call that nothing
155//! outside the box can decompose — so a SECOND panicking destructor there
156//! is a panic-during-unwind process abort, exactly as in any Rust struct
157//! whose field destructors both panic. Keep the destructors of values
158//! captured together panic-free relative to one another.
159//!
160//! # Backpressure: callbacks block the pipeline
161//!
162//! **The callbacks run on the delivery thread. A slow `on_packet` blocks that
163//! thread, the bounded packet queue behind it fills, and the encoders stall —
164//! exactly the backpressure a slow container write exerts today.** No packet
165//! is ever silently dropped. If you need decoupling, copy the borrowed data
166//! out (it is only valid during the callback) and queue it yourself, or use
167//! [`PacketSink::channel`](crate::packet_sink::PacketSink::channel), which
168//! does that copy for you and blocks the
169//! pipeline only while its bounded channel is full. The channel's blocking
170//! send observes job cancellation, so `stop()` terminates even with a full,
171//! undrained channel.
172//!
173//! # Example
174//!
175//! ```rust,no_run
176//! use ez_ffmpeg::packet_sink::PacketSink;
177//! use ez_ffmpeg::{FfmpegContext, Output};
178//!
179//! fn main() -> Result<(), Box<dyn std::error::Error>> {
180//!     let sink = PacketSink::builder(|packet| {
181//!         println!(
182//!             "stream {} pts {} ({} bytes)",
183//!             packet.stream_index(),
184//!             packet.pts(),
185//!             packet.data().len()
186//!         );
187//!         Ok(())
188//!     })
189//!     .on_end(|| println!("done"))
190//!     .build();
191//!
192//!     FfmpegContext::builder()
193//!         .input("input.mp4")
194//!         .output(Output::from(sink).set_video_codec("libx264"))
195//!         .build()?
196//!         .start()?
197//!         .wait()?;
198//!     Ok(())
199//! }
200//! ```
201
202use crate::core::scheduler::ffmpeg_scheduler::{is_stopping, FfmpegScheduler, Running};
203use crate::core::scheduler::owned_run_iter::OwnedRunIter;
204pub use crate::error::PacketSinkError;
205use ffmpeg_sys_next::{AVCodecID, AVMediaType, AVRational};
206use std::num::NonZeroUsize;
207use std::sync::atomic::{AtomicUsize, Ordering};
208use std::sync::{Arc, OnceLock};
209use std::time::Duration;
210
211#[cfg(test)]
212mod bench_nal_scan;
213pub(crate) mod codec;
214mod job_failure;
215pub(crate) mod nal_framing;
216pub(crate) mod registry;
217pub(crate) mod side_data;
218pub(crate) mod strict;
219pub(crate) mod timeline;
220
221pub use job_failure::{JobFailureKind, JobFailureSummary};
222
223/// Delivery tier of a packet sink. Only [`Strict`](PacketSinkTier::Strict)
224/// exists in v1; the enum is `#[non_exhaustive]` so later tiers (generic
225/// passthrough, HEVC, Annex-B) are additive.
226///
227/// The strict construction paths ([`PacketSink::builder`],
228/// [`PacketSink::from_handler`], [`PacketSink::channel`]) do NOT take a tier:
229/// they are strict-tier by definition, because their callback bundle is typed
230/// to the strict [`PacketView`]/[`PacketStreamInfo`] contract (mandatory
231/// `i64` timestamps and durations). A future tier arrives as its own
232/// constructor with its own view/config/callback types — never by routing a
233/// different tier through the strict bundle.
234#[non_exhaustive]
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
236pub enum PacketSinkTier {
237    /// WebCodecs-aligned strict tier: avcC H.264 (registry-verified
238    /// wrappers: libx264, h264_nvenc, h264_videotoolbox, libopenh264;
239    /// VideoToolbox explicit non-zero `bf` / `max_b_frames` is rejected at
240    /// build — `max_b_frames` is a policy admission key, not an FFmpeg
241    /// alias of `bf`, and is never applied to the encoder) + AAC.
242    #[default]
243    Strict,
244}
245
246/// Why a callback rejected delivery. Carries a message and an optional
247/// source error, both preserved on the job result via
248/// [`PacketSinkError::PacketCallbackFailed`].
249#[derive(Debug, Clone)]
250pub struct PacketCallbackError {
251    message: String,
252    source: Option<Arc<dyn std::error::Error + Send + Sync + 'static>>,
253    pub(crate) kind: CallbackFailureKind,
254}
255
256/// Internal classification of a callback failure.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub(crate) enum CallbackFailureKind {
259    /// A consumer-reported failure: the job stops with a typed error.
260    Failure,
261    /// The owned-channel receiver is gone: the job stops with
262    /// [`PacketSinkError::ChannelDisconnected`].
263    Disconnected,
264    /// The job is already stopping WITHOUT a recorded error (explicit
265    /// `stop()`/`abort()`) and a blocking send bailed out cooperatively:
266    /// NOT an error (mirrors the worker's stop observation).
267    Cancelled,
268    /// The job is stopping because some worker recorded a FAILURE while a
269    /// blocking send was parked: delivery is truncated by that job failure
270    /// (the terminal reports it as `JobFailed`), not cancelled.
271    JobStopped,
272}
273
274impl PacketCallbackError {
275    /// A failure described by a message.
276    pub fn new(message: impl Into<String>) -> Self {
277        Self {
278            message: message.into(),
279            source: None,
280            kind: CallbackFailureKind::Failure,
281        }
282    }
283
284    /// A failure wrapping a source error (preserved on the job result).
285    pub fn with_source(
286        message: impl Into<String>,
287        source: impl std::error::Error + Send + Sync + 'static,
288    ) -> Self {
289        Self {
290            message: message.into(),
291            source: Some(Arc::new(source)),
292            kind: CallbackFailureKind::Failure,
293        }
294    }
295
296    pub(crate) fn disconnected() -> Self {
297        Self {
298            message: "packet-sink channel receiver dropped".to_string(),
299            source: None,
300            kind: CallbackFailureKind::Disconnected,
301        }
302    }
303
304    pub(crate) fn job_stopped() -> Self {
305        Self {
306            message: "job failed elsewhere; blocking send abandoned".to_string(),
307            source: None,
308            kind: CallbackFailureKind::JobStopped,
309        }
310    }
311
312    pub(crate) fn cancelled() -> Self {
313        Self {
314            message: "job stopping; blocking send cancelled".to_string(),
315            source: None,
316            kind: CallbackFailureKind::Cancelled,
317        }
318    }
319}
320
321impl std::fmt::Display for PacketCallbackError {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        f.write_str(&self.message)
324    }
325}
326
327impl std::error::Error for PacketCallbackError {
328    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
329        self.source
330            .as_ref()
331            .map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
332    }
333}
334
335/// What every fallible sink callback returns: `Ok(())` continues delivery, an
336/// error stops the job with a typed, source-preserving [`PacketSinkError`].
337pub type PacketCallbackResult = Result<(), PacketCallbackError>;
338
339/// A single stateful packet consumer. All methods run serially on the one
340/// delivery thread (never concurrently, never reentrantly), so `&mut self`
341/// state needs no locking. This is the strict-tier handler shape; see the
342/// [module docs](self) for the callback order and backpressure contract.
343///
344/// Teardown panic containment is per handler box: a panic from a method,
345/// or from ONE of the handler's fields' destructors, is contained; two
346/// fields whose destructors both panic compose into a panic-during-unwind
347/// process abort, as in any Rust struct — see "Failure and panic" in the
348/// [module docs](self).
349pub trait PacketSinkHandler: Send + 'static {
350    /// One-time stream configuration, before any packet.
351    fn on_stream_info(&mut self, _streams: &[PacketStreamInfo]) -> PacketCallbackResult {
352        Ok(())
353    }
354
355    /// One delivered packet; the borrowed view is valid only for this call.
356    fn on_packet(&mut self, packet: &PacketView<'_>) -> PacketCallbackResult;
357
358    /// Terminal success (see the module docs for the exact gate). A panic
359    /// here is contained and cannot change the settled job result.
360    fn on_end(&mut self) {}
361
362    /// Optional structured observer for a job that failed OUTSIDE this
363    /// sink's delivery path.
364    ///
365    /// Fires ONLY on the synthesized-JobFailed path: the job failed
366    /// elsewhere (a sibling output, an upstream demuxer, decoder, filter or
367    /// encoder) while this sink's own delivery was clean, whether that
368    /// failure landed after this sink drained or truncated its delivery. It
369    /// does NOT fire for this sink's own delivery-path errors (strict-tier
370    /// violations, failing callbacks), nor for cancellation, aborts, or
371    /// initial configuration failures.
372    ///
373    /// When it fires, it fires exactly once, immediately BEFORE the
374    /// matching `on_delivery_error(&PacketSinkError::JobFailed { .. })` —
375    /// same delivery thread, same terminal slot — and the summary's
376    /// [`message`](JobFailureSummary::message) is byte-identical to that
377    /// `JobFailed` message. `wait()`/`stop()` keep returning the original
378    /// job error. A panic here is contained per handler box and can neither
379    /// skip the terminal dispatch that follows nor change the settled job
380    /// result. The default implementation does nothing, so existing
381    /// handlers keep their exact behavior.
382    fn on_job_failed(&mut self, _summary: &JobFailureSummary) {}
383
384    /// Terminal failure. For delivery-path errors (strict-tier violations,
385    /// failing callbacks) the same error is also reported as the job result
386    /// unless an earlier failure already settled it (job settlement is
387    /// first-error-wins). When the
388    /// JOB failed elsewhere (after this sink drained or truncating its
389    /// delivery), the callback receives a synthesized
390    /// [`PacketSinkError::JobFailed`] summarizing that failure, while
391    /// `wait()`/`stop()` keep the original error (an overridden
392    /// [`on_job_failed`](Self::on_job_failed) receives the structured
393    /// summary immediately before this dispatch).
394    fn on_delivery_error(&mut self, _error: &PacketSinkError) {}
395}
396
397/// Per-stream video configuration delivered via `on_stream_info` —
398/// everything a WebCodecs `VideoDecoder` / fMP4 packager needs, precomputed.
399#[non_exhaustive]
400#[derive(Debug, Clone)]
401pub struct VideoPacketConfig {
402    pub(crate) stream_index: usize,
403    pub(crate) codec_id: AVCodecID,
404    pub(crate) codec_string: String,
405    pub(crate) profile: u8,
406    pub(crate) compatibility: u8,
407    pub(crate) level: u8,
408    pub(crate) codec_config: Vec<u8>,
409    pub(crate) time_base: AVRational,
410    pub(crate) width: i32,
411    pub(crate) height: i32,
412    pub(crate) sample_aspect_ratio: Option<AVRational>,
413    pub(crate) frame_rate: Option<AVRational>,
414}
415
416impl VideoPacketConfig {
417    /// Output stream index; matches [`PacketView::stream_index`].
418    pub fn stream_index(&self) -> usize {
419        self.stream_index
420    }
421
422    /// FFmpeg codec id (`AV_CODEC_ID_H264` in the strict tier).
423    pub fn codec_id(&self) -> AVCodecID {
424        self.codec_id
425    }
426
427    /// RFC 6381 codec string (`"avc1.PPCCLL"`), suitable as the WebCodecs
428    /// `codec` value.
429    pub fn codec_string(&self) -> &str {
430        &self.codec_string
431    }
432
433    /// H.264 `profile_idc` (the avcC `AVCProfileIndication`; e.g. 66 =
434    /// Baseline, 77 = Main, 100 = High). Same source as
435    /// [`codec_string`](Self::codec_string).
436    pub fn profile(&self) -> u8 {
437        self.profile
438    }
439
440    /// The avcC `profile_compatibility` byte (constraint-set flags).
441    pub fn compatibility(&self) -> u8 {
442        self.compatibility
443    }
444
445    /// H.264 `level_idc` (the avcC `AVCLevelIndication`; e.g. 30 = level
446    /// 3.0, 0x1F = level 3.1).
447    pub fn level(&self) -> u8 {
448        self.level
449    }
450
451    /// The `AVCDecoderConfigurationRecord` (avcC), suitable as the WebCodecs
452    /// `description`.
453    pub fn codec_config(&self) -> &[u8] {
454        &self.codec_config
455    }
456
457    /// FFmpeg-oriented alias of [`codec_config`](Self::codec_config).
458    pub fn extradata(&self) -> &[u8] {
459        &self.codec_config
460    }
461
462    /// Time base every timestamp of this stream is expressed in (the encoder
463    /// time base, passed through verbatim).
464    pub fn time_base(&self) -> AVRational {
465        self.time_base
466    }
467
468    /// Coded width in pixels.
469    pub fn width(&self) -> i32 {
470        self.width
471    }
472
473    /// Coded height in pixels.
474    pub fn height(&self) -> i32 {
475        self.height
476    }
477
478    /// Sample aspect ratio, when known.
479    pub fn sample_aspect_ratio(&self) -> Option<AVRational> {
480        self.sample_aspect_ratio
481    }
482
483    /// Nominal frame rate. `None` when the pipeline did not pin one (VFR
484    /// sources, and CFR jobs without an explicit output rate).
485    pub fn frame_rate(&self) -> Option<AVRational> {
486        self.frame_rate
487    }
488}
489
490/// Per-stream audio configuration delivered via `on_stream_info`.
491#[non_exhaustive]
492#[derive(Debug, Clone)]
493pub struct AudioPacketConfig {
494    pub(crate) stream_index: usize,
495    pub(crate) codec_id: AVCodecID,
496    pub(crate) codec_string: String,
497    pub(crate) codec_config: Vec<u8>,
498    pub(crate) time_base: AVRational,
499    pub(crate) sample_rate: i32,
500    pub(crate) channels: i32,
501    pub(crate) channel_layout: String,
502}
503
504impl AudioPacketConfig {
505    /// Output stream index; matches [`PacketView::stream_index`].
506    pub fn stream_index(&self) -> usize {
507        self.stream_index
508    }
509
510    /// FFmpeg codec id (`AV_CODEC_ID_AAC` in the strict tier).
511    pub fn codec_id(&self) -> AVCodecID {
512        self.codec_id
513    }
514
515    /// RFC 6381 codec string (`"mp4a.40.X"`, X = audio object type).
516    pub fn codec_string(&self) -> &str {
517        &self.codec_string
518    }
519
520    /// The `AudioSpecificConfig`, suitable as the WebCodecs `description`.
521    pub fn codec_config(&self) -> &[u8] {
522        &self.codec_config
523    }
524
525    /// FFmpeg-oriented alias of [`codec_config`](Self::codec_config).
526    pub fn extradata(&self) -> &[u8] {
527        &self.codec_config
528    }
529
530    /// Time base every timestamp of this stream is expressed in.
531    pub fn time_base(&self) -> AVRational {
532        self.time_base
533    }
534
535    /// Sample rate in Hz.
536    pub fn sample_rate(&self) -> i32 {
537        self.sample_rate
538    }
539
540    /// Channel count.
541    pub fn channels(&self) -> i32 {
542        self.channels
543    }
544
545    /// FFmpeg channel-layout description (e.g. `"stereo"`, `"5.1"`).
546    pub fn channel_layout(&self) -> &str {
547        &self.channel_layout
548    }
549}
550
551/// Per-stream configuration delivered once via `on_stream_info`, typed by
552/// media kind (mirrors the crate's `StreamInfo` shape).
553#[non_exhaustive]
554#[derive(Debug, Clone)]
555pub enum PacketStreamInfo {
556    /// H.264 stream configuration: avcC record, RFC 6381 codec string,
557    /// profile/level, dimensions, time base, frame rate.
558    Video(VideoPacketConfig),
559    /// AAC stream configuration: AudioSpecificConfig, RFC 6381 codec string,
560    /// time base, sample rate, channel layout.
561    Audio(AudioPacketConfig),
562}
563
564impl PacketStreamInfo {
565    /// Output stream index; matches [`PacketView::stream_index`].
566    pub fn stream_index(&self) -> usize {
567        match self {
568            PacketStreamInfo::Video(v) => v.stream_index,
569            PacketStreamInfo::Audio(a) => a.stream_index,
570        }
571    }
572
573    /// Media type of the stream.
574    pub fn media_type(&self) -> AVMediaType {
575        match self {
576            PacketStreamInfo::Video(_) => AVMediaType::AVMEDIA_TYPE_VIDEO,
577            PacketStreamInfo::Audio(_) => AVMediaType::AVMEDIA_TYPE_AUDIO,
578        }
579    }
580
581    /// FFmpeg codec id.
582    pub fn codec_id(&self) -> AVCodecID {
583        match self {
584            PacketStreamInfo::Video(v) => v.codec_id,
585            PacketStreamInfo::Audio(a) => a.codec_id,
586        }
587    }
588
589    /// RFC 6381 codec string (`"avc1.PPCCLL"` / `"mp4a.40.X"`).
590    pub fn codec_string(&self) -> &str {
591        match self {
592            PacketStreamInfo::Video(v) => &v.codec_string,
593            PacketStreamInfo::Audio(a) => &a.codec_string,
594        }
595    }
596
597    /// Codec configuration record (avcC / AudioSpecificConfig).
598    pub fn codec_config(&self) -> &[u8] {
599        match self {
600            PacketStreamInfo::Video(v) => &v.codec_config,
601            PacketStreamInfo::Audio(a) => &a.codec_config,
602        }
603    }
604
605    /// FFmpeg-oriented alias of [`codec_config`](Self::codec_config).
606    pub fn extradata(&self) -> &[u8] {
607        self.codec_config()
608    }
609
610    /// Time base every timestamp of this stream is expressed in.
611    pub fn time_base(&self) -> AVRational {
612        match self {
613            PacketStreamInfo::Video(v) => v.time_base,
614            PacketStreamInfo::Audio(a) => a.time_base,
615        }
616    }
617
618    /// The video configuration, when this is a video stream.
619    pub fn video(&self) -> Option<&VideoPacketConfig> {
620        match self {
621            PacketStreamInfo::Video(v) => Some(v),
622            _ => None,
623        }
624    }
625
626    /// The audio configuration, when this is an audio stream.
627    pub fn audio(&self) -> Option<&AudioPacketConfig> {
628        match self {
629            PacketStreamInfo::Audio(a) => Some(a),
630            _ => None,
631        }
632    }
633}
634
635/// Converts stream ticks to microseconds (exact rescale, round-nearest).
636fn ticks_to_us(ticks: i64, time_base: AVRational) -> i64 {
637    // SAFETY: pure integer arithmetic; every stream time base was validated
638    // positive at collection, and the target rational is a constant.
639    unsafe {
640        ffmpeg_sys_next::av_rescale_q(
641            ticks,
642            time_base,
643            AVRational {
644                num: 1,
645                den: 1_000_000,
646            },
647        )
648    }
649}
650
651/// Borrowed view of one delivered packet (strict tier).
652///
653/// The view — including [`data`](Self::data) — is valid **only during the
654/// `on_packet` callback**; the underlying packet is recycled as soon as the
655/// callback returns. Copy out what you keep.
656#[non_exhaustive]
657#[derive(Debug)]
658pub struct PacketView<'a> {
659    pub(crate) stream_index: usize,
660    pub(crate) pts: i64,
661    pub(crate) dts: i64,
662    pub(crate) duration: i64,
663    pub(crate) time_base: AVRational,
664    pub(crate) is_key: bool,
665    pub(crate) applied_offset: i64,
666    pub(crate) data: &'a [u8],
667}
668
669impl<'a> PacketView<'a> {
670    /// Output stream index (matches the `on_stream_info` entries).
671    pub fn stream_index(&self) -> usize {
672        self.stream_index
673    }
674
675    /// Presentation timestamp in [`time_base`](Self::time_base) units, on the
676    /// shared zero-based timeline (see
677    /// [`applied_offset`](Self::applied_offset)).
678    pub fn pts(&self) -> i64 {
679        self.pts
680    }
681
682    /// Decode timestamp in [`time_base`](Self::time_base) units, strictly
683    /// increasing per stream. The strict tier also requires `pts >= dts` on
684    /// every delivered packet: B-frame reordering that would invert that
685    /// fails the job before delivery. May be negative on non-anchor streams
686    /// (a stream whose timeline starts earlier than the anchor stream keeps
687    /// its true relative offset).
688    pub fn dts(&self) -> i64 {
689        self.dts
690    }
691
692    /// Packet duration in [`time_base`](Self::time_base) units. Always
693    /// positive in the strict tier: the encoder's duration is passed through;
694    /// when absent it is derived (video: one CFR frame interval; audio: the
695    /// codec frame size). A packet whose duration cannot be derived fails the
696    /// job before delivery — this field is never a guess of zero.
697    pub fn duration(&self) -> i64 {
698        self.duration
699    }
700
701    /// Time base of this stream (identical to the stream's
702    /// [`PacketStreamInfo::time_base`]).
703    pub fn time_base(&self) -> AVRational {
704        self.time_base
705    }
706
707    /// [`pts`](Self::pts) in microseconds (exact rescale of the ticks).
708    pub fn pts_us(&self) -> i64 {
709        ticks_to_us(self.pts, self.time_base)
710    }
711
712    /// [`dts`](Self::dts) in microseconds.
713    pub fn dts_us(&self) -> i64 {
714        ticks_to_us(self.dts, self.time_base)
715    }
716
717    /// [`duration`](Self::duration) in microseconds.
718    pub fn duration_us(&self) -> i64 {
719        ticks_to_us(self.duration, self.time_base)
720    }
721
722    /// [`applied_offset`](Self::applied_offset) in microseconds.
723    pub fn applied_offset_us(&self) -> i64 {
724        ticks_to_us(self.applied_offset, self.time_base)
725    }
726
727    /// Whether this packet is a fresh-decoder-safe random access point.
728    ///
729    /// For H.264 this is true **iff the access unit contains an IDR NAL
730    /// unit** — deliberately not the encoder's raw `AV_PKT_FLAG_KEY`: with
731    /// open-GOP encoding, encoders flag non-IDR recovery points as key
732    /// frames, and feeding such a packet to a fresh decoder (the WebCodecs
733    /// `"key"` contract) is not safe. Audio packets are always key.
734    pub fn is_key(&self) -> bool {
735        self.is_key
736    }
737
738    /// The per-stream offset that was subtracted from `pts`/`dts` to move
739    /// this stream onto the shared zero-based timeline, in this stream's
740    /// [`time_base`](Self::time_base) units.
741    ///
742    /// All streams share a single origin: the `(dts, time_base)` of the first
743    /// delivered packet of the job. The anchor stream therefore starts at
744    /// dts 0; other streams keep their true audio/video offset (which may be
745    /// negative). `original_ts = delivered_ts + applied_offset` recovers the
746    /// encoder timeline exactly (cross-time-base rounding is at most one tick
747    /// per stream).
748    pub fn applied_offset(&self) -> i64 {
749        self.applied_offset
750    }
751
752    /// The packet payload. H.264: 4-byte length-prefixed (AVCC), parameter
753    /// sets carried out-of-band in the stream configuration. The Trusted
754    /// path does **not** inspect each payload to prove one complete access
755    /// unit; that property is assumed from the admitted encoder wrappers.
756    /// AAC: one raw AAC frame.
757    pub fn data(&self) -> &'a [u8] {
758        self.data
759    }
760}
761
762pub(crate) type StreamInfoFn = Box<dyn FnMut(&[PacketStreamInfo]) -> PacketCallbackResult + Send>;
763pub(crate) type PacketFn = Box<dyn for<'a> FnMut(&PacketView<'a>) -> PacketCallbackResult + Send>;
764pub(crate) type EndFn = Box<dyn FnMut() + Send>;
765pub(crate) type JobFailedFn = Box<dyn FnMut(&JobFailureSummary) + Send>;
766pub(crate) type DeliveryErrorFn = Box<dyn FnMut(&PacketSinkError) + Send>;
767
768/// How the sink dispatches callbacks: independent closures, or one stateful
769/// handler. Either way every call runs serially on the delivery thread.
770enum SinkDispatch {
771    Closures {
772        on_stream_info: Option<StreamInfoFn>,
773        on_packet: PacketFn,
774        on_end: Option<EndFn>,
775        on_job_failed: Option<JobFailedFn>,
776        on_delivery_error: Option<DeliveryErrorFn>,
777    },
778    Handler(Box<dyn PacketSinkHandler>),
779}
780
781/// What the owned-channel adapter observes about the job while a bounded
782/// send is blocked: the scheduler status (has the job stopped?) and the
783/// scheduler result (did it stop because some worker FAILED?). Published by
784/// the worker at collection time.
785pub(crate) struct JobStopObservables {
786    pub(crate) status: Arc<AtomicUsize>,
787    pub(crate) result: Arc<std::sync::Mutex<Option<crate::error::Result<()>>>>,
788}
789
790/// Slot the owned-channel adapter uses to observe job cancellation: see
791/// [`JobStopObservables`]. One slot is allocated per [`PacketSink::channel`]
792/// call and shared by the sink's callbacks and its receiver, so the Arc's
793/// pointer identity doubles as the pair's run token: the muxer keeps a clone
794/// and [`PacketSinkReceiver::into_events`] matches its own clone against the
795/// scheduler's job to reject a cross-wired scheduler.
796pub(crate) type CancellationSlot = Arc<OnceLock<JobStopObservables>>;
797
798/// The consumer bundle handed to `Output::from(sink)` /
799/// [`Output::new_by_packet_sink`](crate::Output::new_by_packet_sink).
800///
801/// Build one with [`PacketSink::builder`] (closures),
802/// [`PacketSink::from_handler`] (one stateful consumer) or
803/// [`PacketSink::channel`] (owned events over a bounded channel). All
804/// construction paths produce a **strict-tier** sink; see the
805/// [module docs](self) for the callback order and the **blocking
806/// backpressure** contract.
807///
808/// **Experimental:** new in 0.15; the surface may still be refined.
809pub struct PacketSink {
810    pub(crate) tier: PacketSinkTier,
811    dispatch: SinkDispatch,
812    /// `Some` only for channel-adapter sinks (see [`CancellationSlot`]).
813    pub(crate) cancellation: Option<CancellationSlot>,
814}
815
816impl std::fmt::Debug for PacketSink {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        f.debug_struct("PacketSink")
819            .field("tier", &self.tier)
820            .finish_non_exhaustive()
821    }
822}
823
824impl PacketSink {
825    /// The delivery tier this sink was built for.
826    ///
827    /// Every v1 construction path produces [`PacketSinkTier::Strict`], so
828    /// today this always returns `Strict`; the accessor exists so consumers
829    /// that route or log sinks can branch on the tier once additional tiers
830    /// land, instead of inferring it from which constructor was used.
831    pub fn tier(&self) -> PacketSinkTier {
832        self.tier
833    }
834
835    /// Starts building a strict-tier sink around the required packet
836    /// consumer. `on_stream_info`, `on_end` and `on_delivery_error` are
837    /// optional extras on the returned builder — but a sink cannot exist
838    /// without a packet consumer (a job that encodes into nothing is a
839    /// configuration mistake, not a default; use [`PacketSink::discard`]
840    /// when discarding is genuinely intended).
841    ///
842    /// Teardown panic containment is per callback box: a panic from the
843    /// closure, or from ONE captured value's destructor, is contained; two
844    /// captures of this same closure whose destructors both panic compose
845    /// into a panic-during-unwind process abort, as in any Rust struct —
846    /// see "Failure and panic" in the [module docs](self).
847    pub fn builder<F>(on_packet: F) -> PacketSinkBuilder
848    where
849        F: for<'a> FnMut(&PacketView<'a>) -> PacketCallbackResult + Send + 'static,
850    {
851        PacketSinkBuilder {
852            tier: PacketSinkTier::Strict,
853            on_stream_info: None,
854            on_packet: Box::new(on_packet),
855            on_end: None,
856            on_job_failed: None,
857            on_delivery_error: None,
858        }
859    }
860
861    /// A sink that deliberately discards every packet (accepting them all).
862    /// Exists so intent is explicit — mainly for validation-only jobs and
863    /// tests.
864    pub fn discard() -> PacketSink {
865        PacketSink::builder(|_| Ok(())).build()
866    }
867
868    /// Builds a strict-tier sink around one stateful [`PacketSinkHandler`] —
869    /// the natural shape for consumers whose stream-info/packet/terminal
870    /// handling shares state (packagers, senders); callbacks are serial, so
871    /// the handler needs no locking.
872    ///
873    /// Teardown panic containment is per handler box: a panic from a
874    /// handler method, or from ONE of the handler's fields' destructors, is
875    /// contained; two fields of this same handler whose destructors both
876    /// panic compose into a panic-during-unwind process abort, as in any
877    /// Rust struct — see "Failure and panic" in the [module docs](self).
878    pub fn from_handler<H: PacketSinkHandler>(handler: H) -> PacketSink {
879        PacketSink {
880            tier: PacketSinkTier::Strict,
881            dispatch: SinkDispatch::Handler(Box::new(handler)),
882            cancellation: None,
883        }
884    }
885
886    /// Builds a strict-tier sink that forwards everything over a **bounded**
887    /// channel of owned events, for consumers that want packets on their own
888    /// thread.
889    ///
890    /// Every payload is copied once into an owned [`EncodedPacket`] (one
891    /// additional adapter copy on top of any Annex-B normalization). The
892    /// channel preserves the callback contract: when it is full, the sending
893    /// callback **blocks the pipeline** until the consumer catches up — no
894    /// packet is dropped. **Drain the receiver concurrently** (its own
895    /// thread, or [`PacketSinkReceiver::into_events`]); draining only after
896    /// `wait()` deadlocks as soon as the channel fills, because `wait()`
897    /// needs the blocked worker to finish. The blocking send observes job
898    /// cancellation, so `stop()`/`abort()` (or a job failing elsewhere)
899    /// terminates even with a full, undrained channel. Dropping the receiver
900    /// cancels the job with [`PacketSinkError::ChannelDisconnected`].
901    ///
902    /// Terminal `End`/`Error` events — and the `JobFailure` summary queued
903    /// immediately before a job-failure `Error` — are delivered best-effort
904    /// ON THE RAW CHANNEL: the send behind them must not block teardown, so
905    /// a consumer that is full at that instant — stalled forever or merely a
906    /// few events behind — loses them, and sender disconnection
907    /// (`Disconnected` on the receiver) is the authoritative end-of-events
908    /// signal. [`PacketSinkReceiver::into_events`] restores the
909    /// deterministic ending on top: a stream without a terminal `Err` always
910    /// ends with `End`.
911    pub fn channel(capacity: NonZeroUsize) -> (PacketSink, PacketSinkReceiver) {
912        let (tx, rx) = crossbeam_channel::bounded::<PacketSinkEvent>(capacity.get());
913        let cancellation: CancellationSlot = Arc::new(OnceLock::new());
914        let info_tx = tx.clone();
915        let info_cancel = cancellation.clone();
916        let pkt_tx = tx.clone();
917        let pkt_cancel = cancellation.clone();
918        let end_tx = tx.clone();
919        let job_failed_tx = tx.clone();
920        let err_tx = tx;
921        let mut sink = PacketSink::builder(move |packet: &PacketView<'_>| {
922            send_with_cancellation(
923                &pkt_tx,
924                &pkt_cancel,
925                PacketSinkEvent::Packet(EncodedPacket::from_view(packet)),
926            )
927        })
928        .on_stream_info(move |infos: &[PacketStreamInfo]| {
929            send_with_cancellation(
930                &info_tx,
931                &info_cancel,
932                PacketSinkEvent::StreamInfo(infos.to_vec()),
933            )
934        })
935        .on_end(move || {
936            // Best-effort terminal event: the job is already in its terminal
937            // state here (a cancellation-aware blocking send would be
938            // indistinguishable from try_send), and sender disconnection is
939            // the authoritative signal.
940            let _ = end_tx.try_send(PacketSinkEvent::End);
941        })
942        .on_job_failed(move |summary: &JobFailureSummary| {
943            // Best-effort like the terminal events it precedes, with one
944            // extra guard: the summary must never consume the LAST free
945            // slot — the Error(JobFailed) event behind it has first claim
946            // on that capacity. A consumer that drained the channel before
947            // the terminal (the documented way to catch the best-effort
948            // terminal on a small adapter) was guaranteed the Error event
949            // before the summary existed and must stay guaranteed it. Only
950            // this thread sends, and the consumer can only FREE slots, so
951            // a two-free-slots check here cannot be raced into starving
952            // the Error send that follows.
953            let free = job_failed_tx
954                .capacity()
955                .unwrap_or(usize::MAX)
956                .saturating_sub(job_failed_tx.len());
957            if free >= 2 {
958                let _ = job_failed_tx.try_send(PacketSinkEvent::JobFailure(summary.clone()));
959            }
960        })
961        .on_delivery_error(move |e: &PacketSinkError| {
962            let _ = err_tx.try_send(PacketSinkEvent::Error(e.clone()));
963        })
964        .build();
965        sink.cancellation = Some(cancellation.clone());
966        (
967            sink,
968            PacketSinkReceiver {
969                inner: rx,
970                token: cancellation,
971            },
972        )
973    }
974
975    // ---- crate-internal dispatch (serial, delivery thread only) ----
976
977    pub(crate) fn dispatch_stream_info(
978        &mut self,
979        infos: &[PacketStreamInfo],
980    ) -> PacketCallbackResult {
981        match &mut self.dispatch {
982            SinkDispatch::Closures { on_stream_info, .. } => match on_stream_info {
983                Some(f) => f(infos),
984                None => Ok(()),
985            },
986            SinkDispatch::Handler(h) => h.on_stream_info(infos),
987        }
988    }
989
990    pub(crate) fn dispatch_packet(&mut self, packet: &PacketView<'_>) -> PacketCallbackResult {
991        match &mut self.dispatch {
992            SinkDispatch::Closures { on_packet, .. } => on_packet(packet),
993            SinkDispatch::Handler(h) => h.on_packet(packet),
994        }
995    }
996
997    pub(crate) fn dispatch_end(&mut self) {
998        match &mut self.dispatch {
999            SinkDispatch::Closures { on_end, .. } => {
1000                if let Some(f) = on_end {
1001                    f()
1002                }
1003            }
1004            SinkDispatch::Handler(h) => h.on_end(),
1005        }
1006    }
1007
1008    /// Dispatches the structured job-failure summary to the registered
1009    /// observer: the builder's `on_job_failed` callback, or the handler's
1010    /// [`PacketSinkHandler::on_job_failed`] (default-empty). The caller
1011    /// (the worker's terminal slot) wraps this call in its own panic
1012    /// containment so a panicking observer can never skip the
1013    /// `on_delivery_error` dispatch that follows it.
1014    pub(crate) fn dispatch_job_failed(&mut self, summary: &JobFailureSummary) {
1015        match &mut self.dispatch {
1016            SinkDispatch::Closures { on_job_failed, .. } => {
1017                if let Some(f) = on_job_failed {
1018                    f(summary)
1019                }
1020            }
1021            SinkDispatch::Handler(h) => h.on_job_failed(summary),
1022        }
1023    }
1024
1025    pub(crate) fn dispatch_delivery_error(&mut self, error: &PacketSinkError) {
1026        match &mut self.dispatch {
1027            SinkDispatch::Closures {
1028                on_delivery_error, ..
1029            } => {
1030                if let Some(f) = on_delivery_error {
1031                    f(error)
1032                }
1033            }
1034            SinkDispatch::Handler(h) => h.on_delivery_error(error),
1035        }
1036    }
1037
1038    /// Consumes the sink, dropping every user callback box under its OWN
1039    /// panic containment. The derived drop glue runs the boxes as one
1040    /// chain: after a first capture destructor panics, the REMAINING boxes
1041    /// are destroyed by that unwind itself, where a second panicking
1042    /// destructor aborts the process — so one `catch_unwind` around a plain
1043    /// `drop` of the whole aggregate contains only the first bomb. Dropping
1044    /// each box under its own catch keeps every unwind single. Returns true
1045    /// when any destructor panicked (each caught payload is disposed
1046    /// through [`dispose_panic_payload`], never re-dropped raw).
1047    pub(crate) fn dispose_contained(self) -> bool {
1048        let Self {
1049            tier: _,
1050            dispatch,
1051            cancellation,
1052        } = self;
1053        let mut panicked = false;
1054        match dispatch {
1055            SinkDispatch::Closures {
1056                on_stream_info,
1057                on_packet,
1058                on_end,
1059                on_job_failed,
1060                on_delivery_error,
1061            } => {
1062                // Field declaration order — the order the derived drop glue
1063                // would have used.
1064                if let Some(f) = on_stream_info {
1065                    panicked |= drop_contained(f);
1066                }
1067                panicked |= drop_contained(on_packet);
1068                if let Some(f) = on_end {
1069                    panicked |= drop_contained(f);
1070                }
1071                if let Some(f) = on_job_failed {
1072                    panicked |= drop_contained(f);
1073                }
1074                if let Some(f) = on_delivery_error {
1075                    panicked |= drop_contained(f);
1076                }
1077            }
1078            SinkDispatch::Handler(handler) => {
1079                panicked |= drop_contained(handler);
1080            }
1081        }
1082        // The cancellation slot is crate data, but the shared job result
1083        // behind it can hold arbitrary error types; the (normally non-final)
1084        // Arc release is contained for the same price as the boxes.
1085        if let Some(slot) = cancellation {
1086            panicked |= drop_contained(slot);
1087        }
1088        panicked
1089    }
1090}
1091
1092/// Drops `value` under its own panic containment. Returns true when the
1093/// destructor panicked; the caught payload is disposed, not re-dropped raw.
1094fn drop_contained<T>(value: T) -> bool {
1095    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(value))) {
1096        Ok(()) => false,
1097        Err(payload) => {
1098            dispose_panic_payload(payload);
1099            true
1100        }
1101    }
1102}
1103
1104/// Disposes a caught panic payload without letting the payload's own
1105/// destructor start a second, uncontained unwind at the discard site.
1106///
1107/// `panic_any` lets panicking user code throw an ARBITRARY payload, and
1108/// nothing forbids one whose `Drop` panics again — with yet another such
1109/// payload. Discarding a `catch_unwind` error via `.is_err()` / `let _` /
1110/// a wildcard therefore runs an uncontained user destructor exactly where
1111/// the containment believed the panic was over. Each drop attempt here runs
1112/// under its own catch, following replacement payloads a bounded number of
1113/// times; a chain still panicking after the last attempt is deliberately
1114/// LEAKED via `mem::forget`. That trade is intentional: a bounded leak on
1115/// an adversarial path is recoverable, while re-throwing would unwind
1116/// frames that may still own user state — and a destructor panic during
1117/// that unwind escalates to a process abort.
1118///
1119/// The containment boundary, here and in every catch this module owns, is
1120/// per BOX: a panic thrown by a callback, or by ONE destructor (a
1121/// capture's, a stashed error source's, or this payload's), is contained.
1122/// A box whose own captured fields' destructors panic DURING that unwind —
1123/// two bombs inside one erased `Box<dyn ..>` — aborts the process by
1124/// Rust's panic-during-unwind rule before any catch regains control: the
1125/// box destroys its captures as one indivisible drop-glue call that no
1126/// code outside the box can decompose. That is identical to any Rust code
1127/// path (a plain struct with two panicking field destructors aborts the
1128/// same way), so it is documented as the boundary, not worked around.
1129pub(crate) fn dispose_panic_payload(payload: Box<dyn std::any::Any + Send>) {
1130    let mut payload = payload;
1131    for _ in 0..4 {
1132        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(payload))) {
1133            Ok(()) => return,
1134            Err(next) => payload = next,
1135        }
1136    }
1137    std::mem::forget(payload);
1138}
1139
1140/// Cancellation-aware bounded send: blocks (in bounded slices) while the
1141/// channel is full, but bails out cooperatively once the job is stopping — so
1142/// `stop()`/`abort()` (or a failure elsewhere) terminates even with a full,
1143/// undrained channel — and reports a dropped receiver as a typed
1144/// disconnection.
1145fn send_with_cancellation(
1146    tx: &crossbeam_channel::Sender<PacketSinkEvent>,
1147    cancellation: &CancellationSlot,
1148    event: PacketSinkEvent,
1149) -> PacketCallbackResult {
1150    // Fast path: `send_timeout` computes a wall-clock deadline up front on
1151    // every call — pure overhead while the channel has capacity (the common
1152    // case). Only a full channel proceeds to the deadline-based slices.
1153    let mut event = match tx.try_send(event) {
1154        Ok(()) => return Ok(()),
1155        Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
1156            return Err(PacketCallbackError::disconnected());
1157        }
1158        Err(crossbeam_channel::TrySendError::Full(back)) => back,
1159    };
1160    loop {
1161        match tx.send_timeout(event, Duration::from_millis(50)) {
1162            Ok(()) => return Ok(()),
1163            Err(crossbeam_channel::SendTimeoutError::Timeout(back)) => {
1164                event = back;
1165                if let Some(observables) = cancellation.get() {
1166                    if is_stopping(observables.status.load(Ordering::Acquire)) {
1167                        // Classify WHY the job is stopping. A natural
1168                        // (all-muxers-done) STATUS_END cannot exist while
1169                        // this sink is still delivering — the sink is itself
1170                        // one of those muxers — so a stopping status here is
1171                        // either explicit stop()/abort() (no error recorded:
1172                        // stay silent as cancellation) or a failure-driven
1173                        // shutdown. Failures record their error BEFORE
1174                        // publishing the terminal status, so the recorded
1175                        // result is already visible on this path and the
1176                        // terminal can report the truncation as JobFailed.
1177                        let failed = observables
1178                            .result
1179                            .lock()
1180                            .unwrap_or_else(std::sync::PoisonError::into_inner)
1181                            .as_ref()
1182                            .is_some_and(|result| result.is_err());
1183                        return Err(if failed {
1184                            PacketCallbackError::job_stopped()
1185                        } else {
1186                            PacketCallbackError::cancelled()
1187                        });
1188                    }
1189                }
1190            }
1191            Err(crossbeam_channel::SendTimeoutError::Disconnected(_)) => {
1192                return Err(PacketCallbackError::disconnected());
1193            }
1194        }
1195    }
1196}
1197
1198/// Builder for a strict-tier [`PacketSink`]; created by
1199/// [`PacketSink::builder`] with the required packet consumer.
1200pub struct PacketSinkBuilder {
1201    tier: PacketSinkTier,
1202    on_stream_info: Option<StreamInfoFn>,
1203    on_packet: PacketFn,
1204    on_end: Option<EndFn>,
1205    on_job_failed: Option<JobFailedFn>,
1206    on_delivery_error: Option<DeliveryErrorFn>,
1207}
1208
1209impl PacketSinkBuilder {
1210    /// One-time stream configuration callback, invoked before any packet.
1211    /// Return `Ok(())` to accept; an error fails the job before any packet is
1212    /// delivered. Teardown panic containment is per callback box — see
1213    /// "Failure and panic" in the [module docs](self).
1214    pub fn on_stream_info<F>(mut self, f: F) -> Self
1215    where
1216        F: FnMut(&[PacketStreamInfo]) -> PacketCallbackResult + Send + 'static,
1217    {
1218        self.on_stream_info = Some(Box::new(f));
1219        self
1220    }
1221
1222    /// Terminal success callback; see the [module docs](self) for the exact
1223    /// gate. Never invoked after an error or lost packets. Cancellation
1224    /// suppresses it only when it interrupts delivery: a `stop()` that lands
1225    /// after this sink fully drained still delivers `on_end`. A panic here
1226    /// is contained per callback box and cannot change the settled job
1227    /// result — see "Failure and panic" in the [module docs](self) for the
1228    /// exact boundary.
1229    pub fn on_end<F>(mut self, f: F) -> Self
1230    where
1231        F: FnMut() + Send + 'static,
1232    {
1233        self.on_end = Some(Box::new(f));
1234        self
1235    }
1236
1237    /// Optional structured observer for a job that failed OUTSIDE this
1238    /// sink's delivery path.
1239    ///
1240    /// Scope — this fires ONLY on the synthesized-JobFailed path: the job
1241    /// failed elsewhere (a sibling output, an upstream demuxer, decoder,
1242    /// filter or encoder) while this sink's own delivery was clean, whether
1243    /// that failure landed after this sink drained or truncated its
1244    /// delivery. It does NOT fire for this sink's own delivery-path errors
1245    /// (strict-tier violations, failing callbacks) — those already deliver
1246    /// the full typed [`PacketSinkError`] by reference to
1247    /// `on_delivery_error`, and no summary is synthesized for them. Like
1248    /// the terminal callbacks, it never fires for cancellation, aborts, or
1249    /// initial configuration failures.
1250    ///
1251    /// When it fires, it fires exactly once, immediately BEFORE the
1252    /// matching `on_delivery_error(&PacketSinkError::JobFailed { .. })` —
1253    /// same delivery thread, same terminal slot. The summary's
1254    /// [`message`](JobFailureSummary::message) is byte-identical to that
1255    /// `JobFailed` message; the summary adds a coarse [`JobFailureKind`]
1256    /// and, where the recorded error visibly carries them, the raw FFmpeg
1257    /// error code and the output stream index. `wait()`/`stop()` keep
1258    /// returning the original job error exactly as without this callback.
1259    ///
1260    /// A panic here is contained per callback box and cannot skip the
1261    /// `on_delivery_error` dispatch that follows, nor change the settled
1262    /// job result — see "Failure and panic" in the [module docs](self).
1263    /// Leaving this unregistered (the default) changes no behavior
1264    /// anywhere.
1265    pub fn on_job_failed<F>(mut self, f: F) -> Self
1266    where
1267        F: FnMut(&JobFailureSummary) + Send + 'static,
1268    {
1269        self.on_job_failed = Some(Box::new(f));
1270        self
1271    }
1272
1273    /// Terminal failure callback. For delivery-path errors (strict-tier
1274    /// violations, failing callbacks) the same error is also returned by
1275    /// `wait()`/`stop()`; when the JOB failed elsewhere (after this sink
1276    /// drained or truncating its delivery), the callback receives a
1277    /// synthesized [`PacketSinkError::JobFailed`] summarizing that failure
1278    /// while the job keeps its original error (a registered
1279    /// [`on_job_failed`](Self::on_job_failed) observer receives the
1280    /// structured summary immediately before this dispatch). Not invoked
1281    /// for cancellation or initial configuration failures — see "Failure
1282    /// and panic" in the [module docs](self).
1283    ///
1284    /// The borrowed error stays in the worker's custody for the whole call
1285    /// (a panic here cannot run the error source's destructor mid-unwind),
1286    /// and the panic is contained per callback box — see "Failure and
1287    /// panic" in the [module docs](self) for the exact boundary.
1288    pub fn on_delivery_error<F>(mut self, f: F) -> Self
1289    where
1290        F: FnMut(&PacketSinkError) + Send + 'static,
1291    {
1292        self.on_delivery_error = Some(Box::new(f));
1293        self
1294    }
1295
1296    /// Finalizes the sink.
1297    pub fn build(self) -> PacketSink {
1298        PacketSink {
1299            tier: self.tier,
1300            dispatch: SinkDispatch::Closures {
1301                on_stream_info: self.on_stream_info,
1302                on_packet: self.on_packet,
1303                on_end: self.on_end,
1304                on_job_failed: self.on_job_failed,
1305                on_delivery_error: self.on_delivery_error,
1306            },
1307            cancellation: None,
1308        }
1309    }
1310}
1311
1312/// Owned copy of one delivered packet, produced by [`PacketSink::channel`].
1313#[non_exhaustive]
1314#[derive(Debug, Clone)]
1315pub struct EncodedPacket {
1316    pub(crate) stream_index: usize,
1317    pub(crate) pts: i64,
1318    pub(crate) dts: i64,
1319    pub(crate) duration: i64,
1320    pub(crate) time_base: AVRational,
1321    pub(crate) is_key: bool,
1322    pub(crate) applied_offset: i64,
1323    pub(crate) data: Vec<u8>,
1324}
1325
1326impl EncodedPacket {
1327    fn from_view(view: &PacketView<'_>) -> Self {
1328        Self {
1329            stream_index: view.stream_index,
1330            pts: view.pts,
1331            dts: view.dts,
1332            duration: view.duration,
1333            time_base: view.time_base,
1334            is_key: view.is_key,
1335            applied_offset: view.applied_offset,
1336            data: view.data.to_vec(),
1337        }
1338    }
1339
1340    /// Output stream index.
1341    pub fn stream_index(&self) -> usize {
1342        self.stream_index
1343    }
1344
1345    /// Presentation timestamp; see [`PacketView::pts`].
1346    pub fn pts(&self) -> i64 {
1347        self.pts
1348    }
1349
1350    /// Decode timestamp; see [`PacketView::dts`].
1351    pub fn dts(&self) -> i64 {
1352        self.dts
1353    }
1354
1355    /// Packet duration; see [`PacketView::duration`].
1356    pub fn duration(&self) -> i64 {
1357        self.duration
1358    }
1359
1360    /// Stream time base.
1361    pub fn time_base(&self) -> AVRational {
1362        self.time_base
1363    }
1364
1365    /// [`pts`](Self::pts) in microseconds.
1366    pub fn pts_us(&self) -> i64 {
1367        ticks_to_us(self.pts, self.time_base)
1368    }
1369
1370    /// [`dts`](Self::dts) in microseconds.
1371    pub fn dts_us(&self) -> i64 {
1372        ticks_to_us(self.dts, self.time_base)
1373    }
1374
1375    /// [`duration`](Self::duration) in microseconds.
1376    pub fn duration_us(&self) -> i64 {
1377        ticks_to_us(self.duration, self.time_base)
1378    }
1379
1380    /// [`applied_offset`](Self::applied_offset) in microseconds.
1381    pub fn applied_offset_us(&self) -> i64 {
1382        ticks_to_us(self.applied_offset, self.time_base)
1383    }
1384
1385    /// Fresh-decoder-safe random access point; see [`PacketView::is_key`].
1386    pub fn is_key(&self) -> bool {
1387        self.is_key
1388    }
1389
1390    /// Applied origin shift; see [`PacketView::applied_offset`].
1391    pub fn applied_offset(&self) -> i64 {
1392        self.applied_offset
1393    }
1394
1395    /// The owned packet payload.
1396    pub fn data(&self) -> &[u8] {
1397        &self.data
1398    }
1399
1400    /// Consumes the packet, returning the payload.
1401    pub fn into_data(self) -> Vec<u8> {
1402        self.data
1403    }
1404}
1405
1406/// One event delivered over a [`PacketSink::channel`] adapter, mirroring the
1407/// callback order: `StreamInfo`, then `Packet`s, then at most one terminal
1408/// `End`/`Error` (terminal events are best-effort under a stalled consumer;
1409/// sender disconnection is authoritative).
1410#[non_exhaustive]
1411#[derive(Debug, Clone)]
1412pub enum PacketSinkEvent {
1413    /// The one-time stream configuration, one entry per output stream.
1414    StreamInfo(Vec<PacketStreamInfo>),
1415    /// One delivered packet, copied into an owned payload.
1416    Packet(EncodedPacket),
1417    /// Terminal success (best-effort; see the enum docs).
1418    End,
1419    /// A structured summary of a job failure recorded OUTSIDE this sink's
1420    /// delivery path, queued immediately before the matching
1421    /// [`Error`](Self::Error) event carrying
1422    /// [`PacketSinkError::JobFailed`]. Emitted only on that synthesis path —
1423    /// a delivery-path error produces just the `Error` event. Best-effort
1424    /// like [`End`](Self::End), and one notch behind the `Error` event it
1425    /// precedes: the summary is dropped rather than ever taking the last
1426    /// free slot the terminal `Error` would have used.
1427    JobFailure(JobFailureSummary),
1428    /// A delivery-path error, or [`PacketSinkError::JobFailed`] when the job
1429    /// failed elsewhere (`wait()` keeps the original error). Best-effort like
1430    /// [`End`](Self::End).
1431    Error(PacketSinkError),
1432}
1433
1434/// Why [`PacketSinkReceiver::recv`] returned no event.
1435#[non_exhaustive]
1436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1437pub enum PacketRecvError {
1438    /// The sending side is gone (job finished or failed; all events
1439    /// consumed).
1440    Disconnected,
1441}
1442
1443impl std::fmt::Display for PacketRecvError {
1444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1445        f.write_str("packet-sink channel disconnected")
1446    }
1447}
1448
1449impl std::error::Error for PacketRecvError {}
1450
1451/// Why [`PacketSinkReceiver::try_recv`] returned no event.
1452#[non_exhaustive]
1453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1454pub enum PacketTryRecvError {
1455    /// No event is currently queued.
1456    Empty,
1457    /// The sending side is gone.
1458    Disconnected,
1459}
1460
1461impl std::fmt::Display for PacketTryRecvError {
1462    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1463        match self {
1464            PacketTryRecvError::Empty => f.write_str("no packet-sink event queued"),
1465            PacketTryRecvError::Disconnected => f.write_str("packet-sink channel disconnected"),
1466        }
1467    }
1468}
1469
1470impl std::error::Error for PacketTryRecvError {}
1471
1472/// Why [`PacketSinkReceiver::recv_timeout`] returned no event.
1473#[non_exhaustive]
1474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1475pub enum PacketRecvTimeoutError {
1476    /// No event arrived within the timeout.
1477    Timeout,
1478    /// The sending side is gone.
1479    Disconnected,
1480}
1481
1482impl std::fmt::Display for PacketRecvTimeoutError {
1483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484        match self {
1485            PacketRecvTimeoutError::Timeout => {
1486                f.write_str("timed out waiting for a packet-sink event")
1487            }
1488            PacketRecvTimeoutError::Disconnected => f.write_str("packet-sink channel disconnected"),
1489        }
1490    }
1491}
1492
1493impl std::error::Error for PacketRecvTimeoutError {}
1494
1495/// Error from [`PacketSinkReceiver::into_events`]: the scheduler passed in
1496/// is not the one running this receiver's sink.
1497///
1498/// Each [`PacketSink::channel`] call shares an identity token between the
1499/// sink and its receiver, and `into_events` requires the scheduler whose job
1500/// contains that sink. Accepting an arbitrary scheduler would silently
1501/// cross-wire two runs: iterate receiver A's events while joining — and, on
1502/// early drop, aborting — job B. Both handles are returned unchanged so the
1503/// caller can pair them correctly (the scheduler's job keeps running).
1504pub struct PacketEventsPairingError {
1505    /// The receiver, returned unchanged.
1506    pub receiver: PacketSinkReceiver,
1507    /// The scheduler, returned unchanged; its job is unaffected.
1508    pub scheduler: FfmpegScheduler<Running>,
1509}
1510
1511impl std::fmt::Debug for PacketEventsPairingError {
1512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1513        f.debug_struct("PacketEventsPairingError")
1514            .finish_non_exhaustive()
1515    }
1516}
1517
1518impl std::fmt::Display for PacketEventsPairingError {
1519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1520        f.write_str("packet-sink receiver paired with a scheduler that is not running its sink")
1521    }
1522}
1523
1524impl std::error::Error for PacketEventsPairingError {}
1525
1526/// Receiving side of a [`PacketSink::channel`] adapter.
1527///
1528/// Drain it concurrently with the running job (its own thread, or
1529/// [`into_events`](Self::into_events)); dropping the receiver cancels the
1530/// job — the next delivery fails typed instead of blocking forever.
1531pub struct PacketSinkReceiver {
1532    inner: crossbeam_channel::Receiver<PacketSinkEvent>,
1533    /// Identity of the `PacketSink::channel` call that produced this
1534    /// receiver: the same `CancellationSlot` Arc the paired sink carries.
1535    /// [`into_events`](Self::into_events) matches it by pointer identity
1536    /// against the scheduler's job.
1537    token: CancellationSlot,
1538}
1539
1540impl PacketSinkReceiver {
1541    /// Blocks until the next event; [`PacketRecvError::Disconnected`] once
1542    /// the sending side is gone and all events were consumed.
1543    pub fn recv(&self) -> Result<PacketSinkEvent, PacketRecvError> {
1544        self.inner.recv().map_err(|_| PacketRecvError::Disconnected)
1545    }
1546
1547    /// Non-blocking receive, distinguishing an empty channel from a
1548    /// disconnected one.
1549    pub fn try_recv(&self) -> Result<PacketSinkEvent, PacketTryRecvError> {
1550        self.inner.try_recv().map_err(|e| match e {
1551            crossbeam_channel::TryRecvError::Empty => PacketTryRecvError::Empty,
1552            crossbeam_channel::TryRecvError::Disconnected => PacketTryRecvError::Disconnected,
1553        })
1554    }
1555
1556    /// Receive with a timeout, distinguishing a timeout from disconnection.
1557    pub fn recv_timeout(
1558        &self,
1559        timeout: Duration,
1560    ) -> Result<PacketSinkEvent, PacketRecvTimeoutError> {
1561        self.inner.recv_timeout(timeout).map_err(|e| match e {
1562            crossbeam_channel::RecvTimeoutError::Timeout => PacketRecvTimeoutError::Timeout,
1563            crossbeam_channel::RecvTimeoutError::Disconnected => {
1564                PacketRecvTimeoutError::Disconnected
1565            }
1566        })
1567    }
1568
1569    /// Blocking iterator over events until the sender disconnects.
1570    pub fn iter(&self) -> impl Iterator<Item = PacketSinkEvent> + '_ {
1571        self.inner.iter()
1572    }
1573
1574    /// Consumes the receiver and the running scheduler into a single
1575    /// owned-run iterator (the frame-export `FrameIter` shape): events stream
1576    /// out as they arrive, the scheduler is joined exactly once when the
1577    /// channel drains, and a job error surfaces as one terminal `Err`.
1578    /// Dropping the iterator mid-run releases the receiver FIRST (unblocking
1579    /// a worker parked in the channel send), then aborts the job.
1580    ///
1581    /// Unlike the raw channel — whose terminal events are best-effort — the
1582    /// iterator's ending is deterministic: a stream that ends without `Err`
1583    /// always ends with exactly one [`PacketSinkEvent::End`]. The channel
1584    /// send behind `on_end` cannot block teardown, so a consumer that is
1585    /// merely a few events behind at that instant loses the queued `End`;
1586    /// the iterator re-synthesizes it after the clean join, where "clean"
1587    /// is `wait()` returning `Ok` — the same authority the completion
1588    /// contract pins to a delivered `on_end`.
1589    ///
1590    /// # Failure path
1591    ///
1592    /// On a failed job the iterator may first yield the best-effort channel
1593    /// events queued at the terminal —
1594    /// `Ok(`[`PacketSinkEvent::JobFailure`]`(summary))`, then
1595    /// `Ok(`[`PacketSinkEvent::Error`]`(PacketSinkError::JobFailed { .. }))`.
1596    /// Both are best-effort: the `Error` event is dropped when the channel
1597    /// is full at that instant, and the `JobFailure` summary is stricter
1598    /// still — it requires two free slots, never taking the last slot the
1599    /// terminal `Error` would use. Then follows the single deterministic
1600    /// terminal `Err` carrying the original job error from the join. That
1601    /// `Err` is authoritative (the same authority as `wait()`); the
1602    /// `JobFailed`/summary message is byte-identical to that error's
1603    /// `Display` output (or to the fixed substitute message when formatting
1604    /// the recorded error panicked).
1605    ///
1606    /// # Errors
1607    ///
1608    /// [`PacketEventsPairingError`] when `scheduler` is not the one running
1609    /// this receiver's sink. The pairing is checked by identity — the token
1610    /// shared by the sink/receiver pair from [`PacketSink::channel`] must
1611    /// belong to the scheduler's job — because a cross-wired iterator would
1612    /// silently stream one run's events while reporting (and, on drop,
1613    /// aborting) another run's outcome. The error returns both handles
1614    /// unchanged so they can be re-paired.
1615    // The Err variant carries the scheduler back to the caller, so it is as
1616    // large as the Ok variant (which owns the same scheduler inside the
1617    // iterator); boxing it would not shrink the Result.
1618    #[allow(clippy::result_large_err)]
1619    pub fn into_events(
1620        self,
1621        scheduler: FfmpegScheduler<Running>,
1622    ) -> Result<PacketEventIter, PacketEventsPairingError> {
1623        if !scheduler.runs_packet_sink(&self.token) {
1624            return Err(PacketEventsPairingError {
1625                receiver: self,
1626                scheduler,
1627            });
1628        }
1629        Ok(PacketEventIter {
1630            inner: OwnedRunIter::new(self.inner, scheduler, std::convert::identity),
1631            saw_end: false,
1632            saw_error: false,
1633        })
1634    }
1635}
1636
1637/// An owned-run iterator over [`PacketSinkEvent`]s; see
1638/// [`PacketSinkReceiver::into_events`].
1639pub struct PacketEventIter {
1640    inner: OwnedRunIter<PacketSinkEvent>,
1641    /// An `End` already streamed through the channel — nothing to add.
1642    saw_end: bool,
1643    /// A terminal `Err` was yielded — an `End` after it would claim a clean
1644    /// finish the join just denied.
1645    saw_error: bool,
1646}
1647
1648impl std::fmt::Debug for PacketEventIter {
1649    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1650        f.debug_struct("PacketEventIter").finish_non_exhaustive()
1651    }
1652}
1653
1654impl Iterator for PacketEventIter {
1655    type Item = Result<PacketSinkEvent, crate::error::Error>;
1656
1657    fn next(&mut self) -> Option<Self::Item> {
1658        match self.inner.next() {
1659            Some(Ok(event)) => {
1660                if matches!(event, PacketSinkEvent::End) {
1661                    self.saw_end = true;
1662                }
1663                Some(Ok(event))
1664            }
1665            Some(Err(e)) => {
1666                self.saw_error = true;
1667                Some(Err(e))
1668            }
1669            // The channel disconnected and the join was clean. `on_end`
1670            // pushes `End` with a non-blocking send (teardown must not wait
1671            // on a lagging consumer), so a consumer a few events behind at
1672            // that instant loses it; restore the invariant that a stream
1673            // without `Err` ends with `End`, exactly once.
1674            None => {
1675                if !self.saw_end && !self.saw_error {
1676                    self.saw_end = true;
1677                    Some(Ok(PacketSinkEvent::End))
1678                } else {
1679                    None
1680                }
1681            }
1682        }
1683    }
1684}
1685
1686impl std::iter::FusedIterator for PacketEventIter {}
1687
1688/// Explicit muxing policy a packet-sink output pins for encoder setup.
1689///
1690/// A packet sink still allocates a real (never-written) output context so the
1691/// existing stream/parameter plumbing works unchanged, but nothing may be
1692/// implicitly decided by which container that happens to be: the flags the
1693/// encoders and the vsync selection observe are synthesized from this policy
1694/// and overwrite the container's snapshot.
1695pub(crate) struct PacketSinkPolicy {
1696    /// Set `AV_CODEC_FLAG_GLOBAL_HEADER` on the encoders, so codec
1697    /// configuration (SPS/PPS, AudioSpecificConfig) materializes as
1698    /// out-of-band extradata at encoder open — the strict tier requires it
1699    /// before the first callback.
1700    pub(crate) global_header: bool,
1701    /// Advertise variable-fps semantics to the vsync selection (`false` in
1702    /// the strict tier: CFR-style timestamps, like mp4).
1703    pub(crate) variable_fps: bool,
1704    /// Advertise a timestamp-free sink to the vsync selection (`false`:
1705    /// timestamps are the product).
1706    pub(crate) no_timestamps: bool,
1707}
1708
1709impl PacketSinkPolicy {
1710    pub(crate) fn for_tier(tier: PacketSinkTier) -> Self {
1711        match tier {
1712            PacketSinkTier::Strict => Self {
1713                global_header: true,
1714                variable_fps: false,
1715                no_timestamps: false,
1716            },
1717        }
1718    }
1719
1720    /// The `AVOutputFormat.flags` projection of this policy, stored as the
1721    /// muxer's `oformat_flags` snapshot (what `enc_init` and the vsync
1722    /// selection read).
1723    pub(crate) fn oformat_flags(&self) -> i32 {
1724        let mut flags = 0;
1725        if self.global_header {
1726            flags |= ffmpeg_sys_next::AVFMT_GLOBALHEADER;
1727        }
1728        if self.variable_fps {
1729            flags |= ffmpeg_sys_next::AVFMT_VARIABLE_FPS;
1730        }
1731        if self.no_timestamps {
1732            flags |= ffmpeg_sys_next::AVFMT_NOTIMESTAMPS;
1733        }
1734        flags
1735    }
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740    use super::*;
1741
1742    fn test_view(data: &[u8]) -> PacketView<'_> {
1743        PacketView {
1744            stream_index: 0,
1745            pts: 10,
1746            dts: 5,
1747            duration: 1,
1748            time_base: AVRational { num: 1, den: 25 },
1749            is_key: false,
1750            applied_offset: 3,
1751            data,
1752        }
1753    }
1754
1755    #[test]
1756    fn builder_requires_a_packet_consumer_and_discard_is_explicit() {
1757        let mut sink = PacketSink::builder(|_| Ok(())).build();
1758        assert_eq!(sink.tier, PacketSinkTier::Strict);
1759        assert!(sink.dispatch_stream_info(&[]).is_ok());
1760        let payload = [0u8, 0, 0, 1, 0x65];
1761        assert!(sink.dispatch_packet(&test_view(&payload)).is_ok());
1762        sink.dispatch_end();
1763        sink.dispatch_delivery_error(&PacketSinkError::NoStreams);
1764
1765        let mut discard = PacketSink::discard();
1766        assert!(discard.dispatch_packet(&test_view(&payload)).is_ok());
1767    }
1768
1769    /// The public tier accessor is the only way consumers can observe a
1770    /// sink's tier; every v1 construction path must report `Strict`, and the
1771    /// enum default must agree so builders can rely on it.
1772    #[test]
1773    fn every_construction_path_reports_the_strict_tier() {
1774        assert_eq!(PacketSinkTier::default(), PacketSinkTier::Strict);
1775        assert_eq!(
1776            PacketSink::builder(|_| Ok(())).build().tier(),
1777            PacketSinkTier::Strict
1778        );
1779        assert_eq!(PacketSink::discard().tier(), PacketSinkTier::Strict);
1780
1781        struct Accepting;
1782        impl PacketSinkHandler for Accepting {
1783            fn on_packet(&mut self, _packet: &PacketView<'_>) -> PacketCallbackResult {
1784                Ok(())
1785            }
1786        }
1787        assert_eq!(
1788            PacketSink::from_handler(Accepting).tier(),
1789            PacketSinkTier::Strict
1790        );
1791
1792        let (sink, _receiver) = PacketSink::channel(NonZeroUsize::new(1).unwrap());
1793        assert_eq!(sink.tier(), PacketSinkTier::Strict);
1794    }
1795
1796    #[test]
1797    fn handler_receives_serial_callbacks_with_shared_state() {
1798        struct Counting {
1799            packets: usize,
1800        }
1801        impl PacketSinkHandler for Counting {
1802            fn on_packet(&mut self, _packet: &PacketView<'_>) -> PacketCallbackResult {
1803                self.packets += 1;
1804                if self.packets > 1 {
1805                    Err(PacketCallbackError::new("enough"))
1806                } else {
1807                    Ok(())
1808                }
1809            }
1810        }
1811        let mut sink = PacketSink::from_handler(Counting { packets: 0 });
1812        let payload = [0u8, 0, 0, 1, 0x65];
1813        assert!(sink.dispatch_packet(&test_view(&payload)).is_ok());
1814        let err = sink
1815            .dispatch_packet(&test_view(&payload))
1816            .expect_err("handler state must persist across calls");
1817        assert_eq!(err.kind, CallbackFailureKind::Failure);
1818        assert_eq!(err.to_string(), "enough");
1819    }
1820
1821    /// A `panic_any` payload whose own `Drop` panics (with another such
1822    /// payload) must be consumed without the disposal itself throwing —
1823    /// both for chains within the attempt bound and for chains beyond it
1824    /// (the remainder is leaked by design, never re-thrown).
1825    #[test]
1826    fn panic_payload_chains_are_disposed_without_escaping() {
1827        struct ChainBomb(u32);
1828        impl Drop for ChainBomb {
1829            fn drop(&mut self) {
1830                if self.0 > 0 {
1831                    std::panic::panic_any(ChainBomb(self.0 - 1));
1832                }
1833            }
1834        }
1835        // Depth 3: attempts 1-3 each panic with the next link, attempt 4
1836        // drops the final link cleanly.
1837        dispose_panic_payload(Box::new(ChainBomb(3)));
1838        // Deeper than the attempt bound: the helper must still return.
1839        dispose_panic_payload(Box::new(ChainBomb(64)));
1840    }
1841
1842    /// Every callback box must be destroyed even when SEVERAL capture
1843    /// destructors panic. One catch around a plain drop of the aggregate
1844    /// contains only the first bomb — the remaining boxes are then
1845    /// destroyed by the unwind itself, where the second bomb aborts the
1846    /// process. Reaching the assertions at all is the point.
1847    #[test]
1848    fn dispose_contained_destroys_every_box_across_multiple_drop_panics() {
1849        use std::sync::atomic::AtomicBool;
1850
1851        struct DropBomb(Arc<AtomicBool>);
1852        impl Drop for DropBomb {
1853            fn drop(&mut self) {
1854                self.0.store(true, Ordering::Release);
1855                panic!("injected capture-destructor panic");
1856            }
1857        }
1858
1859        let flags: Vec<Arc<AtomicBool>> =
1860            (0..4).map(|_| Arc::new(AtomicBool::new(false))).collect();
1861        let (b0, b1, b2, b3) = (
1862            DropBomb(flags[0].clone()),
1863            DropBomb(flags[1].clone()),
1864            DropBomb(flags[2].clone()),
1865            DropBomb(flags[3].clone()),
1866        );
1867        let sink = PacketSink::builder(move |_pkt| {
1868            let _hold = &b0;
1869            Ok(())
1870        })
1871        .on_end(move || {
1872            let _hold = &b1;
1873        })
1874        .on_job_failed(move |_summary| {
1875            let _hold = &b2;
1876        })
1877        .on_delivery_error(move |_e| {
1878            let _hold = &b3;
1879        })
1880        .build();
1881        assert!(
1882            sink.dispose_contained(),
1883            "four panicking capture destructors must be reported"
1884        );
1885        for (i, flag) in flags.iter().enumerate() {
1886            assert!(
1887                flag.load(Ordering::Acquire),
1888                "callback box {i} was never destroyed"
1889            );
1890        }
1891
1892        struct BombHandler(Arc<AtomicBool>);
1893        impl Drop for BombHandler {
1894            fn drop(&mut self) {
1895                self.0.store(true, Ordering::Release);
1896                panic!("injected handler-destructor panic");
1897            }
1898        }
1899        impl PacketSinkHandler for BombHandler {
1900            fn on_packet(&mut self, _packet: &PacketView<'_>) -> PacketCallbackResult {
1901                Ok(())
1902            }
1903        }
1904        let destroyed = Arc::new(AtomicBool::new(false));
1905        assert!(PacketSink::from_handler(BombHandler(destroyed.clone())).dispose_contained());
1906        assert!(destroyed.load(Ordering::Acquire));
1907
1908        // Benign sinks report no panic.
1909        assert!(!PacketSink::builder(|_pkt| Ok(()))
1910            .build()
1911            .dispose_contained());
1912    }
1913
1914    #[test]
1915    fn callback_error_preserves_its_source() {
1916        let io = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "peer gone");
1917        let err = PacketCallbackError::with_source("send failed", io);
1918        assert_eq!(err.to_string(), "send failed");
1919        let source = std::error::Error::source(&err).expect("source preserved");
1920        assert!(source.to_string().contains("peer gone"));
1921    }
1922
1923    #[test]
1924    fn channel_adapter_forwards_events_in_order() {
1925        let (mut sink, rx) = PacketSink::channel(NonZeroUsize::new(8).unwrap());
1926        assert!(sink.dispatch_stream_info(&[]).is_ok());
1927        let payload = [0u8, 0, 0, 1, 0x65];
1928        assert!(sink.dispatch_packet(&test_view(&payload)).is_ok());
1929        sink.dispatch_end();
1930        match rx.recv().unwrap() {
1931            PacketSinkEvent::StreamInfo(v) => assert!(v.is_empty()),
1932            other => panic!("expected StreamInfo, got {other:?}"),
1933        }
1934        match rx.recv().unwrap() {
1935            PacketSinkEvent::Packet(p) => {
1936                assert_eq!(p.pts(), 10);
1937                assert_eq!(p.dts(), 5);
1938                assert_eq!(p.applied_offset(), 3);
1939                assert_eq!(p.data(), &payload);
1940                assert!(!p.is_key());
1941                // Tick conveniences: 10 ticks at 1/25 s = 400_000 us.
1942                assert_eq!(p.pts_us(), 400_000);
1943                assert_eq!(p.duration_us(), 40_000);
1944            }
1945            other => panic!("expected Packet, got {other:?}"),
1946        }
1947        assert!(matches!(rx.recv().unwrap(), PacketSinkEvent::End));
1948        drop(sink);
1949        assert!(matches!(rx.recv(), Err(PacketRecvError::Disconnected)));
1950    }
1951
1952    #[test]
1953    fn dropped_receiver_turns_sends_into_typed_disconnection() {
1954        let (mut sink, rx) = PacketSink::channel(NonZeroUsize::new(1).unwrap());
1955        drop(rx);
1956        let payload = [0u8, 0, 0, 1, 0x65];
1957        let err = sink
1958            .dispatch_packet(&test_view(&payload))
1959            .expect_err("send into a dropped receiver must fail");
1960        assert_eq!(err.kind, CallbackFailureKind::Disconnected);
1961    }
1962
1963    /// A blocked bounded send with a live, undrained receiver must observe
1964    /// the job stopping and bail out promptly — classified as clean
1965    /// cancellation when NO job error is recorded.
1966    #[test]
1967    fn blocked_channel_send_observes_cancellation() {
1968        let (mut sink, rx) = PacketSink::channel(NonZeroUsize::new(1).unwrap());
1969        // Simulate the worker wiring: publish the job observables.
1970        let status = Arc::new(AtomicUsize::new(
1971            crate::core::scheduler::ffmpeg_scheduler::STATUS_RUN,
1972        ));
1973        let result = Arc::new(std::sync::Mutex::new(None));
1974        sink.cancellation
1975            .as_ref()
1976            .expect("channel sinks carry a cancellation slot")
1977            .set(JobStopObservables {
1978                status: status.clone(),
1979                result,
1980            })
1981            .ok();
1982        // Fill the capacity-1 channel; the receiver never drains.
1983        let payload = [0u8, 0, 0, 1, 0x65];
1984        assert!(sink.dispatch_packet(&test_view(&payload)).is_ok());
1985        // Flip to stopping from another thread; the blocked send must
1986        // observe it and bail out with the cancellation kind.
1987        let flip = status.clone();
1988        let flipper = std::thread::spawn(move || {
1989            std::thread::sleep(Duration::from_millis(120));
1990            flip.store(
1991                crate::core::scheduler::ffmpeg_scheduler::STATUS_END,
1992                Ordering::Release,
1993            );
1994        });
1995        let start = std::time::Instant::now();
1996        let err = sink
1997            .dispatch_packet(&test_view(&payload))
1998            .expect_err("blocked send must cancel");
1999        assert_eq!(err.kind, CallbackFailureKind::Cancelled);
2000        assert!(
2001            start.elapsed() < Duration::from_secs(5),
2002            "cancellation must be prompt"
2003        );
2004        flipper.join().unwrap();
2005        drop(rx);
2006    }
2007
2008    /// A stopping status WITH a recorded job error is a failure-driven
2009    /// shutdown, not cancellation: the blocked send must classify it as
2010    /// `JobStopped` so the terminal reports `JobFailed` instead of staying
2011    /// silent.
2012    #[test]
2013    fn blocked_channel_send_classifies_failure_driven_stop() {
2014        let (mut sink, rx) = PacketSink::channel(NonZeroUsize::new(1).unwrap());
2015        let status = Arc::new(AtomicUsize::new(
2016            crate::core::scheduler::ffmpeg_scheduler::STATUS_RUN,
2017        ));
2018        let result: Arc<std::sync::Mutex<Option<crate::error::Result<()>>>> =
2019            Arc::new(std::sync::Mutex::new(None));
2020        sink.cancellation
2021            .as_ref()
2022            .expect("channel sinks carry a cancellation slot")
2023            .set(JobStopObservables {
2024                status: status.clone(),
2025                result: result.clone(),
2026            })
2027            .ok();
2028        let payload = [0u8, 0, 0, 1, 0x65];
2029        assert!(sink.dispatch_packet(&test_view(&payload)).is_ok());
2030        // Record the error BEFORE publishing the stopping status — the
2031        // order every failure path guarantees.
2032        let flipper = std::thread::spawn(move || {
2033            std::thread::sleep(Duration::from_millis(120));
2034            *result.lock().unwrap() = Some(Err(crate::error::Error::WorkerPanicked(
2035                "muxer1:mpegts".to_string(),
2036            )));
2037            status.store(
2038                crate::core::scheduler::ffmpeg_scheduler::STATUS_END,
2039                Ordering::Release,
2040            );
2041        });
2042        let err = sink
2043            .dispatch_packet(&test_view(&payload))
2044            .expect_err("blocked send must abandon on a failed job");
2045        assert_eq!(err.kind, CallbackFailureKind::JobStopped);
2046        flipper.join().unwrap();
2047        drop(rx);
2048    }
2049
2050    #[test]
2051    fn recv_variants_distinguish_empty_timeout_disconnected() {
2052        let (sink, rx) = PacketSink::channel(NonZeroUsize::new(1).unwrap());
2053        assert_eq!(rx.try_recv().unwrap_err(), PacketTryRecvError::Empty);
2054        assert_eq!(
2055            rx.recv_timeout(Duration::from_millis(10)).unwrap_err(),
2056            PacketRecvTimeoutError::Timeout
2057        );
2058        drop(sink);
2059        assert_eq!(rx.try_recv().unwrap_err(), PacketTryRecvError::Disconnected);
2060        assert_eq!(
2061            rx.recv_timeout(Duration::from_millis(10)).unwrap_err(),
2062            PacketRecvTimeoutError::Disconnected
2063        );
2064    }
2065}