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}