use std::ffi::CStr;
use std::os::raw::{c_char, c_int, c_void};
use std::sync::Once;
use log::LevelFilter;
#[cfg(va_list_tag)]
type VaListArg = *mut crate::__va_list_tag;
#[cfg(not(va_list_tag))]
type VaListArg = crate::va_list;
static INSTALL: Once = Once::new();
const TARGET: &str = "ffmpeg";
const LINE_CAPACITY: usize = 1024;
const QUIET: c_int = crate::AV_LOG_QUIET as c_int;
const ERROR: c_int = crate::AV_LOG_ERROR as c_int;
const WARNING: c_int = crate::AV_LOG_WARNING as c_int;
const INFO: c_int = crate::AV_LOG_INFO as c_int;
const VERBOSE: c_int = crate::AV_LOG_VERBOSE as c_int;
const TRACE: c_int = crate::AV_LOG_TRACE as c_int;
fn av_to_filter(av_level: c_int) -> LevelFilter {
if av_level <= QUIET {
LevelFilter::Off
} else if av_level <= ERROR {
LevelFilter::Error
} else if av_level <= WARNING {
LevelFilter::Warn
} else if av_level <= INFO {
LevelFilter::Info
} else if av_level <= VERBOSE {
LevelFilter::Debug
} else {
LevelFilter::Trace
}
}
fn filter_to_av(filter: LevelFilter) -> c_int {
match filter {
LevelFilter::Off => QUIET,
LevelFilter::Error => ERROR,
LevelFilter::Warn => WARNING,
LevelFilter::Info => INFO,
LevelFilter::Debug => VERBOSE,
LevelFilter::Trace => TRACE,
}
}
unsafe extern "C" fn log_callback(
avcl: *mut c_void,
level: c_int,
fmt: *const c_char,
vl: VaListArg,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
log_callback_impl(avcl, level, fmt, vl);
}));
}
unsafe fn log_callback_impl(avcl: *mut c_void, level: c_int, fmt: *const c_char, vl: VaListArg) {
let level = if level >= 0 { level & 0xff } else { level };
if level > unsafe { crate::av_log_get_level() } {
return;
}
let Some(mapped) = av_to_filter(level).to_level() else {
return;
};
if mapped > log::max_level() {
return;
}
if fmt.is_null() {
return;
}
let mut line = [0 as c_char; LINE_CAPACITY];
let mut print_prefix: c_int = 1;
let written = unsafe {
crate::av_log_format_line2(
avcl,
level,
fmt,
vl,
line.as_mut_ptr(),
LINE_CAPACITY as c_int,
&raw mut print_prefix,
)
};
if written <= 0 {
return;
}
let message = unsafe { CStr::from_ptr(line.as_ptr()) }.to_string_lossy();
log::log!(target: TARGET, mapped, "{}", message.trim_end());
}
pub fn install_log_bridge() {
INSTALL.call_once(|| {
unsafe { crate::av_log_set_callback(Some(log_callback)) };
});
}
pub fn set_log_level(level: LevelFilter) {
unsafe { crate::av_log_set_level(filter_to_av(level)) };
}
#[must_use]
pub fn log_level() -> LevelFilter {
let level = unsafe { crate::av_log_get_level() };
av_to_filter(level)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_LOCK: Mutex<()> = Mutex::new(());
static RECORDS: Mutex<Vec<(log::Level, String)>> = Mutex::new(Vec::new());
const PANIC_PROBE: &str = "ff-sys log bridge panic probe";
static COLLECTOR: Collector = Collector;
static LOGGER_INIT: Once = Once::new();
struct Collector;
impl log::Log for Collector {
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &log::Record<'_>) {
if record.target() == TARGET {
let message = record.args().to_string();
assert!(
!message.contains(PANIC_PROBE),
"deliberate panic from the test log backend"
);
RECORDS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((record.level(), message));
}
}
fn flush(&self) {}
}
fn with_collector<T>(f: impl FnOnce() -> T) -> T {
let _guard = TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
LOGGER_INIT.call_once(|| {
log::set_logger(&COLLECTOR).expect("no other logger in this test binary");
});
install_log_bridge();
let previous_ffmpeg = log_level();
log::set_max_level(LevelFilter::Trace);
RECORDS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
let out = f();
set_log_level(previous_ffmpeg);
log::set_max_level(LevelFilter::Trace);
out
}
fn records_matching(marker: &str) -> Vec<(log::Level, String)> {
RECORDS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter(|(_, message)| message.contains(marker))
.cloned()
.collect()
}
#[test]
fn av_to_filter_should_map_each_ffmpeg_level() {
let cases = [
(crate::AV_LOG_QUIET as c_int, LevelFilter::Off),
(crate::AV_LOG_PANIC as c_int, LevelFilter::Error),
(crate::AV_LOG_FATAL as c_int, LevelFilter::Error),
(crate::AV_LOG_ERROR as c_int, LevelFilter::Error),
(crate::AV_LOG_WARNING as c_int, LevelFilter::Warn),
(crate::AV_LOG_INFO as c_int, LevelFilter::Info),
(crate::AV_LOG_VERBOSE as c_int, LevelFilter::Debug),
(crate::AV_LOG_DEBUG as c_int, LevelFilter::Trace),
(crate::AV_LOG_TRACE as c_int, LevelFilter::Trace),
];
for (av_level, expected) in cases {
assert_eq!(
av_to_filter(av_level),
expected,
"AV_LOG level {av_level} must map to {expected}"
);
}
}
#[test]
fn av_to_filter_should_map_values_between_named_levels() {
assert_eq!(
av_to_filter(4),
LevelFilter::Error,
"between PANIC and FATAL"
);
assert_eq!(
av_to_filter(20),
LevelFilter::Warn,
"between ERROR and WARNING"
);
assert_eq!(
av_to_filter(28),
LevelFilter::Info,
"between WARNING and INFO"
);
assert_eq!(av_to_filter(60), LevelFilter::Trace, "above TRACE");
assert_eq!(av_to_filter(-100), LevelFilter::Off, "below QUIET");
}
#[test]
fn filter_to_av_should_round_trip_through_av_to_filter() {
for filter in [
LevelFilter::Off,
LevelFilter::Error,
LevelFilter::Warn,
LevelFilter::Info,
LevelFilter::Debug,
LevelFilter::Trace,
] {
assert_eq!(
av_to_filter(filter_to_av(filter)),
filter,
"{filter} must survive the round trip through AV_LOG levels"
);
}
}
#[test]
fn install_log_bridge_should_be_idempotent_across_threads() {
let threads: Vec<_> = (0..8)
.map(|_| std::thread::spawn(install_log_bridge))
.collect();
for thread in threads {
thread.join().expect("install_log_bridge must not panic");
}
install_log_bridge();
}
#[test]
fn set_log_level_should_round_trip_through_ffmpeg() {
let _guard = TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = log_level();
for filter in [
LevelFilter::Off,
LevelFilter::Error,
LevelFilter::Warn,
LevelFilter::Info,
LevelFilter::Debug,
LevelFilter::Trace,
] {
set_log_level(filter);
assert_eq!(
log_level(),
filter,
"{filter} must survive a round trip through FFmpeg's global level"
);
}
set_log_level(previous);
}
#[test]
fn av_log_should_reach_the_log_facade_with_formatted_arguments() {
with_collector(|| {
set_log_level(LevelFilter::Trace);
unsafe {
crate::av_log(
std::ptr::null_mut(),
crate::AV_LOG_ERROR as c_int,
c"ff-sys log bridge probe %d %s\n".as_ptr(),
1599_i32,
c"marker".as_ptr(),
);
}
let collected = records_matching("ff-sys log bridge probe");
let (level, message) = collected
.first()
.expect("an AV_LOG_ERROR message must reach the log facade");
assert_eq!(*level, log::Level::Error, "AV_LOG_ERROR must map to Error");
assert!(
message.contains("1599"),
"the %d argument must be substituted; got {message:?}"
);
assert!(
message.contains("marker"),
"the %s argument must be substituted; got {message:?}"
);
assert!(
!message.contains("%d"),
"the raw format string must not be recorded; got {message:?}"
);
assert!(
!message.ends_with('\n'),
"FFmpeg's trailing newline must be trimmed; got {message:?}"
);
});
}
#[test]
fn log_callback_should_map_a_tinted_level_by_its_low_byte() {
with_collector(|| {
set_log_level(LevelFilter::Trace);
const TINTED_WARNING: c_int = crate::AV_LOG_WARNING as c_int | (134 << 8);
unsafe {
crate::av_log(
std::ptr::null_mut(),
TINTED_WARNING,
c"ff-sys log bridge tint probe
"
.as_ptr(),
);
}
let collected = records_matching("tint probe");
let (level, _) = collected
.first()
.expect("a tinted warning must not be dropped by the threshold");
assert_eq!(
*level,
log::Level::Warn,
"the tint must be masked off before mapping, leaving AV_LOG_WARNING"
);
});
}
#[test]
fn log_callback_should_contain_a_panic_from_the_log_backend() {
with_collector(|| {
set_log_level(LevelFilter::Trace);
unsafe {
crate::av_log(
std::ptr::null_mut(),
crate::AV_LOG_ERROR as c_int,
c"ff-sys log bridge panic probe
"
.as_ptr(),
);
}
unsafe {
crate::av_log(
std::ptr::null_mut(),
crate::AV_LOG_ERROR as c_int,
c"ff-sys log bridge post-panic probe
"
.as_ptr(),
);
}
assert_eq!(
records_matching("post-panic probe").len(),
1,
"the bridge must keep working after a backend panic"
);
});
}
#[test]
fn set_log_level_should_stop_ffmpeg_from_passing_lower_priority_messages() {
with_collector(|| {
set_log_level(LevelFilter::Warn);
unsafe {
crate::av_log(
std::ptr::null_mut(),
crate::AV_LOG_INFO as c_int,
c"ff-sys log bridge info probe\n".as_ptr(),
);
}
assert!(
records_matching("info probe").is_empty(),
"an Info message must not pass a Warn threshold; got {:?}",
records_matching("info probe")
);
unsafe {
crate::av_log(
std::ptr::null_mut(),
crate::AV_LOG_ERROR as c_int,
c"ff-sys log bridge error probe\n".as_ptr(),
);
}
assert_eq!(
records_matching("error probe").len(),
1,
"an Error message must still pass a Warn threshold"
);
});
}
}