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