scuffle_ffmpeg/
filter_graph.rs

1use std::ffi::CString;
2use std::ptr::NonNull;
3
4use crate::error::{FfmpegError, FfmpegErrorCode};
5use crate::ffi::*;
6use crate::frame::GenericFrame;
7use crate::smart_object::SmartPtr;
8
9/// A filter graph. Used to chain filters together when transforming media data.
10pub struct FilterGraph(SmartPtr<AVFilterGraph>);
11
12/// Safety: `FilterGraph` is safe to send between threads.
13unsafe impl Send for FilterGraph {}
14
15impl FilterGraph {
16    /// Creates a new filter graph.
17    pub fn new() -> Result<Self, FfmpegError> {
18        // Safety: the pointer returned from avfilter_graph_alloc is valid
19        let ptr = unsafe { avfilter_graph_alloc() };
20        // Safety: The pointer here is valid.
21        unsafe { Self::wrap(ptr) }.ok_or(FfmpegError::Alloc)
22    }
23
24    /// Safety: `ptr` must be a valid pointer to an `AVFilterGraph`.
25    const unsafe fn wrap(ptr: *mut AVFilterGraph) -> Option<Self> {
26        let destructor = |ptr: &mut *mut AVFilterGraph| {
27            // Safety: The pointer here is valid.
28            unsafe { avfilter_graph_free(ptr) };
29        };
30
31        if ptr.is_null() {
32            return None;
33        }
34
35        // Safety: The pointer here is valid.
36        Some(Self(unsafe { SmartPtr::wrap(ptr, destructor) }))
37    }
38
39    /// Get the pointer to the filter graph.
40    pub const fn as_ptr(&self) -> *const AVFilterGraph {
41        self.0.as_ptr()
42    }
43
44    /// Get the mutable pointer to the filter graph.
45    pub const fn as_mut_ptr(&mut self) -> *mut AVFilterGraph {
46        self.0.as_mut_ptr()
47    }
48
49    /// Add a filter to the filter graph.
50    pub fn add(&mut self, filter: Filter, name: &str, args: &str) -> Result<FilterContext<'_>, FfmpegError> {
51        let name = CString::new(name).or(Err(FfmpegError::Arguments("name must be non-empty")))?;
52        let args = CString::new(args).or(Err(FfmpegError::Arguments("args must be non-empty")))?;
53
54        let mut filter_context = std::ptr::null_mut();
55
56        // Safety: avfilter_graph_create_filter is safe to call, 'filter_context' is a
57        // valid pointer
58        FfmpegErrorCode(unsafe {
59            avfilter_graph_create_filter(
60                &mut filter_context,
61                filter.as_ptr(),
62                name.as_ptr(),
63                args.as_ptr(),
64                std::ptr::null_mut(),
65                self.as_mut_ptr(),
66            )
67        })
68        .result()?;
69
70        // Safety: 'filter_context' is a valid pointer
71        Ok(FilterContext(unsafe {
72            NonNull::new(filter_context).ok_or(FfmpegError::Alloc)?.as_mut()
73        }))
74    }
75
76    /// Get a filter context by name.
77    pub fn get(&mut self, name: &str) -> Option<FilterContext<'_>> {
78        let name = CString::new(name).ok()?;
79
80        // Safety: avfilter_graph_get_filter is safe to call, and the returned pointer
81        // is valid
82        let mut ptr = NonNull::new(unsafe { avfilter_graph_get_filter(self.as_mut_ptr(), name.as_ptr()) })?;
83        // Safety: The pointer here is valid.
84        Some(FilterContext(unsafe { ptr.as_mut() }))
85    }
86
87    /// Validate the filter graph.
88    pub fn validate(&mut self) -> Result<(), FfmpegError> {
89        // Safety: avfilter_graph_config is safe to call
90        FfmpegErrorCode(unsafe { avfilter_graph_config(self.as_mut_ptr(), std::ptr::null_mut()) }).result()?;
91        Ok(())
92    }
93
94    /// Dump the filter graph to a string.
95    pub fn dump(&mut self) -> Option<String> {
96        // Safety: avfilter_graph_dump is safe to call
97        let dump = unsafe { avfilter_graph_dump(self.as_mut_ptr(), std::ptr::null_mut()) };
98        let destructor = |ptr: &mut *mut libc::c_char| {
99            // Safety: The pointer here is valid.
100            unsafe { av_free(*ptr as *mut libc::c_void) };
101            *ptr = std::ptr::null_mut();
102        };
103
104        // Safety: The pointer here is valid.
105        let c_str = unsafe { SmartPtr::wrap_non_null(dump, destructor)? };
106
107        // Safety: The pointer here is valid.
108        let c_str = unsafe { std::ffi::CStr::from_ptr(c_str.as_ptr()) };
109
110        Some(c_str.to_str().ok()?.to_owned())
111    }
112
113    /// Set the thread count for the filter graph.
114    pub const fn set_thread_count(&mut self, threads: i32) {
115        self.0.as_deref_mut_except().nb_threads = threads;
116    }
117
118    /// Add an input to the filter graph.
119    pub fn input(&mut self, name: &str, pad: i32) -> Result<FilterGraphParser<'_>, FfmpegError> {
120        FilterGraphParser::new(self).input(name, pad)
121    }
122
123    /// Add an output to the filter graph.
124    pub fn output(&mut self, name: &str, pad: i32) -> Result<FilterGraphParser<'_>, FfmpegError> {
125        FilterGraphParser::new(self).output(name, pad)
126    }
127}
128
129/// A parser for the filter graph. Allows you to create a filter graph from a string specification.
130pub struct FilterGraphParser<'a> {
131    graph: &'a mut FilterGraph,
132    inputs: SmartPtr<AVFilterInOut>,
133    outputs: SmartPtr<AVFilterInOut>,
134}
135
136/// Safety: `FilterGraphParser` is safe to send between threads.
137unsafe impl Send for FilterGraphParser<'_> {}
138
139impl<'a> FilterGraphParser<'a> {
140    /// Create a new `FilterGraphParser`.
141    fn new(graph: &'a mut FilterGraph) -> Self {
142        Self {
143            graph,
144            // Safety: 'avfilter_inout_free' is safe to call with a null pointer, and the pointer is valid
145            inputs: SmartPtr::null(|ptr| {
146                // Safety: The pointer here is valid.
147                unsafe { avfilter_inout_free(ptr) };
148            }),
149            // Safety: 'avfilter_inout_free' is safe to call with a null pointer, and the pointer is valid
150            outputs: SmartPtr::null(|ptr| {
151                // Safety: The pointer here is valid.
152                unsafe { avfilter_inout_free(ptr) };
153            }),
154        }
155    }
156
157    /// Add an input to the filter graph.
158    pub fn input(self, name: &str, pad: i32) -> Result<Self, FfmpegError> {
159        self.inout_impl(name, pad, false)
160    }
161
162    /// Add an output to the filter graph.
163    pub fn output(self, name: &str, pad: i32) -> Result<Self, FfmpegError> {
164        self.inout_impl(name, pad, true)
165    }
166
167    /// Parse the filter graph specification.
168    pub fn parse(mut self, spec: &str) -> Result<(), FfmpegError> {
169        let spec = CString::new(spec).unwrap();
170
171        // Safety: 'avfilter_graph_parse_ptr' is safe to call and all the pointers are
172        // valid.
173        FfmpegErrorCode(unsafe {
174            avfilter_graph_parse_ptr(
175                self.graph.as_mut_ptr(),
176                spec.as_ptr(),
177                self.inputs.as_mut(),
178                self.outputs.as_mut(),
179                std::ptr::null_mut(),
180            )
181        })
182        .result()?;
183
184        Ok(())
185    }
186
187    fn inout_impl(mut self, name: &str, pad: i32, output: bool) -> Result<Self, FfmpegError> {
188        let context = self.graph.get(name).ok_or(FfmpegError::Arguments("unknown name"))?;
189
190        let destructor = |ptr: &mut *mut AVFilterInOut| {
191            // Safety: The pointer here is valid allocated via `avfilter_inout_alloc`
192            unsafe { avfilter_inout_free(ptr) };
193        };
194
195        // Safety: `avfilter_inout_alloc` is safe to call.
196        let inout = unsafe { avfilter_inout_alloc() };
197
198        // Safety: 'avfilter_inout_alloc' is safe to call, and the returned pointer is
199        // valid
200        let mut inout = unsafe { SmartPtr::wrap_non_null(inout, destructor) }.ok_or(FfmpegError::Alloc)?;
201
202        let name = CString::new(name).map_err(|_| FfmpegError::Arguments("name must be non-empty"))?;
203
204        // Safety: `av_strdup` is safe to call and `name` is a valid c-string.
205        // Note: This was previously incorrect because we need the string to be allocated by ffmpeg otherwise
206        // ffmpeg will not be able to free the struct.
207        inout.as_deref_mut_except().name = unsafe { av_strdup(name.as_ptr()) };
208        inout.as_deref_mut_except().filter_ctx = context.0;
209        inout.as_deref_mut_except().pad_idx = pad;
210
211        if output {
212            inout.as_deref_mut_except().next = self.outputs.into_inner();
213            self.outputs = inout;
214        } else {
215            inout.as_deref_mut_except().next = self.inputs.into_inner();
216            self.inputs = inout;
217        }
218
219        Ok(self)
220    }
221}
222
223/// A filter. Thin wrapper around [`AVFilter`].
224#[derive(Clone, Copy, PartialEq, Eq)]
225pub struct Filter(*const AVFilter);
226
227impl Filter {
228    /// Get a filter by name.
229    pub fn get(name: &str) -> Option<Self> {
230        let name = std::ffi::CString::new(name).ok()?;
231
232        // Safety: avfilter_get_by_name is safe to call, and the returned pointer is
233        // valid
234        let filter = unsafe { avfilter_get_by_name(name.as_ptr()) };
235
236        if filter.is_null() { None } else { Some(Self(filter)) }
237    }
238
239    /// Get the pointer to the filter.
240    pub const fn as_ptr(&self) -> *const AVFilter {
241        self.0
242    }
243
244    /// # Safety
245    /// `ptr` must be a valid pointer.
246    pub const unsafe fn wrap(ptr: *const AVFilter) -> Self {
247        Self(ptr)
248    }
249}
250
251/// Safety: `Filter` is safe to send between threads.
252unsafe impl Send for Filter {}
253
254/// A filter context. Thin wrapper around `AVFilterContext`.
255pub struct FilterContext<'a>(&'a mut AVFilterContext);
256
257/// Safety: `FilterContext` is safe to send between threads.
258unsafe impl Send for FilterContext<'_> {}
259
260impl<'a> FilterContext<'a> {
261    /// Returns a source for the filter context.
262    pub const fn source(self) -> FilterContextSource<'a> {
263        FilterContextSource(self.0)
264    }
265
266    /// Returns a sink for the filter context.
267    pub const fn sink(self) -> FilterContextSink<'a> {
268        FilterContextSink(self.0)
269    }
270}
271
272/// A source for a filter context. Where this is specifically used to send frames to the filter context.
273pub struct FilterContextSource<'a>(&'a mut AVFilterContext);
274
275/// Safety: `FilterContextSource` is safe to send between threads.
276unsafe impl Send for FilterContextSource<'_> {}
277
278impl FilterContextSource<'_> {
279    /// Sends a frame to the filter context.
280    pub fn send_frame(&mut self, frame: &GenericFrame) -> Result<(), FfmpegError> {
281        // Safety: `frame` is a valid pointer, and `self.0` is a valid pointer.
282        FfmpegErrorCode(unsafe { av_buffersrc_write_frame(self.0, frame.as_ptr()) }).result()?;
283        Ok(())
284    }
285
286    /// Sends an EOF frame to the filter context.
287    pub fn send_eof(&mut self, pts: Option<i64>) -> Result<(), FfmpegError> {
288        if let Some(pts) = pts {
289            // Safety: `av_buffersrc_close` is safe to call.
290            FfmpegErrorCode(unsafe { av_buffersrc_close(self.0, pts, 0) }).result()?;
291        } else {
292            // Safety: `av_buffersrc_write_frame` is safe to call.
293            FfmpegErrorCode(unsafe { av_buffersrc_write_frame(self.0, std::ptr::null()) }).result()?;
294        }
295
296        Ok(())
297    }
298}
299
300/// A sink for a filter context. Where this is specifically used to receive frames from the filter context.
301pub struct FilterContextSink<'a>(&'a mut AVFilterContext);
302
303/// Safety: `FilterContextSink` is safe to send between threads.
304unsafe impl Send for FilterContextSink<'_> {}
305
306impl FilterContextSink<'_> {
307    /// Receives a frame from the filter context.
308    pub fn receive_frame(&mut self) -> Result<Option<GenericFrame>, FfmpegError> {
309        let mut frame = GenericFrame::new()?;
310
311        // Safety: `frame` is a valid pointer, and `self.0` is a valid pointer.
312        match FfmpegErrorCode(unsafe { av_buffersink_get_frame(self.0, frame.as_mut_ptr()) }) {
313            code if code.is_success() => Ok(Some(frame)),
314            FfmpegErrorCode::Eagain | FfmpegErrorCode::Eof => Ok(None),
315            code => Err(FfmpegError::Code(code)),
316        }
317    }
318}
319
320#[cfg(test)]
321#[cfg_attr(all(test, coverage_nightly), coverage(off))]
322mod tests {
323    use std::ffi::CString;
324
325    use crate::AVSampleFormat;
326    use crate::ffi::avfilter_get_by_name;
327    use crate::filter_graph::{Filter, FilterGraph, FilterGraphParser};
328    use crate::frame::{AudioChannelLayout, AudioFrame, GenericFrame};
329
330    #[test]
331    fn test_filter_graph_new() {
332        let filter_graph = FilterGraph::new();
333        assert!(filter_graph.is_ok(), "FilterGraph::new should create a valid filter graph");
334
335        if let Ok(graph) = filter_graph {
336            assert!(!graph.as_ptr().is_null(), "FilterGraph pointer should not be null");
337        }
338    }
339
340    #[test]
341    fn test_filter_graph_as_mut_ptr() {
342        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
343        let raw_ptr = filter_graph.as_mut_ptr();
344
345        assert!(!raw_ptr.is_null(), "FilterGraph::as_mut_ptr should return a valid pointer");
346    }
347
348    #[test]
349    fn test_filter_graph_add() {
350        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
351        let filter_name = "buffer";
352        // Safety: `avfilter_get_by_name` is safe to call.
353        let filter_ptr = unsafe { avfilter_get_by_name(CString::new(filter_name).unwrap().as_ptr()) };
354        assert!(
355            !filter_ptr.is_null(),
356            "avfilter_get_by_name should return a valid pointer for filter '{filter_name}'"
357        );
358
359        // Safety: The pointer here is valid.
360        let filter = unsafe { Filter::wrap(filter_ptr) };
361        let name = "buffer_filter";
362        let args = "width=1920:height=1080:pix_fmt=0:time_base=1/30";
363        let result = filter_graph.add(filter, name, args);
364
365        assert!(
366            result.is_ok(),
367            "FilterGraph::add should successfully add a filter to the graph"
368        );
369
370        if let Ok(context) = result {
371            assert!(
372                !context.0.filter.is_null(),
373                "The filter context should have a valid filter pointer"
374            );
375        }
376    }
377
378    #[test]
379    fn test_filter_graph_get() {
380        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
381        let filter_name = "buffer";
382        // Safety: `avfilter_get_by_name` is safe to call.
383        let filter_ptr = unsafe { avfilter_get_by_name(CString::new(filter_name).unwrap().as_ptr()) };
384        assert!(
385            !filter_ptr.is_null(),
386            "avfilter_get_by_name should return a valid pointer for filter '{filter_name}'"
387        );
388
389        // Safety: The pointer here is valid.
390        let filter = unsafe { Filter::wrap(filter_ptr) };
391        let name = "buffer_filter";
392        let args = "width=1920:height=1080:pix_fmt=0:time_base=1/30";
393        filter_graph
394            .add(filter, name, args)
395            .expect("Failed to add filter to the graph");
396
397        let result = filter_graph.get(name);
398        assert!(
399            result.is_some(),
400            "FilterGraph::get should return Some(FilterContext) for an existing filter"
401        );
402
403        if let Some(filter_context) = result {
404            assert!(
405                !filter_context.0.filter.is_null(),
406                "The retrieved FilterContext should have a valid filter pointer"
407            );
408        }
409
410        let non_existent = filter_graph.get("non_existent_filter");
411        assert!(
412            non_existent.is_none(),
413            "FilterGraph::get should return None for a non-existent filter"
414        );
415    }
416
417    #[test]
418    fn test_filter_graph_validate_and_dump() {
419        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
420        let filter_spec = "anullsrc=sample_rate=44100:channel_layout=stereo [out0]; [out0] anullsink";
421        FilterGraphParser::new(&mut filter_graph)
422            .parse(filter_spec)
423            .expect("Failed to parse filter graph spec");
424
425        filter_graph.validate().expect("FilterGraph::validate should succeed");
426        let dump_output = filter_graph.dump().expect("Failed to dump the filter graph");
427
428        assert!(
429            dump_output.contains("anullsrc"),
430            "Dump output should include the 'anullsrc' filter type"
431        );
432        assert!(
433            dump_output.contains("anullsink"),
434            "Dump output should include the 'anullsink' filter type"
435        );
436    }
437
438    #[test]
439    fn test_filter_graph_set_thread_count() {
440        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
441        filter_graph.set_thread_count(4);
442        assert_eq!(
443            // Safety: The pointer here is valid.
444            unsafe { (*filter_graph.as_mut_ptr()).nb_threads },
445            4,
446            "Thread count should be set to 4"
447        );
448
449        filter_graph.set_thread_count(8);
450        assert_eq!(
451            // Safety: The pointer here is valid.
452            unsafe { (*filter_graph.as_mut_ptr()).nb_threads },
453            8,
454            "Thread count should be set to 8"
455        );
456    }
457
458    #[test]
459    fn test_filter_graph_input() {
460        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
461        let anullsrc = Filter::get("anullsrc").expect("Failed to get 'anullsrc' filter");
462        filter_graph
463            .add(anullsrc, "src", "sample_rate=44100:channel_layout=stereo")
464            .expect("Failed to add 'anullsrc' filter");
465        let input_parser = filter_graph
466            .input("src", 0)
467            .expect("Failed to set input for the filter graph");
468
469        assert!(
470            std::ptr::eq(input_parser.graph.as_ptr(), filter_graph.as_ptr()),
471            "Input parser should belong to the same filter graph"
472        );
473    }
474
475    #[test]
476    fn test_filter_graph_output() {
477        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
478        let anullsink = Filter::get("anullsink").expect("Failed to get 'anullsink' filter");
479        filter_graph
480            .add(anullsink, "sink", "")
481            .expect("Failed to add 'anullsink' filter");
482        let output_parser = filter_graph
483            .output("sink", 0)
484            .expect("Failed to set output for the filter graph");
485
486        assert!(
487            std::ptr::eq(output_parser.graph.as_ptr(), filter_graph.as_ptr()),
488            "Output parser should belong to the same filter graph"
489        );
490    }
491
492    #[test]
493    fn test_filter_context_source() {
494        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
495        let anullsrc = Filter::get("anullsrc").expect("Failed to get 'anullsrc' filter");
496        filter_graph
497            .add(anullsrc, "src", "sample_rate=44100:channel_layout=stereo")
498            .expect("Failed to add 'anullsrc' filter");
499        let filter_context = filter_graph.get("src").expect("Failed to retrieve 'src' filter context");
500        let source_context = filter_context.source();
501
502        assert!(
503            std::ptr::eq(source_context.0, filter_graph.get("src").unwrap().0),
504            "Source context should wrap the same filter as the original filter context"
505        );
506    }
507
508    #[test]
509    fn test_filter_context_sink() {
510        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
511        let anullsink = Filter::get("anullsink").expect("Failed to get 'anullsink' filter");
512        filter_graph
513            .add(anullsink, "sink", "")
514            .expect("Failed to add 'anullsink' filter");
515        let filter_context = filter_graph.get("sink").expect("Failed to retrieve 'sink' filter context");
516        let sink_context = filter_context.sink();
517
518        assert!(
519            std::ptr::eq(sink_context.0, filter_graph.get("sink").unwrap().0),
520            "Sink context should wrap the same filter as the original filter context"
521        );
522    }
523
524    #[test]
525    fn test_filter_context_source_send_and_receive_frame() {
526        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
527        let filter_spec = "\
528            abuffer=sample_rate=44100:sample_fmt=s16:channel_layout=stereo:time_base=1/44100 \
529            [out]; \
530            [out] abuffersink";
531        FilterGraphParser::new(&mut filter_graph)
532            .parse(filter_spec)
533            .expect("Failed to parse filter graph spec");
534        filter_graph.validate().expect("Failed to validate filter graph");
535
536        let source_context_name = "Parsed_abuffer_0";
537        let sink_context_name = "Parsed_abuffersink_1";
538
539        let frame = AudioFrame::builder()
540            .sample_fmt(AVSampleFormat::S16)
541            .nb_samples(1024)
542            .sample_rate(44100)
543            .channel_layout(AudioChannelLayout::new(2).expect("Failed to create a new AudioChannelLayout"))
544            .build()
545            .expect("Failed to create a new AudioFrame");
546
547        let mut source_context = filter_graph
548            .get(source_context_name)
549            .expect("Failed to retrieve source filter context")
550            .source();
551
552        let result = source_context.send_frame(&frame);
553        assert!(result.is_ok(), "send_frame should succeed when sending a valid frame");
554
555        let mut sink_context = filter_graph
556            .get(sink_context_name)
557            .expect("Failed to retrieve sink filter context")
558            .sink();
559        let received_frame = sink_context
560            .receive_frame()
561            .expect("Failed to receive frame from sink context");
562
563        assert!(received_frame.is_some(), "No frame received from sink context");
564
565        insta::assert_debug_snapshot!(received_frame.unwrap(), @r"
566        GenericFrame {
567            pts: None,
568            dts: None,
569            duration: Some(
570                1024,
571            ),
572            best_effort_timestamp: None,
573            time_base: Rational {
574                numerator: 0,
575                denominator: 1,
576            },
577            format: 1,
578            is_audio: true,
579            is_video: false,
580        }
581        ");
582    }
583
584    #[test]
585    fn test_filter_context_source_send_frame_error() {
586        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
587        let filter_spec = "\
588            abuffer=sample_rate=44100:sample_fmt=s16:channel_layout=stereo:time_base=1/44100 \
589            [out]; \
590            [out] anullsink";
591        FilterGraphParser::new(&mut filter_graph)
592            .parse(filter_spec)
593            .expect("Failed to parse filter graph spec");
594        filter_graph.validate().expect("Failed to validate filter graph");
595
596        let mut source_context = filter_graph
597            .get("Parsed_abuffer_0")
598            .expect("Failed to retrieve 'Parsed_abuffer_0' filter context")
599            .source();
600
601        // create frame w/ mismatched format and sample rate
602        let mut frame = GenericFrame::new().expect("Failed to create frame");
603        // Safety: frame was not yet allocated and inner pointer is valid
604        unsafe { frame.as_mut_ptr().as_mut().unwrap().format = AVSampleFormat::Fltp.into() };
605        let result = source_context.send_frame(&frame);
606
607        assert!(result.is_err(), "send_frame should fail when sending an invalid frame");
608    }
609
610    #[test]
611    fn test_filter_context_source_send_and_receive_eof() {
612        let mut filter_graph = FilterGraph::new().expect("Failed to create filter graph");
613        let filter_spec = "\
614            abuffer=sample_rate=44100:sample_fmt=s16:channel_layout=stereo:time_base=1/44100 \
615            [out]; \
616            [out] abuffersink";
617        FilterGraphParser::new(&mut filter_graph)
618            .parse(filter_spec)
619            .expect("Failed to parse filter graph spec");
620        filter_graph.validate().expect("Failed to validate filter graph");
621
622        let source_context_name = "Parsed_abuffer_0";
623        let sink_context_name = "Parsed_abuffersink_1";
624
625        {
626            let mut source_context = filter_graph
627                .get(source_context_name)
628                .expect("Failed to retrieve source filter context")
629                .source();
630            let eof_result_with_pts = source_context.send_eof(Some(12345));
631            assert!(eof_result_with_pts.is_ok(), "send_eof with PTS should succeed");
632
633            let eof_result_without_pts = source_context.send_eof(None);
634            assert!(eof_result_without_pts.is_ok(), "send_eof without PTS should succeed");
635        }
636
637        {
638            let mut sink_context = filter_graph
639                .get(sink_context_name)
640                .expect("Failed to retrieve sink filter context")
641                .sink();
642            let received_frame = sink_context.receive_frame();
643            assert!(received_frame.is_ok(), "receive_frame should succeed after EOF is sent");
644            assert!(received_frame.unwrap().is_none(), "No frame should be received after EOF");
645        }
646    }
647}