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