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