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