Skip to main content

ez_ffmpeg/core/context/
ffmpeg_context.rs

1use crate::core::context::demuxer::Demuxer;
2use crate::core::context::ffmpeg_context_builder::FfmpegContextBuilder;
3use crate::core::context::filter_complex::FilterComplex;
4use crate::core::context::filter_graph::FilterGraph;
5use crate::core::context::input::Input;
6use crate::core::context::input_filter::{InputFilter, IFILTER_FLAG_AUTOROTATE};
7use crate::core::context::muxer::Muxer;
8use crate::core::context::output::{Output, StreamMap};
9use crate::core::metadata::StreamSpecifier;
10use crate::core::context::output_filter::{
11    OutputFilter, OFILTER_FLAG_AUDIO_24BIT, OFILTER_FLAG_AUTOSCALE, OFILTER_FLAG_DISABLE_CONVERT,
12};
13use crate::core::context::{frame_alloc, CodecContext};
14use crate::core::scheduler::ffmpeg_scheduler;
15use crate::core::scheduler::ffmpeg_scheduler::{FfmpegScheduler, Initialization};
16#[cfg(not(docsrs))]
17use crate::core::scheduler::filter_task::graph_opts_apply;
18use crate::core::scheduler::input_controller::SchNode;
19use crate::error::Error::{
20    FileSameAsInput, FilterDescUtf8, FilterNameUtf8, FilterZeroOutputs,
21    FrameFilterStreamTypeNoMatched, FrameFilterTypeNoMatched, ParseInteger,
22};
23use crate::error::FilterGraphParseError::{
24    InvalidFileIndexInFg, InvalidFilterSpecifier, OutputUnconnected,
25};
26use crate::error::{
27    AllocOutputContextError, FilterGraphOperationError, FilterGraphParseError, FindStreamError,
28    OpenInputError, OpenOutputError,
29};
30use crate::error::{Error, Result};
31use crate::filter::frame_pipeline::FramePipeline;
32use crate::util::ffmpeg_utils::{hashmap_to_avdictionary, DictGuard};
33#[cfg(not(docsrs))]
34use ffmpeg_sys_next::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC;
35#[cfg(not(docsrs))]
36use ffmpeg_sys_next::{
37    avformat_query_codec, AVSTREAM_EVENT_FLAG_NEW_PACKETS, AV_DISPOSITION_ATTACHED_PIC,
38    AV_DISPOSITION_DEFAULT,
39};
40#[cfg(not(docsrs))]
41use ffmpeg_sys_next::AVCodecConfig::*;
42use ffmpeg_sys_next::AVCodecID::{AV_CODEC_ID_AC3, AV_CODEC_ID_MP3, AV_CODEC_ID_NONE};
43use ffmpeg_sys_next::AVColorRange::AVCOL_RANGE_UNSPECIFIED;
44use ffmpeg_sys_next::AVColorSpace::AVCOL_SPC_UNSPECIFIED;
45use ffmpeg_sys_next::AVMediaType::{
46    AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
47    AVMEDIA_TYPE_VIDEO,
48};
49use ffmpeg_sys_next::AVPixelFormat::AV_PIX_FMT_NONE;
50use ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_NONE;
51use ffmpeg_sys_next::{
52    av_add_q, av_channel_layout_default, av_codec_get_id, av_codec_get_tag2,
53    av_freep, av_get_exact_bits_per_sample, av_get_pix_fmt, av_guess_codec, av_guess_format, av_guess_frame_rate,
54    av_inv_q, av_malloc, av_rescale_q, av_seek_frame, avcodec_alloc_context3,
55    avcodec_descriptor_get, avcodec_descriptor_get_by_name, avcodec_find_encoder,
56    avcodec_find_encoder_by_name, avcodec_get_name, avcodec_parameters_from_context,
57    avcodec_parameters_to_context,
58    avfilter_pad_get_name, avfilter_pad_get_type, avformat_alloc_context,
59    avformat_alloc_output_context2, avformat_close_input, avformat_find_stream_info,
60    avformat_flush, avformat_free_context, avformat_open_input, avio_alloc_context,
61    avio_open2, AVCodec, AVCodecID, AVColorRange, AVColorSpace, AVFilterContext,
62    AVFilterInOut, AVFilterPad, AVFormatContext, AVMediaType, AVOutputFormat, AVPixelFormat,
63    AVRational, AVSampleFormat, AVStream, AVERROR_ENCODER_NOT_FOUND, AVFMT_FLAG_CUSTOM_IO,
64    AVFMT_GLOBALHEADER, AVFMT_NOBINSEARCH, AVFMT_NOFILE, AVFMT_NOGENSEARCH, AVFMT_NOSTREAMS,
65    AVIO_FLAG_WRITE, AVSEEK_FLAG_BACKWARD, AV_CODEC_PROP_BITMAP_SUB, AV_CODEC_PROP_TEXT_SUB,
66    AV_TIME_BASE,
67};
68#[cfg(not(docsrs))]
69use ffmpeg_sys_next::{
70    av_buffer_ref, av_channel_layout_copy, av_packet_side_data_new,
71    avcodec_get_supported_config, avfilter_graph_segment_apply,
72    avfilter_graph_segment_create_filters, avfilter_graph_segment_free,
73    avfilter_graph_segment_parse, AVChannelLayout, AVFILTER_FLAG_HWDEVICE,
74};
75use crate::hwaccel::{hw_device_for_filter, init_filter_hw_device};
76use log::{debug, error, info, warn};
77use std::collections::HashMap;
78use std::ffi::{c_uint, c_void, CStr, CString};
79use std::ptr::{null, null_mut};
80use std::sync::Arc;
81
82pub struct FfmpegContext {
83    pub(crate) independent_readrate: bool,
84    pub(crate) demuxs: Vec<Demuxer>,
85    pub(crate) filter_graphs: Vec<FilterGraph>,
86    pub(crate) muxs: Vec<Muxer>,
87    // Created at build time so the AVIO interrupt callbacks installed on the
88    // input/output contexts observe the same atomic the scheduler drives.
89    pub(crate) scheduler_status: Arc<std::sync::atomic::AtomicUsize>,
90    pub(crate) interrupt_state: Arc<crate::core::context::InterruptState>,
91}
92
93// SAFETY: FfmpegContext can be sent to another thread because all its fields
94// are either Send or wrapped in thread-safe containers. The raw FFmpeg pointers
95// are only accessed from the thread that owns the FfmpegContext.
96// Note: FfmpegContext is NOT Sync because it contains non-Sync fields like
97// Box<dyn FrameFilter> which only implements Send.
98unsafe impl Send for FfmpegContext {}
99
100impl FfmpegContext {
101    /// Creates a new [`FfmpegContextBuilder`] which allows you to configure
102    /// and construct an [`FfmpegContext`] with custom inputs, outputs, filters,
103    /// and other parameters.
104    ///
105    /// # Examples
106    /// ```rust,ignore
107    /// let context = FfmpegContext::builder()
108    ///     .input("input.mp4")
109    ///     .output("output.mp4")
110    ///     .build()
111    ///     .unwrap();
112    /// ```
113    pub fn builder() -> FfmpegContextBuilder {
114        FfmpegContextBuilder::new()
115    }
116
117    /// Consumes this [`FfmpegContext`] and starts an FFmpeg job, returning
118    /// an [`FfmpegScheduler<ffmpeg_scheduler::Running>`] for further management.
119    ///
120    /// Internally, this method creates an [`FfmpegScheduler`] from the context
121    /// and immediately calls [`FfmpegScheduler::start()`].
122    ///
123    /// # Returns
124    /// - `Ok(FfmpegScheduler<Running>)` if the scheduling process started successfully.
125    /// - `Err(...)` if there was an error initializing or starting FFmpeg.
126    ///
127    /// # Example
128    /// ```rust,ignore
129    /// let context = FfmpegContext::builder()
130    ///     .input("input.mp4")
131    ///     .output("output.mp4")
132    ///     .build()
133    ///     .unwrap();
134    ///
135    /// // Start the FFmpeg job and get a scheduler to manage it
136    /// let scheduler = context.start().expect("Failed to start Ffmpeg job");
137    ///
138    /// // Optionally, wait for it to finish
139    /// let result = scheduler.wait();
140    /// assert!(result.is_ok());
141    /// ```
142    pub fn start(self) -> Result<FfmpegScheduler<ffmpeg_scheduler::Running>> {
143        let ffmpeg_scheduler = FfmpegScheduler::new(self);
144        ffmpeg_scheduler.start()
145    }
146
147    #[allow(dead_code)]
148    pub(crate) fn new(
149        inputs: Vec<Input>,
150        filter_complexs: Vec<FilterComplex>,
151        outputs: Vec<Output>,
152    ) -> Result<FfmpegContext> {
153        Self::new_with_options(false, inputs, filter_complexs, outputs, false)
154    }
155
156    pub(crate) fn new_with_options(
157        mut independent_readrate: bool,
158        mut inputs: Vec<Input>,
159        filter_complexs: Vec<FilterComplex>,
160        mut outputs: Vec<Output>,
161        copy_ts: bool,
162    ) -> Result<FfmpegContext> {
163        check_duplicate_inputs_outputs(&inputs, &outputs)?;
164
165        crate::core::initialize_ffmpeg();
166
167        // The status atomic exists before any context is opened so every
168        // input/output AVFormatContext can carry an interrupt callback bound
169        // to it (fftools installs decode_interrupt_cb the same way).
170        let scheduler_status = Arc::new(std::sync::atomic::AtomicUsize::new(
171            crate::core::scheduler::ffmpeg_scheduler::STATUS_INIT,
172        ));
173        let interrupt_state = Arc::new(crate::core::context::InterruptState::new(
174            scheduler_status.clone(),
175        ));
176
177        let mut demuxs = open_input_files(&mut inputs, copy_ts, &interrupt_state)?;
178
179        if demuxs.len() <= 1 {
180            independent_readrate = false;
181        }
182
183        let mut filter_graphs = if !filter_complexs.is_empty() {
184            let mut filter_graphs = init_filter_graphs(filter_complexs)?;
185            fg_bind_inputs(&mut filter_graphs, &mut demuxs)?;
186            filter_graphs
187        } else {
188            Vec::new()
189        };
190
191        let mut muxs = open_output_files(&mut outputs, copy_ts, &interrupt_state)?;
192
193        outputs_bind(&mut muxs, &mut filter_graphs, &mut demuxs)?;
194
195        // Propagate input recording_time to mux as a convenience feature.
196        // This allows users to set recording_time on Input and have it work
197        // correctly for stream-copy scenarios (where the mux-side check in
198        // streamcopy_rescale needs recording_time). Only propagate when all
199        // mapped streams come from the same input file to avoid incorrect
200        // truncation in multi-input scenarios.
201        for mux in muxs.iter_mut() {
202            if mux.recording_time_us.is_none() {
203                let mapping = mux.stream_input_mapping();
204                if !mapping.is_empty() {
205                    let first_input = mapping[0].1.0;
206                    let all_same_input = mapping.iter().all(|(_, (idx, _))| *idx == first_input);
207                    if all_same_input {
208                        if let Some(demux) = demuxs.get(first_input) {
209                            if let Some(recording_time) = demux.recording_time_us {
210                                mux.recording_time_us = Some(recording_time);
211                            }
212                        }
213                    }
214                }
215            }
216        }
217
218        correct_input_start_times(&mut demuxs, copy_ts);
219
220        check_output_streams(&muxs)?;
221
222        check_fg_bindings(&filter_graphs)?;
223
224        check_frame_filter_pipeline(&muxs, &demuxs)?;
225
226        Ok(Self {
227            independent_readrate,
228            demuxs,
229            filter_graphs,
230            muxs,
231            scheduler_status,
232            interrupt_state,
233        })
234    }
235}
236
237const START_AT_ZERO: bool = false;
238
239fn correct_input_start_times(demuxs: &mut Vec<Demuxer>, copy_ts: bool) {
240    for (i, demux) in demuxs.iter_mut().enumerate() {
241        unsafe {
242            let is = demux.in_fmt_ctx_ptr();
243
244            demux.start_time_effective = (*is).start_time;
245            if (*is).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
246                || (*(*is).iformat).flags & ffmpeg_sys_next::AVFMT_TS_DISCONT == 0
247            {
248                continue;
249            }
250
251            let mut new_start_time = i64::MAX;
252            let stream_count = (*is).nb_streams;
253            for j in 0..stream_count {
254                let st = *(*is).streams.add(j as usize);
255                if (*st).discard == ffmpeg_sys_next::AVDiscard::AVDISCARD_ALL
256                    || (*st).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
257                {
258                    continue;
259                }
260                new_start_time = std::cmp::min(
261                    new_start_time,
262                    av_rescale_q(
263                        (*st).start_time,
264                        (*st).time_base,
265                        ffmpeg_sys_next::AV_TIME_BASE_Q,
266                    ),
267                );
268            }
269            let diff = new_start_time - (*is).start_time;
270            if diff != 0 {
271                debug!("Correcting start time of Input #{i} by {diff}us.");
272                demux.start_time_effective = new_start_time;
273                if copy_ts && START_AT_ZERO {
274                    demux.ts_offset = -new_start_time;
275                } else if !copy_ts {
276                    let abs_start_seek = (*is).start_time + demux.start_time_us.unwrap_or(0);
277                    demux.ts_offset = if abs_start_seek > new_start_time {
278                        -abs_start_seek
279                    } else {
280                        -new_start_time
281                    };
282                } else if copy_ts {
283                    demux.ts_offset = 0;
284                }
285
286                // demux.ts_offset += demux.input_ts_offset;
287            }
288        }
289    }
290}
291
292fn check_pipeline<T>(
293    frame_pipelines: Option<&Vec<FramePipeline>>,
294    streams: &[T],
295    tag: &str,
296    get_stream_index: impl Fn(&T) -> usize,
297    get_codec_type: impl Fn(&T) -> &AVMediaType,
298) -> Result<()> {
299    let tag_cap = {
300        let mut chars = tag.chars();
301        match chars.next() {
302            None => String::new(),
303            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
304        }
305    };
306
307    frame_pipelines
308        .into_iter()
309        .flat_map(|pipelines| pipelines.iter())
310        .try_for_each(|pipeline| {
311            if let Some(idx) = pipeline.stream_index {
312                streams
313                    .iter()
314                    .any(|s| {
315                        get_stream_index(s) == idx && get_codec_type(s) == &pipeline.media_type
316                    })
317                    .then_some(())
318                    .ok_or_else(|| {
319                        Into::<crate::error::Error>::into(FrameFilterStreamTypeNoMatched(
320                            tag_cap.clone(),
321                            idx,
322                            format!("{:?}", pipeline.media_type),
323                        ))
324                    })
325            } else {
326                streams
327                    .iter()
328                    .any(|s| get_codec_type(s) == &pipeline.media_type)
329                    .then_some(())
330                    .ok_or_else(|| {
331                        FrameFilterTypeNoMatched(tag.into(), format!("{:?}", pipeline.media_type))
332                    })
333            }
334        })?;
335    Ok(())
336}
337
338fn check_frame_filter_pipeline(muxs: &[Muxer], demuxs: &[Demuxer]) -> Result<()> {
339    muxs.iter().try_for_each(|mux| {
340        check_pipeline(
341            mux.frame_pipelines.as_ref(),
342            mux.get_streams(),
343            "output",
344            |s| s.stream_index,
345            |s| &s.codec_type,
346        )
347    })?;
348    demuxs.iter().try_for_each(|demux| {
349        check_pipeline(
350            demux.frame_pipelines.as_ref(),
351            demux.get_streams(),
352            "input",
353            |s| s.stream_index,
354            |s| &s.codec_type,
355        )
356    })?;
357    Ok(())
358}
359
360fn check_fg_bindings(filter_graphs: &Vec<FilterGraph>) -> Result<()> {
361    // check that all outputs were bound
362    for filter_graph in filter_graphs {
363        for (i, output_filter) in filter_graph.outputs.iter().enumerate() {
364            if !output_filter.has_dst() {
365                let linklabel = if output_filter.linklabel.is_empty() {
366                    "unlabeled".to_string()
367                } else {
368                    output_filter.linklabel.clone()
369                };
370                return Err(OutputUnconnected(output_filter.name.clone(), i, linklabel).into());
371            }
372        }
373    }
374    Ok(())
375}
376
377impl From<FfmpegContext> for FfmpegScheduler<Initialization> {
378    fn from(val: FfmpegContext) -> Self {
379        FfmpegScheduler::new(val)
380    }
381}
382
383fn check_output_streams(muxs: &Vec<Muxer>) -> Result<()> {
384    for mux in muxs {
385        unsafe {
386            let oformat = (*mux.out_fmt_ctx_ptr()).oformat;
387            if !mux.has_src() && (*oformat).flags & AVFMT_NOSTREAMS == 0 {
388                warn!("Output file does not contain any stream");
389                return Err(OpenOutputError::NotContainStream.into());
390            }
391        }
392    }
393    Ok(())
394}
395
396/// Process metadata for output file
397///
398/// Mirrors FFmpeg's `copy_meta()` flow (ffmpeg_mux_init.c:2913-2983):
399/// 1. Metadata mappings (`copy_metadata`)
400/// 2. Chapter auto-copy (`copy_chapters`)
401/// 3. Default auto-copy (`copy_metadata_default`)
402/// 4. User-specified metadata (`of_add_metadata`)
403unsafe fn process_metadata(mux: &Muxer, demuxs: &Vec<Demuxer>) -> Result<()> {
404    use crate::core::metadata::MetadataType;
405    use crate::core::metadata::{
406        copy_chapters_from_input, copy_metadata, copy_metadata_default, of_add_metadata,
407    };
408
409    // Collect input format contexts for metadata copying
410    let input_ctxs: Vec<*const AVFormatContext> = demuxs
411        .iter()
412        .map(|d| d.in_fmt_ctx_ptr() as *const AVFormatContext)
413        .collect();
414
415    let mut metadata_global_manual = false;
416    let mut metadata_streams_manual = false;
417    let mut metadata_chapters_manual = false;
418
419    // Step 1: Process explicit metadata mappings from user (-map_metadata option)
420    // FFmpeg: ffmpeg_mux_init.c:2923-2937
421    let mut mark_manual = |meta_type: &MetadataType| -> () {
422        match meta_type {
423            MetadataType::Global => metadata_global_manual = true,
424            MetadataType::Stream(_) => metadata_streams_manual = true,
425            MetadataType::Chapter(_) => metadata_chapters_manual = true,
426            MetadataType::Program(_) => {}
427        }
428    };
429
430    for mapping in &mux.metadata_map {
431        mark_manual(&mapping.src_type);
432        mark_manual(&mapping.dst_type);
433
434        if mapping.input_index >= input_ctxs.len() {
435            log::warn!(
436                "Metadata mapping references non-existent input file index {}",
437                mapping.input_index
438            );
439            continue;
440        }
441
442        let input_ctx = input_ctxs[mapping.input_index];
443        if let Err(e) = copy_metadata(
444            input_ctx,
445            mux.out_fmt_ctx_ptr(),
446            &mapping.src_type,
447            &mapping.dst_type,
448        ) {
449            log::warn!("Failed to copy metadata from mapping: {}", e);
450        }
451    }
452
453    // Step 2: Copy chapters from first input with chapters (if auto copy enabled)
454    if mux.auto_copy_metadata && !metadata_chapters_manual {
455        if let Some(source_demux) = demuxs
456            .iter()
457            .find(|d| unsafe { !d.in_fmt_ctx_ptr().is_null() && (*d.in_fmt_ctx_ptr()).nb_chapters > 0 })
458        {
459            if let Err(e) = copy_chapters_from_input(
460                source_demux.in_fmt_ctx_ptr(),
461                source_demux.ts_offset,
462                mux.out_fmt_ctx_ptr(),
463                mux.start_time_us,
464                mux.recording_time_us,
465                true,
466            ) {
467                log::warn!("Failed to copy chapters: {}", e);
468            }
469        }
470    }
471
472    // Step 3: Apply FFmpeg's automatic metadata copying (if not disabled by user)
473    // FFmpeg: ffmpeg_mux_init.c:2962-2983
474    let stream_input_mapping = mux.stream_input_mapping();
475    let encoding_streams = mux.encoding_streams();
476
477    if mux.auto_copy_metadata {
478        if let Err(e) = copy_metadata_default(
479            &input_ctxs,
480            mux.out_fmt_ctx_ptr(),
481            mux.nb_streams,
482            &stream_input_mapping,
483            &encoding_streams,
484            mux.recording_time_us.is_some(),
485            mux.auto_copy_metadata,
486            metadata_global_manual,
487            metadata_streams_manual,
488        ) {
489            log::warn!("Failed to apply default metadata behavior: {}", e);
490        }
491    }
492
493    // Step 4: Apply user-specified metadata values (-metadata option)
494    // FFmpeg: of_add_metadata runs right after copy_meta (ffmpeg_mux_init.c:3344,3356)
495    if let Err(e) = of_add_metadata(
496        mux.out_fmt_ctx_ptr(),
497        &mux.global_metadata,
498        &mux.stream_metadata,
499        &mux.chapter_metadata,
500        &mux.program_metadata,
501    ) {
502        log::warn!("Failed to add user metadata: {}", e);
503    }
504
505    Ok(())
506}
507
508/// Check if linklabel refers to a filter graph output
509/// FFmpeg reference: ffmpeg_opt.c:493 - if (arg[0] == '[')
510fn is_filter_output_linklabel(linklabel: &str) -> bool {
511    linklabel.starts_with('[')
512}
513
514/// Parse and expand stream map specifications
515/// This mimics FFmpeg's opt_map() behavior: parse once, expand immediately
516/// FFmpeg reference: ffmpeg_opt.c:478-596
517unsafe fn expand_stream_maps(
518    mux: &mut Muxer,
519    demuxs: &[Demuxer],
520) -> Result<()> {
521    let stream_map_specs = std::mem::take(&mut mux.stream_map_specs);
522
523    for spec in stream_map_specs {
524        // FFmpeg reference: opt_map line 488-491 - check for negative map
525        let (linklabel, is_negative) = if spec.linklabel.starts_with('-') {
526            (&spec.linklabel[1..], true)
527        } else {
528            (spec.linklabel.as_str(), false)
529        };
530
531        // FFmpeg reference: opt_map line 493 - check if this mapping refers to lavfi output
532        if is_filter_output_linklabel(linklabel) {
533            // FFmpeg reference: opt_map line 494-507 - extract linklabel from brackets
534            let pure_linklabel = if linklabel.starts_with('[') {
535                // Extract content between '[' and ']'
536                if let Some(end_pos) = linklabel.find(']') {
537                    &linklabel[1..end_pos]
538                } else {
539                    warn!("Invalid output link label: {}.", linklabel);
540                    return Err(Error::OpenOutput(OpenOutputError::InvalidArgument));
541                }
542            } else {
543                linklabel
544            };
545
546            // FFmpeg reference: opt_map line 504 - validate non-empty
547            if pure_linklabel.is_empty() {
548                warn!("Invalid output link label: {}.", linklabel);
549                return Err(Error::OpenOutput(OpenOutputError::InvalidArgument));
550            }
551
552            // Store pure linklabel (without brackets) for later matching in map_manual()
553            mux.stream_maps.push(StreamMap {
554                file_index: 0,  // Not used for filter outputs
555                stream_index: 0,  // Not used for filter outputs
556                linklabel: Some(pure_linklabel.to_string()),
557                copy: spec.copy,
558                disabled: false,
559            });
560            continue;
561        }
562
563        // FFmpeg reference: opt_map line 512 - parse file index using strtol
564        // Try to parse as file stream; if it fails, treat as filter output
565        let parse_result = strtol(linklabel);
566        if parse_result.is_err() {
567            // Failed to parse file index - treat as filter output linklabel
568            // This allows bare filter output names like "my-out" (without brackets)
569            mux.stream_maps.push(StreamMap {
570                file_index: 0,
571                stream_index: 0,
572                linklabel: Some(linklabel.to_string()),
573                copy: spec.copy,
574                disabled: false,
575            });
576            continue;
577        }
578
579        let (file_idx, remainder) = parse_result.unwrap();
580
581        // FFmpeg reference: opt_map line 513-517 - validate file index
582        if file_idx < 0 || file_idx as usize >= demuxs.len() {
583            warn!("Invalid input file index: {}.", file_idx);
584            return Err(Error::OpenOutput(OpenOutputError::InvalidArgument));
585        }
586        let file_idx = file_idx as usize;
587
588        // FFmpeg reference: opt_map line 520 - parse stream specifier
589        // FFmpeg reference: opt_map line 533 - handle '?' suffix for allow_unused
590        let (spec_str, allow_unused) = if remainder.ends_with('?') {
591            (&remainder[..remainder.len()-1], true)
592        } else {
593            (remainder, false)
594        };
595
596        let stream_spec = if spec_str.is_empty() {
597            // Empty specifier matches all streams (FFmpeg behavior)
598            StreamSpecifier::default()
599        } else {
600            // Strip leading ':' and parse stream specifier
601            let spec_str = if spec_str.starts_with(':') {
602                &spec_str[1..]
603            } else {
604                spec_str
605            };
606
607            StreamSpecifier::parse(spec_str).map_err(|e| {
608                warn!("Invalid stream specifier in '{}': {}", linklabel, e);
609                Error::OpenOutput(OpenOutputError::InvalidArgument)
610            })?
611        };
612
613        // FFmpeg reference: opt_map line 543-553 - negative map: disable matching streams
614        if is_negative {
615            for existing_map in &mut mux.stream_maps {
616                // Only process file-based maps (not filter outputs)
617                if existing_map.linklabel.is_none() &&
618                   existing_map.file_index == file_idx {
619                    // Check if stream specifier matches
620                    let demux = &demuxs[file_idx];
621                    let fmt_ctx = demux.in_fmt_ctx_ptr();
622                    let avstream = *(*fmt_ctx).streams.add(existing_map.stream_index);
623
624                    if stream_spec.matches(fmt_ctx, avstream) {
625                        existing_map.disabled = true;
626                    }
627                }
628            }
629            // Negative map doesn't add new entries, just disables existing ones
630            continue;
631        }
632
633        // FFmpeg reference: opt_map line 555-574 - expand to one StreamMap per matched stream
634        let demux = &demuxs[file_idx];
635        let fmt_ctx = demux.in_fmt_ctx_ptr();
636        let mut matched_count = 0;
637
638        for (stream_idx, _) in demux.get_streams().iter().enumerate() {
639            let avstream = *(*fmt_ctx).streams.add(stream_idx);
640
641            // FFmpeg reference: opt_map line 556 - stream_specifier_match
642            if stream_spec.matches(fmt_ctx, avstream) {
643                mux.stream_maps.push(StreamMap {
644                    file_index: file_idx,
645                    stream_index: stream_idx,
646                    linklabel: None,
647                    copy: spec.copy,
648                    disabled: false,
649                });
650                matched_count += 1;
651            }
652        }
653
654        // FFmpeg reference: opt_map lines 577-590 - error handling for no matches
655        if matched_count == 0 {
656            if allow_unused {
657                // FFmpeg line 579: verbose log for optional mappings
658                info!(
659                    "Stream map '{}' matches no streams; ignoring.",
660                    linklabel
661                );
662            } else {
663                // FFmpeg line 586-587: fatal error with hint about '?' suffix
664                warn!(
665                    "Stream map '{}' matches no streams.\n\
666                     To ignore this, add a trailing '?' to the map.",
667                    linklabel
668                );
669                return Err(Error::OpenOutput(
670                    OpenOutputError::MatchesNoStreams(linklabel.to_string())
671                ));
672            }
673        }
674    }
675
676    Ok(())
677}
678
679fn outputs_bind(
680    muxs: &mut Vec<Muxer>,
681    filter_graphs: &mut Vec<FilterGraph>,
682    demuxs: &mut Vec<Demuxer>,
683) -> Result<()> {
684    // FFmpeg reference: ffmpeg.c calls opt_map during command-line parsing
685    // We parse and expand stream maps early, before processing individual streams
686    // This must happen AFTER demuxers are opened (need stream info for matching)
687    // but BEFORE map_manual() is called (which uses the expanded StreamMap entries)
688    unsafe {
689        for mux in muxs.iter_mut() {
690            if !mux.stream_map_specs.is_empty() {
691                expand_stream_maps(mux, demuxs)?;
692            }
693        }
694    }
695
696    for (i, mux) in muxs.iter_mut().enumerate() {
697        if mux.stream_maps.is_empty() {
698            // Initialize auto_disable with muxer's stream disable flags
699            // FFmpeg reference: fftools/ffmpeg_mux_init.c:1891-1895
700            // auto_disable bitmask: 1 << AVMEDIA_TYPE_* disables that stream type
701            let mut auto_disable = 0i32;
702            if mux.video_disable {
703                auto_disable |= 1 << (AVMEDIA_TYPE_VIDEO as i32);
704            }
705            if mux.audio_disable {
706                auto_disable |= 1 << (AVMEDIA_TYPE_AUDIO as i32);
707            }
708            if mux.subtitle_disable {
709                auto_disable |= 1 << (AVMEDIA_TYPE_SUBTITLE as i32);
710            }
711            if mux.data_disable {
712                auto_disable |= 1 << (AVMEDIA_TYPE_DATA as i32);
713            }
714            output_bind_by_unlabeled_filter(i, mux, filter_graphs, &mut auto_disable)?;
715            /* pick the first stream of each type */
716            map_auto_streams(i, mux, demuxs, filter_graphs, auto_disable)?;
717        } else {
718            for stream_map in mux.stream_maps.clone() {
719                map_manual(i, mux, &stream_map, filter_graphs, demuxs)?;
720            }
721        }
722
723        //TODO add_attachments
724
725        // Process metadata
726        unsafe {
727            process_metadata(mux, demuxs)?;
728        }
729    }
730
731    Ok(())
732}
733
734fn map_manual(
735    index: usize,
736    mux: &mut Muxer,
737    stream_map: &StreamMap,
738    filter_graphs: &mut Vec<FilterGraph>,
739    demuxs: &mut Vec<Demuxer>,
740) -> Result<()> {
741    // FFmpeg reference: ffmpeg_mux_init.c:1720-1721 - check disabled flag
742    if stream_map.disabled {
743        return Ok(());
744    }
745
746    // FFmpeg reference: ffmpeg_mux_init.c:1723 - check for filter output
747    if let Some(linklabel) = &stream_map.linklabel {
748        // This is a filter graph output - match by linklabel
749        for filter_graph in filter_graphs.iter_mut() {
750            for i in 0..filter_graph.outputs.len() {
751                let option = {
752                    let output_filter = &filter_graph.outputs[i];
753                    if output_filter.has_dst()
754                        || output_filter.linklabel.is_empty()
755                        || &output_filter.linklabel != linklabel
756                    {
757                        continue;
758                    }
759
760                    choose_encoder(mux, output_filter.media_type)?
761                };
762
763                match option {
764                    None => {
765                        // FFmpeg reference: ffmpeg_mux_init.c:1237-1242
766                        // Filtering and streamcopy cannot be used together
767                        error!(
768                            "Filtering and streamcopy cannot be used together. \
769                             No encoder available for filter output type {:?}.",
770                            filter_graph.outputs[i].media_type
771                        );
772                        return Err(OpenOutputError::InvalidArgument.into());
773                    }
774                    Some((codec_id, enc)) => {
775                        return ofilter_bind_ost(
776                            index,
777                            mux,
778                            filter_graph,
779                            i,
780                            codec_id,
781                            enc,
782                            None,
783                            false,
784                        )
785                        .map(|_| ());
786                    }
787                }
788            }
789        }
790
791        // FFmpeg reference: ffmpeg_mux_init.c:1740-1742 - filter output not found error
792        warn!(
793            "Output with label '{}' does not exist in any defined filter graph, \
794             or was already used elsewhere.",
795            linklabel
796        );
797        return Err(OpenOutputError::InvalidArgument.into());
798    }
799
800    // This is an input file stream - use pre-parsed file_index and stream_index
801    // These were already validated and expanded in expand_stream_maps()
802    let demux_idx = stream_map.file_index;
803    let stream_index = stream_map.stream_index;
804
805    let demux = &mut demuxs[demux_idx];
806
807    // Get immutable data first to avoid borrow checker issues
808    let demux_node = demux.node.clone();
809    let (media_type, input_stream_duration, input_stream_time_base) = {
810        let input_stream = demux.get_stream_mut(stream_index);
811        (input_stream.codec_type, input_stream.duration, input_stream.time_base)
812    };
813
814    // FFmpeg reference: fftools/ffmpeg_mux_init.c:1761-1768
815    // Check stream disable flags for manual mapping
816    // If a stream type is disabled, skip mapping even if explicitly requested
817    match media_type {
818        AVMEDIA_TYPE_VIDEO if mux.video_disable => {
819            info!("Skipping video stream mapping (video_disable=true)");
820            return Ok(());
821        }
822        AVMEDIA_TYPE_AUDIO if mux.audio_disable => {
823            info!("Skipping audio stream mapping (audio_disable=true)");
824            return Ok(());
825        }
826        AVMEDIA_TYPE_SUBTITLE if mux.subtitle_disable => {
827            info!("Skipping subtitle stream mapping (subtitle_disable=true)");
828            return Ok(());
829        }
830        AVMEDIA_TYPE_DATA if mux.data_disable => {
831            info!("Skipping data stream mapping (data_disable=true)");
832            return Ok(());
833        }
834        _ => {}
835    }
836
837    info!(
838        "Binding output stream to input {}:{} ({})",
839        demux_idx, stream_index,
840        match media_type {
841            AVMEDIA_TYPE_VIDEO => "video",
842            AVMEDIA_TYPE_AUDIO => "audio",
843            AVMEDIA_TYPE_SUBTITLE => "subtitle",
844            AVMEDIA_TYPE_DATA => "data",
845            AVMEDIA_TYPE_ATTACHMENT => "attachment",
846            _ => "unknown",
847        }
848    );
849
850    let option = choose_encoder(mux, media_type)?;
851
852    match option {
853        None => {
854            // copy
855            let (packet_sender, _st, output_stream_index) = mux.new_stream(demux_node)?;
856            demux.add_packet_dst(packet_sender, stream_index, output_stream_index);
857            mux.register_stream_source(output_stream_index, demux_idx, stream_index, false);
858
859            unsafe {
860                streamcopy_init(
861                    mux,
862                    *(*demux.in_fmt_ctx_ptr()).streams.add(stream_index),
863                    *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
864                    demux.framerate,
865                )?;
866                rescale_duration(
867                    input_stream_duration,
868                    input_stream_time_base,
869                    *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
870                );
871                mux.stream_ready()
872            }
873        }
874        Some((codec_id, enc)) => {
875            // connect input_stream to output
876            if stream_map.copy {
877                // copy
878                let (packet_sender, _st, output_stream_index) = mux.new_stream(demux_node)?;
879                demux.add_packet_dst(packet_sender, stream_index, output_stream_index);
880                mux.register_stream_source(output_stream_index, demux_idx, stream_index, false);
881
882                unsafe {
883                    streamcopy_init(
884                        mux,
885                        *(*demux.in_fmt_ctx_ptr()).streams.add(stream_index),
886                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
887                        demux.framerate,
888                    )?;
889                    rescale_duration(
890                        input_stream_duration,
891                        input_stream_time_base,
892                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
893                    );
894                    mux.stream_ready()
895                }
896            } else if media_type == AVMEDIA_TYPE_VIDEO || media_type == AVMEDIA_TYPE_AUDIO {
897                init_simple_filtergraph(
898                    demux,
899                    stream_index,
900                    codec_id,
901                    enc,
902                    index,
903                    mux,
904                    filter_graphs,
905                    demux_idx,
906                )?;
907            } else {
908                let (frame_sender, output_stream_index) =
909                    mux.add_enc_stream(media_type, enc, demux_node, false)?;
910                let input_stream = demux.get_stream_mut(stream_index);
911                input_stream.add_dst(frame_sender);
912                demux.connect_stream(stream_index);
913                mux.register_stream_source(output_stream_index, demux_idx, stream_index, true);
914
915                unsafe {
916                    rescale_duration(
917                        input_stream_duration,
918                        input_stream_time_base,
919                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
920                    );
921                }
922            }
923        }
924    }
925
926    Ok(())
927}
928
929#[cfg(not(docsrs))]
930fn set_channel_layout(
931    ch_layout: &mut AVChannelLayout,
932    ch_layouts: &Option<Vec<AVChannelLayout>>,
933    layout_requested: &AVChannelLayout,
934) -> Result<()> {
935    unsafe {
936        // Scenario 1: If the requested layout has a specified order (not UNSPEC), copy it directly
937        if layout_requested.order != AV_CHANNEL_ORDER_UNSPEC {
938            let ret = av_channel_layout_copy(ch_layout, layout_requested);
939            if ret < 0 {
940                return Err(OpenOutputError::from(ret).into());
941            }
942            return Ok(());
943        }
944
945        // Scenario 2: Requested layout is UNSPEC and no encoder-supported layouts available
946        // Use default layout based on channel count
947        if ch_layouts.is_none() {
948            av_channel_layout_default(ch_layout, layout_requested.nb_channels);
949            return Ok(());
950        }
951
952        // Scenario 3: Try to match channel count from encoder's supported layouts
953        if let Some(layouts) = ch_layouts {
954            for layout in layouts {
955                if layout.nb_channels == layout_requested.nb_channels {
956                    let ret = av_channel_layout_copy(ch_layout, layout);
957                    if ret < 0 {
958                        return Err(OpenOutputError::from(ret).into());
959                    }
960                    return Ok(());
961                }
962            }
963        }
964
965        // Scenario 4: No matching channel count found, use default layout
966        av_channel_layout_default(ch_layout, layout_requested.nb_channels);
967        Ok(())
968    }
969}
970
971#[cfg(docsrs)]
972fn configure_output_filter_opts(
973    index: usize,
974    mux: &mut Muxer,
975    output_filter: &mut OutputFilter,
976    codec_id: AVCodecID,
977    enc: *const AVCodec,
978    output_stream_index: usize,
979) -> Result<()> {
980    Ok(())
981}
982
983#[cfg(not(docsrs))]
984fn configure_output_filter_opts(
985    index: usize,
986    mux: &mut Muxer,
987    output_filter: &mut OutputFilter,
988    codec_id: AVCodecID,
989    enc: *const AVCodec,
990    output_stream_index: usize,
991) -> Result<()> {
992    unsafe {
993        output_filter.opts.name = format!("#{index}:{output_stream_index}");
994        output_filter.opts.enc = enc;
995        output_filter.opts.trim_start_us = mux.start_time_us;
996        output_filter.opts.trim_duration_us = mux.recording_time_us;
997        output_filter.opts.ts_offset = mux.start_time_us;
998
999        output_filter.opts.flags = OFILTER_FLAG_DISABLE_CONVERT
1000            | OFILTER_FLAG_AUTOSCALE
1001            | if av_get_exact_bits_per_sample(codec_id) == 24 {
1002                OFILTER_FLAG_AUDIO_24BIT
1003            } else {
1004                0
1005            };
1006
1007        let enc_ctx = avcodec_alloc_context3(enc);
1008        if enc_ctx.is_null() {
1009            return Err(OpenOutputError::OutOfMemory.into());
1010        }
1011        let _codec_ctx = CodecContext::new(enc_ctx);
1012
1013        (*enc_ctx).thread_count = 0;
1014
1015        if output_filter.media_type == AVMEDIA_TYPE_VIDEO {
1016            // formats
1017            let mut formats: *const AVPixelFormat = null();
1018            let mut ret = avcodec_get_supported_config(
1019                enc_ctx,
1020                null(),
1021                AV_CODEC_CONFIG_PIX_FORMAT,
1022                0,
1023                &mut formats as *mut _ as *mut *const libc::c_void,
1024                null_mut(),
1025            );
1026            if ret < 0 {
1027                return Err(OpenOutputError::from(ret).into());
1028            }
1029
1030            let mut current = formats;
1031            let mut format_list = Vec::new();
1032            let mut count = 0;
1033            const MAX_FORMATS: usize = 512;
1034            while !current.is_null() && *current != AV_PIX_FMT_NONE && count < MAX_FORMATS {
1035                format_list.push(*current);
1036                current = current.add(1);
1037                count += 1;
1038            }
1039            if count >= MAX_FORMATS {
1040                warn!("Reached maximum format limit");
1041            }
1042            output_filter.opts.formats = Some(format_list);
1043
1044            // framerates
1045            let mut framerates: *const AVRational = null();
1046            ret = avcodec_get_supported_config(
1047                enc_ctx,
1048                null(),
1049                AV_CODEC_CONFIG_FRAME_RATE,
1050                0,
1051                &mut framerates as *mut _ as *mut *const libc::c_void,
1052                null_mut(),
1053            );
1054            if ret < 0 {
1055                return Err(OpenOutputError::from(ret).into());
1056            }
1057            let mut framerate_list = Vec::new();
1058            let mut current = framerates;
1059            let mut count = 0;
1060            const MAX_FRAMERATES: usize = 64;
1061            while !current.is_null()
1062                && (*current).num != 0
1063                && (*current).den != 0
1064                && count < MAX_FRAMERATES
1065            {
1066                framerate_list.push(*current);
1067                current = current.add(1);
1068                count += 1;
1069            }
1070            if count >= MAX_FRAMERATES {
1071                warn!("Reached maximum framerate limit");
1072            }
1073            output_filter.opts.framerates = Some(framerate_list);
1074
1075            if let Some(framerate) = mux.framerate {
1076                output_filter.opts.framerate = framerate;
1077            }
1078
1079            // -fpsmax: only one of -r/-fpsmax may be set per stream
1080            // (ffmpeg_mux_init.c:618-621).
1081            if let Some(framerate_max) = mux.framerate_max {
1082                if mux.framerate.is_some() {
1083                    error!("Only one of framerate and framerate_max can be set for an output");
1084                    return Err(Error::OpenOutput(OpenOutputError::InvalidArgument));
1085                }
1086                output_filter.opts.framerate_max = framerate_max;
1087            }
1088
1089            // Set user-requested pixel format (equivalent to -pix_fmt in FFmpeg)
1090            // FFmpeg reference: fftools/ffmpeg_filter.c - ofilter_bind_ost sets format from OptionsContext
1091            if let Some(pix_fmt) = mux.pix_fmt {
1092                output_filter.opts.format = pix_fmt;
1093            }
1094
1095            // color_spaces
1096            let mut color_spaces: *const AVColorSpace = null();
1097            ret = avcodec_get_supported_config(
1098                enc_ctx,
1099                null(),
1100                AV_CODEC_CONFIG_COLOR_SPACE,
1101                0,
1102                &mut color_spaces as *mut _ as *mut *const libc::c_void,
1103                null_mut(),
1104            );
1105            if ret < 0 {
1106                return Err(OpenOutputError::from(ret).into());
1107            }
1108            let mut color_space_list = Vec::new();
1109            let mut current = color_spaces;
1110            let mut count = 0;
1111            const MAX_COLOR_SPACES: usize = 128;
1112            while !current.is_null()
1113                && *current != AVCOL_SPC_UNSPECIFIED
1114                && count < MAX_COLOR_SPACES
1115            {
1116                color_space_list.push(*current);
1117                current = current.add(1);
1118                count += 1;
1119            }
1120            if count >= MAX_COLOR_SPACES {
1121                warn!("Reached maximum color space limit");
1122            }
1123            output_filter.opts.color_spaces = Some(color_space_list);
1124
1125            //color_ranges
1126            let mut color_ranges: *const AVColorRange = null();
1127            ret = avcodec_get_supported_config(
1128                enc_ctx,
1129                null(),
1130                AV_CODEC_CONFIG_COLOR_RANGE,
1131                0,
1132                &mut color_ranges as *mut _ as *mut *const libc::c_void,
1133                null_mut(),
1134            );
1135            if ret < 0 {
1136                return Err(OpenOutputError::from(ret).into());
1137            }
1138            let mut color_range_list = Vec::new();
1139            let mut current = color_ranges;
1140            let mut count = 0;
1141            const MAX_COLOR_RANGES: usize = 64;
1142            while !current.is_null()
1143                && *current != AVCOL_RANGE_UNSPECIFIED
1144                && count < MAX_COLOR_RANGES
1145            {
1146                color_range_list.push(*current);
1147                current = current.add(1);
1148                count += 1;
1149            }
1150            if count >= MAX_COLOR_RANGES {
1151                warn!("Reached maximum color range limit");
1152            }
1153            output_filter.opts.color_ranges = Some(color_range_list);
1154
1155            let stream = &mux.get_streams()[output_stream_index];
1156            output_filter.opts.vsync_method = stream.vsync_method;
1157        } else {
1158            if let Some(sample_fmt) = &mux.audio_sample_fmt {
1159                output_filter.opts.audio_format = *sample_fmt;
1160            }
1161            // audio formats
1162            let mut audio_formats: *const AVSampleFormat = null();
1163            let mut ret = avcodec_get_supported_config(
1164                enc_ctx,
1165                null(),
1166                AV_CODEC_CONFIG_SAMPLE_FORMAT,
1167                0,
1168                &mut audio_formats as *mut _ as *mut _,
1169                null_mut(),
1170            );
1171            if ret < 0 {
1172                return Err(OpenOutputError::from(ret).into());
1173            }
1174
1175            let mut current = audio_formats;
1176            let mut audio_format_list = Vec::new();
1177            let mut count = 0;
1178            const MAX_AUDIO_FORMATS: usize = 32;
1179            while !current.is_null() && *current != AV_SAMPLE_FMT_NONE && count < MAX_AUDIO_FORMATS
1180            {
1181                audio_format_list.push(*current);
1182                current = current.add(1);
1183                count += 1;
1184            }
1185            if count >= MAX_AUDIO_FORMATS {
1186                warn!("Reached maximum audio format limit");
1187            }
1188            output_filter.opts.audio_formats = Some(audio_format_list);
1189
1190            if let Some(audio_sample_rate) = &mux.audio_sample_rate {
1191                output_filter.opts.sample_rate = *audio_sample_rate;
1192            }
1193            // sample_rates
1194            let mut rates: *const i32 = null();
1195            ret = avcodec_get_supported_config(
1196                enc_ctx,
1197                null(),
1198                AV_CODEC_CONFIG_SAMPLE_RATE,
1199                0,
1200                &mut rates as *mut _ as *mut _,
1201                null_mut(),
1202            );
1203            if ret < 0 {
1204                return Err(OpenOutputError::from(ret).into());
1205            }
1206            let mut rate_list = Vec::new();
1207            let mut current = rates;
1208            let mut count = 0;
1209            const MAX_SAMPLE_RATES: usize = 64;
1210            while !current.is_null() && *current != 0 && count < MAX_SAMPLE_RATES {
1211                rate_list.push(*current);
1212                current = current.add(1);
1213                count += 1;
1214            }
1215            if count >= MAX_SAMPLE_RATES {
1216                warn!("Reached maximum sample rate limit");
1217            }
1218            output_filter.opts.sample_rates = Some(rate_list);
1219
1220            if let Some(channels) = &mux.audio_channels {
1221                output_filter.opts.ch_layout.nb_channels = *channels;
1222            }
1223            // channel_layouts
1224            let mut layouts: *const AVChannelLayout = null();
1225            ret = avcodec_get_supported_config(
1226                enc_ctx,
1227                null(),
1228                AV_CODEC_CONFIG_CHANNEL_LAYOUT,
1229                0,
1230                &mut layouts as *mut _ as *mut _,
1231                null_mut(),
1232            );
1233            if ret < 0 {
1234                return Err(OpenOutputError::from(ret).into());
1235            }
1236            let mut layout_list = Vec::new();
1237            let mut current = layouts;
1238            let mut count = 0;
1239            const MAX_CHANNEL_LAYOUTS: usize = 128;
1240            while !current.is_null()
1241                && (*current).order != AV_CHANNEL_ORDER_UNSPEC
1242                && count < MAX_CHANNEL_LAYOUTS
1243            {
1244                layout_list.push(*current);
1245                current = current.add(1);
1246                count += 1;
1247            }
1248            if count >= MAX_CHANNEL_LAYOUTS {
1249                warn!("Reached maximum channel layout limit");
1250            }
1251            output_filter.opts.ch_layouts = Some(layout_list);
1252
1253            // Call set_channel_layout to resolve UNSPEC layouts to proper defaults
1254            // Corresponds to FFmpeg ffmpeg_filter.c:879-882
1255            if output_filter.opts.ch_layout.nb_channels > 0 {
1256                let layout_requested = output_filter.opts.ch_layout;
1257                set_channel_layout(
1258                    &mut output_filter.opts.ch_layout,
1259                    &output_filter.opts.ch_layouts,
1260                    &layout_requested,
1261                )?;
1262            }
1263        }
1264    };
1265    Ok(())
1266}
1267
1268fn map_auto_streams(
1269    mux_index: usize,
1270    mux: &mut Muxer,
1271    demuxs: &mut Vec<Demuxer>,
1272    filter_graphs: &mut Vec<FilterGraph>,
1273    auto_disable: i32,
1274) -> Result<()> {
1275    unsafe {
1276        let oformat = (*mux.out_fmt_ctx_ptr()).oformat;
1277        map_auto_stream(
1278            mux_index,
1279            mux,
1280            demuxs,
1281            oformat,
1282            AVMEDIA_TYPE_VIDEO,
1283            filter_graphs,
1284            auto_disable,
1285        )?;
1286        map_auto_stream(
1287            mux_index,
1288            mux,
1289            demuxs,
1290            oformat,
1291            AVMEDIA_TYPE_AUDIO,
1292            filter_graphs,
1293            auto_disable,
1294        )?;
1295        map_auto_subtitle(mux, demuxs, oformat, auto_disable)?;
1296        map_auto_data(mux, demuxs, oformat, auto_disable)?;
1297    }
1298    Ok(())
1299}
1300
1301#[cfg(docsrs)]
1302unsafe fn map_auto_subtitle(
1303    mux: &mut Muxer,
1304    demuxs: &mut Vec<Demuxer>,
1305    oformat: *const AVOutputFormat,
1306    auto_disable: i32,
1307) -> Result<()> {
1308    Ok(())
1309}
1310
1311#[cfg(not(docsrs))]
1312unsafe fn map_auto_subtitle(
1313    mux: &mut Muxer,
1314    demuxs: &mut Vec<Demuxer>,
1315    oformat: *const AVOutputFormat,
1316    auto_disable: i32,
1317) -> Result<()> {
1318    if auto_disable & (1 << AVMEDIA_TYPE_SUBTITLE as i32) != 0 {
1319        return Ok(());
1320    }
1321
1322    // An explicit user codec (including "copy") must not be gated on the
1323    // container's default subtitle encoder being available
1324    // (matches ffmpeg_mux_init.c:1660 `!avcodec_find_encoder(...) && !subtitle_codec_name`).
1325    let output_codec = avcodec_find_encoder((*oformat).subtitle_codec);
1326    if output_codec.is_null() && mux.subtitle_codec.is_none() {
1327        return Ok(());
1328    }
1329    let output_descriptor = if output_codec.is_null() {
1330        null()
1331    } else {
1332        avcodec_descriptor_get((*output_codec).id)
1333    };
1334
1335    // Scan every subtitle stream of every input and map the first compatible
1336    // one (matches ffmpeg_mux_init.c:1701-1725, which iterates all input
1337    // streams and only stops after a successful mapping — an incompatible
1338    // first track, e.g. PGS before srt, must not end the search).
1339    for (demux_idx, demux) in demuxs.iter_mut().enumerate() {
1340        for stream_index in 0..demux.get_streams().len() {
1341            if demux.get_stream(stream_index).codec_type != AVMEDIA_TYPE_SUBTITLE {
1342                continue;
1343            }
1344
1345            let input_descriptor =
1346                avcodec_descriptor_get((*demux.get_stream(stream_index).codec_parameters).codec_id);
1347            let mut input_props = 0;
1348            if !input_descriptor.is_null() {
1349                input_props =
1350                    (*input_descriptor).props & (AV_CODEC_PROP_TEXT_SUB | AV_CODEC_PROP_BITMAP_SUB);
1351            }
1352            let mut output_props = 0;
1353            if !output_descriptor.is_null() {
1354                output_props =
1355                    (*output_descriptor).props & (AV_CODEC_PROP_TEXT_SUB | AV_CODEC_PROP_BITMAP_SUB);
1356            }
1357
1358            // A user-chosen codec short-circuits the text/bitmap check
1359            // (matches ffmpeg_mux_init.c:1679 `subtitle_codec_name || ...`).
1360            let compatible = mux.subtitle_codec.is_some()
1361                || input_props & output_props != 0
1362                // Map dvb teletext which has neither property to any output subtitle encoder
1363                || !input_descriptor.is_null() && !output_descriptor.is_null()
1364                    && ((*input_descriptor).props == 0 || (*output_descriptor).props == 0);
1365            if !compatible {
1366                continue;
1367            }
1368
1369            // choose_encoder returns None for "copy": take the streamcopy
1370            // path like map_auto_stream instead of failing.
1371            let option = choose_encoder(mux, AVMEDIA_TYPE_SUBTITLE)?;
1372            if let Some((_codec_id, enc)) = option {
1373                let (frame_sender, output_stream_index) =
1374                    mux.add_enc_stream(AVMEDIA_TYPE_SUBTITLE, enc, demux.node.clone(), false)?;
1375                demux.get_stream_mut(stream_index).add_dst(frame_sender);
1376                demux.connect_stream(stream_index);
1377                mux.register_stream_source(output_stream_index, demux_idx, stream_index, true);
1378                let input_stream = demux.get_stream(stream_index);
1379                unsafe {
1380                    rescale_duration(
1381                        input_stream.duration,
1382                        input_stream.time_base,
1383                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1384                    );
1385                }
1386            } else {
1387                let input_stream = demux.get_stream(stream_index);
1388                let input_stream_duration = input_stream.duration;
1389                let input_stream_time_base = input_stream.time_base;
1390
1391                let (packet_sender, _st, output_stream_index) =
1392                    mux.new_stream(demux.node.clone())?;
1393                demux.add_packet_dst(packet_sender, stream_index, output_stream_index);
1394                mux.register_stream_source(output_stream_index, demux_idx, stream_index, false);
1395
1396                unsafe {
1397                    streamcopy_init(
1398                        mux,
1399                        *(*demux.in_fmt_ctx_ptr()).streams.add(stream_index),
1400                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1401                        demux.framerate,
1402                    )?;
1403                    rescale_duration(
1404                        input_stream_duration,
1405                        input_stream_time_base,
1406                        *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1407                    );
1408                    mux.stream_ready()
1409                }
1410            }
1411            return Ok(());
1412        }
1413    }
1414
1415    Ok(())
1416}
1417
1418#[cfg(docsrs)]
1419unsafe fn map_auto_data(
1420    mux: &mut Muxer,
1421    demuxs: &mut Vec<Demuxer>,
1422    oformat: *const AVOutputFormat,
1423    auto_disable: i32,
1424) -> Result<()> {
1425    Ok(())
1426}
1427
1428#[cfg(not(docsrs))]
1429unsafe fn map_auto_data(
1430    mux: &mut Muxer,
1431    demuxs: &mut Vec<Demuxer>,
1432    oformat: *const AVOutputFormat,
1433    auto_disable: i32,
1434) -> Result<()> {
1435    if auto_disable & (1 << AVMEDIA_TYPE_DATA as i32) != 0 {
1436        return Ok(());
1437    }
1438
1439    /* Data only if codec id match */
1440    let codec_id = av_guess_codec(
1441        oformat,
1442        null(),
1443        (*mux.out_fmt_ctx_ptr()).url,
1444        null(),
1445        AVMEDIA_TYPE_DATA,
1446    );
1447
1448    if codec_id == AV_CODEC_ID_NONE {
1449        return Ok(());
1450    }
1451
1452    for (demux_idx, demux) in demuxs.iter_mut().enumerate() {
1453        let option = demux
1454            .get_streams()
1455            .iter()
1456            .enumerate()
1457            .find_map(|(index, input_stream)| {
1458                if input_stream.codec_type == AVMEDIA_TYPE_DATA
1459                    && (*input_stream.codec_parameters).codec_id == codec_id
1460                {
1461                    Some(index)
1462                } else {
1463                    None
1464                }
1465            });
1466
1467        if option.is_none() {
1468            continue;
1469        }
1470
1471        let stream_index = option.unwrap();
1472        let option = choose_encoder(mux, AVMEDIA_TYPE_DATA)?;
1473
1474        if option.is_some() {
1475            // FFmpeg reference: ffmpeg_mux_init.c:79-89
1476            // choose_encoder always returns enc=NULL for AVMEDIA_TYPE_DATA
1477            unreachable!("DATA streams do not have encoders in FFmpeg");
1478        } else {
1479            // FFmpeg behavior: DATA streams use stream copy when no encoder is available
1480            // Reference: fftools/ffmpeg_mux_init.c:79-89 - choose_encoder returns enc=NULL for DATA
1481            // Reference: fftools/ffmpeg_mux_init.c:1236-1246 - ost_add uses stream copy when enc=NULL
1482            let input_stream = demux.get_stream(stream_index);
1483            let input_stream_duration = input_stream.duration;
1484            let input_stream_time_base = input_stream.time_base;
1485
1486            let (packet_sender, _st, output_stream_index) = mux.new_stream(demux.node.clone())?;
1487            demux.add_packet_dst(packet_sender, stream_index, output_stream_index);
1488            mux.register_stream_source(output_stream_index, demux_idx, stream_index, false);
1489
1490            streamcopy_init(
1491                mux,
1492                *(*demux.in_fmt_ctx_ptr()).streams.add(stream_index),
1493                *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1494                demux.framerate,
1495            )?;
1496            rescale_duration(
1497                input_stream_duration,
1498                input_stream_time_base,
1499                *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1500            );
1501            mux.stream_ready()
1502        }
1503
1504        break;
1505    }
1506
1507    Ok(())
1508}
1509
1510#[cfg(docsrs)]
1511unsafe fn map_auto_stream(
1512    mux_index: usize,
1513    mux: &mut Muxer,
1514    demuxs: &mut Vec<Demuxer>,
1515    oformat: *const AVOutputFormat,
1516    media_type: AVMediaType,
1517    filter_graphs: &mut Vec<FilterGraph>,
1518    auto_disable: i32,
1519) -> Result<()> {
1520    Ok(())
1521}
1522
1523#[cfg(not(docsrs))]
1524unsafe fn map_auto_stream(
1525    mux_index: usize,
1526    mux: &mut Muxer,
1527    demuxs: &mut Vec<Demuxer>,
1528    oformat: *const AVOutputFormat,
1529    media_type: AVMediaType,
1530    filter_graphs: &mut Vec<FilterGraph>,
1531    auto_disable: i32,
1532) -> Result<()> {
1533    if auto_disable & (1 << media_type as i32) != 0 {
1534        return Ok(());
1535    }
1536    if (media_type == AVMEDIA_TYPE_VIDEO
1537        || media_type == AVMEDIA_TYPE_AUDIO
1538        || media_type == AVMEDIA_TYPE_DATA)
1539        && av_guess_codec(oformat, null(), (*mux.out_fmt_ctx_ptr()).url, null(), media_type)
1540            == AV_CODEC_ID_NONE
1541        {
1542            return Ok(());
1543        }
1544
1545    // Mirror ffmpeg_mux_init.c map_auto_video/map_auto_audio: score every
1546    // stream of every input and map the single global best (video: highest
1547    // resolution; audio: most channels). Other types keep first-found.
1548    let selected = if media_type == AVMEDIA_TYPE_VIDEO || media_type == AVMEDIA_TYPE_AUDIO {
1549        unsafe { select_best_stream(demuxs, oformat, media_type) }
1550    } else {
1551        demuxs.iter().enumerate().find_map(|(demux_idx, demux)| {
1552            demux
1553                .get_streams()
1554                .iter()
1555                .position(|input_stream| input_stream.codec_type == media_type)
1556                .map(|stream_index| (demux_idx, stream_index))
1557        })
1558    };
1559    let Some((demux_idx, stream_index)) = selected else {
1560        return Ok(());
1561    };
1562
1563    let demux = &mut demuxs[demux_idx];
1564    let input_file_idx = demux_idx;
1565    let option = choose_encoder(mux, media_type)?;
1566
1567    if let Some((codec_id, enc)) = option {
1568        if media_type == AVMEDIA_TYPE_VIDEO || media_type == AVMEDIA_TYPE_AUDIO {
1569            init_simple_filtergraph(
1570                demux,
1571                stream_index,
1572                codec_id,
1573                enc,
1574                mux_index,
1575                mux,
1576                filter_graphs,
1577                input_file_idx,
1578            )?;
1579        } else {
1580            let (frame_sender, output_stream_index) =
1581                mux.add_enc_stream(media_type, enc, demux.node.clone(), false)?;
1582            demux.get_stream_mut(stream_index).add_dst(frame_sender);
1583            demux.connect_stream(stream_index);
1584            mux.register_stream_source(output_stream_index, input_file_idx, stream_index, true);
1585            let input_stream = demux.get_stream(stream_index);
1586            unsafe {
1587                rescale_duration(
1588                    input_stream.duration,
1589                    input_stream.time_base,
1590                    *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1591                );
1592            }
1593        }
1594
1595        return Ok(());
1596    }
1597
1598    // copy: like the encoder branch, the CLI maps exactly one stream per
1599    // media type.
1600    let input_stream = demux.get_stream(stream_index);
1601    let input_stream_duration = input_stream.duration;
1602    let input_stream_time_base = input_stream.time_base;
1603
1604    let (packet_sender, _st, output_stream_index) = mux.new_stream(demux.node.clone())?;
1605    demux.add_packet_dst(packet_sender, stream_index, output_stream_index);
1606    mux.register_stream_source(output_stream_index, input_file_idx, stream_index, false);
1607
1608    unsafe {
1609        streamcopy_init(
1610            mux,
1611            *(*demux.in_fmt_ctx_ptr()).streams.add(stream_index),
1612            *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1613            demux.framerate,
1614        )?;
1615        rescale_duration(
1616            input_stream_duration,
1617            input_stream_time_base,
1618            *(*mux.out_fmt_ctx_ptr()).streams.add(output_stream_index),
1619        );
1620        mux.stream_ready()
1621    }
1622
1623    Ok(())
1624}
1625
1626/// Pick the globally best video/audio stream across all inputs
1627/// (ffmpeg_mux_init.c map_auto_video:1539 / map_auto_audio:1648): video is
1628/// scored by resolution, audio by channel count, both raised by
1629/// NEW_PACKETS/DEFAULT-disposition bonuses; attached pictures only win where
1630/// the container itself is picture-based (avformat_query_codec == 'APIC').
1631#[cfg(not(docsrs))]
1632unsafe fn select_best_stream(
1633    demuxs: &[Demuxer],
1634    oformat: *const AVOutputFormat,
1635    media_type: AVMediaType,
1636) -> Option<(usize, usize)> {
1637    const APIC_TAG: i32 =
1638        (b'A' as i32) | ((b'P' as i32) << 8) | ((b'I' as i32) << 16) | ((b'C' as i32) << 24);
1639    let qcr = if media_type == AVMEDIA_TYPE_VIDEO {
1640        avformat_query_codec(oformat, (*oformat).video_codec, 0)
1641    } else {
1642        0
1643    };
1644
1645    let mut best: Option<(usize, usize)> = None;
1646    let mut best_score: i64 = 0;
1647
1648    for (demux_idx, demux) in demuxs.iter().enumerate() {
1649        let mut file_best: Option<usize> = None;
1650        let mut file_best_score: i64 = 0;
1651
1652        for (stream_index, input_stream) in demux.get_streams().iter().enumerate() {
1653            if input_stream.codec_type != media_type {
1654                continue;
1655            }
1656            // The CLI also skips AV_CODEC_PROP_ENHANCEMENT streams (LCEVC
1657            // enhancement layers); ffmpeg-sys-next 7.1 has no binding for
1658            // that flag yet, so the check is deferred to the bindings bump.
1659
1660            let par = input_stream.codec_parameters;
1661            let st = input_stream.stream.inner;
1662            let attached_pic = (*st).disposition & AV_DISPOSITION_ATTACHED_PIC != 0;
1663
1664            let mut score: i64 = if media_type == AVMEDIA_TYPE_VIDEO {
1665                (*par).width as i64 * (*par).height as i64
1666            } else {
1667                (*par).ch_layout.nb_channels as i64
1668            };
1669            if (*st).event_flags & AVSTREAM_EVENT_FLAG_NEW_PACKETS != 0 {
1670                score += 100_000_000;
1671            }
1672            if (*st).disposition & AV_DISPOSITION_DEFAULT != 0 {
1673                score += 5_000_000;
1674            }
1675            if media_type == AVMEDIA_TYPE_VIDEO && qcr != APIC_TAG && attached_pic {
1676                // Cover art only wins when nothing else is available.
1677                score = 1;
1678            }
1679
1680            if score > file_best_score {
1681                if media_type == AVMEDIA_TYPE_VIDEO && qcr == APIC_TAG && !attached_pic {
1682                    // This container carries video only as an attached picture.
1683                    continue;
1684                }
1685                file_best_score = score;
1686                file_best = Some(stream_index);
1687            }
1688        }
1689
1690        if let Some(stream_index) = file_best {
1691            let st = demux.get_stream(stream_index).stream.inner;
1692            let attached_pic = (*st).disposition & AV_DISPOSITION_ATTACHED_PIC != 0;
1693            // The DEFAULT-disposition bonus ranks streams within one file
1694            // only; drop it before comparing across files.
1695            if (media_type != AVMEDIA_TYPE_VIDEO || qcr == APIC_TAG || !attached_pic)
1696                && (*st).disposition & AV_DISPOSITION_DEFAULT != 0
1697            {
1698                file_best_score -= 5_000_000;
1699            }
1700            if file_best_score > best_score {
1701                best_score = file_best_score;
1702                best = Some((demux_idx, stream_index));
1703            }
1704        }
1705    }
1706
1707    best
1708}
1709
1710fn init_simple_filtergraph(
1711    demux: &mut Demuxer,
1712    stream_index: usize,
1713    codec_id: AVCodecID,
1714    enc: *const AVCodec,
1715    mux_index: usize,
1716    mux: &mut Muxer,
1717    filter_graphs: &mut Vec<FilterGraph>,
1718    input_file_idx: usize,
1719) -> Result<()> {
1720    let codec_type = demux.get_stream(stream_index).codec_type;
1721
1722    let filter_desc = if codec_type == AVMEDIA_TYPE_VIDEO {
1723        "null"
1724    } else {
1725        "anull"
1726    };
1727    let mut filter_graph = init_filter_graph(filter_graphs.len(), filter_desc, None)?;
1728
1729    // filter_graph.inputs[0].media_type = codec_type;
1730    // filter_graph.outputs[0].media_type = codec_type;
1731
1732    ifilter_bind_ist(&mut filter_graph, 0, stream_index, demux)?;
1733    // fftools ost->ist rule: fed directly by a single-stream input
1734    // (ffmpeg_mux_init.c:817-822).
1735    let single_stream_direct_input = demux.get_streams().len() == 1;
1736    ofilter_bind_ost(
1737        mux_index,
1738        mux,
1739        &mut filter_graph,
1740        0,
1741        codec_id,
1742        enc,
1743        Some((input_file_idx, stream_index)),
1744        single_stream_direct_input,
1745    )?;
1746
1747    filter_graphs.push(filter_graph);
1748
1749    Ok(())
1750}
1751
1752unsafe fn rescale_duration(src_duration: i64, src_time_base: AVRational, stream: *mut AVStream) {
1753    (*stream).duration = av_rescale_q(src_duration, src_time_base, (*stream).time_base);
1754}
1755
1756#[cfg(docsrs)]
1757fn streamcopy_init(
1758    mux: &mut Muxer,
1759    input_stream: *mut AVStream,
1760    output_stream: *mut AVStream,
1761    input_framerate: AVRational,
1762) -> Result<()> {
1763    Ok(())
1764}
1765
1766#[cfg(not(docsrs))]
1767fn streamcopy_init(
1768    mux: &mut Muxer,
1769    input_stream: *mut AVStream,
1770    output_stream: *mut AVStream,
1771    input_framerate: AVRational,
1772) -> Result<()> {
1773    unsafe {
1774        let codec_ctx = avcodec_alloc_context3(null_mut());
1775        if codec_ctx.is_null() {
1776            return Err(OpenOutputError::OutOfMemory.into());
1777        }
1778        let _codec_context = CodecContext::new(codec_ctx);
1779
1780        let mut ret = avcodec_parameters_to_context(codec_ctx, (*input_stream).codecpar);
1781        if ret < 0 {
1782            error!("Error setting up codec context options.");
1783            return Err(OpenOutputError::from(ret).into());
1784        }
1785
1786        ret = avcodec_parameters_from_context((*output_stream).codecpar, codec_ctx);
1787        if ret < 0 {
1788            error!("Error getting reference codec parameters.");
1789            return Err(OpenOutputError::from(ret).into());
1790        }
1791
1792        // In the CLI this is the user's -tag value, 0 when absent; ez has no
1793        // per-stream tag option yet. Seeding it from the copied parameters
1794        // made the check below unreachable, so a source tag the target
1795        // container cannot represent (e.g. AVI's FMP4 into mp4) was kept and
1796        // avformat_write_header rejected it.
1797        let mut codec_tag = 0;
1798        if codec_tag == 0 {
1799            let ct = (*(*mux.out_fmt_ctx_ptr()).oformat).codec_tag;
1800            let mut codec_tag_tmp = 0;
1801            if ct.is_null()
1802                || av_codec_get_id(ct, (*(*output_stream).codecpar).codec_tag)
1803                    == (*(*output_stream).codecpar).codec_id
1804                || av_codec_get_tag2(
1805                    ct,
1806                    (*(*output_stream).codecpar).codec_id,
1807                    &mut codec_tag_tmp,
1808                ) == 0
1809            {
1810                codec_tag = (*(*output_stream).codecpar).codec_tag;
1811            }
1812        }
1813        (*(*output_stream).codecpar).codec_tag = codec_tag;
1814
1815        // Match FFmpeg CLI: framerate only applies to video streams.
1816        // In CLI, ist->framerate is only set for video (ffmpeg_demux.c:1429-1432, case AVMEDIA_TYPE_VIDEO)
1817        // and ost->frame_rate is only set in new_stream_video() (ffmpeg_mux_init.c:607).
1818        // Since ez-ffmpeg stores framerate per-file (not per-stream), we need an explicit guard
1819        // to prevent applying framerate to audio/subtitle streams in streamcopy mode.
1820        let codec_type = (*(*output_stream).codecpar).codec_type;
1821        let mut fr = AVRational { num: 0, den: 0 };
1822        if codec_type == AVMEDIA_TYPE_VIDEO {
1823            fr = mux.framerate.unwrap_or(AVRational { num: 0, den: 0 });
1824            if fr.num == 0 {
1825                fr = input_framerate;
1826            }
1827        }
1828
1829        if fr.num != 0 {
1830            (*output_stream).avg_frame_rate = fr;
1831        } else {
1832            (*output_stream).avg_frame_rate = (*input_stream).avg_frame_rate;
1833        }
1834
1835        // copy timebase while removing common factors
1836        if (*output_stream).time_base.num <= 0 || (*output_stream).time_base.den <= 0 {
1837            if fr.num != 0 {
1838                (*output_stream).time_base = av_inv_q(fr);
1839            } else {
1840                (*output_stream).time_base =
1841                    av_add_q((*input_stream).time_base, AVRational { num: 0, den: 1 });
1842            }
1843        }
1844
1845        for i in 0..(*(*input_stream).codecpar).nb_coded_side_data {
1846            let sd_src = (*(*input_stream).codecpar)
1847                .coded_side_data
1848                .offset(i as isize);
1849
1850            let sd_dst = av_packet_side_data_new(
1851                &mut (*(*output_stream).codecpar).coded_side_data,
1852                &mut (*(*output_stream).codecpar).nb_coded_side_data,
1853                (*sd_src).type_,
1854                (*sd_src).size,
1855                0,
1856            );
1857            if sd_dst.is_null() {
1858                return Err(OpenOutputError::OutOfMemory.into());
1859            }
1860            std::ptr::copy_nonoverlapping(
1861                (*sd_src).data as *const u8,
1862                (*sd_dst).data,
1863                (*sd_src).size,
1864            );
1865        }
1866
1867        match (*(*output_stream).codecpar).codec_type {
1868            AVMEDIA_TYPE_AUDIO => {
1869                if ((*(*output_stream).codecpar).block_align == 1
1870                    || (*(*output_stream).codecpar).block_align == 1152
1871                    || (*(*output_stream).codecpar).block_align == 576)
1872                    && (*(*output_stream).codecpar).codec_id == AV_CODEC_ID_MP3
1873                {
1874                    (*(*output_stream).codecpar).block_align = 0;
1875                }
1876                if (*(*output_stream).codecpar).codec_id == AV_CODEC_ID_AC3 {
1877                    (*(*output_stream).codecpar).block_align = 0;
1878                }
1879            }
1880            AVMEDIA_TYPE_VIDEO => {
1881                let sar = if (*input_stream).sample_aspect_ratio.num != 0 {
1882                    (*input_stream).sample_aspect_ratio
1883                } else {
1884                    (*(*output_stream).codecpar).sample_aspect_ratio
1885                };
1886                (*output_stream).sample_aspect_ratio = sar;
1887                (*(*output_stream).codecpar).sample_aspect_ratio = sar;
1888                (*output_stream).r_frame_rate = (*input_stream).r_frame_rate;
1889            }
1890            _ => {}
1891        }
1892    };
1893    Ok(())
1894}
1895
1896fn output_bind_by_unlabeled_filter(
1897    index: usize,
1898    mux: &mut Muxer,
1899    filter_graphs: &mut Vec<FilterGraph>,
1900    auto_disable: &mut i32,
1901) -> Result<()> {
1902    let fg_len = filter_graphs.len();
1903
1904    for i in 0..fg_len {
1905        let filter_graph = &mut filter_graphs[i];
1906
1907        for i in 0..filter_graph.outputs.len() {
1908            let media_type = filter_graph.outputs[i].media_type;
1909
1910            // Check if this stream type is disabled
1911            if *auto_disable & (1 << media_type as i32) != 0 {
1912                continue;
1913            }
1914
1915            let option = {
1916                let output_filter = &filter_graph.outputs[i];
1917                if (!output_filter.linklabel.is_empty() && output_filter.linklabel != "out")
1918                    || output_filter.has_dst()
1919                {
1920                    continue;
1921                }
1922
1923                choose_encoder(mux, output_filter.media_type)?
1924            };
1925
1926            match option {
1927                None => {
1928                    warn!(
1929                        "An unexpected media_type {:?} appears in output_filter",
1930                        media_type
1931                    );
1932                }
1933                Some((codec_id, enc)) => {
1934                    *auto_disable |= 1 << media_type as i32;
1935                    ofilter_bind_ost(index, mux, filter_graph, i, codec_id, enc, None, false)?;
1936                }
1937            }
1938        }
1939    }
1940
1941    Ok(())
1942}
1943
1944fn ofilter_bind_ost(
1945    index: usize,
1946    mux: &mut Muxer,
1947    filter_graph: &mut FilterGraph,
1948    output_filter_index: usize,
1949    codec_id: AVCodecID,
1950    enc: *const AVCodec,
1951    stream_source: Option<(usize, usize)>,
1952    single_stream_direct_input: bool,
1953) -> Result<usize> {
1954    let output_filter = &mut filter_graph.outputs[output_filter_index];
1955    let (frame_sender, output_stream_index) = mux.add_enc_stream(
1956        output_filter.media_type,
1957        enc,
1958        filter_graph.node.clone(),
1959        single_stream_direct_input,
1960    )?;
1961    output_filter.set_dst(frame_sender);
1962
1963    if let Some((file_idx, stream_idx)) = stream_source {
1964        mux.register_stream_source(output_stream_index, file_idx, stream_idx, true);
1965    }
1966
1967    configure_output_filter_opts(
1968        index,
1969        mux,
1970        output_filter,
1971        codec_id,
1972        enc,
1973        output_stream_index,
1974    )?;
1975    Ok(output_stream_index)
1976}
1977
1978fn choose_encoder(
1979    mux: &Muxer,
1980    media_type: AVMediaType,
1981) -> Result<Option<(AVCodecID, *const AVCodec)>> {
1982    let media_codec = match media_type {
1983        AVMEDIA_TYPE_VIDEO => mux.video_codec.clone(),
1984        AVMEDIA_TYPE_AUDIO => mux.audio_codec.clone(),
1985        AVMEDIA_TYPE_SUBTITLE => mux.subtitle_codec.clone(),
1986        _ => return Ok(None),
1987    };
1988
1989    match media_codec {
1990        None => {
1991            let url = CString::new(&*mux.url)?;
1992            unsafe {
1993                let codec_id = av_guess_codec(
1994                    (*mux.out_fmt_ctx_ptr()).oformat,
1995                    null(),
1996                    url.as_ptr(),
1997                    null(),
1998                    media_type,
1999                );
2000                let enc = avcodec_find_encoder(codec_id);
2001                if enc.is_null() {
2002                    let format_name = (*(*mux.out_fmt_ctx_ptr()).oformat).name;
2003                    let format_name = CStr::from_ptr(format_name).to_str();
2004                    let codec_name = avcodec_get_name(codec_id);
2005                    let codec_name = CStr::from_ptr(codec_name).to_str();
2006                    if let (Ok(format_name), Ok(codec_name)) = (format_name, codec_name) {
2007                        error!("Automatic encoder selection failed Default encoder for format {format_name} (codec {codec_name}) is probably disabled. Please choose an encoder manually.");
2008                    }
2009                    return Err(OpenOutputError::from(AVERROR_ENCODER_NOT_FOUND).into());
2010                }
2011
2012                return Ok(Some((codec_id, enc)));
2013            }
2014        }
2015        Some(media_codec) if media_codec != "copy" => unsafe {
2016            let media_codec_cstr = CString::new(media_codec.clone())?;
2017
2018            let mut enc = avcodec_find_encoder_by_name(media_codec_cstr.as_ptr());
2019            let desc = avcodec_descriptor_get_by_name(media_codec_cstr.as_ptr());
2020
2021            if enc.is_null() && !desc.is_null() {
2022                enc = avcodec_find_encoder((*desc).id);
2023                if !enc.is_null() {
2024                    let codec_name = (*enc).name;
2025                    let codec_name = CStr::from_ptr(codec_name).to_str();
2026                    let desc_name = (*desc).name;
2027                    let desc_name = CStr::from_ptr(desc_name).to_str();
2028                    if let (Ok(codec_name), Ok(desc_name)) = (codec_name, desc_name) {
2029                        debug!("Matched encoder '{codec_name}' for codec '{desc_name}'.");
2030                    }
2031                }
2032            }
2033
2034            if enc.is_null() {
2035                error!("Unknown encoder '{media_codec}'");
2036                return Err(OpenOutputError::from(AVERROR_ENCODER_NOT_FOUND).into());
2037            }
2038
2039            if (*enc).type_ != media_type {
2040                error!("Invalid encoder type '{media_codec}'");
2041                return Err(OpenOutputError::InvalidArgument.into());
2042            }
2043            let codec_id = (*enc).id;
2044            return Ok(Some((codec_id, enc)));
2045        },
2046        _ => {}
2047    };
2048
2049    Ok(None)
2050}
2051
2052fn check_duplicate_inputs_outputs(inputs: &[Input], outputs: &[Output]) -> Result<()> {
2053    for output in outputs {
2054        if let Some(output_url) = &output.url {
2055            for input in inputs {
2056                if let Some(input_url) = &input.url {
2057                    if input_url == output_url {
2058                        return Err(FileSameAsInput(input_url.clone()));
2059                    }
2060                }
2061            }
2062        }
2063    }
2064    Ok(())
2065}
2066
2067fn open_output_files(
2068    outputs: &mut Vec<Output>,
2069    copy_ts: bool,
2070    interrupt_state: &Arc<crate::core::context::InterruptState>,
2071) -> Result<Vec<Muxer>> {
2072    let mut muxs = Vec::new();
2073
2074    for (i, output) in outputs.iter_mut().enumerate() {
2075        unsafe {
2076            let result = open_output_file(i, output, copy_ts, interrupt_state);
2077            if let Err(e) = result {
2078                // Already-built muxers free their contexts on drop.
2079                return Err(e);
2080            }
2081            let mux = result.unwrap();
2082            muxs.push(mux)
2083        }
2084    }
2085    Ok(muxs)
2086}
2087
2088#[cfg(docsrs)]
2089unsafe fn open_output_file(
2090    index: usize,
2091    output: &mut Output,
2092    copy_ts: bool,
2093    interrupt_state: &Arc<crate::core::context::InterruptState>,
2094) -> Result<Muxer> {
2095    Err(Error::Bug)
2096}
2097
2098#[cfg(not(docsrs))]
2099unsafe fn open_output_file(
2100    index: usize,
2101    output: &mut Output,
2102    copy_ts: bool,
2103    interrupt_state: &Arc<crate::core::context::InterruptState>,
2104) -> Result<Muxer> {
2105    let mut out_fmt_ctx = null_mut();
2106    // Frees out_fmt_ctx (and, for custom IO, its AVIO + callback box) on any
2107    // early return until the Muxer takes ownership below.
2108    let mut ctx_guard = crate::core::context::FmtCtxGuard::disarmed();
2109    let format = get_format(&output.format)?;
2110    match &output.url {
2111        None => {
2112            if output.write_callback.is_none() {
2113                error!("input url and write_callback is none.");
2114                return Err(OpenOutputError::InvalidSink.into());
2115            }
2116
2117            let write_callback = output.write_callback.take().unwrap();
2118
2119            let avio_ctx_buffer_size = 1024 * 64;
2120            let mut avio_ctx_buffer = av_malloc(avio_ctx_buffer_size);
2121            if avio_ctx_buffer.is_null() {
2122                return Err(OpenOutputError::OutOfMemory.into());
2123            }
2124
2125            let have_seek_callback = output.seek_callback.is_some();
2126            let input_opaque = Box::new(OutputOpaque {
2127                write: write_callback,
2128                seek: output.seek_callback.take(),
2129            });
2130            let opaque = Box::into_raw(input_opaque) as *mut libc::c_void;
2131
2132            let avio_ctx = avio_alloc_context(
2133                avio_ctx_buffer as *mut libc::c_uchar,
2134                avio_ctx_buffer_size as i32,
2135                1,
2136                opaque,
2137                None,
2138                Some(write_packet_wrapper),
2139                if have_seek_callback {
2140                    Some(seek_output_packet_wrapper)
2141                } else {
2142                    None
2143                },
2144            );
2145            if avio_ctx.is_null() {
2146                av_freep(&mut avio_ctx_buffer as *mut _ as *mut c_void);
2147                // avio_alloc_context never took ownership: reclaim the Box.
2148                let _ = Box::from_raw(opaque as *mut OutputOpaque);
2149                return Err(OpenOutputError::OutOfMemory.into());
2150            }
2151
2152            let ret = avformat_alloc_output_context2(&mut out_fmt_ctx, format, null(), null());
2153            if out_fmt_ctx.is_null() {
2154                warn!("Error initializing the muxer for write_callback");
2155                // Reclaims buffer, callback Box and the AVIOContext itself.
2156                crate::core::context::free_output_opaque(avio_ctx);
2157                return Err(AllocOutputContextError::from(ret).into());
2158            }
2159
2160            if !have_seek_callback && output_requires_seek(out_fmt_ctx) {
2161                crate::core::context::free_output_opaque(avio_ctx);
2162                avformat_free_context(out_fmt_ctx);
2163                warn!("The output format supports seeking, but no seek callback is provided. This may cause issues.");
2164                return Err(OpenOutputError::SeekFunctionMissing.into());
2165            }
2166
2167            (*out_fmt_ctx).pb = avio_ctx;
2168            (*out_fmt_ctx).flags |= AVFMT_FLAG_CUSTOM_IO;
2169            ctx_guard.arm(out_fmt_ctx, crate::raw::Mode::OutputCustomIo);
2170        }
2171        Some(url) => {
2172            let url_cstr = if url == "-" {
2173                CString::new("pipe:")?
2174            } else {
2175                CString::new(url.as_str())?
2176            };
2177            let ret =
2178                avformat_alloc_output_context2(&mut out_fmt_ctx, format, null(), url_cstr.as_ptr());
2179            if out_fmt_ctx.is_null() {
2180                warn!("Error initializing the muxer for {url}");
2181                return Err(AllocOutputContextError::from(ret).into());
2182            }
2183            ctx_guard.arm(out_fmt_ctx, crate::raw::Mode::Output);
2184
2185            // Interrupt callback before avio_open: stop()/abort() can break
2186            // a blocking network open and any later write on this output
2187            // (matches ffmpeg_mux_init.c:3326,3371).
2188            (*out_fmt_ctx).interrupt_callback = ffmpeg_sys_next::AVIOInterruptCB {
2189                callback: Some(crate::core::context::output_interrupt_cb),
2190                opaque: Arc::as_ptr(interrupt_state) as *mut c_void,
2191            };
2192
2193            let output_format = (*out_fmt_ctx).oformat;
2194            if (*output_format).flags & AVFMT_NOFILE == 0 {
2195                let ret = avio_open2(
2196                    &mut (*out_fmt_ctx).pb,
2197                    url_cstr.as_ptr(),
2198                    AVIO_FLAG_WRITE,
2199                    &(*out_fmt_ctx).interrupt_callback,
2200                    null_mut(),
2201                );
2202                if ret < 0 {
2203                    warn!("Error opening output {url}");
2204                    return Err(OpenOutputError::from(ret).into());
2205                }
2206            }
2207        }
2208    }
2209
2210    let recording_time_us = match output.stop_time_us {
2211        None => output.recording_time_us,
2212        Some(stop_time_us) => {
2213            let start_time_us = output.start_time_us.unwrap_or(0);
2214            if stop_time_us <= start_time_us {
2215                error!("stop_time_us value smaller than start_time_us; aborting.");
2216                return Err(OpenOutputError::InvalidArgument.into());
2217            } else {
2218                Some(stop_time_us - start_time_us)
2219            }
2220        }
2221    };
2222
2223    let url = output
2224        .url
2225        .clone()
2226        .unwrap_or_else(|| format!("write_callback[{index}]"));
2227
2228    let video_codec_opts = convert_options(output.video_codec_opts.clone())?;
2229    let audio_codec_opts = convert_options(output.audio_codec_opts.clone())?;
2230    let subtitle_codec_opts = convert_options(output.subtitle_codec_opts.clone())?;
2231    let format_opts = convert_options(output.format_opts.clone())?;
2232    let format_opts = maybe_enable_image2_update(
2233        out_fmt_ctx,
2234        output.url.as_deref(),
2235        output.max_video_frames,
2236        format_opts,
2237    );
2238
2239    // Parse pix_fmt string to AVPixelFormat
2240    // FFmpeg CLI also fails on invalid format names (e.g., `ffmpeg -pix_fmt foobar` errors with
2241    // "Unknown pixel format requested: foobar"). Valid but encoder-incompatible formats are
2242    // auto-converted by the filter graph, matching FFmpeg behavior.
2243    let pix_fmt = match &output.pix_fmt {
2244        Some(fmt_str) => {
2245            let cstr = CString::new(fmt_str.as_str())?;
2246            let pf = av_get_pix_fmt(cstr.as_ptr());
2247            if pf == AV_PIX_FMT_NONE {
2248                return Err(OpenOutputError::UnknownPixelFormat(fmt_str.clone()).into());
2249            } else {
2250                Some(pf)
2251            }
2252        }
2253        None => None,
2254    };
2255
2256    // Ownership of out_fmt_ctx transfers to the Muxer. `release()` disarms the
2257    // guard and hands back the pointer; wrap it in a `FormatContext` (custom-IO
2258    // variant for the write_callback path — the same discriminant the old
2259    // `is_set_write_callback` carried) and move it into the infallible
2260    // `Muxer::new`. There is no `?` between here and construction, so the pointer
2261    // always reaches its next Drop-home.
2262    // SAFETY: out_fmt_ctx is a fully-initialized output context; for custom IO its
2263    // pb/AVIO+box are already wired (see the arm(...,true) point above), which
2264    // from_output_custom_io's teardown requires.
2265    let out_fmt_ctx = ctx_guard.release();
2266    let fc = if output.url.is_none() {
2267        crate::raw::FormatContext::from_output_custom_io(out_fmt_ctx)
2268    } else {
2269        crate::raw::FormatContext::from_output(out_fmt_ctx)
2270    };
2271    let mux = Muxer::new(
2272        url,
2273        fc,
2274        output.frame_pipelines.take(),
2275        output.stream_map_specs.clone(),
2276        output.stream_maps.clone(),
2277        output.video_codec.clone(),
2278        output.audio_codec.clone(),
2279        output.subtitle_codec.clone(),
2280        output.start_time_us,
2281        recording_time_us,
2282        output.framerate,
2283        output.framerate_max,
2284        output.vsync_method,
2285        output.bits_per_raw_sample,
2286        output.audio_sample_rate,
2287        output.audio_channels,
2288        output.audio_sample_fmt,
2289        output.video_qscale,
2290        output.audio_qscale,
2291        output.max_video_frames,
2292        output.max_audio_frames,
2293        output.max_subtitle_frames,
2294        video_codec_opts,
2295        audio_codec_opts,
2296        subtitle_codec_opts,
2297        format_opts,
2298        copy_ts,
2299        output.global_metadata.clone(),
2300        output.stream_metadata.clone(),
2301        output.chapter_metadata.clone(),
2302        output.program_metadata.clone(),
2303        output.metadata_map.clone(),
2304        output.auto_copy_metadata,
2305        output.video_disable,
2306        output.audio_disable,
2307        output.subtitle_disable,
2308        output.data_disable,
2309        pix_fmt,
2310    );
2311
2312    Ok(mux)
2313}
2314
2315fn get_format(format_option: &Option<String>) -> Result<*const AVOutputFormat> {
2316    match format_option {
2317        None => Ok(null()),
2318        Some(format_str) => unsafe {
2319            let mut format_cstr = CString::new(format_str.to_string())?;
2320            let mut format = av_guess_format(format_cstr.as_ptr(), null(), null());
2321            if format.is_null() {
2322                format_cstr = CString::new(format!("tmp.{format_str}"))?;
2323                format = av_guess_format(null(), format_cstr.as_ptr(), null());
2324            }
2325            if format.is_null() {
2326                return Err(OpenOutputError::FormatUnsupported(format_str.to_string()).into());
2327            }
2328            Ok(format)
2329        },
2330    }
2331}
2332
2333unsafe fn output_requires_seek(fmt_ctx: *mut AVFormatContext) -> bool {
2334    if fmt_ctx.is_null() {
2335        return false;
2336    }
2337
2338    let mut format_name = "unknown".to_string();
2339
2340    if !(*fmt_ctx).oformat.is_null() {
2341        let oformat = (*fmt_ctx).oformat;
2342        format_name = CStr::from_ptr((*oformat).name)
2343            .to_string_lossy()
2344            .into_owned();
2345        let flags = (*oformat).flags;
2346        let no_file = flags & AVFMT_NOFILE != 0;
2347        let global_header = flags & AVFMT_GLOBALHEADER != 0;
2348
2349        log::debug!(
2350            "Output format '{format_name}' - No file: {}, Global header: {}",
2351            if no_file { "True" } else { "False" },
2352            if global_header { "True" } else { "False" }
2353        );
2354
2355        // List of formats that typically require seeking
2356        let format_names: Vec<&str> = format_name.split(',').collect();
2357        if format_names
2358            .iter()
2359            .any(|&f| matches!(f, "mp4" | "mov" | "mkv" | "avi" | "flac" | "ogg" | "webm"))
2360        {
2361            log::debug!("Output format '{format_name}' typically requires seeking.");
2362            return true;
2363        }
2364
2365        // List of streaming formats that do not require seeking
2366        if format_names.iter().any(|&f| {
2367            matches!(
2368                f,
2369                "mpegts" | "hls" | "m3u8" | "udp" | "rtp" | "rtp_mpegts" | "http" | "srt"
2370            )
2371        }) {
2372            log::debug!("Output format '{format_name}' does not typically require seeking.");
2373            return false;
2374        }
2375
2376        // Special handling for FLV format
2377        if format_name == "flv" {
2378            log::debug!("Output format 'flv' detected. It is highly recommended to set `seek_callback()` to avoid potential issues with 'Failed to update header with correct duration' and 'Failed to update header with correct filesize'.");
2379            return false;
2380        }
2381
2382        // If AVFMT_NOFILE is set, the format does not use standard file I/O and may not need seeking
2383        if no_file {
2384            log::debug!(
2385                "Output format '{format_name}' uses AVFMT_NOFILE. Seeking is likely unnecessary."
2386            );
2387            return false;
2388        }
2389
2390        // If the format uses global headers, it typically means the codec requires a separate metadata section
2391        if global_header {
2392            log::debug!(
2393                "Output format '{format_name}' uses AVFMT_GLOBALHEADER. Seeking may be required."
2394            );
2395            return true;
2396        }
2397    } else {
2398        log::debug!("Output format is null. Cannot determine if seeking is required.");
2399    }
2400
2401    // Default case: assume seeking is not required
2402    log::debug!("Output format '{format_name}' does not match any known rules. Assuming seeking is not required.");
2403    false
2404}
2405
2406pub(super) struct InputOpaque {
2407    pub(super) read: Box<dyn FnMut(&mut [u8]) -> i32 + Send>,
2408    pub(super) seek: Option<Box<dyn FnMut(i64, i32) -> i64 + Send>>,
2409}
2410
2411pub(super) struct OutputOpaque {
2412    pub(super) write: Box<dyn FnMut(&[u8]) -> i32 + Send>,
2413    pub(super) seek: Option<Box<dyn FnMut(i64, i32) -> i64 + Send>>,
2414}
2415
2416unsafe extern "C" fn write_packet_wrapper(
2417    opaque: *mut libc::c_void,
2418    buf: *const u8,
2419    buf_size: libc::c_int,
2420) -> libc::c_int {
2421    // buf_size is a C int: a non-positive value must not be cast to usize
2422    // (it would produce a giant slice length).
2423    if buf.is_null() || buf_size <= 0 {
2424        return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
2425    }
2426    let context = &mut *(opaque as *mut OutputOpaque);
2427
2428    let slice = std::slice::from_raw_parts(buf, buf_size as usize);
2429
2430    (context.write)(slice)
2431}
2432
2433unsafe extern "C" fn read_packet_wrapper(
2434    opaque: *mut libc::c_void,
2435    buf: *mut u8,
2436    buf_size: libc::c_int,
2437) -> libc::c_int {
2438    // buf_size is a C int: a non-positive value must not be cast to usize
2439    // (it would produce a giant slice length).
2440    if buf.is_null() || buf_size <= 0 {
2441        return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
2442    }
2443
2444    let context = &mut *(opaque as *mut InputOpaque);
2445
2446    let slice = std::slice::from_raw_parts_mut(buf, buf_size as usize);
2447
2448    (context.read)(slice)
2449}
2450
2451unsafe extern "C" fn seek_input_packet_wrapper(
2452    opaque: *mut libc::c_void,
2453    offset: i64,
2454    whence: libc::c_int,
2455) -> i64 {
2456    let context = &mut *(opaque as *mut InputOpaque);
2457
2458    if let Some(seek_func) = &mut context.seek {
2459        (*seek_func)(offset, whence)
2460    } else {
2461        ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64
2462    }
2463}
2464
2465unsafe extern "C" fn seek_output_packet_wrapper(
2466    opaque: *mut libc::c_void,
2467    offset: i64,
2468    whence: libc::c_int,
2469) -> i64 {
2470    let context = &mut *(opaque as *mut OutputOpaque);
2471
2472    if let Some(seek_func) = &mut context.seek {
2473        (*seek_func)(offset, whence)
2474    } else {
2475        ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64
2476    }
2477}
2478
2479fn fg_bind_inputs(filter_graphs: &mut Vec<FilterGraph>, demuxs: &mut Vec<Demuxer>) -> Result<()> {
2480    if filter_graphs.is_empty() {
2481        return Ok(());
2482    }
2483    bind_fg_inputs_by_fg(filter_graphs)?;
2484
2485    for filter_graph in filter_graphs.iter_mut() {
2486        for i in 0..filter_graph.inputs.len() {
2487            fg_complex_bind_input(filter_graph, i, demuxs)?;
2488        }
2489    }
2490
2491    Ok(())
2492}
2493
2494struct FilterLabel {
2495    linklabel: String,
2496    media_type: AVMediaType,
2497}
2498
2499fn bind_fg_inputs_by_fg(filter_graphs: &mut Vec<FilterGraph>) -> Result<()> {
2500    let fg_labels = filter_graphs
2501        .iter()
2502        .map(|filter_graph| {
2503            let inputs = filter_graph
2504                .inputs
2505                .iter()
2506                .map(|input| FilterLabel {
2507                    linklabel: input.linklabel.clone(),
2508                    media_type: input.media_type,
2509                })
2510                .collect::<Vec<_>>();
2511            let outputs = filter_graph
2512                .outputs
2513                .iter()
2514                .map(|output| FilterLabel {
2515                    linklabel: output.linklabel.clone(),
2516                    media_type: output.media_type,
2517                })
2518                .collect::<Vec<_>>();
2519            (inputs, outputs)
2520        })
2521        .collect::<Vec<_>>();
2522
2523    for (i, (inputs, _outputs)) in fg_labels.iter().enumerate() {
2524        for (input_pad_idx, input_filter_label) in inputs.iter().enumerate() {
2525            if input_filter_label.linklabel.is_empty() {
2526                continue;
2527            }
2528
2529            'outer: for (j, (_inputs, outputs)) in fg_labels.iter().enumerate() {
2530                if i == j {
2531                    continue;
2532                }
2533
2534                for (output_idx, output_filter_label) in outputs.iter().enumerate() {
2535                    if output_filter_label.linklabel != input_filter_label.linklabel {
2536                        continue;
2537                    }
2538                    if output_filter_label.media_type != input_filter_label.media_type {
2539                        warn!(
2540                            "Tried to connect {:?} output to {:?} input",
2541                            output_filter_label.media_type, input_filter_label.media_type
2542                        );
2543                        return Err(FilterGraphParseError::InvalidArgument.into());
2544                    }
2545
2546                    {
2547                        let filter_graph = &filter_graphs[j];
2548                        let output_filter = &filter_graph.outputs[output_idx];
2549                        if output_filter.has_dst() {
2550                            continue;
2551                        }
2552                    }
2553
2554                    let (sender, finished_flag_list) = {
2555                        let filter_graph = &mut filter_graphs[i];
2556                        filter_graph.get_src_sender()
2557                    };
2558
2559                    {
2560                        let filter_graph = &mut filter_graphs[j];
2561                        filter_graph.outputs[output_idx].set_dst(sender);
2562                        // The consumer routes frames and indexes its per-pad
2563                        // finished_flag_list by INPUT PAD index, not by the
2564                        // consumer's graph index.
2565                        filter_graph.outputs[output_idx].fg_input_index = input_pad_idx;
2566                        filter_graph.outputs[output_idx].finished_flag_list = finished_flag_list;
2567                    }
2568                    // Mark the pad so fg_complex_bind_input does not bind it
2569                    // to a demuxer stream on top of this connection.
2570                    filter_graphs[i].inputs[input_pad_idx].bound = true;
2571
2572                    break 'outer;
2573                }
2574            }
2575        }
2576    }
2577    Ok(())
2578}
2579
2580fn fg_complex_bind_input(
2581    filter_graph: &mut FilterGraph,
2582    input_filter_index: usize,
2583    demuxs: &mut Vec<Demuxer>,
2584) -> Result<()> {
2585    // A pad already connected to another filtergraph's output must not also
2586    // be bound to a demuxer stream (covers labeled pads including the
2587    // reserved "in" label, which otherwise takes the auto-bind branch).
2588    if filter_graph.inputs[input_filter_index].bound {
2589        return Ok(());
2590    }
2591
2592    let graph_desc = &filter_graph.graph_desc;
2593    let input_filter = &mut filter_graph.inputs[input_filter_index];
2594    let (demux_idx, stream_idx) = if !input_filter.linklabel.is_empty()
2595        && input_filter.linklabel != "in"
2596    {
2597        let (demux_idx, stream_idx) = fg_find_input_idx_by_linklabel(
2598            &input_filter.linklabel,
2599            input_filter.media_type,
2600            demuxs,
2601            graph_desc,
2602        )?;
2603
2604        info!(
2605            "Binding filter input with label '{}' to input stream {stream_idx}:{demux_idx}",
2606            input_filter.linklabel
2607        );
2608        (demux_idx, stream_idx)
2609    } else {
2610        let mut demux_idx = -1i32;
2611        let mut stream_idx = 0;
2612        for (d_idx, demux) in demuxs.iter().enumerate() {
2613            for (st_idx, intput_stream) in demux.get_streams().iter().enumerate() {
2614                if intput_stream.is_used() {
2615                    continue;
2616                }
2617                if intput_stream.codec_type == input_filter.media_type {
2618                    demux_idx = d_idx as i32;
2619                    stream_idx = st_idx;
2620                    break;
2621                }
2622            }
2623            if demux_idx >= 0 {
2624                break;
2625            }
2626        }
2627
2628        if demux_idx < 0 {
2629            warn!(
2630                "Cannot find a matching stream for unlabeled input pad {}",
2631                input_filter.name
2632            );
2633            return Err(FilterGraphParseError::InvalidArgument.into());
2634        }
2635
2636        debug!("FilterGraph binding unlabeled input {input_filter_index} to input stream {stream_idx}:{demux_idx}");
2637
2638        (demux_idx as usize, stream_idx)
2639    };
2640
2641    let demux = &mut demuxs[demux_idx];
2642
2643    ifilter_bind_ist(filter_graph, input_filter_index, stream_idx, demux)
2644}
2645
2646#[cfg(docsrs)]
2647fn ifilter_bind_ist(
2648    filter_graph: &mut FilterGraph,
2649    input_index: usize,
2650    stream_idx: usize,
2651    demux: &mut Demuxer,
2652) -> Result<()> {
2653    Ok(())
2654}
2655
2656#[cfg(not(docsrs))]
2657fn ifilter_bind_ist(
2658    filter_graph: &mut FilterGraph,
2659    input_index: usize,
2660    stream_idx: usize,
2661    demux: &mut Demuxer,
2662) -> Result<()> {
2663    unsafe {
2664        let input_filter = &mut filter_graph.inputs[input_index];
2665        let ist = *(*demux.in_fmt_ctx_ptr()).streams.add(stream_idx);
2666        let par = (*ist).codecpar;
2667        if (*par).codec_type == AVMEDIA_TYPE_VIDEO {
2668            // A user-forced input framerate feeds the filtergraph directly;
2669            // only guess from the container when none was forced
2670            // (ffmpeg_demux.c ist_filter_add: ist->framerate ?: guess).
2671            let framerate = if demux.framerate.num > 0 && demux.framerate.den > 0 {
2672                demux.framerate
2673            } else {
2674                av_guess_frame_rate(demux.in_fmt_ctx_ptr(), ist, null_mut())
2675            };
2676            input_filter.opts.framerate = framerate;
2677        } else if (*par).codec_type == AVMEDIA_TYPE_SUBTITLE {
2678            input_filter.opts.sub2video_width = (*par).width;
2679            input_filter.opts.sub2video_height = (*par).height;
2680
2681            if input_filter.opts.sub2video_width <= 0 || input_filter.opts.sub2video_height <= 0 {
2682                let nb_streams = (*demux.in_fmt_ctx_ptr()).nb_streams;
2683                for j in 0..nb_streams {
2684                    let par1 = (**(*demux.in_fmt_ctx_ptr()).streams.add(j as usize)).codecpar;
2685                    if (*par1).codec_type == AVMEDIA_TYPE_VIDEO {
2686                        input_filter.opts.sub2video_width =
2687                            std::cmp::max(input_filter.opts.sub2video_width, (*par1).width);
2688                        input_filter.opts.sub2video_height =
2689                            std::cmp::max(input_filter.opts.sub2video_height, (*par1).height);
2690                    }
2691                }
2692            }
2693
2694            if input_filter.opts.sub2video_width <= 0 || input_filter.opts.sub2video_height <= 0 {
2695                input_filter.opts.sub2video_width =
2696                    std::cmp::max(input_filter.opts.sub2video_width, 720);
2697                input_filter.opts.sub2video_height =
2698                    std::cmp::max(input_filter.opts.sub2video_height, 576);
2699            }
2700
2701            demux.get_stream_mut(stream_idx).have_sub2video = true;
2702        }
2703
2704        let dec_ctx = {
2705            let input_stream = demux.get_stream_mut(stream_idx);
2706            avcodec_alloc_context3(input_stream.codec.as_ptr())
2707        };
2708        if dec_ctx.is_null() {
2709            return Err(FilterGraphParseError::OutOfMemory.into());
2710        }
2711        let _codec_ctx = CodecContext::new(dec_ctx);
2712
2713        // A freshly allocated context only carries codec defaults (format
2714        // -1, 0x0, timebase 0/1), which made the fallback below useless: the
2715        // EOF-before-first-frame path could never configure the graph
2716        // ("Cannot determine format of input ... after EOF"). fftools fills
2717        // the fallback from the opened decoder (dec_open's param_out); at
2718        // bind time no decoder is open, so the stream's codecpar — the same
2719        // values a decoder would start from — is the faithful source.
2720        let ret = avcodec_parameters_to_context(dec_ctx, par);
2721        if ret < 0 {
2722            return Err(FilterGraphParseError::from(ret).into());
2723        }
2724        (*dec_ctx).pkt_timebase = (*ist).time_base;
2725
2726        let fallback = input_filter.opts.fallback.as_mut_ptr();
2727        if (*dec_ctx).codec_type == AVMEDIA_TYPE_AUDIO {
2728            (*fallback).format = (*dec_ctx).sample_fmt as i32;
2729            (*fallback).sample_rate = (*dec_ctx).sample_rate;
2730
2731            let ret = av_channel_layout_copy(&mut (*fallback).ch_layout, &(*dec_ctx).ch_layout);
2732            if ret < 0 {
2733                return Err(FilterGraphParseError::from(ret).into());
2734            }
2735        } else if (*dec_ctx).codec_type == AVMEDIA_TYPE_VIDEO {
2736            (*fallback).format = (*dec_ctx).pix_fmt as i32;
2737            (*fallback).width = (*dec_ctx).width;
2738            (*fallback).height = (*dec_ctx).height;
2739            (*fallback).sample_aspect_ratio = (*dec_ctx).sample_aspect_ratio;
2740            (*fallback).colorspace = (*dec_ctx).colorspace;
2741            (*fallback).color_range = (*dec_ctx).color_range;
2742        }
2743        (*fallback).time_base = (*dec_ctx).pkt_timebase;
2744
2745        // Set autorotate flag based on demuxer configuration
2746        // FFmpeg source: ffmpeg_demux.c:1137, ffmpeg_filter.c:1744-1778 (FFmpeg 7.x)
2747        if demux.autorotate {
2748            input_filter.opts.flags |= IFILTER_FLAG_AUTOROTATE;
2749        }
2750
2751        let tsoffset = if demux.copy_ts {
2752            let mut tsoffset = if demux.start_time_us.is_some() {
2753                demux.start_time_us.unwrap()
2754            } else {
2755                0
2756            };
2757            if (*demux.in_fmt_ctx_ptr()).start_time != ffmpeg_sys_next::AV_NOPTS_VALUE {
2758                tsoffset += (*demux.in_fmt_ctx_ptr()).start_time
2759            }
2760            tsoffset
2761        } else {
2762            0
2763        };
2764        if demux.start_time_us.is_some() {
2765            input_filter.opts.trim_start_us = Some(tsoffset);
2766        }
2767        input_filter.opts.trim_end_us = demux.recording_time_us;
2768
2769        let (sender, finished_flag_list) = filter_graph.get_src_sender();
2770        {
2771            let input_stream = demux.get_stream_mut(stream_idx);
2772            input_stream.add_fg_dst(sender, input_index, finished_flag_list);
2773        };
2774
2775        let node = Arc::make_mut(&mut filter_graph.node);
2776        let SchNode::Filter { inputs, .. } = node else {
2777            unreachable!()
2778        };
2779        inputs.insert(input_index, demux.node.clone());
2780
2781        demux.connect_stream(stream_idx);
2782        Ok(())
2783    }
2784}
2785
2786/// Find input stream index by filter graph linklabel
2787/// FFmpeg reference: ffmpeg_filter.c - fg_create logic for parsing filter input specifiers
2788/// Uses StreamSpecifier for complete stream specifier parsing
2789fn fg_find_input_idx_by_linklabel(
2790    linklabel: &str,
2791    filter_media_type: AVMediaType,
2792    demuxs: &mut Vec<Demuxer>,
2793    desc: &str,
2794) -> Result<(usize, usize)> {
2795    // Remove brackets if present
2796    let new_linklabel = if linklabel.starts_with("[") && linklabel.ends_with("]") {
2797        if linklabel.len() <= 2 {
2798            warn!("Filter linklabel is empty");
2799            return Err(InvalidFilterSpecifier(desc.to_string()).into());
2800        } else {
2801            &linklabel[1..linklabel.len() - 1]
2802        }
2803    } else {
2804        linklabel
2805    };
2806
2807    // Parse file index using strtol (FFmpeg reference: ffmpeg_opt.c:512)
2808    let (file_idx, remainder) = strtol(new_linklabel).map_err(|_| {
2809        FilterGraphParseError::InvalidArgument
2810    })?;
2811
2812    if file_idx < 0 || file_idx as usize >= demuxs.len() {
2813        return Err(InvalidFileIndexInFg(file_idx as usize, desc.to_string()).into());
2814    }
2815    let file_idx = file_idx as usize;
2816
2817    // Parse stream specifier using StreamSpecifier
2818    let spec_str = if remainder.is_empty() {
2819        // No specifier - will match by media type
2820        ""
2821    } else if remainder.starts_with(':') {
2822        &remainder[1..]
2823    } else {
2824        remainder
2825    };
2826
2827    let stream_spec = if spec_str.is_empty() {
2828        // No specifier: create one matching the filter's media type
2829        let mut spec = StreamSpecifier::default();
2830        spec.media_type = Some(filter_media_type);
2831        spec
2832    } else {
2833        // Parse the specifier
2834        StreamSpecifier::parse(spec_str).map_err(|e| {
2835            warn!("Invalid stream specifier in filter linklabel '{}': {}", linklabel, e);
2836            FilterGraphParseError::InvalidArgument
2837        })?
2838    };
2839
2840    // Find first matching stream
2841    let demux = &demuxs[file_idx];
2842    unsafe {
2843        let fmt_ctx = demux.in_fmt_ctx_ptr();
2844
2845        let mut subtitle_only_match = false;
2846        for (idx, _) in demux.get_streams().iter().enumerate() {
2847            let avstream = *(*fmt_ctx).streams.add(idx);
2848
2849            if stream_spec.matches(fmt_ctx, avstream) {
2850                // Additional check: must match filter's media type
2851                let codec_type = (*avstream).codecpar.as_ref().unwrap().codec_type;
2852                if codec_type == filter_media_type {
2853                    return Ok((file_idx, idx));
2854                }
2855                if codec_type == AVMEDIA_TYPE_SUBTITLE
2856                    && filter_media_type == AVMEDIA_TYPE_VIDEO
2857                {
2858                    subtitle_only_match = true;
2859                }
2860            }
2861        }
2862
2863        if subtitle_only_match {
2864            // The spec names a subtitle stream feeding a VIDEO pad: that is
2865            // fftools' sub2video hack, which this crate does not implement.
2866            // Fail with a specific message instead of "matches no streams".
2867            error!(
2868                "Stream specifier '{remainder}' in filtergraph description {desc} \
2869                 matches a subtitle stream, but subtitle streams as filtergraph \
2870                 inputs (sub2video) are not supported"
2871            );
2872            return Err(FilterGraphParseError::InvalidArgument.into());
2873        }
2874    }
2875
2876    // No matching stream found
2877    warn!(
2878        "Stream specifier '{}' in filtergraph description {} matches no streams.",
2879        remainder, desc
2880    );
2881    Err(FilterGraphParseError::InvalidArgument.into())
2882}
2883
2884/// Similar to strtol() in C
2885/// FFmpeg reference: ffmpeg_opt.c:512 - strtol(arg, &endptr, 0)
2886/// Used for parsing file indices and other integers in stream specifiers
2887fn strtol(input: &str) -> Result<(i64, &str)> {
2888    let mut chars = input.chars().peekable();
2889    let mut negative = false;
2890
2891    if let Some(&ch) = chars.peek() {
2892        if ch == '-' {
2893            negative = true;
2894            chars.next();
2895        } else if !ch.is_ascii_digit() {
2896            return Err(ParseInteger);
2897        }
2898    }
2899
2900    let number_start = input.len() - chars.clone().collect::<String>().len();
2901
2902    let number_str: String = chars.by_ref().take_while(|ch| ch.is_ascii_digit()).collect();
2903
2904    if number_str.is_empty() {
2905        return Err(ParseInteger);
2906    }
2907
2908    let number: i64 = number_str.parse().map_err(|_| ParseInteger)?;
2909
2910    let remainder_index = number_start + number_str.len();
2911    let remainder = &input[remainder_index..];
2912
2913    if negative {
2914        Ok((-number, remainder))
2915    } else {
2916        Ok((number, remainder))
2917    }
2918}
2919
2920fn init_filter_graphs(filter_complexs: Vec<FilterComplex>) -> Result<Vec<FilterGraph>> {
2921    let mut filter_graphs = Vec::with_capacity(filter_complexs.len());
2922    for (i, filter) in filter_complexs.iter().enumerate() {
2923        let filter_graph = init_filter_graph(i, &filter.filter_descs, filter.hw_device.clone())?;
2924        filter_graphs.push(filter_graph);
2925    }
2926    Ok(filter_graphs)
2927}
2928
2929#[cfg(docsrs)]
2930fn init_filter_graph(
2931    fg_index: usize,
2932    filter_desc: &str,
2933    hw_device: Option<String>,
2934) -> Result<FilterGraph> {
2935    Err(Error::Bug)
2936}
2937
2938#[cfg(not(docsrs))]
2939fn init_filter_graph(
2940    fg_index: usize,
2941    filter_desc: &str,
2942    hw_device: Option<String>,
2943) -> Result<FilterGraph> {
2944    let desc_cstr = CString::new(filter_desc)?;
2945
2946    // fftools 0f5592cfc737: hardware devices must exist before fg_create's
2947    // probe parse, because some HW filters refuse to initialize without one.
2948    // ez-ffmpeg's equivalent ordering: register the graph's filter device
2949    // here at build time (it used to happen at filter-task startup, i.e.
2950    // after this probe had already run — and failed — for such graphs).
2951    if let Some(hw_device) = &hw_device {
2952        let err = init_filter_hw_device(hw_device);
2953        if err < 0 {
2954            // Keep the pre-move error shape: this used to surface from the
2955            // filter task as FilterGraph(ParseError(..)) at scheduler start;
2956            // only the timing moved to build(), not the variant callers
2957            // match on.
2958            return Err(Error::FilterGraph(FilterGraphOperationError::ParseError(
2959                FilterGraphParseError::from(err),
2960            )));
2961        }
2962    }
2963
2964    unsafe {
2965        /* this graph is only used for determining the kinds of inputs
2966        and outputs we have, and is discarded on exit from this function */
2967        // Owned handle: `raw::FilterGraph`'s Drop frees the graph (and every
2968        // filter context it holds) on every return path below — including the
2969        // two `inouts_to_*_filters(..)?` early returns, which the hand-balanced
2970        // `avfilter_graph_free` calls used to miss, leaking the graph.
2971        let graph = crate::raw::FilterGraph::alloc().ok_or(FilterGraphParseError::OutOfMemory)?;
2972        (*graph.as_ptr()).nb_threads = 1;
2973
2974        let mut seg = null_mut();
2975        let mut ret = avfilter_graph_segment_parse(graph.as_ptr(), desc_cstr.as_ptr(), 0, &mut seg);
2976        if ret < 0 {
2977            return Err(FilterGraphParseError::from(ret).into());
2978        }
2979
2980        ret = avfilter_graph_segment_create_filters(seg, 0);
2981        if ret < 0 {
2982            avfilter_graph_segment_free(&mut seg);
2983            return Err(FilterGraphParseError::from(ret).into());
2984        }
2985
2986        // Same injection point as the scheduler's graph_parse: the filters
2987        // exist but are not initialized yet, so a HW-flagged filter can still
2988        // receive its device — fftools 0f5592cfc737 gave the probe-only parse
2989        // this ability, and FFmpeg 8 filters may hard-require it in init.
2990        if let Some(dev) = hw_device_for_filter() {
2991            for i in 0..(*graph.as_ptr()).nb_filters {
2992                let f = *(*graph.as_ptr()).filters.add(i as usize);
2993                if (*(*f).filter).flags & AVFILTER_FLAG_HWDEVICE == 0 {
2994                    continue;
2995                }
2996                (*f).hw_device_ctx = av_buffer_ref(dev.device_ref);
2997                if (*f).hw_device_ctx.is_null() {
2998                    avfilter_graph_segment_free(&mut seg);
2999                    return Err(FilterGraphParseError::OutOfMemory.into());
3000                }
3001            }
3002        }
3003
3004        #[cfg(not(docsrs))]
3005        {
3006            ret = graph_opts_apply(seg);
3007        }
3008        if ret < 0 {
3009            avfilter_graph_segment_free(&mut seg);
3010            return Err(FilterGraphParseError::from(ret).into());
3011        }
3012
3013        let mut inputs = crate::raw::FilterInOut::empty();
3014        let mut outputs = crate::raw::FilterInOut::empty();
3015        ret = avfilter_graph_segment_apply(seg, 0, inputs.as_out_ptr(), outputs.as_out_ptr());
3016        avfilter_graph_segment_free(&mut seg);
3017
3018        if ret < 0 {
3019            return Err(FilterGraphParseError::from(ret).into());
3020        }
3021
3022        // `inputs`/`outputs` own the parsed AVFilterInOut lists; their Drop frees
3023        // them on every path below — the two `?` early returns included.
3024        let input_filters = inouts_to_input_filters(fg_index, inputs.as_ptr())?;
3025        let output_filters = inouts_to_output_filters(outputs.as_ptr())?;
3026
3027        if output_filters.is_empty() {
3028            return Err(FilterZeroOutputs);
3029        }
3030
3031        let filter_graph = FilterGraph::new(
3032            filter_desc.to_string(),
3033            input_filters,
3034            output_filters,
3035        );
3036
3037        Ok(filter_graph)
3038    }
3039}
3040
3041unsafe fn inouts_to_input_filters(
3042    fg_index: usize,
3043    inouts: *mut AVFilterInOut,
3044) -> Result<Vec<InputFilter>> {
3045    let mut cur = inouts;
3046    let mut filterinouts = Vec::new();
3047    let mut filter_index = 0;
3048    while !cur.is_null() {
3049        let linklabel = if (*cur).name.is_null() {
3050            ""
3051        } else {
3052            let linklabel = CStr::from_ptr((*cur).name);
3053            let result = linklabel.to_str();
3054            if result.is_err() {
3055                return Err(FilterDescUtf8);
3056            }
3057            result.unwrap()
3058        };
3059
3060        let filter_ctx = (*cur).filter_ctx;
3061        let media_type = avfilter_pad_get_type((*filter_ctx).input_pads, (*cur).pad_idx);
3062
3063        let pads = (*filter_ctx).input_pads;
3064        let nb_pads = (*filter_ctx).nb_inputs;
3065
3066        let name = describe_filter_link(cur, filter_ctx, pads, nb_pads)?;
3067
3068        let fallback = frame_alloc()?;
3069
3070        let mut filter = InputFilter::new(linklabel.to_string(), media_type, name, fallback);
3071        filter.opts.name = format!("fg:{fg_index}:{filter_index}");
3072        filterinouts.push(filter);
3073
3074        cur = (*cur).next;
3075        filter_index += 1;
3076    }
3077    Ok(filterinouts)
3078}
3079
3080unsafe fn inouts_to_output_filters(inouts: *mut AVFilterInOut) -> Result<Vec<OutputFilter>> {
3081    let mut cur = inouts;
3082    let mut output_filters = Vec::new();
3083    while !cur.is_null() {
3084        let linklabel = if (*cur).name.is_null() {
3085            ""
3086        } else {
3087            let linklabel = CStr::from_ptr((*cur).name);
3088            let result = linklabel.to_str();
3089            if result.is_err() {
3090                return Err(FilterDescUtf8);
3091            }
3092            result.unwrap()
3093        };
3094
3095        let filter_ctx = (*cur).filter_ctx;
3096        let media_type = avfilter_pad_get_type((*filter_ctx).output_pads, (*cur).pad_idx);
3097
3098        let pads = (*filter_ctx).output_pads;
3099        let nb_pads = (*filter_ctx).nb_outputs;
3100
3101        let name = describe_filter_link(cur, filter_ctx, pads, nb_pads)?;
3102
3103        let filter = OutputFilter::new(linklabel.to_string(), media_type, name);
3104        output_filters.push(filter);
3105
3106        cur = (*cur).next;
3107    }
3108    Ok(output_filters)
3109}
3110
3111unsafe fn describe_filter_link(
3112    cur: *mut AVFilterInOut,
3113    filter_ctx: *mut AVFilterContext,
3114    pads: *mut AVFilterPad,
3115    nb_pads: c_uint,
3116) -> Result<String> {
3117    let filter = (*filter_ctx).filter;
3118    let name = (*filter).name;
3119    let name = CStr::from_ptr(name);
3120    let result = name.to_str();
3121    if result.is_err() {
3122        return Err(FilterNameUtf8);
3123    }
3124    let name = result.unwrap();
3125
3126    let name = if nb_pads > 1 {
3127        name.to_string()
3128    } else {
3129        let pad_name = avfilter_pad_get_name(pads, (*cur).pad_idx);
3130        let pad_name = CStr::from_ptr(pad_name);
3131        let result = pad_name.to_str();
3132        if result.is_err() {
3133            return Err(FilterNameUtf8);
3134        }
3135        let pad_name = result.unwrap();
3136        format!("{name}:{pad_name}")
3137    };
3138    Ok(name)
3139}
3140
3141fn open_input_files(
3142    inputs: &mut Vec<Input>,
3143    copy_ts: bool,
3144    interrupt_state: &Arc<crate::core::context::InterruptState>,
3145) -> Result<Vec<Demuxer>> {
3146    let mut demuxs = Vec::new();
3147    for (i, input) in inputs.iter_mut().enumerate() {
3148        unsafe {
3149            let result = open_input_file(i, input, copy_ts, interrupt_state);
3150            if let Err(e) = result {
3151                // Already-built demuxers free their contexts on drop.
3152                return Err(e);
3153            }
3154            let demux = result.unwrap();
3155            demuxs.push(demux)
3156        }
3157    }
3158    Ok(demuxs)
3159}
3160
3161#[cfg(docsrs)]
3162unsafe fn open_input_file(
3163    index: usize,
3164    input: &mut Input,
3165    copy_ts: bool,
3166    interrupt_state: &Arc<crate::core::context::InterruptState>,
3167) -> Result<Demuxer> {
3168    Err(Error::Bug)
3169}
3170
3171#[cfg(not(docsrs))]
3172unsafe fn open_input_file(
3173    index: usize,
3174    input: &mut Input,
3175    copy_ts: bool,
3176    interrupt_state: &Arc<crate::core::context::InterruptState>,
3177) -> Result<Demuxer> {
3178    let mut in_fmt_ctx = avformat_alloc_context();
3179    if in_fmt_ctx.is_null() {
3180        return Err(OpenInputError::OutOfMemory.into());
3181    }
3182    // Frees in_fmt_ctx on early returns in the pre-open window (before the
3183    // branches' own close_input paths take over). Disarmed at each handoff.
3184    let mut ctx_guard = crate::core::context::FmtCtxGuard::disarmed();
3185    ctx_guard.arm(in_fmt_ctx, crate::raw::Mode::Input);
3186
3187    // Interrupt callback: lets stop()/abort() break a blocking open, read or
3188    // find_stream_info on this input (fftools decode_interrupt_cb).
3189    (*in_fmt_ctx).interrupt_callback = ffmpeg_sys_next::AVIOInterruptCB {
3190        callback: Some(crate::core::context::input_interrupt_cb),
3191        opaque: Arc::as_ptr(interrupt_state) as *mut c_void,
3192    };
3193
3194    let recording_time_us = match input.stop_time_us {
3195        None => input.recording_time_us,
3196        Some(stop_time_us) => {
3197            let start_time_us = input.start_time_us.unwrap_or(0);
3198            if stop_time_us <= start_time_us {
3199                error!("stop_time_us value smaller than start_time_us; aborting.");
3200                return Err(OpenOutputError::InvalidArgument.into());
3201            } else {
3202                Some(stop_time_us - start_time_us)
3203            }
3204        }
3205    };
3206
3207    let file_iformat = if let Some(format) = &input.format {
3208        let format_cstr = CString::new(format.clone())?;
3209
3210        let file_iformat = ffmpeg_sys_next::av_find_input_format(format_cstr.as_ptr());
3211        if file_iformat.is_null() {
3212            error!("Unknown input format: '{format}'");
3213            return Err(OpenInputError::InvalidFormat(format.clone()).into());
3214        }
3215        file_iformat
3216    } else {
3217        null()
3218    };
3219
3220    let input_opts = convert_options(input.input_opts.clone())?;
3221    // Guard owns the dict on every path: avformat_open_input reallocates it
3222    // to hold unrecognized entries, which leaked on all early returns.
3223    let mut input_opts = DictGuard::new(hashmap_to_avdictionary(&input_opts));
3224
3225    let mut injected_scan_all_pmts = false;
3226    // The branches below own their cleanup on error; release before entering.
3227    ctx_guard.release();
3228    match &input.url {
3229        None => {
3230            if input.read_callback.is_none() {
3231                error!("input url and read_callback is none.");
3232                avformat_close_input(&mut in_fmt_ctx);
3233                return Err(OpenInputError::InvalidSource.into());
3234            }
3235
3236            let avio_ctx_buffer_size = 1024 * 64;
3237            let mut avio_ctx_buffer = av_malloc(avio_ctx_buffer_size);
3238            if avio_ctx_buffer.is_null() {
3239                avformat_close_input(&mut in_fmt_ctx);
3240                return Err(OpenInputError::OutOfMemory.into());
3241            }
3242
3243            let have_seek_callback = input.seek_callback.is_some();
3244            let input_opaque = Box::new(InputOpaque {
3245                read: input.read_callback.take().unwrap(),
3246                seek: input.seek_callback.take(),
3247            });
3248            let opaque = Box::into_raw(input_opaque) as *mut libc::c_void;
3249
3250            let avio_ctx = avio_alloc_context(
3251                avio_ctx_buffer as *mut libc::c_uchar,
3252                avio_ctx_buffer_size as i32,
3253                0,
3254                opaque,
3255                Some(read_packet_wrapper),
3256                None,
3257                if have_seek_callback {
3258                    Some(seek_input_packet_wrapper)
3259                } else {
3260                    None
3261                },
3262            );
3263            if avio_ctx.is_null() {
3264                av_freep(&mut avio_ctx_buffer as *mut _ as *mut c_void);
3265                // avio_alloc_context never took ownership: reclaim the Box.
3266                let _ = Box::from_raw(opaque as *mut InputOpaque);
3267                avformat_close_input(&mut in_fmt_ctx);
3268                return Err(OpenInputError::OutOfMemory.into());
3269            }
3270
3271            (*in_fmt_ctx).pb = avio_ctx;
3272            (*in_fmt_ctx).flags = AVFMT_FLAG_CUSTOM_IO;
3273
3274            let ret =
3275                avformat_open_input(&mut in_fmt_ctx, null(), file_iformat, input_opts.as_double_ptr());
3276            if ret < 0 {
3277                // close_input first: read_close may still touch s->pb. The
3278                // helper also reclaims the callback Box, which leaked here.
3279                avformat_close_input(&mut in_fmt_ctx);
3280                crate::core::context::free_input_opaque(avio_ctx);
3281                return Err(OpenInputError::from(ret).into());
3282            }
3283
3284            let ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
3285            if ret < 0 {
3286                avformat_close_input(&mut in_fmt_ctx);
3287                crate::core::context::free_input_opaque(avio_ctx);
3288                return Err(FindStreamError::from(ret).into());
3289            }
3290
3291            if !have_seek_callback && input_requires_seek(in_fmt_ctx) {
3292                avformat_close_input(&mut in_fmt_ctx);
3293                crate::core::context::free_input_opaque(avio_ctx);
3294                warn!("The input format supports seeking, but no seek callback is provided. This may cause issues.");
3295                return Err(OpenInputError::SeekFunctionMissing.into());
3296            }
3297        }
3298        Some(url) => {
3299            // Guard the pre-open window (CString/dict `?`); released before the
3300            // manual close_input paths below take over.
3301            ctx_guard.arm(in_fmt_ctx, crate::raw::Mode::Input);
3302            let url_cstr = CString::new(url.as_str())?;
3303
3304            let scan_all_pmts_key = CString::new("scan_all_pmts")?;
3305            if ffmpeg_sys_next::av_dict_get(
3306                input_opts.as_ptr(),
3307                scan_all_pmts_key.as_ptr(),
3308                null(),
3309                ffmpeg_sys_next::AV_DICT_MATCH_CASE,
3310            )
3311            .is_null()
3312            {
3313                let scan_all_pmts_value = CString::new("1")?;
3314                ffmpeg_sys_next::av_dict_set(
3315                    input_opts.as_double_ptr(),
3316                    scan_all_pmts_key.as_ptr(),
3317                    scan_all_pmts_value.as_ptr(),
3318                    ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
3319                );
3320                injected_scan_all_pmts = true;
3321            };
3322            (*in_fmt_ctx).flags |= ffmpeg_sys_next::AVFMT_FLAG_NONBLOCK;
3323
3324            // From here the manual close_input paths below own cleanup.
3325            ctx_guard.release();
3326            let mut ret = avformat_open_input(
3327                &mut in_fmt_ctx,
3328                url_cstr.as_ptr(),
3329                file_iformat,
3330                input_opts.as_double_ptr(),
3331            );
3332            if ret < 0 {
3333                avformat_close_input(&mut in_fmt_ctx);
3334                return Err(OpenInputError::from(ret).into());
3335            }
3336
3337            ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
3338            if ret < 0 {
3339                avformat_close_input(&mut in_fmt_ctx);
3340                return Err(FindStreamError::from(ret).into());
3341            }
3342        }
3343    }
3344
3345    // Open succeeded. Take ownership of the raw context in a `FormatContext` now:
3346    // it owns the pointer (and, for custom IO, its AVIO + callback box) through the
3347    // fallible tail below (option cleanup, seek, `Demuxer::new`). Any early return
3348    // here drops `fc`, freeing exactly once — this replaces the old re-armed guard.
3349    // The custom-IO variant (no url == read_callback path) frees the AVIO + box.
3350    // SAFETY: in_fmt_ctx is a successfully-opened input context; ownership moves in.
3351    let fc = if input.url.is_none() {
3352        crate::raw::FormatContext::from_input_custom_io(in_fmt_ctx)
3353    } else {
3354        crate::raw::FormatContext::from_input(in_fmt_ctx)
3355    };
3356
3357    // Options no demuxer consumed are user typos; report them instead of
3358    // silently swallowing (fftools check_avoptions aborts here — we warn).
3359    // The auto-injected scan_all_pmts must not be blamed on the user.
3360    if injected_scan_all_pmts {
3361        input_opts.remove(&CString::new("scan_all_pmts")?);
3362    }
3363    for key in input_opts.leftover_keys() {
3364        warn!("Option '{key}' was not recognized by input {index}");
3365    }
3366
3367    let mut timestamp = input.start_time_us.unwrap_or(0);
3368    /* add the stream start time */
3369    // SAFETY: fc owns the opened input context; as_ptr yields the live pointer.
3370    let in_fmt_ctx = fc.as_ptr();
3371    if (*in_fmt_ctx).start_time != ffmpeg_sys_next::AV_NOPTS_VALUE {
3372        timestamp += (*in_fmt_ctx).start_time;
3373    }
3374
3375    /* if seeking requested, we execute it */
3376    if let Some(start_time_us) = input.start_time_us {
3377        let mut seek_timestamp = timestamp;
3378
3379        if (*(*in_fmt_ctx).iformat).flags & ffmpeg_sys_next::AVFMT_SEEK_TO_PTS == 0 {
3380            let mut dts_heuristic = false;
3381            let stream_count = (*in_fmt_ctx).nb_streams;
3382
3383            for i in 0..stream_count {
3384                let stream = *(*in_fmt_ctx).streams.add(i as usize);
3385                let par = (*stream).codecpar;
3386                if (*par).video_delay != 0 {
3387                    dts_heuristic = true;
3388                    break;
3389                }
3390            }
3391            if dts_heuristic {
3392                seek_timestamp -= 3 * AV_TIME_BASE as i64 / 23;
3393            }
3394        }
3395        let ret = ffmpeg_sys_next::avformat_seek_file(
3396            in_fmt_ctx,
3397            -1,
3398            i64::MIN,
3399            seek_timestamp,
3400            seek_timestamp,
3401            0,
3402        );
3403        if ret < 0 {
3404            warn!(
3405                "could not seek to position {:.3}",
3406                start_time_us as f64 / AV_TIME_BASE as f64
3407            );
3408        }
3409    }
3410
3411    let url = input
3412        .url
3413        .clone()
3414        .unwrap_or_else(|| format!("read_callback[{index}]"));
3415
3416    let demux = Demuxer::new(
3417        url,
3418        fc,
3419        0 - if copy_ts { 0 } else { timestamp },
3420        input.frame_pipelines.take(),
3421        input.video_codec.clone(),
3422        input.audio_codec.clone(),
3423        input.subtitle_codec.clone(),
3424        input.readrate,
3425        input.start_time_us,
3426        recording_time_us,
3427        input.exit_on_error,
3428        input.stream_loop,
3429        input.hwaccel.clone(),
3430        input.hwaccel_device.clone(),
3431        input.hwaccel_output_format.clone(),
3432        copy_ts,
3433        input.autorotate.unwrap_or(true),  // Default to true (enabled)
3434        input.ts_scale.unwrap_or(1.0),     // Default to 1.0 (no scaling)
3435        match input.framerate {            // Default to {0, 0} (use packet duration)
3436            Some((num, den)) => AVRational { num, den },
3437            None => AVRational { num: 0, den: 0 },
3438        },
3439        input.log_level_offset.unwrap_or(0),
3440    )?;
3441
3442    // `Demuxer` now owns the context (via its `Option<FormatContext>`); a failure
3443    // above dropped `fc` exactly once. No guard to disarm.
3444    Ok(demux)
3445}
3446
3447/// Single-image convenience, equivalent to `ffmpeg ... -frames:v 1 -update 1`.
3448///
3449/// Writing one frame to an image2 output whose filename has no `%d` sequence
3450/// pattern makes the muxer warn on the first frame and hard-fail on a second
3451/// (libavformat/img2enc.c). With `max_video_frames == Some(1)` the
3452/// single-image intent is explicit, so enable the muxer's `update` mode
3453/// automatically — unless the user already configured a conflicting image2
3454/// option (`update`, `strftime`, `frame_pts`) themselves. Multi-frame
3455/// outputs are left untouched, keeping FFmpeg's missing-pattern protection.
3456fn maybe_enable_image2_update(
3457    out_fmt_ctx: *mut AVFormatContext,
3458    url: Option<&str>,
3459    max_video_frames: Option<i64>,
3460    format_opts: Option<HashMap<CString, CString>>,
3461) -> Option<HashMap<CString, CString>> {
3462    if max_video_frames != Some(1) {
3463        return format_opts;
3464    }
3465    // A filename carrying a sequence pattern ('%03d', strftime '%'-codes, …)
3466    // must keep image2's pattern expansion: update mode would write the
3467    // pattern string as a literal filename. Only plain URLs qualify;
3468    // write-callback outputs (no URL) are left untouched.
3469    let Some(url) = url else {
3470        return format_opts;
3471    };
3472    if url.contains('%') {
3473        return format_opts;
3474    }
3475    // SAFETY: out_fmt_ctx was successfully allocated by
3476    // avformat_alloc_output_context2 earlier in open_output_file and is not
3477    // freed before Muxer::new takes ownership; oformat/name are read-only
3478    // static muxer metadata.
3479    let is_image2 = unsafe {
3480        let oformat = (*out_fmt_ctx).oformat;
3481        !oformat.is_null()
3482            && !(*oformat).name.is_null()
3483            && std::ffi::CStr::from_ptr((*oformat).name).to_bytes() == b"image2"
3484    };
3485    if !is_image2 {
3486        return format_opts;
3487    }
3488
3489    let mut opts = format_opts.unwrap_or_default();
3490    let user_configured = opts.keys().any(|key| {
3491        matches!(key.to_bytes(), b"update" | b"strftime" | b"frame_pts")
3492    });
3493    if !user_configured {
3494        info!("single-image output detected (max_video_frames=1): enabling image2 'update' mode");
3495        // Both literals are NUL-free; unwrap cannot fail.
3496        opts.insert(CString::new("update").unwrap(), CString::new("1").unwrap());
3497    }
3498    Some(opts)
3499}
3500
3501fn convert_options(
3502    opts: Option<HashMap<String, String>>,
3503) -> Result<Option<HashMap<CString, CString>>> {
3504    if opts.is_none() {
3505        return Ok(None);
3506    }
3507
3508    let converted = opts.map(|map| {
3509        map.into_iter()
3510            .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?)))
3511            .collect::<Result<HashMap<CString, CString>, _>>() // Collect into a HashMap
3512    });
3513
3514    converted.transpose() // Convert `Result<Option<T>>` into `Option<Result<T>>`
3515}
3516
3517unsafe fn input_requires_seek(fmt_ctx: *mut AVFormatContext) -> bool {
3518    if fmt_ctx.is_null() {
3519        return false;
3520    }
3521
3522    let mut format_name = "unknown".to_string();
3523    let mut format_names: Vec<&str> = Vec::with_capacity(0);
3524
3525    if !(*fmt_ctx).iformat.is_null() {
3526        let iformat = (*fmt_ctx).iformat;
3527        format_name = CStr::from_ptr((*iformat).name)
3528            .to_string_lossy()
3529            .into_owned();
3530        let flags = (*iformat).flags;
3531        let no_binsearch = flags & AVFMT_NOBINSEARCH != 0;
3532        let no_gensearch = flags & AVFMT_NOGENSEARCH != 0;
3533
3534        log::debug!(
3535            "Input format '{format_name}' - Binary search: {}, Generic search: {}",
3536            if no_binsearch { "Disabled" } else { "Enabled" },
3537            if no_gensearch { "Disabled" } else { "Enabled" }
3538        );
3539
3540        format_names = format_name.split(',').collect();
3541
3542        if format_names.iter().any(|&f| {
3543            matches!(
3544                f,
3545                "mp4" | "mkv" | "avi" | "mov" | "flac" | "wav" | "aac" | "ogg" | "mp3" | "webm"
3546            )
3547        })
3548            && !no_binsearch && !no_gensearch {
3549                return true;
3550            }
3551
3552        if format_names.iter().any(|&f| {
3553            matches!(
3554                f,
3555                "hls" | "m3u8" | "mpegts" | "mms" | "udp" | "rtp" | "rtp_mpegts" | "http" | "srt"
3556            )
3557        }) {
3558            log::debug!("Live stream detected ({format_name}). Seeking is not possible.");
3559            return false;
3560        }
3561
3562        if no_binsearch && no_gensearch {
3563            log::debug!("Input format '{format_name}' has both NOBINSEARCH and NOGENSEARCH set. Seeking is likely restricted.");
3564        }
3565    }
3566
3567    let format_duration = (*fmt_ctx).duration;
3568
3569    if format_names.contains(&"flv") {
3570        if format_duration <= 0 {
3571            log::debug!(
3572                "Input format 'flv' detected with no valid duration. Seeking is not possible."
3573            );
3574        } else {
3575            log::warn!("Input format 'flv' detected with a valid duration. While seeking may still be possible, it is highly recommended to add a `seek_callback()` for optimal input handling, especially when seeking or random access to specific segments is required.");
3576        }
3577        return false;
3578    }
3579
3580    if format_duration > 0 {
3581        log::debug!("Format '{format_name}' has a duration of {format_duration}. Seeking is likely possible.");
3582        return true;
3583    }
3584
3585    let mut video_stream_index = -1;
3586    for i in 0..(*fmt_ctx).nb_streams {
3587        let stream = *(*fmt_ctx).streams.offset(i as isize);
3588        if (*stream).codecpar.is_null() {
3589            continue;
3590        }
3591        if (*(*stream).codecpar).codec_type == AVMEDIA_TYPE_VIDEO {
3592            video_stream_index = i as i32;
3593            break;
3594        }
3595    }
3596
3597    let stream_index = if video_stream_index >= 0 {
3598        video_stream_index
3599    } else {
3600        -1
3601    };
3602
3603    let original_pos = if !(*fmt_ctx).pb.is_null() {
3604        (*(*fmt_ctx).pb).pos
3605    } else {
3606        -1
3607    };
3608
3609    if original_pos >= 0 {
3610        let seek_target = AV_TIME_BASE as i64;
3611        let seek_result = av_seek_frame(fmt_ctx, stream_index, seek_target, AVSEEK_FLAG_BACKWARD);
3612
3613        if seek_result >= 0 {
3614            log::debug!("Seek test successful.");
3615
3616            (*(*fmt_ctx).pb).pos = original_pos;
3617            avformat_flush(fmt_ctx);
3618            log::debug!("Restored fmt_ctx.pb.pos to {original_pos} and flushed format context.",);
3619            return true;
3620        } else {
3621            log::debug!("Seek test failed (return code {seek_result}). This format likely does not support seeking.");
3622        }
3623    }
3624
3625    false
3626}
3627
3628#[cfg(test)]
3629mod tests {
3630    use std::ffi::{CStr, CString};
3631    use std::ptr::null_mut;
3632
3633    use crate::core::context::ffmpeg_context::{strtol, FfmpegContext, Output};
3634    use ffmpeg_sys_next::avfilter_graph_parse_ptr;
3635
3636    use crate::core::context::ffmpeg_context::{bind_fg_inputs_by_fg, fg_complex_bind_input};
3637    use crate::core::context::filter_graph::FilterGraph;
3638    use crate::core::context::input_filter::InputFilter;
3639    use crate::core::context::null_frame;
3640    use crate::core::context::output_filter::OutputFilter;
3641    use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
3642
3643    fn test_input(linklabel: &str, name: &str) -> InputFilter {
3644        InputFilter::new(
3645            linklabel.to_string(),
3646            AVMEDIA_TYPE_VIDEO,
3647            name.to_string(),
3648            null_frame(),
3649        )
3650    }
3651
3652    fn test_graph(inputs: Vec<InputFilter>, outputs: Vec<OutputFilter>) -> FilterGraph {
3653        FilterGraph::new("null".to_string(), inputs, outputs)
3654    }
3655
3656    #[test]
3657    fn cross_graph_binding_uses_input_pad_index_and_marks_bound() {
3658        // Producer (graph 0): one output labeled "mid".
3659        // Consumer (graph 1): pad 0 labeled "mid", pad 1 unlabeled.
3660        // The consumer's GRAPH index (1) differs from the matching PAD
3661        // index (0) on purpose: routing and finished_flag_list indexing
3662        // work per input pad, not per graph.
3663        let producer = test_graph(
3664            vec![test_input("", "in0")],
3665            vec![OutputFilter::new(
3666                "mid".to_string(),
3667                AVMEDIA_TYPE_VIDEO,
3668                "out0".to_string(),
3669            )],
3670        );
3671        let consumer = test_graph(
3672            vec![test_input("mid", "in0"), test_input("", "in1")],
3673            vec![OutputFilter::new(
3674                String::new(),
3675                AVMEDIA_TYPE_VIDEO,
3676                "out0".to_string(),
3677            )],
3678        );
3679        let mut graphs = vec![producer, consumer];
3680
3681        bind_fg_inputs_by_fg(&mut graphs).unwrap();
3682
3683        assert!(
3684            graphs[0].outputs[0].has_dst(),
3685            "producer output must be connected to the consumer"
3686        );
3687        assert_eq!(
3688            graphs[0].outputs[0].fg_input_index, 0,
3689            "fg_input_index must be the consumer's input PAD index, not its graph index"
3690        );
3691        assert_eq!(
3692            graphs[0].outputs[0].finished_flag_list.len(),
3693            2,
3694            "the producer must hold the consumer's per-pad finished flags"
3695        );
3696        assert!(
3697            graphs[1].inputs[0].bound,
3698            "the cross-connected pad must be marked bound"
3699        );
3700        assert!(
3701            !graphs[1].inputs[1].bound,
3702            "unrelated pads must stay unbound"
3703        );
3704    }
3705
3706    #[test]
3707    fn complex_bind_skips_already_bound_labeled_input() {
3708        let mut consumer = test_graph(
3709            vec![test_input("mid", "in0")],
3710            vec![OutputFilter::new(
3711                String::new(),
3712                AVMEDIA_TYPE_VIDEO,
3713                "out0".to_string(),
3714            )],
3715        );
3716        consumer.inputs[0].bound = true;
3717
3718        // No demuxers exist: if the pad were (re-)bound to an input stream
3719        // this would fail with "stream not found".
3720        let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
3721        assert!(
3722            result.is_ok(),
3723            "a pad already bound to another graph must not be re-bound: {result:?}"
3724        );
3725    }
3726
3727    #[test]
3728    fn complex_bind_skips_bound_reserved_in_label() {
3729        // The reserved label "in" takes the auto-bind branch, which must
3730        // also respect an existing cross-graph binding.
3731        let mut consumer = test_graph(
3732            vec![test_input("in", "in0")],
3733            vec![OutputFilter::new(
3734                String::new(),
3735                AVMEDIA_TYPE_VIDEO,
3736                "out0".to_string(),
3737            )],
3738        );
3739        consumer.inputs[0].bound = true;
3740
3741        let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
3742        assert!(
3743            result.is_ok(),
3744            "a bound pad labeled 'in' must not fall through to stream auto-binding: {result:?}"
3745        );
3746    }
3747
3748    #[test]
3749    fn test_filter() {
3750        let desc_cstr = CString::new("[1:v][2:v]concat=n=2:v=1:a=0[vout]").unwrap();
3751        // let desc_cstr = CString::new("fps=15").unwrap();
3752
3753        unsafe {
3754            let graph = crate::raw::FilterGraph::alloc().unwrap();
3755            let mut inputs = crate::raw::FilterInOut::empty();
3756            let mut outputs = crate::raw::FilterInOut::empty();
3757
3758            let ret = avfilter_graph_parse_ptr(
3759                graph.as_ptr(),
3760                desc_cstr.as_ptr(),
3761                inputs.as_out_ptr(),
3762                outputs.as_out_ptr(),
3763                null_mut(),
3764            );
3765            if ret < 0 {
3766                println!("err ret:{}", crate::util::ffmpeg_utils::av_err2str(ret));
3767                return;
3768            }
3769
3770            println!("inputs.is_null:{}", inputs.as_ptr().is_null());
3771            println!("outputs.is_null:{}", outputs.as_ptr().is_null());
3772
3773            let mut cur = inputs.as_ptr();
3774            while !cur.is_null() {
3775                let input_name = CStr::from_ptr((*cur).name);
3776                println!("Input name: {}", input_name.to_str().unwrap());
3777                cur = (*cur).next;
3778            }
3779
3780            let output_name = CStr::from_ptr((*outputs.as_ptr()).name);
3781            println!("Output name: {}", output_name.to_str().unwrap());
3782
3783            let filter_ctx = (*outputs.as_ptr()).filter_ctx;
3784            println!("filter_ctx.is_null:{}", filter_ctx.is_null());
3785        }
3786    }
3787
3788    #[test]
3789    fn fallback_frame_carries_real_stream_parameters() {
3790        // Regression: the probe dec_ctx used to stay at codec defaults, so
3791        // the fallback frame bound to each filtergraph input had format=-1
3792        // and the EOF-before-first-frame path could never configure a graph.
3793        let ctx = FfmpegContext::new(
3794            vec!["test.mp4".into()],
3795            vec!["hue=s=0".into()],
3796            vec!["output_fallback_probe.mp4".to_string().into()],
3797        )
3798        .unwrap();
3799        let fallback = unsafe { &*ctx.filter_graphs[0].inputs[0].opts.fallback.as_ptr() };
3800        assert!(
3801            fallback.format >= 0,
3802            "fallback must carry the stream's real format, got {}",
3803            fallback.format
3804        );
3805        assert!(
3806            fallback.width > 0 && fallback.height > 0,
3807            "fallback must carry the stream's dimensions, got {}x{}",
3808            fallback.width,
3809            fallback.height
3810        );
3811        assert!(
3812            fallback.time_base.num > 0 && fallback.time_base.den > 0,
3813            "fallback must carry the stream's packet time base, got {}/{}",
3814            fallback.time_base.num,
3815            fallback.time_base.den
3816        );
3817    }
3818
3819    #[test]
3820    fn test_new() {
3821        let _ = env_logger::builder()
3822            .filter_level(log::LevelFilter::Debug)
3823            .is_test(true)
3824            .try_init();
3825        let _ffmpeg_context = FfmpegContext::new(
3826            vec!["test.mp4".to_string().into()],
3827            vec!["hue=s=0".to_string().into()],
3828            vec!["output.mp4".to_string().into()],
3829        )
3830        .unwrap();
3831        let _ffmpeg_context = FfmpegContext::new(
3832            vec!["test.mp4".into()],
3833            vec!["[0:v]hue=s=0".into()],
3834            vec!["output.mp4".to_string().into()],
3835        )
3836        .unwrap();
3837        let _ffmpeg_context = FfmpegContext::new(
3838            vec!["test.mp4".into()],
3839            vec!["hue=s=0[my-out]".into()],
3840            vec![Output::from("output.mp4").add_stream_map("my-out")],
3841        )
3842        .unwrap();
3843        let result = FfmpegContext::new(
3844            vec!["test.mp4".into()],
3845            vec!["hue=s=0".into()],
3846            vec![Output::from("output.mp4").add_stream_map("0:v?")],
3847        );
3848        assert!(result.is_err());
3849        let result = FfmpegContext::new(
3850            vec!["test.mp4".into()],
3851            vec!["hue=s=0".into()],
3852            vec![Output::from("output.mp4").add_stream_map_with_copy("1:v?")],
3853        );
3854        assert!(result.is_err());
3855        let result = FfmpegContext::new(
3856            vec!["test.mp4".into()],
3857            vec!["hue=s=0[fg-out]".into()],
3858            vec![
3859                Output::from("output.mp4").add_stream_map("my-out?"),
3860                Output::from("output.mp4").add_stream_map("fg-out"),
3861            ],
3862        );
3863        assert!(result.is_err());
3864        // ignore filter
3865        let result = FfmpegContext::new(
3866            vec!["test.mp4".into()],
3867            vec!["hue=s=0".into()],
3868            vec![Output::from("output.mp4").add_stream_map_with_copy("1:v")],
3869        );
3870        assert!(result.is_err());
3871        let result = FfmpegContext::new(
3872            vec!["test.mp4".into()],
3873            vec!["hue=s=0[fg-out]".into()],
3874            vec![Output::from("output.mp4").add_stream_map("fg-out?")],
3875        );
3876        assert!(result.is_err());
3877    }
3878
3879    #[test]
3880    fn test_builder() {
3881        let _ = env_logger::builder()
3882            .filter_level(log::LevelFilter::Debug)
3883            .is_test(true)
3884            .try_init();
3885
3886        let _context1 = FfmpegContext::builder()
3887            .input("test.mp4")
3888            .filter_desc("hue=s=0")
3889            .output("output.mp4")
3890            .build()
3891            .unwrap();
3892
3893        let _context2 = FfmpegContext::builder()
3894            .inputs(vec!["test.mp4"])
3895            .filter_descs(vec!["hue=s=0"])
3896            .outputs(vec!["output.mp4"])
3897            .build()
3898            .unwrap();
3899    }
3900
3901    #[test]
3902    fn test_strtol() {
3903        let input = "-123---abc";
3904        let result = strtol(input);
3905        assert_eq!(result.unwrap(), (-123, "---abc"));
3906
3907        let input = "123---abc";
3908        let result = strtol(input);
3909        assert_eq!(result.unwrap(), (123, "---abc"));
3910
3911        let input = "-123aa";
3912        let result = strtol(input);
3913        assert_eq!(result.unwrap(), (-123, "aa"));
3914
3915        let input = "-aa";
3916        let result = strtol(input);
3917        assert!(result.is_err());
3918
3919        let input = "abc";
3920        let result = strtol(input);
3921        assert!(result.is_err())
3922    }
3923}