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_free_context,
55 avformat_open_input, avio_alloc_context, AVCodec, AVCodecID, AVColorRange, AVColorSpace,
56 AVFormatContext, AVMediaType, AVOutputFormat, AVPixelFormat, AVRational, AVSampleFormat,
57 AVStream, AVERROR_ENCODER_NOT_FOUND, AVFMT_FLAG_CUSTOM_IO, AVFMT_GLOBALHEADER,
58 AVFMT_NOBINSEARCH, 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(all(test, not(docsrs)))]
80mod per_stream_encoder_tests;
81#[cfg(not(docsrs))]
82mod fg_probe;
83pub(crate) mod open_input;
84mod open_output;
85mod opt_util;
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::write_packet_wrapper;
110
111pub struct FfmpegContext {
112 pub(crate) independent_readrate: bool,
113 pub(crate) demuxs: Vec<Demuxer>,
114 pub(crate) filter_graphs: Vec<FilterGraph>,
115 pub(crate) muxs: Vec<Muxer>,
116 pub(crate) frame_sources: Vec<crate::core::context::frame_source::FrameSource>,
120 pub(crate) scheduler_status: Arc<std::sync::atomic::AtomicUsize>,
123 pub(crate) interrupt_state: Arc<crate::core::context::InterruptState>,
124}
125
126unsafe impl Send for FfmpegContext {}
132
133impl FfmpegContext {
134 pub fn builder() -> FfmpegContextBuilder {
147 FfmpegContextBuilder::new()
148 }
149
150 pub fn start(self) -> Result<FfmpegScheduler<ffmpeg_scheduler::Running>> {
176 let ffmpeg_scheduler = FfmpegScheduler::new(self);
177 ffmpeg_scheduler.start()
178 }
179
180 #[allow(dead_code)]
181 pub(crate) fn new(
182 inputs: Vec<Input>,
183 filter_complexs: Vec<FilterComplex>,
184 outputs: Vec<Output>,
185 ) -> Result<FfmpegContext> {
186 Self::new_with_options(false, inputs, filter_complexs, outputs, false, Vec::new())
187 }
188
189 pub(crate) fn new_with_options(
190 mut independent_readrate: bool,
191 mut inputs: Vec<Input>,
192 mut filter_complexs: Vec<FilterComplex>,
193 mut outputs: Vec<Output>,
194 copy_ts: bool,
195 deferred_filter_descs: Vec<
196 crate::core::context::ffmpeg_context_builder::DeferredFilterDesc,
197 >,
198 ) -> Result<FfmpegContext> {
199 check_duplicate_inputs_outputs(&inputs, &outputs)?;
200
201 crate::core::initialize_ffmpeg();
202
203 let scheduler_status = Arc::new(std::sync::atomic::AtomicUsize::new(
207 crate::core::scheduler::ffmpeg_scheduler::STATUS_INIT,
208 ));
209 let interrupt_state = Arc::new(crate::core::context::InterruptState::new(
210 scheduler_status.clone(),
211 ));
212
213 let mut demuxs = open_input_files(&mut inputs, copy_ts, &interrupt_state)?;
214
215 if demuxs.len() <= 1 {
216 independent_readrate = false;
217 }
218
219 for deferred in deferred_filter_descs {
226 filter_complexs.push(deferred(&demuxs)?);
227 }
228
229 let mut filter_graphs = if !filter_complexs.is_empty() {
230 let mut filter_graphs = init_filter_graphs(filter_complexs)?;
231 fg_bind_inputs(&mut filter_graphs, &mut demuxs)?;
232 filter_graphs
233 } else {
234 Vec::new()
235 };
236
237 let mut muxs = open_output_files(&mut outputs, copy_ts, &interrupt_state)?;
238
239 outputs_bind(&mut muxs, &mut filter_graphs, &mut demuxs)?;
240
241 for mux in muxs.iter_mut() {
248 if mux.recording_time_us.is_none() {
249 let mapping = mux.stream_input_mapping();
250 if !mapping.is_empty() {
251 let first_input = mapping[0].1 .0;
252 let all_same_input = mapping.iter().all(|(_, (idx, _))| *idx == first_input);
253 if all_same_input {
254 if let Some(demux) = demuxs.get(first_input) {
255 if let Some(recording_time) = demux.recording_time_us {
256 mux.recording_time_us = Some(recording_time);
257 }
258 }
259 }
260 }
261 }
262 }
263
264 correct_input_start_times(&mut demuxs, copy_ts);
265
266 check_output_streams(&muxs)?;
267
268 check_fg_bindings(&filter_graphs)?;
269
270 check_frame_filter_pipeline(&muxs, &demuxs)?;
271
272 Ok(Self {
273 independent_readrate,
274 demuxs,
275 filter_graphs,
276 muxs,
277 frame_sources: Vec::new(),
278 scheduler_status,
279 interrupt_state,
280 })
281 }
282}
283
284const START_AT_ZERO: bool = false;
285
286fn correct_input_start_times(demuxs: &mut Vec<Demuxer>, copy_ts: bool) {
287 for (i, demux) in demuxs.iter_mut().enumerate() {
288 unsafe {
289 let is = demux.in_fmt_ctx_ptr();
290
291 demux.start_time_effective = (*is).start_time;
292 if (*is).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
293 || (*(*is).iformat).flags & ffmpeg_sys_next::AVFMT_TS_DISCONT == 0
294 {
295 continue;
296 }
297
298 let mut new_start_time = i64::MAX;
299 let stream_count = (*is).nb_streams;
300 for j in 0..stream_count {
301 let st = *(*is).streams.add(j as usize);
302 if (*st).discard == ffmpeg_sys_next::AVDiscard::AVDISCARD_ALL
303 || (*st).start_time == ffmpeg_sys_next::AV_NOPTS_VALUE
304 {
305 continue;
306 }
307 new_start_time = std::cmp::min(
308 new_start_time,
309 av_rescale_q(
310 (*st).start_time,
311 (*st).time_base,
312 ffmpeg_sys_next::AV_TIME_BASE_Q,
313 ),
314 );
315 }
316 let diff = new_start_time - (*is).start_time;
317 if diff != 0 {
318 debug!("Correcting start time of Input #{i} by {diff}us.");
319 demux.start_time_effective = new_start_time;
320 if copy_ts && START_AT_ZERO {
321 demux.ts_offset = -new_start_time;
322 } else if !copy_ts {
323 let abs_start_seek = (*is).start_time + demux.start_time_us.unwrap_or(0);
324 demux.ts_offset = if abs_start_seek > new_start_time {
325 -abs_start_seek
326 } else {
327 -new_start_time
328 };
329 } else if copy_ts {
330 demux.ts_offset = 0;
331 }
332
333 }
335 }
336 }
337}
338
339fn check_pipeline<T>(
340 frame_pipelines: Option<&Vec<FramePipeline>>,
341 streams: &[T],
342 tag: &str,
343 get_stream_index: impl Fn(&T) -> usize,
344 get_codec_type: impl Fn(&T) -> &AVMediaType,
345) -> Result<()> {
346 let tag_cap = {
347 let mut chars = tag.chars();
348 match chars.next() {
349 None => String::new(),
350 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
351 }
352 };
353
354 frame_pipelines
355 .into_iter()
356 .flat_map(|pipelines| pipelines.iter())
357 .try_for_each(|pipeline| {
358 if let Some(idx) = pipeline.stream_index {
359 streams
360 .iter()
361 .any(|s| {
362 get_stream_index(s) == idx && get_codec_type(s) == &pipeline.media_type
363 })
364 .then_some(())
365 .ok_or_else(|| {
366 Into::<crate::error::Error>::into(FrameFilterStreamTypeNoMatched(
367 tag_cap.clone(),
368 idx,
369 format!("{:?}", pipeline.media_type),
370 ))
371 })
372 } else {
373 streams
374 .iter()
375 .any(|s| get_codec_type(s) == &pipeline.media_type)
376 .then_some(())
377 .ok_or_else(|| {
378 FrameFilterTypeNoMatched(tag.into(), format!("{:?}", pipeline.media_type))
379 })
380 }
381 })?;
382 Ok(())
383}
384
385fn check_frame_filter_pipeline(muxs: &[Muxer], demuxs: &[Demuxer]) -> Result<()> {
386 muxs.iter().try_for_each(|mux| {
387 check_pipeline(
388 mux.frame_pipelines.as_ref(),
389 mux.get_streams(),
390 "output",
391 |s| s.stream_index,
392 |s| &s.codec_type,
393 )
394 })?;
395 demuxs.iter().try_for_each(|demux| {
396 check_pipeline(
397 demux.frame_pipelines.as_ref(),
398 demux.get_streams(),
399 "input",
400 |s| s.stream_index,
401 |s| &s.codec_type,
402 )
403 })?;
404 Ok(())
405}
406
407fn check_fg_bindings(filter_graphs: &Vec<FilterGraph>) -> Result<()> {
408 for filter_graph in filter_graphs {
410 for (i, output_filter) in filter_graph.outputs.iter().enumerate() {
411 if !output_filter.has_dst() {
412 let linklabel = if output_filter.linklabel.is_empty() {
413 "unlabeled".to_string()
414 } else {
415 output_filter.linklabel.clone()
416 };
417 return Err(OutputUnconnected(output_filter.name.clone(), i, linklabel).into());
418 }
419 }
420 }
421 Ok(())
422}
423
424impl From<FfmpegContext> for FfmpegScheduler<Initialization> {
425 fn from(val: FfmpegContext) -> Self {
426 FfmpegScheduler::new(val)
427 }
428}
429
430fn check_output_streams(muxs: &Vec<Muxer>) -> Result<()> {
431 for mux in muxs {
432 unsafe {
433 let oformat = (*mux.out_fmt_ctx_ptr()).oformat;
434 if !mux.has_src() && (*oformat).flags & AVFMT_NOSTREAMS == 0 {
435 if mux.is_packet_sink() {
439 warn!("Packet-sink output does not contain any stream");
440 return Err(crate::error::PacketSinkError::NoStreams.into());
441 }
442 warn!("Output file does not contain any stream");
443 return Err(OpenOutputError::NotContainStream.into());
444 }
445 }
446 }
447 Ok(())
448}
449
450fn check_duplicate_inputs_outputs(inputs: &[Input], outputs: &[Output]) -> Result<()> {
451 for output in outputs {
452 if let Some(output_url) = output.url() {
453 for input in inputs {
454 if let Some(input_url) = &input.url {
455 if input_url == output_url {
456 return Err(FileSameAsInput(input_url.clone()));
457 }
458 }
459 }
460 }
461 }
462 Ok(())
463}
464
465fn strtol(input: &str) -> Result<(i64, &str)> {
469 let mut chars = input.chars().peekable();
470 let mut negative = false;
471
472 if let Some(&ch) = chars.peek() {
473 if ch == '-' {
474 negative = true;
475 chars.next();
476 } else if !ch.is_ascii_digit() {
477 return Err(ParseInteger);
478 }
479 }
480
481 let number_start = input.len() - chars.clone().collect::<String>().len();
482
483 let number_str: String = chars
484 .by_ref()
485 .take_while(|ch| ch.is_ascii_digit())
486 .collect();
487
488 if number_str.is_empty() {
489 return Err(ParseInteger);
490 }
491
492 let number: i64 = number_str.parse().map_err(|_| ParseInteger)?;
493
494 let remainder_index = number_start + number_str.len();
495 let remainder = &input[remainder_index..];
496
497 if negative {
498 Ok((-number, remainder))
499 } else {
500 Ok((number, remainder))
501 }
502}
503
504fn convert_options(
505 opts: Option<HashMap<String, String>>,
506) -> Result<Option<HashMap<CString, CString>>> {
507 if opts.is_none() {
508 return Ok(None);
509 }
510
511 let converted = opts.map(|map| {
512 map.into_iter()
513 .map(|(k, v)| Ok((CString::new(k)?, CString::new(v)?)))
514 .collect::<Result<HashMap<CString, CString>, _>>() });
516
517 converted.transpose() }
519
520#[cfg(test)]
521mod tests {
522 use std::ffi::{CStr, CString};
523 use std::ptr::null_mut;
524
525 use crate::core::context::ffmpeg_context::{strtol, FfmpegContext, Output};
526 use ffmpeg_sys_next::avfilter_graph_parse_ptr;
527
528 use crate::core::context::ffmpeg_context::{bind_fg_inputs_by_fg, fg_complex_bind_input};
529 use crate::core::context::ffmpeg_context::{
530 read_packet_wrapper, seek_input_packet_wrapper, write_packet_wrapper, InputOpaque,
531 OutputOpaque,
532 };
533 use crate::core::context::filter_graph::FilterGraph;
534 use crate::core::context::input_filter::InputFilter;
535 use crate::core::context::null_frame;
536 use crate::core::context::output_filter::OutputFilter;
537 use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
538
539 fn test_input(linklabel: &str, name: &str) -> InputFilter {
540 InputFilter::new(
541 linklabel.to_string(),
542 AVMEDIA_TYPE_VIDEO,
543 name.to_string(),
544 null_frame(),
545 )
546 }
547
548 fn test_graph(inputs: Vec<InputFilter>, outputs: Vec<OutputFilter>) -> FilterGraph {
549 FilterGraph::new("null".to_string(), inputs, outputs, None, None)
550 }
551
552 #[test]
559 fn builder_leaves_cross_graph_pad_as_a_hole_in_scheduler_inputs() {
560 use crate::core::context::input::Input;
561 use crate::core::scheduler::input_controller::SchNode;
562
563 let out = std::env::temp_dir().join(format!(
564 "ez_ffmpeg_xgraph_struct_{}.mp4",
565 std::process::id()
566 ));
567 let ctx = FfmpegContext::builder()
568 .input(Input::from("color=c=red:s=64x64:r=15:d=0.2").set_format("lavfi"))
569 .input(Input::from("color=c=blue:s=64x64:r=15:d=0.2").set_format("lavfi"))
570 .filter_desc("[0:v]hue=s=0[mid]")
571 .filter_desc("[mid][1:v]overlay[vout]")
572 .output(
573 Output::from(out.to_str().unwrap())
574 .add_stream_map("vout")
575 .set_video_codec("mpeg4"),
576 )
577 .build()
578 .expect("cross-graph build");
579
580 let consumer = ctx
581 .filter_graphs
582 .iter()
583 .find(|fg| fg.graph_desc.contains("overlay"))
584 .expect("consumer graph present");
585 let SchNode::Filter { inputs, .. } = consumer.node.as_ref() else {
586 panic!("consumer node must be a Filter");
587 };
588 assert_eq!(inputs.len(), 2, "one scheduler-input slot per filter pad");
589 assert!(inputs[0].is_none(), "pad 0 ([mid]) is a cross-graph hole");
590 assert!(inputs[1].is_some(), "pad 1 ([1:v]) binds a demuxer");
591 }
592
593 #[test]
594 fn cross_graph_binding_uses_input_pad_index_and_marks_bound() {
595 let producer = test_graph(
601 vec![test_input("", "in0")],
602 vec![OutputFilter::new(
603 "mid".to_string(),
604 AVMEDIA_TYPE_VIDEO,
605 "out0".to_string(),
606 )],
607 );
608 let consumer = test_graph(
609 vec![test_input("mid", "in0"), test_input("", "in1")],
610 vec![OutputFilter::new(
611 String::new(),
612 AVMEDIA_TYPE_VIDEO,
613 "out0".to_string(),
614 )],
615 );
616 let mut graphs = vec![producer, consumer];
617
618 bind_fg_inputs_by_fg(&mut graphs).unwrap();
619
620 assert!(
621 graphs[0].outputs[0].has_dst(),
622 "producer output must be connected to the consumer"
623 );
624 assert_eq!(
625 graphs[0].outputs[0].fg_input_index, 0,
626 "fg_input_index must be the consumer's input PAD index, not its graph index"
627 );
628 assert_eq!(
629 graphs[0].outputs[0].finished_flag_list.len(),
630 2,
631 "the producer must hold the consumer's per-pad finished flags"
632 );
633 assert!(
634 graphs[1].inputs[0].bound,
635 "the cross-connected pad must be marked bound"
636 );
637 assert!(
638 !graphs[1].inputs[1].bound,
639 "unrelated pads must stay unbound"
640 );
641 }
642
643 #[test]
644 fn complex_bind_skips_already_bound_labeled_input() {
645 let mut consumer = test_graph(
646 vec![test_input("mid", "in0")],
647 vec![OutputFilter::new(
648 String::new(),
649 AVMEDIA_TYPE_VIDEO,
650 "out0".to_string(),
651 )],
652 );
653 consumer.inputs[0].bound = true;
654
655 let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
658 assert!(
659 result.is_ok(),
660 "a pad already bound to another graph must not be re-bound: {result:?}"
661 );
662 }
663
664 #[test]
665 fn complex_bind_skips_bound_reserved_in_label() {
666 let mut consumer = test_graph(
669 vec![test_input("in", "in0")],
670 vec![OutputFilter::new(
671 String::new(),
672 AVMEDIA_TYPE_VIDEO,
673 "out0".to_string(),
674 )],
675 );
676 consumer.inputs[0].bound = true;
677
678 let result = fg_complex_bind_input(&mut consumer, 0, &mut Vec::new());
679 assert!(
680 result.is_ok(),
681 "a bound pad labeled 'in' must not fall through to stream auto-binding: {result:?}"
682 );
683 }
684
685 #[test]
686 fn test_filter() {
687 let desc_cstr = CString::new("[1:v][2:v]concat=n=2:v=1:a=0[vout]").unwrap();
688 unsafe {
691 let graph = crate::raw::FilterGraph::alloc().unwrap();
692 let mut inputs = crate::raw::FilterInOut::empty();
693 let mut outputs = crate::raw::FilterInOut::empty();
694
695 let ret = avfilter_graph_parse_ptr(
696 graph.as_ptr(),
697 desc_cstr.as_ptr(),
698 inputs.as_out_ptr(),
699 outputs.as_out_ptr(),
700 null_mut(),
701 );
702 if ret < 0 {
703 println!("err ret:{}", crate::util::ffmpeg_utils::av_err2str(ret));
704 return;
705 }
706
707 println!("inputs.is_null:{}", inputs.as_ptr().is_null());
708 println!("outputs.is_null:{}", outputs.as_ptr().is_null());
709
710 let mut cur = inputs.as_ptr();
711 while !cur.is_null() {
712 let input_name = CStr::from_ptr((*cur).name);
713 println!("Input name: {}", input_name.to_str().unwrap());
714 cur = (*cur).next;
715 }
716
717 let output_name = CStr::from_ptr((*outputs.as_ptr()).name);
718 println!("Output name: {}", output_name.to_str().unwrap());
719
720 let filter_ctx = (*outputs.as_ptr()).filter_ctx;
721 println!("filter_ctx.is_null:{}", filter_ctx.is_null());
722 }
723 }
724
725 #[test]
726 fn fallback_frame_carries_real_stream_parameters() {
727 let ctx = FfmpegContext::new(
731 vec!["test.mp4".into()],
732 vec!["hue=s=0".into()],
733 vec!["output_fallback_probe.mp4".to_string().into()],
734 )
735 .unwrap();
736 let fallback = unsafe { &*ctx.filter_graphs[0].inputs[0].opts.fallback.as_ptr() };
737 assert!(
738 fallback.format >= 0,
739 "fallback must carry the stream's real format, got {}",
740 fallback.format
741 );
742 assert!(
743 fallback.width > 0 && fallback.height > 0,
744 "fallback must carry the stream's dimensions, got {}x{}",
745 fallback.width,
746 fallback.height
747 );
748 assert!(
749 fallback.time_base.num > 0 && fallback.time_base.den > 0,
750 "fallback must carry the stream's packet time base, got {}/{}",
751 fallback.time_base.num,
752 fallback.time_base.den
753 );
754 }
755
756 #[test]
757 fn test_new() {
758 let _ = env_logger::builder()
759 .filter_level(log::LevelFilter::Debug)
760 .is_test(true)
761 .try_init();
762 let _ffmpeg_context = FfmpegContext::new(
763 vec!["test.mp4".to_string().into()],
764 vec!["hue=s=0".to_string().into()],
765 vec!["output.mp4".to_string().into()],
766 )
767 .unwrap();
768 let _ffmpeg_context = FfmpegContext::new(
769 vec!["test.mp4".into()],
770 vec!["[0:v]hue=s=0".into()],
771 vec!["output.mp4".to_string().into()],
772 )
773 .unwrap();
774 let _ffmpeg_context = FfmpegContext::new(
775 vec!["test.mp4".into()],
776 vec!["hue=s=0[my-out]".into()],
777 vec![Output::from("output.mp4").add_stream_map("my-out")],
778 )
779 .unwrap();
780 let result = FfmpegContext::new(
781 vec!["test.mp4".into()],
782 vec!["hue=s=0".into()],
783 vec![Output::from("output.mp4").add_stream_map("0:v?")],
784 );
785 assert!(result.is_err());
786 let result = FfmpegContext::new(
787 vec!["test.mp4".into()],
788 vec!["hue=s=0".into()],
789 vec![Output::from("output.mp4").add_stream_map_with_copy("1:v?")],
790 );
791 assert!(result.is_err());
792 let result = FfmpegContext::new(
793 vec!["test.mp4".into()],
794 vec!["hue=s=0[fg-out]".into()],
795 vec![
796 Output::from("output.mp4").add_stream_map("my-out?"),
797 Output::from("output.mp4").add_stream_map("fg-out"),
798 ],
799 );
800 assert!(result.is_err());
801 let result = FfmpegContext::new(
803 vec!["test.mp4".into()],
804 vec!["hue=s=0".into()],
805 vec![Output::from("output.mp4").add_stream_map_with_copy("1:v")],
806 );
807 assert!(result.is_err());
808 let result = FfmpegContext::new(
809 vec!["test.mp4".into()],
810 vec!["hue=s=0[fg-out]".into()],
811 vec![Output::from("output.mp4").add_stream_map("fg-out?")],
812 );
813 assert!(result.is_err());
814 }
815
816 #[test]
817 fn test_builder() {
818 let _ = env_logger::builder()
819 .filter_level(log::LevelFilter::Debug)
820 .is_test(true)
821 .try_init();
822
823 let _context1 = FfmpegContext::builder()
824 .input("test.mp4")
825 .filter_desc("hue=s=0")
826 .output("output.mp4")
827 .build()
828 .unwrap();
829
830 let _context2 = FfmpegContext::builder()
831 .inputs(vec!["test.mp4"])
832 .filter_descs(vec!["hue=s=0"])
833 .outputs(vec!["output.mp4"])
834 .build()
835 .unwrap();
836 }
837
838 #[test]
839 fn test_strtol() {
840 let input = "-123---abc";
841 let result = strtol(input);
842 assert_eq!(result.unwrap(), (-123, "---abc"));
843
844 let input = "123---abc";
845 let result = strtol(input);
846 assert_eq!(result.unwrap(), (123, "---abc"));
847
848 let input = "-123aa";
849 let result = strtol(input);
850 assert_eq!(result.unwrap(), (-123, "aa"));
851
852 let input = "-aa";
853 let result = strtol(input);
854 assert!(result.is_err());
855
856 let input = "abc";
857 let result = strtol(input);
858 assert!(result.is_err())
859 }
860
861 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
869 use std::sync::Arc;
870
871 fn eio() -> libc::c_int {
872 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)
873 }
874
875 #[test]
876 fn read_wrapper_clamps_oversized_length_and_allows_exact_fit() {
877 let opaque = Box::into_raw(Box::new(InputOpaque {
878 read: Box::new(|buf| buf.len() as i32 + 64),
879 seek: None,
880 poisoned: false,
881 }));
882 let mut buf = [0u8; 32];
883 let ret = unsafe {
884 read_packet_wrapper(
885 opaque as *mut libc::c_void,
886 buf.as_mut_ptr(),
887 buf.len() as libc::c_int,
888 )
889 };
890 assert_eq!(ret, eio(), "a forged over-length must clamp to EIO");
891 unsafe {
892 (*opaque).read = Box::new(|buf| buf.len() as i32);
894 let ret = read_packet_wrapper(
895 opaque as *mut libc::c_void,
896 buf.as_mut_ptr(),
897 buf.len() as libc::c_int,
898 );
899 assert_eq!(
900 ret,
901 buf.len() as libc::c_int,
902 "ret == buf_size is within bounds and must pass through"
903 );
904 drop(Box::from_raw(opaque));
905 }
906 }
907
908 #[test]
909 fn read_wrapper_contains_panic_and_poisons() {
910 let calls = Arc::new(AtomicUsize::new(0));
911 let probe = Arc::clone(&calls);
912 let opaque = Box::into_raw(Box::new(InputOpaque {
913 read: Box::new(move |_buf| {
914 probe.fetch_add(1, AtomicOrdering::SeqCst);
915 panic!("test-injected read panic");
916 }),
917 seek: None,
918 poisoned: false,
919 }));
920 let mut buf = [0u8; 32];
921 for _ in 0..3 {
922 let ret = unsafe {
923 read_packet_wrapper(
924 opaque as *mut libc::c_void,
925 buf.as_mut_ptr(),
926 buf.len() as libc::c_int,
927 )
928 };
929 assert_eq!(ret, eio());
930 }
931 assert_eq!(
932 calls.load(AtomicOrdering::SeqCst),
933 1,
934 "the panicking closure must never be re-entered once poisoned"
935 );
936 unsafe { drop(Box::from_raw(opaque)) };
937 }
938
939 #[test]
940 fn write_wrapper_contains_panic_and_poisons() {
941 let calls = Arc::new(AtomicUsize::new(0));
942 let probe = Arc::clone(&calls);
943 let opaque = Box::into_raw(Box::new(OutputOpaque {
944 write: Box::new(move |_buf| {
945 probe.fetch_add(1, AtomicOrdering::SeqCst);
946 panic!("test-injected write panic");
947 }),
948 seek: None,
949 poisoned: false,
950 }));
951 let buf = [0u8; 32];
952 for _ in 0..3 {
953 let ret = unsafe {
954 write_packet_wrapper(
955 opaque as *mut libc::c_void,
956 buf.as_ptr(),
957 buf.len() as libc::c_int,
958 )
959 };
960 assert_eq!(ret, eio());
961 }
962 assert_eq!(
963 calls.load(AtomicOrdering::SeqCst),
964 1,
965 "the panicking closure must never be re-entered once poisoned"
966 );
967 unsafe { drop(Box::from_raw(opaque)) };
968 }
969
970 #[test]
971 fn seek_panic_poisons_the_whole_input_context() {
972 let reads = Arc::new(AtomicUsize::new(0));
973 let read_probe = Arc::clone(&reads);
974 let opaque = Box::into_raw(Box::new(InputOpaque {
975 read: Box::new(move |buf| {
976 read_probe.fetch_add(1, AtomicOrdering::SeqCst);
977 buf.len() as i32
978 }),
979 seek: Some(Box::new(|_offset, _whence| {
980 panic!("test-injected seek panic")
981 })),
982 poisoned: false,
983 }));
984 let mut buf = [0u8; 32];
985 unsafe {
986 let ret = read_packet_wrapper(
988 opaque as *mut libc::c_void,
989 buf.as_mut_ptr(),
990 buf.len() as libc::c_int,
991 );
992 assert_eq!(ret, buf.len() as libc::c_int);
993
994 let ret = seek_input_packet_wrapper(opaque as *mut libc::c_void, 0, 0);
997 assert_eq!(ret, eio() as i64);
998
999 let ret = read_packet_wrapper(
1001 opaque as *mut libc::c_void,
1002 buf.as_mut_ptr(),
1003 buf.len() as libc::c_int,
1004 );
1005 assert_eq!(ret, eio());
1006 drop(Box::from_raw(opaque));
1007 }
1008 assert_eq!(
1009 reads.load(AtomicOrdering::SeqCst),
1010 1,
1011 "a seek panic must poison reads on the same context"
1012 );
1013 }
1014
1015 #[test]
1018 fn write_wrapper_resubmits_short_writes() {
1019 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1020 let probe = Arc::clone(&seen);
1021 let opaque = Box::into_raw(Box::new(OutputOpaque {
1022 write: Box::new(move |buf| {
1023 let n = buf.len().min(10);
1025 probe.lock().unwrap().extend_from_slice(&buf[..n]);
1026 n as i32
1027 }),
1028 seek: None,
1029 poisoned: false,
1030 }));
1031 let data: Vec<u8> = (0..64u8).collect();
1032 let ret = unsafe {
1033 write_packet_wrapper(
1034 opaque as *mut libc::c_void,
1035 data.as_ptr(),
1036 data.len() as libc::c_int,
1037 )
1038 };
1039 assert_eq!(
1040 ret,
1041 data.len() as libc::c_int,
1042 "the whole buffer must report written"
1043 );
1044 assert_eq!(
1045 *seen.lock().unwrap(),
1046 data,
1047 "no byte may be lost or reordered"
1048 );
1049 unsafe { drop(Box::from_raw(opaque)) };
1050 }
1051
1052 #[test]
1055 fn write_wrapper_rejects_zero_progress_and_over_claims() {
1056 let opaque = Box::into_raw(Box::new(OutputOpaque {
1057 write: Box::new(|_buf| 0),
1058 seek: None,
1059 poisoned: false,
1060 }));
1061 let data = [7u8; 16];
1062 let ret = unsafe { write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 16) };
1063 assert_eq!(
1064 ret,
1065 eio(),
1066 "a zero-progress sink must fail, not spin or succeed"
1067 );
1068 unsafe {
1069 (*opaque).write = Box::new(|buf| buf.len() as i32 + 4);
1070 let ret = write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 16);
1071 assert_eq!(ret, eio(), "an over-claimed write length must fail");
1072 drop(Box::from_raw(opaque));
1073 }
1074 }
1075
1076 #[test]
1078 fn write_wrapper_passes_sink_errors_through() {
1079 let opaque = Box::into_raw(Box::new(OutputOpaque {
1080 write: Box::new(|_buf| ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ENOSPC)),
1081 seek: None,
1082 poisoned: false,
1083 }));
1084 let data = [7u8; 8];
1085 let ret = unsafe { write_packet_wrapper(opaque as *mut libc::c_void, data.as_ptr(), 8) };
1086 assert_eq!(ret, ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ENOSPC));
1087 unsafe { drop(Box::from_raw(opaque)) };
1088 }
1089
1090 #[test]
1091 fn absent_seek_callback_stays_espipe() {
1092 let opaque = Box::into_raw(Box::new(InputOpaque {
1093 read: Box::new(|buf| buf.len() as i32),
1094 seek: None,
1095 poisoned: false,
1096 }));
1097 let ret = unsafe { seek_input_packet_wrapper(opaque as *mut libc::c_void, 0, 0) };
1098 assert_eq!(
1099 ret,
1100 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64,
1101 "no seek callback means genuinely unseekable, not an I/O fault"
1102 );
1103 unsafe { drop(Box::from_raw(opaque)) };
1104 }
1105}