1use crate::core::context::demuxer::{CopyMuxHandle, 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::{ExpandedStreamMap, Output};
9use crate::core::context::output_filter::{
10 OutputFilter, OFILTER_FLAG_AUDIO_24BIT, OFILTER_FLAG_AUTOSCALE, OFILTER_FLAG_DISABLE_CONVERT,
11};
12use crate::core::context::{frame_alloc, CodecContext};
13use crate::core::metadata::StreamSpecifier;
14use crate::core::scheduler::ffmpeg_scheduler;
15use crate::core::scheduler::ffmpeg_scheduler::{FfmpegScheduler, Initialization};
16#[cfg(not(docsrs))]
17use crate::core::scheduler::filter_task::graph_opts_apply;
18use crate::core::scheduler::input_controller::SchNode;
19use crate::error::Error::{
20 FileSameAsInput, FilterZeroInputs, FilterZeroOutputs, FrameFilterStreamTypeNoMatched,
21 FrameFilterTypeNoMatched, ParseInteger,
22};
23use crate::error::FilterGraphParseError::{
24 InvalidFileIndexInFg, InvalidFilterSpecifier, OutputUnconnected,
25};
26use crate::error::{
27 AllocOutputContextError, FilterGraphOperationError, FilterGraphParseError, FindStreamError,
28 OpenInputError, OpenOutputError,
29};
30use crate::error::{Error, Result};
31use crate::filter::frame_pipeline::FramePipeline;
32use crate::hwaccel::{hw_device_for_filter, init_filter_hw_device};
33use crate::util::ffmpeg_utils::{hashmap_to_avdictionary, DictGuard};
34#[cfg(not(docsrs))]
35use ffmpeg_sys_next::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC;
36#[cfg(not(docsrs))]
37use ffmpeg_sys_next::AVCodecConfig::*;
38use ffmpeg_sys_next::AVCodecID::{AV_CODEC_ID_AC3, AV_CODEC_ID_MP3, AV_CODEC_ID_NONE};
39use ffmpeg_sys_next::AVColorRange::AVCOL_RANGE_UNSPECIFIED;
40use ffmpeg_sys_next::AVColorSpace::AVCOL_SPC_UNSPECIFIED;
41use ffmpeg_sys_next::AVMediaType::{
42 AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
43 AVMEDIA_TYPE_VIDEO,
44};
45use ffmpeg_sys_next::AVPixelFormat::AV_PIX_FMT_NONE;
46use ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_NONE;
47use ffmpeg_sys_next::{
48 av_add_q, av_channel_layout_default, av_codec_get_id, av_codec_get_tag2, av_freep,
49 av_get_exact_bits_per_sample, av_get_pix_fmt, av_guess_codec, av_guess_format,
50 av_guess_frame_rate, av_inv_q, av_malloc, av_rescale_q, av_seek_frame, avcodec_alloc_context3,
51 avcodec_descriptor_get, avcodec_descriptor_get_by_name, avcodec_find_encoder,
52 avcodec_find_encoder_by_name, avcodec_get_name, avcodec_parameters_from_context,
53 avcodec_parameters_to_context, avformat_alloc_context, avformat_alloc_output_context2,
54 avformat_close_input, avformat_find_stream_info, avformat_flush, avformat_open_input,
55 avio_alloc_context, AVCodec, AVCodecID, AVColorRange, AVColorSpace, AVFormatContext,
56 AVMediaType, AVOutputFormat, AVPixelFormat, AVRational, AVSampleFormat, AVStream,
57 AVERROR_ENCODER_NOT_FOUND, AVFMT_FLAG_CUSTOM_IO, AVFMT_GLOBALHEADER, AVFMT_NOBINSEARCH,
58 AVFMT_NOFILE, AVFMT_NOGENSEARCH, AVFMT_NOSTREAMS, AVSEEK_FLAG_BACKWARD,
59 AV_CODEC_PROP_BITMAP_SUB, AV_CODEC_PROP_TEXT_SUB, AV_TIME_BASE,
60};
61#[cfg(not(docsrs))]
62use ffmpeg_sys_next::{
63 av_buffer_ref, av_channel_layout_copy, av_packet_side_data_new, avcodec_get_supported_config,
64 avfilter_graph_segment_create_filters, avfilter_graph_segment_free,
65 avfilter_graph_segment_parse, AVChannelLayout, AVFILTER_FLAG_HWDEVICE,
66};
67#[cfg(not(docsrs))]
68use ffmpeg_sys_next::{
69 avformat_query_codec, AVSTREAM_EVENT_FLAG_NEW_PACKETS, AV_DISPOSITION_ATTACHED_PIC,
70 AV_DISPOSITION_DEFAULT,
71};
72use log::{debug, error, info, warn};
73use std::collections::HashMap;
74use std::ffi::{c_void, CStr, CString};
75use std::ptr::{null, null_mut};
76use std::sync::Arc;
77
78mod fg_bind;
79#[cfg(not(docsrs))]
80mod fg_probe;
81pub(crate) mod open_input;
82mod open_output;
83mod opt_util;
84#[cfg(all(test, not(docsrs)))]
85mod per_stream_encoder_tests;
86mod writer_build;
87
88pub(crate) use writer_build::build_writer_context;
89
90const LOG_TARGET: &str = module_path!();
93
94use fg_bind::{fg_bind_inputs, init_filter_graphs};
95use open_input::open_input_files;
96use open_output::open_output_files;
97use opt_util::outputs_bind;
98
99pub(super) use open_input::InputOpaque;
100pub(super) use open_output::OutputOpaque;
101
102#[cfg(test)]
105use fg_bind::{bind_fg_inputs_by_fg, fg_complex_bind_input};
106#[cfg(test)]
107use open_input::{read_packet_wrapper, seek_input_packet_wrapper};
108#[cfg(test)]
109use open_output::{seek_output_packet_wrapper, write_packet_wrapper};
110
111pub struct FfmpegContext {
119 pub(crate) independent_readrate: bool,
120 pub(crate) demuxs: Vec<Demuxer>,
121 pub(crate) filter_graphs: Vec<FilterGraph>,
122 pub(crate) muxs: Vec<Muxer>,
123 pub(crate) frame_sources: Vec<crate::core::context::frame_source::FrameSource>,
127 pub(crate) scheduler_status: Arc<std::sync::atomic::AtomicUsize>,
130 pub(crate) interrupt_state: Arc<crate::core::context::InterruptState>,
131}
132
133unsafe impl Send for FfmpegContext {}
139
140impl FfmpegContext {
141 pub fn builder() -> FfmpegContextBuilder {
154 FfmpegContextBuilder::new()
155 }
156
157 pub fn start(self) -> Result<FfmpegScheduler<ffmpeg_scheduler::Running>> {
183 let ffmpeg_scheduler = FfmpegScheduler::new(self);
184 ffmpeg_scheduler.start()
185 }
186
187 #[allow(dead_code)]
188 pub(crate) fn new(
189 inputs: Vec<Input>,
190 filter_complexs: Vec<FilterComplex>,
191 outputs: Vec<Output>,
192 ) -> Result<FfmpegContext> {
193 Self::new_with_options(false, inputs, filter_complexs, outputs, false, Vec::new())
194 }
195
196 pub(crate) fn new_with_options(
197 mut independent_readrate: bool,
198 mut inputs: Vec<Input>,
199 mut filter_complexs: Vec<FilterComplex>,
200 mut outputs: Vec<Output>,
201 copy_ts: bool,
202 deferred_filter_descs: Vec<
203 crate::core::context::ffmpeg_context_builder::DeferredFilterDesc,
204 >,
205 ) -> Result<FfmpegContext> {
206 check_duplicate_inputs_outputs(&inputs, &outputs)?;
207
208 crate::core::initialize_ffmpeg();
209
210 let scheduler_status = Arc::new(std::sync::atomic::AtomicUsize::new(
214 crate::core::scheduler::ffmpeg_scheduler::STATUS_INIT,
215 ));
216 let interrupt_state = Arc::new(crate::core::context::InterruptState::new(
217 scheduler_status.clone(),
218 ));
219
220 let mut demuxs = open_input_files(&mut inputs, copy_ts, &interrupt_state)?;
221
222 if demuxs.len() <= 1 {
223 independent_readrate = false;
224 }
225
226 for deferred in deferred_filter_descs {
233 filter_complexs.push(deferred(&demuxs)?);
234 }
235
236 let mut filter_graphs = if !filter_complexs.is_empty() {
237 let mut filter_graphs = init_filter_graphs(filter_complexs)?;
238 fg_bind_inputs(&mut filter_graphs, &mut demuxs)?;
239 filter_graphs
240 } else {
241 Vec::new()
242 };
243
244 let mut muxs = open_output_files(&mut outputs, copy_ts, &interrupt_state)?;
245
246 outputs_bind(&mut muxs, &mut filter_graphs, &mut demuxs)?;
247
248 for mux in muxs.iter_mut() {
255 if mux.recording_time_us.is_none() {
256 let mapping = mux.stream_input_mapping();
257 if !mapping.is_empty() {
258 let first_input = mapping[0].1 .0;
259 let all_same_input = mapping.iter().all(|(_, (idx, _))| *idx == first_input);
260 if all_same_input {
261 if let Some(demux) = demuxs.get(first_input) {
262 if let Some(recording_time) = demux.recording_time_us {
263 mux.recording_time_us = Some(recording_time);
264 }
265 }
266 }
267 }
268 }
269 }
270
271 correct_input_start_times(&mut demuxs, copy_ts);
272
273 check_output_streams(&muxs)?;
274
275 check_fg_bindings(&filter_graphs)?;
276
277 check_frame_filter_pipeline(&muxs, &demuxs)?;
278
279 Ok(Self {
280 independent_readrate,
281 demuxs,
282 filter_graphs,
283 muxs,
284 frame_sources: Vec::new(),
285 scheduler_status,
286 interrupt_state,
287 })
288 }
289}
290
291const START_AT_ZERO: bool = false;
292
293fn correct_input_start_times(demuxs: &mut Vec<Demuxer>, copy_ts: bool) {
294 for (i, demux) in demuxs.iter_mut().enumerate() {
295 unsafe {
296 let is = demux.in_fmt_ctx_ptr();
297
298 demux.start_time_effective = (*is).start_time;
299 if (*is).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
300 || (*(*is).iformat).flags & ffmpeg_sys_next::AVFMT_TS_DISCONT == 0
301 {
302 continue;
303 }
304
305 let mut new_start_time = i64::MAX;
306 let stream_count = (*is).nb_streams;
307 for j in 0..stream_count {
308 let st = *(*is).streams.add(j as usize);
309 if (*st).discard == ffmpeg_sys_next::AVDiscard::AVDISCARD_ALL
310 || (*st).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
311 {
312 continue;
313 }
314 new_start_time = std::cmp::min(
315 new_start_time,
316 av_rescale_q(
317 (*st).start_time,
318 (*st).time_base,
319 ffmpeg_sys_next::AV_TIME_BASE_Q,
320 ),
321 );
322 }
323 let diff = new_start_time - (*is).start_time;
324 if diff != 0 {
325 debug!("Correcting start time of Input #{i} by {diff}us.");
326 demux.start_time_effective = new_start_time;
327 if copy_ts && START_AT_ZERO {
328 demux.ts_offset = -new_start_time;
329 } else if !copy_ts {
330 let abs_start_seek = (*is).start_time + demux.start_time_us.unwrap_or(0);
331 demux.ts_offset = if abs_start_seek > new_start_time {
332 -abs_start_seek
333 } else {
334 -new_start_time
335 };
336 } else if copy_ts {
337 demux.ts_offset = 0;
338 }
339
340 }
342 }
343 }
344}
345
346fn check_pipeline<T>(
347 frame_pipelines: Option<&Vec<FramePipeline>>,
348 streams: &[T],
349 tag: &str,
350 get_stream_index: impl Fn(&T) -> usize,
351 get_codec_type: impl Fn(&T) -> &AVMediaType,
352) -> Result<()> {
353 let tag_cap = {
354 let mut chars = tag.chars();
355 match chars.next() {
356 None => String::new(),
357 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
358 }
359 };
360
361 frame_pipelines
362 .into_iter()
363 .flat_map(|pipelines| pipelines.iter())
364 .try_for_each(|pipeline| {
365 if let Some(idx) = pipeline.stream_index {
366 streams
367 .iter()
368 .any(|s| {
369 get_stream_index(s) == idx && get_codec_type(s) == &pipeline.media_type
370 })
371 .then_some(())
372 .ok_or_else(|| {
373 Into::<crate::error::Error>::into(FrameFilterStreamTypeNoMatched(
374 tag_cap.clone(),
375 idx,
376 format!("{:?}", pipeline.media_type),
377 ))
378 })
379 } else {
380 streams
381 .iter()
382 .any(|s| get_codec_type(s) == &pipeline.media_type)
383 .then_some(())
384 .ok_or_else(|| {
385 FrameFilterTypeNoMatched(tag.into(), format!("{:?}", pipeline.media_type))
386 })
387 }
388 })?;
389 Ok(())
390}
391
392fn check_frame_filter_pipeline(muxs: &[Muxer], demuxs: &[Demuxer]) -> Result<()> {
393 muxs.iter().try_for_each(|mux| {
394 check_pipeline(
395 mux.frame_pipelines.as_ref(),
396 mux.get_streams(),
397 "output",
398 |s| s.stream_index,
399 |s| &s.codec_type,
400 )
401 })?;
402 demuxs.iter().try_for_each(|demux| {
403 check_pipeline(
404 demux.frame_pipelines.as_ref(),
405 demux.get_streams(),
406 "input",
407 |s| s.stream_index,
408 |s| &s.codec_type,
409 )
410 })?;
411 Ok(())
412}
413
414fn check_fg_bindings(filter_graphs: &Vec<FilterGraph>) -> Result<()> {
415 for filter_graph in filter_graphs {
417 for (i, output_filter) in filter_graph.outputs.iter().enumerate() {
418 if !output_filter.has_dst() {
419 let linklabel = if output_filter.linklabel.is_empty() {
420 "unlabeled".to_string()
421 } else {
422 output_filter.linklabel.clone()
423 };
424 return Err(OutputUnconnected(output_filter.name.clone(), i, linklabel).into());
425 }
426 }
427 }
428 Ok(())
429}
430
431impl From<FfmpegContext> for FfmpegScheduler<Initialization> {
432 fn from(val: FfmpegContext) -> Self {
433 FfmpegScheduler::new(val)
434 }
435}
436
437fn check_output_streams(muxs: &Vec<Muxer>) -> Result<()> {
438 for mux in muxs {
439 unsafe {
440 let oformat = (*mux.out_fmt_ctx_ptr()).oformat;
441 if !mux.has_src() && (*oformat).flags & AVFMT_NOSTREAMS == 0 {
442 if mux.is_packet_sink() {
446 warn!("Packet-sink output does not contain any stream");
447 return Err(crate::error::PacketSinkError::NoStreams.into());
448 }
449 warn!("Output file does not contain any stream");
450 return Err(OpenOutputError::NotContainStream.into());
451 }
452 }
453 }
454 Ok(())
455}
456
457fn check_duplicate_inputs_outputs(inputs: &[Input], outputs: &[Output]) -> Result<()> {
458 for output in outputs {
459 if let Some(output_url) = output.url() {
460 for input in inputs {
461 if let Some(input_url) = &input.url {
462 if input_url == output_url {
463 return Err(FileSameAsInput(input_url.clone()));
464 }
465 }
466 }
467 }
468 }
469 Ok(())
470}
471
472fn strtol(input: &str) -> Result<(i64, &str)> {
476 let mut chars = input.chars().peekable();
477 let mut negative = false;
478
479 if let Some(&ch) = chars.peek() {
480 if ch == '-' {
481 negative = true;
482 chars.next();
483 } else if !ch.is_ascii_digit() {
484 return Err(ParseInteger);
485 }
486 }
487
488 let number_start = input.len() - chars.clone().collect::<String>().len();
489
490 let number_str: String = chars
491 .by_ref()
492 .take_while(|ch| ch.is_ascii_digit())
493 .collect();
494
495 if number_str.is_empty() {
496 return Err(ParseInteger);
497 }
498
499 let number: i64 = number_str.parse().map_err(|_| ParseInteger)?;
500
501 let remainder_index = number_start + number_str.len();
502 let remainder = &input[remainder_index..];
503
504 if negative {
505 Ok((-number, remainder))
506 } else {
507 Ok((number, remainder))
508 }
509}
510
511fn convert_options(
512 opts: Option<HashMap<String, String>>,
513) -> Result<Option<HashMap<CString, CString>>> {
514 if opts.is_none() {
515 return Ok(None);
516 }
517
518 let converted = opts.map(|map| {
519 map.into_iter()
520 .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?)))
521 .collect::<Result<HashMap<CString, CString>, _>>() });
523
524 converted.transpose() }
526
527#[cfg(test)]
528mod tests {
529 use std::ffi::{CStr, CString};
530 use std::ptr::null_mut;
531
532 use crate::core::context::ffmpeg_context::{strtol, FfmpegContext, Output};
533 use ffmpeg_sys_next::avfilter_graph_parse_ptr;
534
535 use crate::core::context::ffmpeg_context::{bind_fg_inputs_by_fg, fg_complex_bind_input};
536 use crate::core::context::ffmpeg_context::{
537 read_packet_wrapper, seek_input_packet_wrapper, seek_output_packet_wrapper,
538 write_packet_wrapper, InputOpaque, OutputOpaque,
539 };
540 use crate::core::context::filter_graph::FilterGraph;
541 use crate::core::context::input_filter::InputFilter;
542 use crate::core::context::null_frame;
543 use crate::core::context::output_filter::OutputFilter;
544 use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
545
546 fn test_input(linklabel: &str, name: &str) -> InputFilter {
547 InputFilter::new(
548 linklabel.to_string(),
549 AVMEDIA_TYPE_VIDEO,
550 name.to_string(),
551 null_frame(),
552 )
553 }
554
555 fn test_graph(inputs: Vec<InputFilter>, outputs: Vec<OutputFilter>) -> FilterGraph {
556 FilterGraph::new("null".to_string(), inputs, outputs, None, None)
557 }
558
559 #[test]
566 fn builder_leaves_cross_graph_pad_as_a_hole_in_scheduler_inputs() {
567 use crate::core::context::input::Input;
568 use crate::core::scheduler::input_controller::SchNode;
569
570 let out = std::env::temp_dir().join(format!(
571 "ez_ffmpeg_xgraph_struct_{}.mp4",
572 std::process::id()
573 ));
574 let ctx = FfmpegContext::builder()
575 .input(Input::from("color=c=red:s=64x64:r=15:d=0.2").set_format("lavfi"))
576 .input(Input::from("color=c=blue:s=64x64:r=15:d=0.2").set_format("lavfi"))
577 .filter_desc("[0:v]hue=s=0[mid]")
578 .filter_desc("[mid][1:v]overlay[vout]")
579 .output(
580 Output::from(out.to_str().unwrap())
581 .add_stream_map("vout")
582 .set_video_codec("mpeg4"),
583 )
584 .build()
585 .expect("cross-graph build");
586
587 let consumer = ctx
588 .filter_graphs
589 .iter()
590 .find(|fg| fg.graph_desc.contains("overlay"))
591 .expect("consumer graph present");
592 let SchNode::Filter { inputs, .. } = consumer.node.as_ref() else {
593 panic!("consumer node must be a Filter");
594 };
595 assert_eq!(inputs.len(), 2, "one scheduler-input slot per filter pad");
596 assert!(inputs[0].is_none(), "pad 0 ([mid]) is a cross-graph hole");
597 assert!(inputs[1].is_some(), "pad 1 ([1:v]) binds a demuxer");
598 }
599
600 #[test]
601 fn cross_graph_binding_uses_input_pad_index_and_marks_bound() {
602 let producer = test_graph(
608 vec![test_input("", "in0")],
609 vec![OutputFilter::new(
610 "mid".to_string(),
611 AVMEDIA_TYPE_VIDEO,
612 "out0".to_string(),
613 )],
614 );
615 let consumer = test_graph(
616 vec![test_input("mid", "in0"), test_input("", "in1")],
617 vec![OutputFilter::new(
618 String::new(),
619 AVMEDIA_TYPE_VIDEO,
620 "out0".to_string(),
621 )],
622 );
623 let mut graphs = vec![producer, consumer];
624
625 bind_fg_inputs_by_fg(&mut graphs).unwrap();
626
627 assert!(
628 graphs[0].outputs[0].has_dst(),
629 "producer output must be connected to the consumer"
630 );
631 assert_eq!(
632 graphs[0].outputs[0].fg_input_index, 0,
633 "fg_input_index must be the consumer's input PAD index, not its graph index"
634 );
635 assert_eq!(
636 graphs[0].outputs[0].finished_flag_list.len(),
637 2,
638 "the producer must hold the consumer's per-pad finished flags"
639 );
640 assert!(
641 graphs[1].inputs[0].bound,
642 "the cross-connected pad must be marked bound"
643 );
644 assert!(
645 !graphs[1].inputs[1].bound,
646 "unrelated pads must stay unbound"
647 );
648 }
649
650 #[test]
651 fn complex_bind_skips_already_bound_labeled_input() {
652 let mut consumer = test_graph(
653 vec![test_input("mid", "in0")],
654 vec![OutputFilter::new(
655 String::new(),
656 AVMEDIA_TYPE_VIDEO,
657 "out0".to_string(),
658 )],
659 );
660 consumer.inputs[0].bound = true;
661
662 let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
665 assert!(
666 result.is_ok(),
667 "a pad already bound to another graph must not be re-bound: {result:?}"
668 );
669 }
670
671 #[test]
672 fn complex_bind_skips_bound_reserved_in_label() {
673 let mut consumer = test_graph(
676 vec![test_input("in", "in0")],
677 vec![OutputFilter::new(
678 String::new(),
679 AVMEDIA_TYPE_VIDEO,
680 "out0".to_string(),
681 )],
682 );
683 consumer.inputs[0].bound = true;
684
685 let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
686 assert!(
687 result.is_ok(),
688 "a bound pad labeled 'in' must not fall through to stream auto-binding: {result:?}"
689 );
690 }
691
692 #[test]
693 fn test_filter() {
694 let desc_cstr = CString::new("[1:v][2:v]concat=n=2:v=1:a=0[vout]").unwrap();
695 unsafe {
698 let graph = crate::raw::FilterGraph::alloc().unwrap();
699 let mut inputs = crate::raw::FilterInOut::empty();
700 let mut outputs = crate::raw::FilterInOut::empty();
701
702 let ret = avfilter_graph_parse_ptr(
703 graph.as_ptr(),
704 desc_cstr.as_ptr(),
705 inputs.as_out_ptr(),
706 outputs.as_out_ptr(),
707 null_mut(),
708 );
709 if ret < 0 {
710 println!("err ret:{}", crate::util::ffmpeg_utils::av_err2str(ret));
711 return;
712 }
713
714 println!("inputs.is_null:{}", inputs.as_ptr().is_null());
715 println!("outputs.is_null:{}", outputs.as_ptr().is_null());
716
717 let mut cur = inputs.as_ptr();
718 while !cur.is_null() {
719 let input_name = CStr::from_ptr((*cur).name);
720 println!("Input name: {}", input_name.to_str().unwrap());
721 cur = (*cur).next;
722 }
723
724 let output_name = CStr::from_ptr((*outputs.as_ptr()).name);
725 println!("Output name: {}", output_name.to_str().unwrap());
726
727 let filter_ctx = (*outputs.as_ptr()).filter_ctx;
728 println!("filter_ctx.is_null:{}", filter_ctx.is_null());
729 }
730 }
731
732 #[test]
733 fn fallback_frame_carries_real_stream_parameters() {
734 let ctx = FfmpegContext::new(
738 vec!["test.mp4".into()],
739 vec!["hue=s=0".into()],
740 vec!["output_fallback_probe.mp4".to_string().into()],
741 )
742 .unwrap();
743 let fallback = unsafe { &*ctx.filter_graphs[0].inputs[0].opts.fallback.as_ptr() };
744 assert!(
745 fallback.format >= 0,
746 "fallback must carry the stream's real format, got {}",
747 fallback.format
748 );
749 assert!(
750 fallback.width > 0 && fallback.height > 0,
751 "fallback must carry the stream's dimensions, got {}x{}",
752 fallback.width,
753 fallback.height
754 );
755 assert!(
756 fallback.time_base.num > 0 && fallback.time_base.den > 0,
757 "fallback must carry the stream's packet time base, got {}/{}",
758 fallback.time_base.num,
759 fallback.time_base.den
760 );
761 }
762
763 #[test]
764 fn test_new() {
765 let _ = env_logger::builder()
766 .filter_level(log::LevelFilter::Debug)
767 .is_test(true)
768 .try_init();
769 let _ffmpeg_context = FfmpegContext::new(
770 vec!["test.mp4".to_string().into()],
771 vec!["hue=s=0".to_string().into()],
772 vec!["output.mp4".to_string().into()],
773 )
774 .unwrap();
775 let _ffmpeg_context = FfmpegContext::new(
776 vec!["test.mp4".into()],
777 vec!["[0:v]hue=s=0".into()],
778 vec!["output.mp4".to_string().into()],
779 )
780 .unwrap();
781 let _ffmpeg_context = FfmpegContext::new(
782 vec!["test.mp4".into()],
783 vec!["hue=s=0[my-out]".into()],
784 vec![Output::from("output.mp4").add_stream_map("my-out")],
785 )
786 .unwrap();
787 let result = FfmpegContext::new(
788 vec!["test.mp4".into()],
789 vec!["hue=s=0".into()],
790 vec![Output::from("output.mp4").add_stream_map("0:v?")],
791 );
792 assert!(result.is_err());
793 let result = FfmpegContext::new(
794 vec!["test.mp4".into()],
795 vec!["hue=s=0".into()],
796 vec![Output::from("output.mp4").add_stream_map_with_copy("1:v?")],
797 );
798 assert!(result.is_err());
799 let result = FfmpegContext::new(
800 vec!["test.mp4".into()],
801 vec!["hue=s=0[fg-out]".into()],
802 vec![
803 Output::from("output.mp4").add_stream_map("my-out?"),
804 Output::from("output.mp4").add_stream_map("fg-out"),
805 ],
806 );
807 assert!(result.is_err());
808 let result = FfmpegContext::new(
810 vec!["test.mp4".into()],
811 vec!["hue=s=0".into()],
812 vec![Output::from("output.mp4").add_stream_map_with_copy("1:v")],
813 );
814 assert!(result.is_err());
815 let result = FfmpegContext::new(
816 vec!["test.mp4".into()],
817 vec!["hue=s=0[fg-out]".into()],
818 vec![Output::from("output.mp4").add_stream_map("fg-out?")],
819 );
820 assert!(result.is_err());
821 }
822
823 #[test]
824 fn test_builder() {
825 let _ = env_logger::builder()
826 .filter_level(log::LevelFilter::Debug)
827 .is_test(true)
828 .try_init();
829
830 let _context1 = FfmpegContext::builder()
831 .input("test.mp4")
832 .filter_desc("hue=s=0")
833 .output("output.mp4")
834 .build()
835 .unwrap();
836
837 let _context2 = FfmpegContext::builder()
838 .inputs(vec!["test.mp4"])
839 .filter_descs(vec!["hue=s=0"])
840 .outputs(vec!["output.mp4"])
841 .build()
842 .unwrap();
843 }
844
845 #[test]
846 fn test_strtol() {
847 let input = "-123---abc";
848 let result = strtol(input);
849 assert_eq!(result.unwrap(), (-123, "---abc"));
850
851 let input = "123---abc";
852 let result = strtol(input);
853 assert_eq!(result.unwrap(), (123, "---abc"));
854
855 let input = "-123aa";
856 let result = strtol(input);
857 assert_eq!(result.unwrap(), (-123, "aa"));
858
859 let input = "-aa";
860 let result = strtol(input);
861 assert!(result.is_err());
862
863 let input = "abc";
864 let result = strtol(input);
865 assert!(result.is_err())
866 }
867
868 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
876 use std::sync::Arc;
877
878 fn eio() -> libc::c_int {
879 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)
880 }
881
882 #[test]
883 fn read_wrapper_clamps_oversized_length_and_allows_exact_fit() {
884 let opaque = Box::into_raw(Box::new(InputOpaque {
885 read: Box::new(|buf| buf.len() as i32 + 64),
886 seek: None,
887 poisoned: false,
888 }));
889 let mut buf = [0u8; 32];
890 let ret = unsafe {
891 read_packet_wrapper(
892 opaque as *mut libc::c_void,
893 buf.as_mut_ptr(),
894 buf.len() as libc::c_int,
895 )
896 };
897 assert_eq!(ret, eio(), "a forged over-length must clamp to EIO");
898 unsafe {
899 (*opaque).read = Box::new(|buf| buf.len() as i32);
901 let ret = read_packet_wrapper(
902 opaque as *mut libc::c_void,
903 buf.as_mut_ptr(),
904 buf.len() as libc::c_int,
905 );
906 assert_eq!(
907 ret,
908 buf.len() as libc::c_int,
909 "ret == buf_size is within bounds and must pass through"
910 );
911 drop(Box::from_raw(opaque));
912 }
913 }
914
915 #[test]
916 fn read_wrapper_contains_panic_and_poisons() {
917 let calls = Arc::new(AtomicUsize::new(0));
918 let probe = Arc::clone(&calls);
919 let opaque = Box::into_raw(Box::new(InputOpaque {
920 read: Box::new(move |_buf| {
921 probe.fetch_add(1, AtomicOrdering::SeqCst);
922 panic!("test-injected read panic");
923 }),
924 seek: None,
925 poisoned: false,
926 }));
927 let mut buf = [0u8; 32];
928 for _ in 0..3 {
929 let ret = unsafe {
930 read_packet_wrapper(
931 opaque as *mut libc::c_void,
932 buf.as_mut_ptr(),
933 buf.len() as libc::c_int,
934 )
935 };
936 assert_eq!(ret, eio());
937 }
938 assert_eq!(
939 calls.load(AtomicOrdering::SeqCst),
940 1,
941 "the panicking closure must never be re-entered once poisoned"
942 );
943 unsafe { drop(Box::from_raw(opaque)) };
944 }
945
946 #[test]
947 fn write_wrapper_contains_panic_and_poisons() {
948 let calls = Arc::new(AtomicUsize::new(0));
949 let probe = Arc::clone(&calls);
950 let opaque = Box::into_raw(Box::new(OutputOpaque {
951 write: Box::new(move |_buf| {
952 probe.fetch_add(1, AtomicOrdering::SeqCst);
953 panic!("test-injected write panic");
954 }),
955 seek: None,
956 poisoned: false,
957 }));
958 let buf = [0u8; 32];
959 for _ in 0..3 {
960 let ret = unsafe {
961 write_packet_wrapper(
962 opaque as *mut libc::c_void,
963 buf.as_ptr(),
964 buf.len() as libc::c_int,
965 )
966 };
967 assert_eq!(ret, eio());
968 }
969 assert_eq!(
970 calls.load(AtomicOrdering::SeqCst),
971 1,
972 "the panicking closure must never be re-entered once poisoned"
973 );
974 unsafe { drop(Box::from_raw(opaque)) };
975 }
976
977 #[test]
978 fn all_avio_wrappers_dispose_panicking_panic_payloads() {
979 struct PanicOnDrop(Arc<AtomicUsize>);
980
981 impl Drop for PanicOnDrop {
982 fn drop(&mut self) {
983 self.0.fetch_add(1, AtomicOrdering::SeqCst);
984 panic!("test panic payload destructor");
985 }
986 }
987
988 let payload_drops = Arc::new(AtomicUsize::new(0));
989 let mut read_buf = [0u8; 8];
990 let write_buf = [0u8; 8];
991
992 let read_drop = Arc::clone(&payload_drops);
993 let input_read = Box::into_raw(Box::new(InputOpaque {
994 read: Box::new(move |_buf| -> i32 {
995 std::panic::panic_any(PanicOnDrop(Arc::clone(&read_drop)))
996 }),
997 seek: None,
998 poisoned: false,
999 }));
1000 let ret = unsafe {
1001 read_packet_wrapper(
1002 input_read as *mut libc::c_void,
1003 read_buf.as_mut_ptr(),
1004 read_buf.len() as libc::c_int,
1005 )
1006 };
1007 assert_eq!(ret, eio());
1008 unsafe { drop(Box::from_raw(input_read)) };
1009
1010 let seek_drop = Arc::clone(&payload_drops);
1011 let input_seek = Box::into_raw(Box::new(InputOpaque {
1012 read: Box::new(|buf| buf.len() as i32),
1013 seek: Some(Box::new(move |_offset, _whence| -> i64 {
1014 std::panic::panic_any(PanicOnDrop(Arc::clone(&seek_drop)))
1015 })),
1016 poisoned: false,
1017 }));
1018 let ret = unsafe { seek_input_packet_wrapper(input_seek.cast(), 0, 0) };
1019 assert_eq!(ret, eio() as i64);
1020 unsafe { drop(Box::from_raw(input_seek)) };
1021
1022 let write_drop = Arc::clone(&payload_drops);
1023 let output_write = Box::into_raw(Box::new(OutputOpaque {
1024 write: Box::new(move |_buf| -> i32 {
1025 std::panic::panic_any(PanicOnDrop(Arc::clone(&write_drop)))
1026 }),
1027 seek: None,
1028 poisoned: false,
1029 }));
1030 let ret = unsafe {
1031 write_packet_wrapper(
1032 output_write.cast(),
1033 write_buf.as_ptr(),
1034 write_buf.len() as libc::c_int,
1035 )
1036 };
1037 assert_eq!(ret, eio());
1038 unsafe { drop(Box::from_raw(output_write)) };
1039
1040 let output_seek_drop = Arc::clone(&payload_drops);
1041 let output_seek = Box::into_raw(Box::new(OutputOpaque {
1042 write: Box::new(|buf| buf.len() as i32),
1043 seek: Some(Box::new(move |_offset, _whence| -> i64 {
1044 std::panic::panic_any(PanicOnDrop(Arc::clone(&output_seek_drop)))
1045 })),
1046 poisoned: false,
1047 }));
1048 let ret = unsafe { seek_output_packet_wrapper(output_seek.cast(), 0, 0) };
1049 assert_eq!(ret, eio() as i64);
1050 unsafe { drop(Box::from_raw(output_seek)) };
1051
1052 assert_eq!(
1053 payload_drops.load(AtomicOrdering::SeqCst),
1054 4,
1055 "every caught panic payload destructor must run under containment"
1056 );
1057 }
1058
1059 #[test]
1060 fn seek_panic_poisons_the_whole_input_context() {
1061 let reads = Arc::new(AtomicUsize::new(0));
1062 let read_probe = Arc::clone(&reads);
1063 let opaque = Box::into_raw(Box::new(InputOpaque {
1064 read: Box::new(move |buf| {
1065 read_probe.fetch_add(1, AtomicOrdering::SeqCst);
1066 buf.len() as i32
1067 }),
1068 seek: Some(Box::new(|_offset, _whence| {
1069 panic!("test-injected seek panic")
1070 })),
1071 poisoned: false,
1072 }));
1073 let mut buf = [0u8; 32];
1074 unsafe {
1075 let ret = read_packet_wrapper(
1077 opaque as *mut libc::c_void,
1078 buf.as_mut_ptr(),
1079 buf.len() as libc::c_int,
1080 );
1081 assert_eq!(ret, buf.len() as libc::c_int);
1082
1083 let ret = seek_input_packet_wrapper(opaque as *mut libc::c_void, 0, 0);
1086 assert_eq!(ret, eio() as i64);
1087
1088 let ret = read_packet_wrapper(
1090 opaque as *mut libc::c_void,
1091 buf.as_mut_ptr(),
1092 buf.len() as libc::c_int,
1093 );
1094 assert_eq!(ret, eio());
1095 drop(Box::from_raw(opaque));
1096 }
1097 assert_eq!(
1098 reads.load(AtomicOrdering::SeqCst),
1099 1,
1100 "a seek panic must poison reads on the same context"
1101 );
1102 }
1103
1104 #[test]
1107 fn write_wrapper_resubmits_short_writes() {
1108 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1109 let probe = Arc::clone(&seen);
1110 let opaque = Box::into_raw(Box::new(OutputOpaque {
1111 write: Box::new(move |buf| {
1112 let n = buf.len().min(10);
1114 probe.lock().unwrap().extend_from_slice(&buf[..n]);
1115 n as i32
1116 }),
1117 seek: None,
1118 poisoned: false,
1119 }));
1120 let data: Vec<u8> = (0..64u8).collect();
1121 let ret = unsafe {
1122 write_packet_wrapper(
1123 opaque as *mut libc::c_void,
1124 data.as_ptr(),
1125 data.len() as libc::c_int,
1126 )
1127 };
1128 assert_eq!(
1129 ret,
1130 data.len() as libc::c_int,
1131 "the whole buffer must report written"
1132 );
1133 assert_eq!(
1134 *seen.lock().unwrap(),
1135 data,
1136 "no byte may be lost or reordered"
1137 );
1138 unsafe { drop(Box::from_raw(opaque)) };
1139 }
1140
1141 #[test]
1144 fn write_wrapper_rejects_zero_progress_and_over_claims() {
1145 let opaque = Box::into_raw(Box::new(OutputOpaque {
1146 write: Box::new(|_buf| 0),
1147 seek: None,
1148 poisoned: false,
1149 }));
1150 let data = [7u8; 16];
1151 let ret = unsafe { write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 16) };
1152 assert_eq!(
1153 ret,
1154 eio(),
1155 "a zero-progress sink must fail, not spin or succeed"
1156 );
1157 unsafe {
1158 (*opaque).write = Box::new(|buf| buf.len() as i32 + 4);
1159 let ret = write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 16);
1160 assert_eq!(ret, eio(), "an over-claimed write length must fail");
1161 drop(Box::from_raw(opaque));
1162 }
1163 }
1164
1165 #[test]
1167 fn write_wrapper_passes_sink_errors_through() {
1168 let opaque = Box::into_raw(Box::new(OutputOpaque {
1169 write: Box::new(|_buf| ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ENOSPC)),
1170 seek: None,
1171 poisoned: false,
1172 }));
1173 let data = [7u8; 8];
1174 let ret = unsafe { write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 8) };
1175 assert_eq!(ret, ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ENOSPC));
1176 unsafe { drop(Box::from_raw(opaque)) };
1177 }
1178
1179 #[test]
1180 fn absent_seek_callback_stays_espipe() {
1181 let opaque = Box::into_raw(Box::new(InputOpaque {
1182 read: Box::new(|buf| buf.len() as i32),
1183 seek: None,
1184 poisoned: false,
1185 }));
1186 let ret = unsafe { seek_input_packet_wrapper(opaque as *mut libc::c_void, 0, 0) };
1187 assert_eq!(
1188 ret,
1189 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64,
1190 "no seek callback means genuinely unseekable, not an I/O fault"
1191 );
1192 unsafe { drop(Box::from_raw(opaque)) };
1193 }
1194}