scuffle_ffmpeg/
log.rs

1use std::ffi::CStr;
2use std::ptr::NonNull;
3use std::sync::Arc;
4
5use arc_swap::ArcSwapOption;
6use nutype_enum::nutype_enum;
7
8use crate::ffi::*;
9
10nutype_enum! {
11    /// The logging level
12    pub enum LogLevel(i32) {
13        /// Quiet logging level.
14        Quiet = AV_LOG_QUIET,
15        /// Panic logging level.
16        Panic = AV_LOG_PANIC as i32,
17        /// Fatal logging level.
18        Fatal = AV_LOG_FATAL as i32,
19        /// Error logging level.
20        Error = AV_LOG_ERROR as i32,
21        /// Warning logging level.
22        Warning = AV_LOG_WARNING as i32,
23        /// Info logging level.
24        Info = AV_LOG_INFO as i32,
25        /// Verbose logging level.
26        Verbose = AV_LOG_VERBOSE as i32,
27        /// Debug logging level.
28        Debug = AV_LOG_DEBUG as i32,
29        /// Trace logging level.
30        Trace = AV_LOG_TRACE as i32,
31    }
32}
33
34impl std::fmt::Display for LogLevel {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match *self {
37            Self::Quiet => write!(f, "quiet"),
38            Self::Panic => write!(f, "panic"),
39            Self::Fatal => write!(f, "fatal"),
40            Self::Error => write!(f, "error"),
41            Self::Warning => write!(f, "warning"),
42            Self::Info => write!(f, "info"),
43            Self::Verbose => write!(f, "verbose"),
44            Self::Debug => write!(f, "debug"),
45            Self::Trace => write!(f, "trace"),
46            Self(int) => write!(f, "unknown({int})"),
47        }
48    }
49}
50
51/// Sets the log level.
52pub fn set_log_level(level: LogLevel) {
53    // Safety: `av_log_set_level` is safe to call.
54    unsafe {
55        av_log_set_level(level.0);
56    }
57}
58
59type Function = Box<dyn Fn(LogLevel, Option<String>, String) + Send + Sync>;
60static LOG_CALLBACK: ArcSwapOption<Function> = ArcSwapOption::const_empty();
61
62/// Sets the log callback.
63#[inline(always)]
64pub fn log_callback_set(callback: impl Fn(LogLevel, Option<String>, String) + Send + Sync + 'static) {
65    log_callback_set_boxed(Box::new(callback));
66}
67
68/// Sets the log callback.
69pub fn log_callback_set_boxed(callback: Function) {
70    LOG_CALLBACK.store(Some(Arc::new(callback)));
71
72    // Safety: `av_log_set_callback` is safe to call.
73    unsafe {
74        av_log_set_callback(Some(log_cb));
75    }
76}
77
78/// Unsets the log callback.
79pub fn log_callback_unset() {
80    LOG_CALLBACK.store(None);
81
82    // Safety: `av_log_set_callback` is safe to call.
83    unsafe {
84        av_log_set_callback(None);
85    }
86}
87
88#[cfg(unix)]
89type VaList = *mut __va_list_tag;
90
91#[cfg(windows)]
92type VaList = va_list;
93
94#[cfg(windows)]
95extern "C" {
96    fn vsnprintf(buffer: *mut libc::c_char, count: libc::size_t, format: *const libc::c_char, ap: VaList) -> i32;
97}
98
99unsafe extern "C" fn log_cb(ptr: *mut libc::c_void, level: libc::c_int, fmt: *const libc::c_char, va: VaList) {
100    let level = LogLevel::from(level);
101    let class = NonNull::new(ptr as *mut *mut AVClass)
102        .and_then(|class| NonNull::new(*class.as_ptr()))
103        .and_then(|class| {
104            class
105                .as_ref()
106                .item_name
107                .map(|im| CStr::from_ptr(im(ptr)).to_string_lossy().trim().to_owned())
108        });
109
110    let mut buf = [0u8; 1024];
111
112    vsnprintf(buf.as_mut_ptr() as *mut i8, buf.len() as _, fmt, va);
113
114    let msg = CStr::from_ptr(buf.as_ptr() as *const i8).to_string_lossy().trim().to_owned();
115
116    if let Some(cb) = LOG_CALLBACK.load().as_ref() {
117        cb(level, class, msg);
118    }
119}
120
121/// Sets the log callback to use tracing.
122#[cfg(feature = "tracing")]
123#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
124pub fn log_callback_tracing() {
125    log_callback_set(|mut level, class, msg| {
126        let class = class.as_deref().unwrap_or("ffmpeg");
127
128        // We purposely ignore this message because it's a false positive
129        if msg == "deprecated pixel format used, make sure you did set range correctly" {
130            level = LogLevel::Debug;
131        }
132
133        match level {
134            LogLevel::Trace => tracing::trace!("{level}: {class} @ {msg}"),
135            LogLevel::Verbose => tracing::trace!("{level}: {class} @ {msg}"),
136            LogLevel::Debug => tracing::debug!("{level}: {class} @ {msg}"),
137            LogLevel::Info => tracing::info!("{level}: {class} @ {msg}"),
138            LogLevel::Warning => tracing::warn!("{level}: {class} @ {msg}"),
139            LogLevel::Quiet => tracing::error!("{level}: {class} @ {msg}"),
140            LogLevel::Error => tracing::error!("{level}: {class} @ {msg}"),
141            LogLevel::Panic => tracing::error!("{level}: {class} @ {msg}"),
142            LogLevel::Fatal => tracing::error!("{level}: {class} @ {msg}"),
143            LogLevel(_) => tracing::debug!("{level}: {class} @ {msg}"),
144        }
145    });
146}
147
148#[cfg(test)]
149#[cfg_attr(all(test, coverage_nightly), coverage(off))]
150mod tests {
151    use std::ffi::CString;
152    use std::sync::{Arc, Mutex};
153
154    use crate::ffi::{av_log, av_log_get_level, avcodec_find_decoder};
155    use crate::log::{log_callback_set, log_callback_unset, set_log_level, LogLevel};
156    use crate::AVCodecID;
157
158    #[test]
159    fn test_log_level_as_str_using_from_i32() {
160        let test_cases = [
161            (LogLevel::Quiet, "quiet"),
162            (LogLevel::Panic, "panic"),
163            (LogLevel::Fatal, "fatal"),
164            (LogLevel::Error, "error"),
165            (LogLevel::Warning, "warning"),
166            (LogLevel::Info, "info"),
167            (LogLevel::Verbose, "verbose"),
168            (LogLevel::Debug, "debug"),
169            (LogLevel::Trace, "trace"),
170            (LogLevel(100), "unknown(100)"),
171            (LogLevel(-1), "unknown(-1)"),
172        ];
173
174        for &(input, expected) in &test_cases {
175            let log_level = input;
176            assert_eq!(
177                log_level.to_string(),
178                expected,
179                "Expected '{}' for input {}, but got '{}'",
180                expected,
181                input,
182                log_level
183            );
184        }
185    }
186
187    #[test]
188    fn test_set_log_level() {
189        let log_levels = [
190            LogLevel::Quiet,
191            LogLevel::Panic,
192            LogLevel::Fatal,
193            LogLevel::Error,
194            LogLevel::Warning,
195            LogLevel::Info,
196            LogLevel::Verbose,
197            LogLevel::Debug,
198            LogLevel::Trace,
199        ];
200
201        for &level in &log_levels {
202            set_log_level(level);
203            // Safety: `av_log_get_level` is safe to call.
204            let current_level = unsafe { av_log_get_level() };
205
206            assert_eq!(
207                current_level, level.0,
208                "Expected log level to be {}, but got {}",
209                level.0, current_level
210            );
211        }
212    }
213
214    #[test]
215    fn test_log_callback_set() {
216        let captured_logs = Arc::new(Mutex::new(Vec::new()));
217        let callback_logs = Arc::clone(&captured_logs);
218        log_callback_set(move |level, class, message| {
219            let mut logs = callback_logs.lock().unwrap();
220            logs.push((level, class, message));
221        });
222
223        let log_message = CString::new("Test warning log message").expect("Failed to create CString");
224        // Safety: `av_log` is safe to call.
225        unsafe {
226            av_log(std::ptr::null_mut(), LogLevel::Warning.0, log_message.as_ptr());
227        }
228
229        let logs = captured_logs.lock().unwrap();
230        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
231
232        let (level, class, message) = &logs[0];
233        assert_eq!(*level, LogLevel::Warning, "Expected log level to be Warning");
234        assert!(class.is_none(), "Expected class to be None for this test");
235        assert_eq!(message, "Test warning log message", "Expected log message to match");
236        log_callback_unset();
237    }
238
239    #[test]
240    fn test_log_callback_with_class() {
241        // Safety: `avcodec_find_decoder` is safe to call.
242        let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
243        assert!(!codec.is_null(), "Failed to find H264 codec");
244
245        // Safety: `(*codec).priv_class` is safe to access.
246        let av_class_ptr = unsafe { (*codec).priv_class };
247        assert!(!av_class_ptr.is_null(), "AVClass for codec is null");
248
249        let captured_logs = Arc::new(Mutex::new(Vec::new()));
250
251        let callback_logs = Arc::clone(&captured_logs);
252        log_callback_set(move |level, class, message| {
253            let mut logs = callback_logs.lock().unwrap();
254            logs.push((level, class, message));
255        });
256
257        // Safety: `av_log` is safe to call.
258        unsafe {
259            av_log(
260                &av_class_ptr as *const _ as *mut _,
261                LogLevel::Info.0,
262                CString::new("Test log message with real AVClass").unwrap().as_ptr(),
263            );
264        }
265
266        let logs = captured_logs.lock().unwrap();
267        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
268
269        let (level, class, message) = &logs[0];
270        assert_eq!(*level, LogLevel::Info, "Expected log level to be Info");
271        assert!(class.is_some(), "Expected class name to be captured");
272        assert_eq!(message, "Test log message with real AVClass", "Expected log message to match");
273        log_callback_unset();
274    }
275
276    #[test]
277    fn test_log_callback_unset() {
278        let captured_logs = Arc::new(Mutex::new(Vec::new()));
279        let callback_logs = Arc::clone(&captured_logs);
280        log_callback_set(move |level, class, message| {
281            let mut logs = callback_logs.lock().unwrap();
282            logs.push((level, class, message));
283        });
284
285        // Safety: `av_log` is safe to call.
286        unsafe {
287            av_log(
288                std::ptr::null_mut(),
289                LogLevel::Info.0,
290                CString::new("Test log message before unset").unwrap().as_ptr(),
291            );
292        }
293
294        {
295            let logs = captured_logs.lock().unwrap();
296            assert_eq!(
297                logs.len(),
298                1,
299                "Expected one log message to be captured before unsetting the callback"
300            );
301            let (_, _, message) = &logs[0];
302            assert_eq!(message, "Test log message before unset", "Expected the log message to match");
303        }
304
305        log_callback_unset();
306
307        // Safety: `av_log` is safe to call.
308        unsafe {
309            av_log(
310                std::ptr::null_mut(),
311                LogLevel::Info.0,
312                CString::new("Test log message after unset").unwrap().as_ptr(),
313            );
314        }
315
316        let logs = captured_logs.lock().unwrap();
317        assert_eq!(
318            logs.len(),
319            1,
320            "Expected no additional log messages to be captured after unsetting the callback"
321        );
322    }
323
324    #[cfg(feature = "tracing")]
325    #[test]
326    #[tracing_test::traced_test]
327    fn test_log_callback_tracing() {
328        use tracing::subscriber::set_default;
329        use tracing::Level;
330        use tracing_subscriber::FmtSubscriber;
331
332        use crate::log::log_callback_tracing;
333
334        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
335        let _ = set_default(subscriber);
336        log_callback_tracing();
337
338        let levels_and_expected_tracing = [
339            (LogLevel::Trace, "trace"),
340            (LogLevel::Verbose, "trace"),
341            (LogLevel::Debug, "debug"),
342            (LogLevel::Info, "info"),
343            (LogLevel::Warning, "warning"),
344            (LogLevel::Quiet, "error"),
345            (LogLevel::Error, "error"),
346            (LogLevel::Panic, "error"),
347            (LogLevel::Fatal, "error"),
348        ];
349
350        for (level, expected_tracing_level) in &levels_and_expected_tracing {
351            let message = format!("Test {} log message", expected_tracing_level);
352            // Safety: `av_log` is safe to call.
353            unsafe {
354                av_log(
355                    std::ptr::null_mut(),
356                    level.0,
357                    CString::new(message.clone()).expect("Failed to create CString").as_ptr(),
358                );
359            }
360        }
361
362        for (_level, expected_tracing_level) in &levels_and_expected_tracing {
363            let expected_message = format!(
364                "{}: ffmpeg @ Test {} log message",
365                expected_tracing_level, expected_tracing_level
366            );
367
368            assert!(
369                logs_contain(&expected_message),
370                "Expected log message for '{}'",
371                expected_message
372            );
373        }
374        log_callback_unset();
375    }
376
377    #[cfg(feature = "tracing")]
378    #[test]
379    #[tracing_test::traced_test]
380    fn test_log_callback_tracing_deprecated_message() {
381        use tracing::subscriber::set_default;
382        use tracing::Level;
383        use tracing_subscriber::FmtSubscriber;
384
385        use crate::log::log_callback_tracing;
386
387        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
388        let _ = set_default(subscriber);
389        log_callback_tracing();
390
391        let deprecated_message = "deprecated pixel format used, make sure you did set range correctly";
392        // Safety: `av_log` is safe to call.
393        unsafe {
394            av_log(
395                std::ptr::null_mut(),
396                LogLevel::Trace.0,
397                CString::new(deprecated_message).expect("Failed to create CString").as_ptr(),
398            );
399        }
400
401        assert!(
402            logs_contain(&format!("debug: ffmpeg @ {}", deprecated_message)),
403            "Expected log message for '{}'",
404            deprecated_message
405        );
406        log_callback_unset();
407    }
408}