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