1use crate::error::AllocFrameError;
2use ffmpeg_sys_next::AVMediaType::{
3 AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
4 AVMEDIA_TYPE_VIDEO,
5};
6#[cfg(ffmpeg_8_0)]
11use ffmpeg_sys_next::av_frame_side_data_clone;
12#[cfg(not(docsrs))]
13use ffmpeg_sys_next::av_frame_side_data_free;
14use ffmpeg_sys_next::{
15 av_freep, av_gettime_relative, avcodec_free_context, avformat_close_input,
16 avformat_free_context, avio_closep, avio_context_free, AVCodecContext, AVFormatContext,
17 AVFrameSideData, AVIOContext, AVMediaType, AVRational, AVStream, AVFMT_NOFILE,
18};
19use std::ffi::c_void;
20use std::ptr::null_mut;
21use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
22use std::sync::Arc;
23
24const OUTPUT_END_GRACE_US: i64 = 500_000;
31
32pub(crate) const DEFAULT_CUSTOM_IO_BUFFER_SIZE: usize = 64 * 1024;
38
39pub(crate) struct InterruptState {
48 scheduler_status: Arc<AtomicUsize>,
49 end_grace_start_us: AtomicI64,
52 finalizing_outputs: AtomicUsize,
58}
59
60impl InterruptState {
61 pub(crate) fn new(scheduler_status: Arc<AtomicUsize>) -> Self {
62 Self {
63 scheduler_status,
64 end_grace_start_us: AtomicI64::new(0),
65 finalizing_outputs: AtomicUsize::new(0),
66 }
67 }
68
69 pub(crate) fn begin_output_finalize(self: &Arc<Self>) -> OutputFinalizeGuard {
71 self.finalizing_outputs.fetch_add(1, Ordering::AcqRel);
72 OutputFinalizeGuard {
73 state: Arc::clone(self),
74 }
75 }
76
77 fn should_interrupt_input(&self) -> bool {
78 crate::core::scheduler::ffmpeg_scheduler::is_stopping(
79 self.scheduler_status.load(Ordering::Acquire),
80 )
81 }
82
83 fn should_interrupt_output(&self) -> bool {
84 let status = self.scheduler_status.load(Ordering::Acquire);
85 if status == crate::core::scheduler::ffmpeg_scheduler::STATUS_ABORT {
86 return true;
87 }
88 if status != crate::core::scheduler::ffmpeg_scheduler::STATUS_END {
89 return false;
90 }
91 if self.finalizing_outputs.load(Ordering::Acquire) > 0 {
92 return false;
98 }
99 let now = unsafe { av_gettime_relative() };
100 let start = match self.end_grace_start_us.compare_exchange(
101 0,
102 now,
103 Ordering::AcqRel,
104 Ordering::Acquire,
105 ) {
106 Ok(_) => now,
107 Err(previous) => previous,
108 };
109 now - start > OUTPUT_END_GRACE_US
110 }
111}
112
113pub(crate) struct OutputFinalizeGuard {
117 state: Arc<InterruptState>,
118}
119
120impl Drop for OutputFinalizeGuard {
121 fn drop(&mut self) {
122 self.state.finalizing_outputs.fetch_sub(1, Ordering::AcqRel);
123 }
124}
125
126pub(crate) unsafe extern "C" fn input_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
131 let state = &*(opaque as *const InterruptState);
132 state.should_interrupt_input() as libc::c_int
133}
134
135pub(crate) unsafe extern "C" fn output_interrupt_cb(opaque: *mut c_void) -> libc::c_int {
138 let state = &*(opaque as *const InterruptState);
139 let interrupt = state.should_interrupt_output();
140 #[cfg(test)]
144 if interrupt {
145 crate::core::scheduler::tcp_write_probe::note_cut_election();
146 }
147 interrupt as libc::c_int
148}
149
150pub(crate) struct MuxStartGate {
160 started: std::sync::atomic::AtomicBool,
161 lock: std::sync::Mutex<()>,
162}
163
164pub(crate) enum PreSendOutcome {
166 Sent,
168 Started(PacketBox),
170 Full(PacketBox),
173 Disconnected(PacketBox),
175}
176
177impl MuxStartGate {
178 pub(crate) fn new() -> Self {
179 Self {
180 started: std::sync::atomic::AtomicBool::new(false),
181 lock: std::sync::Mutex::new(()),
182 }
183 }
184
185 pub(crate) fn is_started(&self) -> bool {
186 self.started.load(Ordering::Acquire)
187 }
188
189 pub(crate) fn start_with(&self, drain: impl FnOnce()) {
191 let _guard = self.lock.lock().unwrap();
192 drain();
193 self.started.store(true, Ordering::Release);
194 }
195
196 pub(crate) fn send_pre(
198 &self,
199 pre_sender: &pre_mux_queue::PreMuxQueueSender,
200 packet_box: PacketBox,
201 ) -> PreSendOutcome {
202 let _guard = self.lock.lock().unwrap();
203 if self.started.load(Ordering::Acquire) {
204 return PreSendOutcome::Started(packet_box);
205 }
206 match pre_sender.try_push(packet_box) {
207 pre_mux_queue::PreQueueTryPush::Sent => PreSendOutcome::Sent,
208 pre_mux_queue::PreQueueTryPush::Full(pb) => PreSendOutcome::Full(pb),
209 pre_mux_queue::PreQueueTryPush::Disconnected(pb) => PreSendOutcome::Disconnected(pb),
210 }
211 }
212}
213
214use ffmpeg_context::{InputOpaque, OutputOpaque};
215
216pub mod ffmpeg_context;
233
234pub mod ffmpeg_context_builder;
262
263pub mod input;
288
289pub mod output;
316
317pub mod filter_complex;
337
338pub(super) mod attachment;
339pub(super) mod decoder_stream;
340pub(super) mod demuxer;
341pub(super) mod encoder_stream;
342pub(super) mod filter_graph;
343pub(super) mod frame_source;
344pub(super) mod input_filter;
345pub(super) mod muxer;
346pub(super) mod obj_pool;
347pub(super) mod output_filter;
348pub(super) mod pre_mux_queue;
349
350pub mod null_output;
379
380pub(crate) struct CodecContext {
381 inner: *mut AVCodecContext,
382}
383
384unsafe impl Send for CodecContext {}
393
394impl CodecContext {
395 pub(crate) fn new(avcodec_context: *mut AVCodecContext) -> Self {
396 Self {
397 inner: avcodec_context,
398 }
399 }
400
401 pub(crate) fn as_mut_ptr(&self) -> *mut AVCodecContext {
402 self.inner
403 }
404
405 pub(crate) fn as_ptr(&self) -> *const AVCodecContext {
406 self.inner as *const AVCodecContext
407 }
408}
409
410impl Drop for CodecContext {
411 fn drop(&mut self) {
412 unsafe {
413 avcodec_free_context(&mut self.inner);
414 }
415 }
416}
417
418#[derive(Copy, Clone)]
419pub(crate) struct Stream {
420 pub(crate) inner: *mut AVStream,
421}
422
423unsafe impl Send for Stream {}
427
428pub(crate) struct FrameBox {
429 pub(crate) frame: ffmpeg_next::Frame,
430 pub(crate) frame_data: FrameData,
432}
433
434unsafe impl Send for FrameBox {}
437
438pub fn frame_alloc() -> crate::error::Result<ffmpeg_next::Frame> {
439 unsafe {
440 let frame = ffmpeg_next::Frame::empty();
441 if frame.as_ptr().is_null() {
442 return Err(AllocFrameError::OutOfMemory.into());
443 }
444 Ok(frame)
445 }
446}
447
448pub fn null_frame() -> ffmpeg_next::Frame {
449 unsafe { ffmpeg_next::Frame::wrap(null_mut()) }
450}
451
452pub(crate) struct SideDataList {
458 entries: *mut *mut AVFrameSideData,
459 count: i32,
460}
461
462impl SideDataList {
463 pub(crate) fn new() -> Self {
464 Self {
465 entries: null_mut(),
466 count: 0,
467 }
468 }
469
470 #[cfg(ffmpeg_8_0)]
473 pub(crate) fn push_clone(&mut self, sd: *const AVFrameSideData, flags: u32) -> i32 {
474 unsafe { av_frame_side_data_clone(&mut self.entries, &mut self.count, sd, flags) }
475 }
476
477 pub(crate) fn clear(&mut self) {
478 #[cfg(not(docsrs))]
483 unsafe {
484 av_frame_side_data_free(&mut self.entries, &mut self.count)
485 }
486 }
487
488 #[cfg(ffmpeg_8_0)]
489 pub(crate) fn len(&self) -> i32 {
490 self.count
491 }
492
493 #[cfg(ffmpeg_8_0)]
499 pub(crate) fn as_mut_ptr(&mut self) -> *mut *mut AVFrameSideData {
500 self.entries
501 }
502
503 #[cfg(ffmpeg_8_0)]
504 pub(crate) fn iter(&self) -> impl Iterator<Item = *const AVFrameSideData> + '_ {
505 (0..self.count)
506 .map(|i| unsafe { *self.entries.offset(i as isize) as *const AVFrameSideData })
507 }
508}
509
510impl Drop for SideDataList {
511 fn drop(&mut self) {
512 self.clear();
513 }
514}
515
516unsafe impl Send for SideDataList {}
521unsafe impl Sync for SideDataList {}
522
523#[derive(Clone)]
524pub(crate) struct FrameData {
525 pub(crate) framerate: Option<AVRational>,
526 pub(crate) bits_per_raw_sample: i32,
527 pub(crate) input_stream_width: i32,
528 pub(crate) input_stream_height: i32,
529 pub(crate) subtitle_header: Option<Arc<[u8]>>,
533
534 pub(crate) fg_input_index: usize,
535
536 #[cfg_attr(not(ffmpeg_8_0), allow(dead_code))]
542 pub(crate) side_data: Option<Arc<SideDataList>>,
543}
544pub(crate) struct PacketBox {
549 pub(crate) packet: ffmpeg_next::Packet,
550 pub(crate) packet_data: PacketData,
551}
552
553unsafe impl Send for PacketBox {}
556
557#[derive(Clone, Copy)]
559pub(crate) struct PacketData {
560 pub(crate) dts_est: i64,
563 pub(crate) codec_type: AVMediaType,
564 pub(crate) output_stream_index: i32,
565 pub(crate) is_copy: bool,
566}
567
568pub(crate) fn out_fmt_ctx_free(out_fmt_ctx: *mut AVFormatContext, is_set_write_callback: bool) {
569 if out_fmt_ctx.is_null() {
570 return;
571 }
572 unsafe {
573 let custom_io_teardown_panic = if is_set_write_callback {
574 let avio_ctx = (*out_fmt_ctx).pb;
579 (*out_fmt_ctx).pb = null_mut();
580 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
581 free_output_opaque(avio_ctx)
582 }))
583 .err()
584 } else if (*(*out_fmt_ctx).oformat).flags & AVFMT_NOFILE == 0 {
585 let mut pb = (*out_fmt_ctx).pb;
589 if !pb.is_null() {
590 avio_closep(&mut pb);
591 }
592 None
593 } else {
594 None
595 };
596 avformat_free_context(out_fmt_ctx);
597 if let Some(payload) = custom_io_teardown_panic {
598 std::panic::resume_unwind(payload);
599 }
600 }
601}
602
603pub(crate) unsafe fn input_custom_io_is_poisoned(avio_ctx: *mut AVIOContext) -> bool {
612 if avio_ctx.is_null() {
613 return false;
614 }
615 let opaque_ptr = (*avio_ctx).opaque as *const InputOpaque;
616 !opaque_ptr.is_null() && (*opaque_ptr).poisoned
617}
618
619pub(crate) unsafe fn output_custom_io_is_poisoned(avio_ctx: *mut AVIOContext) -> bool {
628 if avio_ctx.is_null() {
629 return false;
630 }
631 let opaque_ptr = (*avio_ctx).opaque as *const OutputOpaque;
632 !opaque_ptr.is_null() && (*opaque_ptr).poisoned
633}
634
635fn drop_custom_io_callback_contained<T>(callback: T) -> bool {
638 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(callback))) {
639 Ok(()) => false,
640 Err(payload) => {
641 crate::core::packet_sink::dispose_panic_payload(payload);
642 true
643 }
644 }
645}
646
647unsafe fn dispose_output_opaque_callbacks(opaque_ptr: *mut OutputOpaque) -> bool {
648 if opaque_ptr.is_null() {
649 return false;
650 }
651 let OutputOpaque {
652 write,
653 seek,
654 poisoned: _,
655 } = *Box::from_raw(opaque_ptr);
656 let mut panicked = drop_custom_io_callback_contained(write);
657 if let Some(seek) = seek {
658 panicked |= drop_custom_io_callback_contained(seek);
659 }
660 panicked
661}
662
663unsafe fn dispose_input_opaque_callbacks(opaque_ptr: *mut InputOpaque) -> bool {
664 if opaque_ptr.is_null() {
665 return false;
666 }
667 let InputOpaque {
668 read,
669 seek,
670 poisoned: _,
671 } = *Box::from_raw(opaque_ptr);
672 let mut panicked = drop_custom_io_callback_contained(read);
673 if let Some(seek) = seek {
674 panicked |= drop_custom_io_callback_contained(seek);
675 }
676 panicked
677}
678
679pub(crate) unsafe fn free_output_opaque(mut avio_ctx: *mut AVIOContext) {
680 if avio_ctx.is_null() {
681 return;
682 }
683
684 let already_panicking = std::thread::panicking();
685 let mut callback_state_panicked = false;
686 let opaque_ptr = (*avio_ctx).opaque as *mut OutputOpaque;
687 (*avio_ctx).opaque = null_mut();
688 callback_state_panicked |= dispose_output_opaque_callbacks(opaque_ptr);
689
690 if !(*avio_ctx).buffer.is_null() {
691 av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
692 }
693 avio_context_free(&mut avio_ctx);
694
695 if callback_state_panicked && !already_panicking {
699 panic!("custom-IO output callback state panicked during teardown");
700 }
701}
702
703pub(crate) fn in_fmt_ctx_free(mut in_fmt_ctx: *mut AVFormatContext, is_set_read_callback: bool) {
704 if in_fmt_ctx.is_null() {
705 return;
706 }
707 unsafe {
708 let avio_ctx = if is_set_read_callback {
713 (*in_fmt_ctx).pb
714 } else {
715 null_mut()
716 };
717 avformat_close_input(&mut in_fmt_ctx);
718 free_input_opaque(avio_ctx);
719 }
720}
721
722pub(crate) unsafe fn free_input_opaque(mut avio_ctx: *mut AVIOContext) {
723 if avio_ctx.is_null() {
724 return;
725 }
726
727 let already_panicking = std::thread::panicking();
728 let mut callback_state_panicked = false;
729 let opaque_ptr = (*avio_ctx).opaque as *mut InputOpaque;
730 (*avio_ctx).opaque = null_mut();
731 callback_state_panicked |= dispose_input_opaque_callbacks(opaque_ptr);
732
733 if !(*avio_ctx).buffer.is_null() {
734 av_freep(&mut (*avio_ctx).buffer as *mut _ as *mut c_void);
735 }
736 avio_context_free(&mut avio_ctx);
737
738 if callback_state_panicked && !already_panicking {
739 panic!("custom-IO input callback state panicked during teardown");
740 }
741}
742
743pub(crate) struct FmtCtxGuard {
762 ctx: *mut AVFormatContext,
763 mode: crate::raw::Mode,
764}
765
766impl FmtCtxGuard {
767 pub(crate) fn disarmed() -> Self {
768 Self {
770 ctx: null_mut(),
771 mode: crate::raw::Mode::Input,
772 }
773 }
774
775 pub(crate) fn arm(&mut self, ctx: *mut AVFormatContext, mode: crate::raw::Mode) {
778 self.ctx = ctx;
779 self.mode = mode;
780 }
781
782 pub(crate) fn release(&mut self) -> *mut AVFormatContext {
784 let ctx = self.ctx;
785 self.ctx = null_mut();
786 ctx
787 }
788
789 pub(crate) fn release_into(&mut self) -> crate::raw::FormatContext {
797 let mode = self.mode;
798 let ctx = self.release();
799 assert!(!ctx.is_null(), "release_into on a disarmed FmtCtxGuard");
800 unsafe { crate::raw::FormatContext::from_mode(ctx, mode) }
805 }
806}
807
808impl Drop for FmtCtxGuard {
809 fn drop(&mut self) {
810 if self.ctx.is_null() {
811 return;
812 }
813 match self.mode {
815 crate::raw::Mode::Input => in_fmt_ctx_free(self.ctx, false),
816 crate::raw::Mode::InputCustomIo => in_fmt_ctx_free(self.ctx, true),
817 crate::raw::Mode::Output => out_fmt_ctx_free(self.ctx, false),
818 crate::raw::Mode::OutputCustomIo => out_fmt_ctx_free(self.ctx, true),
819 }
820 }
821}
822
823#[allow(dead_code)]
824pub(crate) fn type_to_linklabel(media_type: AVMediaType, index: usize) -> Option<String> {
825 match media_type {
826 AVMediaType::AVMEDIA_TYPE_UNKNOWN => None,
827 AVMEDIA_TYPE_VIDEO => Some(format!("{index}:v")),
828 AVMEDIA_TYPE_AUDIO => Some(format!("{index}:a")),
829 AVMEDIA_TYPE_DATA => Some(format!("{index}:d")),
830 AVMEDIA_TYPE_SUBTITLE => Some(format!("{index}:s")),
831 AVMEDIA_TYPE_ATTACHMENT => Some(format!("{index}:t")),
832 AVMediaType::AVMEDIA_TYPE_NB => None,
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839 use crate::core::scheduler::ffmpeg_scheduler::{STATUS_ABORT, STATUS_END, STATUS_RUN};
840
841 unsafe fn test_avio(opaque: *mut c_void, write_flag: libc::c_int) -> *mut AVIOContext {
842 const BUFFER_SIZE: usize = 128;
843 let buffer = ffmpeg_sys_next::av_malloc(BUFFER_SIZE) as *mut u8;
844 assert!(!buffer.is_null(), "test AVIO buffer allocation failed");
845 let avio_ctx = ffmpeg_sys_next::avio_alloc_context(
846 buffer,
847 BUFFER_SIZE as libc::c_int,
848 write_flag,
849 opaque,
850 None,
851 None,
852 None,
853 );
854 if avio_ctx.is_null() {
855 ffmpeg_sys_next::av_free(buffer.cast());
856 panic!("test AVIO context allocation failed");
857 }
858 avio_ctx
859 }
860
861 fn panic_message(payload: &(dyn std::any::Any + Send)) -> &str {
862 payload
863 .downcast_ref::<&str>()
864 .copied()
865 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
866 .unwrap_or("non-string panic payload")
867 }
868
869 struct CallbackDropBomb(Arc<AtomicUsize>);
870
871 impl Drop for CallbackDropBomb {
872 fn drop(&mut self) {
873 self.0.fetch_add(1, Ordering::SeqCst);
874 panic!("test callback capture destructor");
875 }
876 }
877
878 struct FreeInputAvioOnDrop(*mut AVIOContext);
879
880 impl Drop for FreeInputAvioOnDrop {
881 fn drop(&mut self) {
882 unsafe { free_input_opaque(self.0) };
883 }
884 }
885
886 struct FreeOutputAvioOnDrop(*mut AVIOContext);
887
888 impl Drop for FreeOutputAvioOnDrop {
889 fn drop(&mut self) {
890 unsafe { free_output_opaque(self.0) };
891 }
892 }
893
894 fn state_with(status: usize) -> Arc<InterruptState> {
895 Arc::new(InterruptState::new(Arc::new(AtomicUsize::new(status))))
896 }
897
898 fn expire_grace(state: &InterruptState) {
900 let past = unsafe { av_gettime_relative() } - OUTPUT_END_GRACE_US - 1;
901 state.end_grace_start_us.store(past, Ordering::Release);
902 }
903
904 #[test]
905 fn output_grace_cut_fires_after_the_window() {
906 let state = state_with(STATUS_END);
907 expire_grace(&state);
908 assert!(state.should_interrupt_output());
909 }
910
911 #[test]
912 fn finalize_guard_holds_the_grace_cut_open() {
913 let state = state_with(STATUS_END);
914 expire_grace(&state);
915 let guard = state.begin_output_finalize();
916 assert!(
917 !state.should_interrupt_output(),
918 "a finalizing output must not be cut by the STATUS_END grace"
919 );
920 drop(guard);
921 assert!(
922 state.should_interrupt_output(),
923 "after the last finalize guard drops, the expired grace cuts again"
924 );
925 }
926
927 #[test]
928 fn abort_cuts_through_a_finalize_window() {
929 let state = state_with(STATUS_ABORT);
930 let _guard = state.begin_output_finalize();
931 assert!(
932 state.should_interrupt_output(),
933 "abort() is the hard cancel: it must cut even a finalizing output"
934 );
935 }
936
937 #[test]
938 fn running_scheduler_never_cuts_output() {
939 let state = state_with(STATUS_RUN);
940 assert!(!state.should_interrupt_output());
941 }
942
943 #[test]
944 fn overlapping_finalize_windows_hold_until_the_last_drops() {
945 let state = state_with(STATUS_END);
946 expire_grace(&state);
947 let g1 = state.begin_output_finalize();
948 let g2 = state.begin_output_finalize();
949 drop(g1);
950 assert!(
951 !state.should_interrupt_output(),
952 "one of two overlapping finalize windows dropping must keep the hold"
953 );
954 drop(g2);
955 assert!(state.should_interrupt_output());
956 }
957
958 #[test]
959 fn custom_io_poison_helpers_read_live_opaque_state() {
960 unsafe {
961 assert!(!input_custom_io_is_poisoned(null_mut()));
962 assert!(!output_custom_io_is_poisoned(null_mut()));
963
964 let input_opaque = Box::into_raw(Box::new(InputOpaque {
965 read: Box::new(|buf| buf.len() as i32),
966 seek: None,
967 poisoned: false,
968 }));
969 let input_avio = test_avio(input_opaque.cast(), 0);
970 assert!(!input_custom_io_is_poisoned(input_avio));
971 (*input_opaque).poisoned = true;
972 assert!(input_custom_io_is_poisoned(input_avio));
973 free_input_opaque(input_avio);
974
975 let output_opaque = Box::into_raw(Box::new(OutputOpaque {
976 write: Box::new(|buf| buf.len() as i32),
977 seek: None,
978 poisoned: false,
979 }));
980 let output_avio = test_avio(output_opaque.cast(), 1);
981 assert!(!output_custom_io_is_poisoned(output_avio));
982 (*output_opaque).poisoned = true;
983 assert!(output_custom_io_is_poisoned(output_avio));
984 free_output_opaque(output_avio);
985 }
986 }
987
988 #[test]
989 fn input_opaque_teardown_drops_each_callback_before_stable_repanic() {
990 let drops = Arc::new(AtomicUsize::new(0));
991 let read_bomb = CallbackDropBomb(Arc::clone(&drops));
992 let seek_bomb = CallbackDropBomb(Arc::clone(&drops));
993 let opaque = Box::into_raw(Box::new(InputOpaque {
994 read: Box::new(move |_buf| {
995 let _hold = &read_bomb;
996 0
997 }),
998 seek: Some(Box::new(move |_offset, _whence| {
999 let _hold = &seek_bomb;
1000 0
1001 })),
1002 poisoned: false,
1003 }));
1004 let avio_ctx = unsafe { test_avio(opaque.cast(), 0) };
1005
1006 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
1007 free_input_opaque(avio_ctx)
1008 }))
1009 .expect_err("normal teardown must publish a stable panic after reclamation");
1010 assert_eq!(
1011 panic_message(payload.as_ref()),
1012 "custom-IO input callback state panicked during teardown"
1013 );
1014 assert_eq!(
1015 drops.load(Ordering::SeqCst),
1016 2,
1017 "read and seek callback boxes must both be disposed"
1018 );
1019 }
1020
1021 #[test]
1022 fn output_opaque_teardown_drops_each_callback_before_stable_repanic() {
1023 let drops = Arc::new(AtomicUsize::new(0));
1024 let write_bomb = CallbackDropBomb(Arc::clone(&drops));
1025 let seek_bomb = CallbackDropBomb(Arc::clone(&drops));
1026 let opaque = Box::into_raw(Box::new(OutputOpaque {
1027 write: Box::new(move |_buf| {
1028 let _hold = &write_bomb;
1029 0
1030 }),
1031 seek: Some(Box::new(move |_offset, _whence| {
1032 let _hold = &seek_bomb;
1033 0
1034 })),
1035 poisoned: false,
1036 }));
1037 let avio_ctx = unsafe { test_avio(opaque.cast(), 1) };
1038
1039 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
1040 free_output_opaque(avio_ctx)
1041 }))
1042 .expect_err("normal teardown must publish a stable panic after reclamation");
1043 assert_eq!(
1044 panic_message(payload.as_ref()),
1045 "custom-IO output callback state panicked during teardown"
1046 );
1047 assert_eq!(
1048 drops.load(Ordering::SeqCst),
1049 2,
1050 "write and seek callback boxes must both be disposed"
1051 );
1052 }
1053
1054 #[test]
1055 fn output_format_teardown_preserves_the_deferred_stable_panic() {
1056 let drops = Arc::new(AtomicUsize::new(0));
1057 let write_bomb = CallbackDropBomb(Arc::clone(&drops));
1058 let opaque = Box::into_raw(Box::new(OutputOpaque {
1059 write: Box::new(move |_buf| {
1060 let _hold = &write_bomb;
1061 0
1062 }),
1063 seek: None,
1064 poisoned: false,
1065 }));
1066 let avio_ctx = unsafe { test_avio(opaque.cast(), 1) };
1067 let format_ctx = unsafe { ffmpeg_sys_next::avformat_alloc_context() };
1068 assert!(
1069 !format_ctx.is_null(),
1070 "test format context allocation failed"
1071 );
1072 unsafe {
1073 (*format_ctx).pb = avio_ctx;
1074 }
1075
1076 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1077 out_fmt_ctx_free(format_ctx, true)
1078 }))
1079 .expect_err("the owner-level teardown must preserve the stable panic");
1080 assert_eq!(
1081 panic_message(payload.as_ref()),
1082 "custom-IO output callback state panicked during teardown"
1083 );
1084 assert_eq!(drops.load(Ordering::SeqCst), 1);
1085 }
1086
1087 #[test]
1088 fn custom_io_teardown_preserves_an_existing_unwind() {
1089 let input_drops = Arc::new(AtomicUsize::new(0));
1090 let read_bomb = CallbackDropBomb(Arc::clone(&input_drops));
1091 let seek_bomb = CallbackDropBomb(Arc::clone(&input_drops));
1092 let input_opaque = Box::into_raw(Box::new(InputOpaque {
1093 read: Box::new(move |_buf| {
1094 let _hold = &read_bomb;
1095 0
1096 }),
1097 seek: Some(Box::new(move |_offset, _whence| {
1098 let _hold = &seek_bomb;
1099 0
1100 })),
1101 poisoned: false,
1102 }));
1103 let input_avio = unsafe { test_avio(input_opaque.cast(), 0) };
1104 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1105 let _free_on_unwind = FreeInputAvioOnDrop(input_avio);
1106 panic!("primary input unwind");
1107 }))
1108 .expect_err("the primary input panic must propagate");
1109 assert_eq!(panic_message(payload.as_ref()), "primary input unwind");
1110 assert_eq!(input_drops.load(Ordering::SeqCst), 2);
1111
1112 let output_drops = Arc::new(AtomicUsize::new(0));
1113 let write_bomb = CallbackDropBomb(Arc::clone(&output_drops));
1114 let seek_bomb = CallbackDropBomb(Arc::clone(&output_drops));
1115 let output_opaque = Box::into_raw(Box::new(OutputOpaque {
1116 write: Box::new(move |_buf| {
1117 let _hold = &write_bomb;
1118 0
1119 }),
1120 seek: Some(Box::new(move |_offset, _whence| {
1121 let _hold = &seek_bomb;
1122 0
1123 })),
1124 poisoned: false,
1125 }));
1126 let output_avio = unsafe { test_avio(output_opaque.cast(), 1) };
1127 let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1128 let _free_on_unwind = FreeOutputAvioOnDrop(output_avio);
1129 panic!("primary output unwind");
1130 }))
1131 .expect_err("the primary output panic must propagate");
1132 assert_eq!(panic_message(payload.as_ref()), "primary output unwind");
1133 assert_eq!(output_drops.load(Ordering::SeqCst), 2);
1134 }
1135}