Skip to main content

ff_sys/
log_bridge.rs

1//! Bridge FFmpeg's `av_log` output into the Rust `log` facade.
2//!
3//! FFmpeg writes its internal diagnostics to stderr by default, bypassing
4//! whatever logging the consuming application configured. [`install_log_bridge`]
5//! replaces that with a callback forwarding each message to the `log` crate
6//! under the `ffmpeg` target, so a backend filter (`RUST_LOG=ffmpeg=warn` and
7//! the like) governs FFmpeg's output alongside the rest of the workspace's.
8//!
9//! [`ensure_initialized`](crate::ensure_initialized) installs the bridge, so
10//! every crate in the family gets it without doing anything.
11//!
12//! # Formatting without interpreting `va_list`
13//!
14//! FFmpeg hands the callback a `va_list`, which Rust cannot read on stable
15//! (`c_variadic` is unstable). It does not need to: the list is forwarded
16//! **once, untouched**, to `av_log_format_line2`, and FFmpeg does the
17//! formatting.
18//!
19//! The callback's parameter type cannot simply be spelled
20//! [`va_list`](crate::va_list), because that alias does not always match what
21//! bindgen puts in *parameter* position:
22//!
23//! | target | `va_list` typedef | parameter type |
24//! |---|---|---|
25//! | x86_64 Linux / Intel macOS | `[__va_list_tag; 1]` | `*mut __va_list_tag` |
26//! | aarch64 Linux, aarch64 macOS, Windows MSVC | pointer or struct | the alias |
27//!
28//! Where the typedef is an array, C's array-to-pointer adjustment strips it off
29//! parameters, and bindgen follows clang in dropping the typedef sugar there.
30//! Naming the alias then fails to compile on exactly the platforms that build
31//! against real FFmpeg in CI, while passing on Windows where the two coincide.
32//! [`VaListArg`] selects the right shape from a `build.rs` cfg derived from the
33//! generated bindings.
34//!
35//! # The level check comes with the callback
36//!
37//! `av_vlog` dispatches to the installed callback unconditionally; the
38//! `av_log_set_level` threshold is applied inside `av_log_default_callback`, not
39//! before it. Replacing that callback therefore takes the check over, which is
40//! why [`log_callback`] consults `av_log_get_level` itself. Without it
41//! [`set_log_level`] would still update FFmpeg's global and still round-trip
42//! through [`log_level`], while silently filtering nothing.
43//!
44//! # Deviations from `av_log_default_callback`
45//!
46//! - **Repeated lines are not collapsed.** FFmpeg's own callback folds an
47//!   identical line into "Last message repeated N times"; doing that here would
48//!   mean shared mutable state in a callback FFmpeg invokes from its internal
49//!   threads, so a component that repeats a warning produces one record per
50//!   repeat. A `log` backend can deduplicate if it matters.
51//! - **A level's colour tint is discarded.** `AV_LOG_C` tint bits are masked off
52//!   the level before it is mapped, since `log` has no colour channel.
53//! - **The message prefix is per-record.** FFmpeg carries `print_prefix` across
54//!   calls so a line emitted as several fragments is prefixed once. Each record
55//!   here identifies itself instead, so such a line arrives as several records.
56
57use std::ffi::CStr;
58use std::os::raw::{c_char, c_int, c_void};
59use std::sync::Once;
60
61use log::LevelFilter;
62
63/// The type bindgen gave the `va_list` parameter of `av_log_format_line2` and of
64/// the `av_log_set_callback` function pointer on this target.
65///
66/// `build.rs` sets `cfg(va_list_tag)` when the generated bindings mention
67/// `__va_list_tag`, which is exactly the case where `va_list` is an array
68/// typedef and parameters therefore decay to a pointer. See the module docs.
69#[cfg(va_list_tag)]
70type VaListArg = *mut crate::__va_list_tag;
71/// The type bindgen gave the `va_list` parameter on this target (the alias
72/// itself, where no array-to-pointer adjustment happened).
73#[cfg(not(va_list_tag))]
74type VaListArg = crate::va_list;
75
76/// Installation guard: FFmpeg keeps one global callback pointer, so the bridge
77/// is installed exactly once no matter how many threads race to do it.
78static INSTALL: Once = Once::new();
79
80/// The `log` target every bridged message carries, so consumers can filter
81/// FFmpeg's chatter separately from the workspace's own records.
82const TARGET: &str = "ffmpeg";
83
84/// Buffer for one formatted message. Matches the size FFmpeg's own default
85/// callback uses; a longer line is truncated rather than allocated for, which is
86/// what keeps the callback allocation-free.
87const LINE_CAPACITY: usize = 1024;
88
89// The bindgen constants, cast once. `AV_LOG_QUIET` comes through as `i32` and
90// the rest as `u32`, so comparing them needs a common type.
91const QUIET: c_int = crate::AV_LOG_QUIET as c_int;
92const ERROR: c_int = crate::AV_LOG_ERROR as c_int;
93const WARNING: c_int = crate::AV_LOG_WARNING as c_int;
94const INFO: c_int = crate::AV_LOG_INFO as c_int;
95const VERBOSE: c_int = crate::AV_LOG_VERBOSE as c_int;
96const TRACE: c_int = crate::AV_LOG_TRACE as c_int;
97
98/// Map an `AV_LOG_*` level onto the `log` filter that would record it.
99///
100/// Ranges rather than equality: `av_log` takes an arbitrary `int` and FFmpeg
101/// compares it numerically, so a level between two named constants has to land
102/// on the next one that would print it. Mapping `AV_LOG_QUIET` to `Off` also
103/// gives the callback its "drop this" case for free, since
104/// [`LevelFilter::to_level`] is `None` there.
105///
106/// `AV_LOG_DEBUG` maps to `Trace` rather than `Debug` because FFmpeg's debug
107/// level is per-frame chatter, which belongs at Rust's most verbose level.
108fn av_to_filter(av_level: c_int) -> LevelFilter {
109    if av_level <= QUIET {
110        LevelFilter::Off
111    } else if av_level <= ERROR {
112        LevelFilter::Error
113    } else if av_level <= WARNING {
114        LevelFilter::Warn
115    } else if av_level <= INFO {
116        LevelFilter::Info
117    } else if av_level <= VERBOSE {
118        LevelFilter::Debug
119    } else {
120        LevelFilter::Trace
121    }
122}
123
124/// The inverse of [`av_to_filter`]: the `AV_LOG_*` threshold that lets exactly
125/// the messages `filter` admits through.
126fn filter_to_av(filter: LevelFilter) -> c_int {
127    match filter {
128        LevelFilter::Off => QUIET,
129        LevelFilter::Error => ERROR,
130        LevelFilter::Warn => WARNING,
131        LevelFilter::Info => INFO,
132        LevelFilter::Debug => VERBOSE,
133        LevelFilter::Trace => TRACE,
134    }
135}
136
137/// Forward one FFmpeg message to the `log` facade.
138///
139/// # Safety
140///
141/// Installed only through [`install_log_bridge`], so FFmpeg is the sole caller
142/// and supplies the arguments under `av_log`'s contract: `fmt` is a valid
143/// null-terminated format string and `vl` the matching argument list. `vl` is
144/// forwarded to `av_log_format_line2` exactly once and never inspected here,
145/// which is what makes this sound without `c_variadic`. The function holds no
146/// state, so FFmpeg may call it from any thread.
147unsafe extern "C" fn log_callback(
148    avcl: *mut c_void,
149    level: c_int,
150    fmt: *const c_char,
151    vl: VaListArg,
152) {
153    // FFmpeg is a C caller, so an unwind escaping this function aborts the whole
154    // process. The body ends in a call into whatever `log::Log` the application
155    // installed - arbitrary code this crate does not control, and a panicking
156    // logger (a poisoned mutex, a closed pipe) is an ordinary bug rather than an
157    // exotic one. Swallow the payload: there is nowhere to report it, and
158    // re-entering `log` to complain risks a second panic.
159    //
160    // SAFETY: forwards its arguments unchanged to `log_callback_impl`, whose
161    // contract is the same one FFmpeg guarantees for this callback.
162    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
163        log_callback_impl(avcl, level, fmt, vl);
164    }));
165}
166
167/// The body of [`log_callback`], separated so the `extern "C"` boundary above is
168/// nothing but the unwind guard.
169///
170/// # Safety
171///
172/// Same contract as [`log_callback`]: `fmt` is a valid null-terminated format
173/// string and `vl` its matching argument list, and `vl` is consumed exactly once.
174unsafe fn log_callback_impl(avcl: *mut c_void, level: c_int, fmt: *const c_char, vl: VaListArg) {
175    // A level can carry a colour tint in its high bits (`AV_LOG_C(x) = (x) << 8`,
176    // libavutil/log.h), which `av_log_default_callback` masks off before
177    // thresholding. Without this a tinted warning arrives as ~34328, fails every
178    // threshold, and is silently dropped. The tint itself is discarded: the `log`
179    // facade has no colour channel.
180    let level = if level >= 0 { level & 0xff } else { level };
181
182    // FFmpeg does *not* apply `av_log_set_level` before dispatching: `av_vlog`
183    // calls the installed callback unconditionally, and the threshold check
184    // lives inside `av_log_default_callback`. Replacing that callback therefore
185    // takes the check over, and dropping this line silently turns
186    // `set_log_level` into a no-op.
187    //
188    // SAFETY: `av_log_get_level` only reads an FFmpeg global.
189    if level > unsafe { crate::av_log_get_level() } {
190        return;
191    }
192    let Some(mapped) = av_to_filter(level).to_level() else {
193        return;
194    };
195    // Then the facade's own threshold, so a message no logger would record does
196    // not cost a `vsnprintf`. `max_level` is a plain atomic load, and this runs
197    // on FFmpeg's decode threads. (`log::log!` filters again, so unlike the
198    // check above this one changes cost, not behaviour.)
199    if mapped > log::max_level() {
200        return;
201    }
202    if fmt.is_null() {
203        return;
204    }
205
206    let mut line = [0 as c_char; LINE_CAPACITY];
207    // FFmpeg carries this across calls so a line emitted as several fragments is
208    // prefixed once; keeping that state would mean sharing it across FFmpeg's
209    // threads, so every record identifies itself instead.
210    let mut print_prefix: c_int = 1;
211
212    // SAFETY: `line` is a live buffer of `LINE_CAPACITY` elements that outlives
213    // the call, and `LINE_CAPACITY` is what we declare its size to be. `fmt` is
214    // non-null (checked above) and `vl` matches it, both guaranteed by
215    // `av_log`'s contract with its callback. `av_log_format_line2` consumes `vl`
216    // exactly once and null-terminates within `line_size`.
217    let written = unsafe {
218        crate::av_log_format_line2(
219            avcl,
220            level,
221            fmt,
222            vl,
223            line.as_mut_ptr(),
224            LINE_CAPACITY as c_int,
225            &raw mut print_prefix,
226        )
227    };
228    if written <= 0 {
229        return;
230    }
231
232    // SAFETY: `av_log_format_line2` reported a positive length, so it wrote a
233    // null-terminated string into `line`.
234    let message = unsafe { CStr::from_ptr(line.as_ptr()) }.to_string_lossy();
235    // FFmpeg terminates its lines; `log` records are lines already.
236    log::log!(target: TARGET, mapped, "{}", message.trim_end());
237}
238
239/// Route FFmpeg's internal diagnostics into the `log` facade under the `ffmpeg`
240/// target, instead of letting them go to stderr.
241///
242/// Idempotent and safe to call from any thread: FFmpeg holds a single global
243/// callback pointer, and repeat calls do nothing.
244/// [`ensure_initialized`](crate::ensure_initialized) already calls this, so most
245/// callers never need to.
246///
247/// This changes **process-global** FFmpeg state. An application that wants
248/// FFmpeg's messages on stderr should not call `ensure_initialized` (or should
249/// install its own callback afterwards).
250pub fn install_log_bridge() {
251    INSTALL.call_once(|| {
252        // SAFETY: `av_log_set_callback` only stores the pointer in an FFmpeg
253        // global. `log_callback` is a `'static` function whose signature is the
254        // one FFmpeg declares for the callback (see [`VaListArg`]).
255        unsafe { crate::av_log_set_callback(Some(log_callback)) };
256    });
257}
258
259/// Set the threshold below which FFmpeg's messages are discarded.
260///
261/// This writes FFmpeg's own global level (`av_log_set_level`), which the bridge
262/// then enforces: FFmpeg dispatches to an installed callback *without* checking
263/// the level, so the check belongs to whoever replaced the default callback.
264/// A message dropped by it is never formatted.
265///
266/// The `log` backend filters again, so the effective level is the stricter of
267/// the two. FFmpeg's default is [`LevelFilter::Info`].
268pub fn set_log_level(level: LevelFilter) {
269    // SAFETY: `av_log_set_level` only stores an int in an FFmpeg global.
270    unsafe { crate::av_log_set_level(filter_to_av(level)) };
271}
272
273/// The level FFmpeg is currently logging at, as the `log` filter that matches
274/// it.
275///
276/// Round-trips with [`set_log_level`]. Reading back a level FFmpeg was given by
277/// other means can only be approximate, since several `AV_LOG_*` values map onto
278/// one [`LevelFilter`].
279#[must_use]
280pub fn log_level() -> LevelFilter {
281    // SAFETY: `av_log_get_level` only reads an FFmpeg global.
282    let level = unsafe { crate::av_log_get_level() };
283    av_to_filter(level)
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use std::sync::Mutex;
290
291    /// Serialises the tests that touch FFmpeg's global log level or the process
292    /// logger, which cargo would otherwise run concurrently.
293    static TEST_LOCK: Mutex<()> = Mutex::new(());
294
295    /// Records the bridge produced, so a test can assert on them.
296    static RECORDS: Mutex<Vec<(log::Level, String)>> = Mutex::new(Vec::new());
297    /// A message the collector deliberately panics on, so a test can prove the
298    /// callback's unwind guard holds. A real backend panics for duller reasons
299    /// (a poisoned mutex, a closed pipe); the effect at the FFI boundary is the
300    /// same.
301    const PANIC_PROBE: &str = "ff-sys log bridge panic probe";
302    static COLLECTOR: Collector = Collector;
303    static LOGGER_INIT: Once = Once::new();
304
305    struct Collector;
306
307    impl log::Log for Collector {
308        fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
309            true
310        }
311
312        fn log(&self, record: &log::Record<'_>) {
313            if record.target() == TARGET {
314                let message = record.args().to_string();
315                assert!(
316                    !message.contains(PANIC_PROBE),
317                    "deliberate panic from the test log backend"
318                );
319                RECORDS
320                    .lock()
321                    .unwrap_or_else(std::sync::PoisonError::into_inner)
322                    .push((record.level(), message));
323            }
324        }
325
326        fn flush(&self) {}
327    }
328
329    /// Run `f` with the collecting logger installed, the bridge live, and the
330    /// record list empty. Restores FFmpeg's log level afterwards so the tests
331    /// stay independent of each other.
332    fn with_collector<T>(f: impl FnOnce() -> T) -> T {
333        let _guard = TEST_LOCK
334            .lock()
335            .unwrap_or_else(std::sync::PoisonError::into_inner);
336        LOGGER_INIT.call_once(|| {
337            log::set_logger(&COLLECTOR).expect("no other logger in this test binary");
338        });
339        install_log_bridge();
340        let previous_ffmpeg = log_level();
341        log::set_max_level(LevelFilter::Trace);
342        RECORDS
343            .lock()
344            .unwrap_or_else(std::sync::PoisonError::into_inner)
345            .clear();
346
347        let out = f();
348
349        set_log_level(previous_ffmpeg);
350        log::set_max_level(LevelFilter::Trace);
351        out
352    }
353
354    /// The records collected so far whose message contains `marker`.
355    ///
356    /// Scoped by marker rather than taken wholesale because `log::set_logger` is
357    /// process-global: the ~146 other tests in this binary build real FFmpeg
358    /// contexts, and once `set_log_level` opens the threshold their messages land
359    /// in `RECORDS` too (measured: 95 foreign records in a 2.5s window, from
360    /// swresample and swscale). Asserting on `first()` or on a total count would
361    /// be a race against whichever test happens to run alongside.
362    fn records_matching(marker: &str) -> Vec<(log::Level, String)> {
363        RECORDS
364            .lock()
365            .unwrap_or_else(std::sync::PoisonError::into_inner)
366            .iter()
367            .filter(|(_, message)| message.contains(marker))
368            .cloned()
369            .collect()
370    }
371
372    #[test]
373    fn av_to_filter_should_map_each_ffmpeg_level() {
374        let cases = [
375            (crate::AV_LOG_QUIET as c_int, LevelFilter::Off),
376            (crate::AV_LOG_PANIC as c_int, LevelFilter::Error),
377            (crate::AV_LOG_FATAL as c_int, LevelFilter::Error),
378            (crate::AV_LOG_ERROR as c_int, LevelFilter::Error),
379            (crate::AV_LOG_WARNING as c_int, LevelFilter::Warn),
380            (crate::AV_LOG_INFO as c_int, LevelFilter::Info),
381            (crate::AV_LOG_VERBOSE as c_int, LevelFilter::Debug),
382            (crate::AV_LOG_DEBUG as c_int, LevelFilter::Trace),
383            (crate::AV_LOG_TRACE as c_int, LevelFilter::Trace),
384        ];
385        for (av_level, expected) in cases {
386            assert_eq!(
387                av_to_filter(av_level),
388                expected,
389                "AV_LOG level {av_level} must map to {expected}"
390            );
391        }
392    }
393
394    #[test]
395    fn av_to_filter_should_map_values_between_named_levels() {
396        // `av_log` takes an arbitrary int, so the mapping has to be a range. A
397        // lookup table keyed on the named constants would fall through on all of
398        // these.
399        assert_eq!(
400            av_to_filter(4),
401            LevelFilter::Error,
402            "between PANIC and FATAL"
403        );
404        assert_eq!(
405            av_to_filter(20),
406            LevelFilter::Warn,
407            "between ERROR and WARNING"
408        );
409        assert_eq!(
410            av_to_filter(28),
411            LevelFilter::Info,
412            "between WARNING and INFO"
413        );
414        assert_eq!(av_to_filter(60), LevelFilter::Trace, "above TRACE");
415        assert_eq!(av_to_filter(-100), LevelFilter::Off, "below QUIET");
416    }
417
418    #[test]
419    fn filter_to_av_should_round_trip_through_av_to_filter() {
420        for filter in [
421            LevelFilter::Off,
422            LevelFilter::Error,
423            LevelFilter::Warn,
424            LevelFilter::Info,
425            LevelFilter::Debug,
426            LevelFilter::Trace,
427        ] {
428            assert_eq!(
429                av_to_filter(filter_to_av(filter)),
430                filter,
431                "{filter} must survive the round trip through AV_LOG levels"
432            );
433        }
434    }
435
436    #[test]
437    fn install_log_bridge_should_be_idempotent_across_threads() {
438        // FFmpeg holds one global callback pointer, so racing installs must
439        // collapse onto a single one rather than tearing.
440        let threads: Vec<_> = (0..8)
441            .map(|_| std::thread::spawn(install_log_bridge))
442            .collect();
443        for thread in threads {
444            thread.join().expect("install_log_bridge must not panic");
445        }
446        install_log_bridge();
447    }
448
449    #[test]
450    fn set_log_level_should_round_trip_through_ffmpeg() {
451        let _guard = TEST_LOCK
452            .lock()
453            .unwrap_or_else(std::sync::PoisonError::into_inner);
454        let previous = log_level();
455        for filter in [
456            LevelFilter::Off,
457            LevelFilter::Error,
458            LevelFilter::Warn,
459            LevelFilter::Info,
460            LevelFilter::Debug,
461            LevelFilter::Trace,
462        ] {
463            set_log_level(filter);
464            assert_eq!(
465                log_level(),
466                filter,
467                "{filter} must survive a round trip through FFmpeg's global level"
468            );
469        }
470        set_log_level(previous);
471    }
472
473    #[test]
474    fn av_log_should_reach_the_log_facade_with_formatted_arguments() {
475        with_collector(|| {
476            set_log_level(LevelFilter::Trace);
477
478            // The format specifiers are the point. A bridge that ignored `vl`
479            // and copied the format string verbatim would still produce a
480            // record, so only asserting that the *substituted* values appear
481            // proves the va_list reached `av_log_format_line2`.
482            //
483            // SAFETY: `av_log` accepts a null `avcl` (no AVClass context); the
484            // format string is null-terminated and its specifiers match the
485            // arguments that follow.
486            unsafe {
487                crate::av_log(
488                    std::ptr::null_mut(),
489                    crate::AV_LOG_ERROR as c_int,
490                    c"ff-sys log bridge probe %d %s\n".as_ptr(),
491                    1599_i32,
492                    c"marker".as_ptr(),
493                );
494            }
495
496            let collected = records_matching("ff-sys log bridge probe");
497            let (level, message) = collected
498                .first()
499                .expect("an AV_LOG_ERROR message must reach the log facade");
500            assert_eq!(*level, log::Level::Error, "AV_LOG_ERROR must map to Error");
501            assert!(
502                message.contains("1599"),
503                "the %d argument must be substituted; got {message:?}"
504            );
505            assert!(
506                message.contains("marker"),
507                "the %s argument must be substituted; got {message:?}"
508            );
509            assert!(
510                !message.contains("%d"),
511                "the raw format string must not be recorded; got {message:?}"
512            );
513            assert!(
514                !message.ends_with('\n'),
515                "FFmpeg's trailing newline must be trimmed; got {message:?}"
516            );
517        });
518    }
519
520    #[test]
521    fn log_callback_should_map_a_tinted_level_by_its_low_byte() {
522        with_collector(|| {
523            set_log_level(LevelFilter::Trace);
524
525            // `AV_LOG_C(134)` is 134 << 8, so this arrives as 34328. Unmasked it
526            // exceeds every threshold and is dropped silently, and would map to
527            // Trace rather than Warn if it did get through.
528            const TINTED_WARNING: c_int = crate::AV_LOG_WARNING as c_int | (134 << 8);
529
530            // SAFETY: null `avcl` is accepted and the format string is
531            // null-terminated with no specifiers to satisfy.
532            unsafe {
533                crate::av_log(
534                    std::ptr::null_mut(),
535                    TINTED_WARNING,
536                    c"ff-sys log bridge tint probe
537"
538                    .as_ptr(),
539                );
540            }
541
542            let collected = records_matching("tint probe");
543            let (level, _) = collected
544                .first()
545                .expect("a tinted warning must not be dropped by the threshold");
546            assert_eq!(
547                *level,
548                log::Level::Warn,
549                "the tint must be masked off before mapping, leaving AV_LOG_WARNING"
550            );
551        });
552    }
553
554    #[test]
555    fn log_callback_should_contain_a_panic_from_the_log_backend() {
556        with_collector(|| {
557            set_log_level(LevelFilter::Trace);
558
559            // The collector panics on this one. Without the unwind guard the
560            // panic would cross the `extern "C"` boundary and abort the whole
561            // process, taking the test binary with it — so merely reaching the
562            // next statement is most of the assertion. (A panic message on
563            // stderr here is expected.)
564            // SAFETY: as above.
565            unsafe {
566                crate::av_log(
567                    std::ptr::null_mut(),
568                    crate::AV_LOG_ERROR as c_int,
569                    c"ff-sys log bridge panic probe
570"
571                    .as_ptr(),
572                );
573            }
574
575            // And the bridge must still work afterwards, not be left wedged.
576            // SAFETY: as above.
577            unsafe {
578                crate::av_log(
579                    std::ptr::null_mut(),
580                    crate::AV_LOG_ERROR as c_int,
581                    c"ff-sys log bridge post-panic probe
582"
583                    .as_ptr(),
584                );
585            }
586            assert_eq!(
587                records_matching("post-panic probe").len(),
588                1,
589                "the bridge must keep working after a backend panic"
590            );
591        });
592    }
593
594    #[test]
595    fn set_log_level_should_stop_ffmpeg_from_passing_lower_priority_messages() {
596        with_collector(|| {
597            // The `log` facade is wide open, so anything dropped here was
598            // dropped by FFmpeg's own threshold.
599            set_log_level(LevelFilter::Warn);
600
601            // SAFETY: as in the test above.
602            unsafe {
603                crate::av_log(
604                    std::ptr::null_mut(),
605                    crate::AV_LOG_INFO as c_int,
606                    c"ff-sys log bridge info probe\n".as_ptr(),
607                );
608            }
609            assert!(
610                records_matching("info probe").is_empty(),
611                "an Info message must not pass a Warn threshold; got {:?}",
612                records_matching("info probe")
613            );
614
615            // Non-vacuous: a bridge that recorded nothing at all would satisfy
616            // the assertion above, so prove the path is live at a level the
617            // threshold admits.
618            // SAFETY: as above.
619            unsafe {
620                crate::av_log(
621                    std::ptr::null_mut(),
622                    crate::AV_LOG_ERROR as c_int,
623                    c"ff-sys log bridge error probe\n".as_ptr(),
624                );
625            }
626            assert_eq!(
627                records_matching("error probe").len(),
628                1,
629                "an Error message must still pass a Warn threshold"
630            );
631        });
632    }
633}