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: the `log_cb` function has the same structure as the required `AVLogCallback` function.
73    // The reason we do this transmute is because of the way `VaList` is defined on different architectures.
74    #[allow(clippy::missing_transmute_annotations)]
75    let log_cb_transmuted = unsafe { std::mem::transmute(log_cb as *const ()) };
76    // Safety: `av_log_set_callback` is safe to call.
77    unsafe {
78        av_log_set_callback(Some(log_cb_transmuted));
79    }
80}
81
82/// Unsets the log callback.
83pub fn log_callback_unset() {
84    LOG_CALLBACK.store(None);
85
86    // Safety: `av_log_set_callback` is safe to call.
87    unsafe {
88        av_log_set_callback(None);
89    }
90}
91
92unsafe extern "C" {
93    fn vsnprintf(
94        str: *mut libc::c_char,
95        size: libc::size_t,
96        format: *const libc::c_char,
97        ap: ::va_list::VaList,
98    ) -> libc::c_int;
99}
100
101unsafe extern "C" fn log_cb(ptr: *mut libc::c_void, level: libc::c_int, fmt: *const libc::c_char, va: ::va_list::VaList) {
102    let guard = LOG_CALLBACK.load();
103    let Some(cb) = guard.as_ref() else {
104        return;
105    };
106
107    let level = LogLevel::from(level);
108    let class = NonNull::new(ptr as *mut *mut AVClass)
109        .and_then(|class| {
110            // Safety: The pointer is valid
111            NonNull::new(unsafe { *class.as_ptr() })
112        })
113        .and_then(|class| {
114            // Safety: The pointer is valid
115            let class = unsafe { class.as_ref() };
116            let im = class.item_name?;
117            // Safety: The pointer is valid
118            let c_str = unsafe { im(ptr) };
119            // Safety: The returned pointer is a valid CString
120            let c_str = unsafe { CStr::from_ptr(c_str as *const _) };
121
122            Some(c_str.to_string_lossy().trim().to_owned())
123        });
124
125    let mut buf: [std::os::raw::c_char; 1024] = [0; 1024];
126
127    // Safety: The pointer is valid and the buffer has enough bytes with the max length set.
128    unsafe {
129        vsnprintf(buf.as_mut_ptr() as *mut _, buf.len() as _, fmt, va);
130    }
131
132    // Safety: The pointer is valid and the buffer has enough bytes with the max length set.
133    let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const _) };
134    let msg = c_str.to_string_lossy().trim().to_owned();
135
136    cb(level, class, msg);
137}
138
139/// Sets the log callback to use tracing.
140#[cfg(feature = "tracing")]
141#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
142pub fn log_callback_tracing() {
143    log_callback_set(|mut level, class, msg| {
144        let class = class.as_deref().unwrap_or("ffmpeg");
145
146        // We purposely ignore this message because it's a false positive
147        if msg == "deprecated pixel format used, make sure you did set range correctly" {
148            level = LogLevel::Debug;
149        }
150
151        match level {
152            LogLevel::Trace => tracing::trace!("{level}: {class} @ {msg}"),
153            LogLevel::Verbose => tracing::trace!("{level}: {class} @ {msg}"),
154            LogLevel::Debug => tracing::debug!("{level}: {class} @ {msg}"),
155            LogLevel::Info => tracing::info!("{level}: {class} @ {msg}"),
156            LogLevel::Warning => tracing::warn!("{level}: {class} @ {msg}"),
157            LogLevel::Quiet => tracing::error!("{level}: {class} @ {msg}"),
158            LogLevel::Error => tracing::error!("{level}: {class} @ {msg}"),
159            LogLevel::Panic => tracing::error!("{level}: {class} @ {msg}"),
160            LogLevel::Fatal => tracing::error!("{level}: {class} @ {msg}"),
161            LogLevel(_) => tracing::debug!("{level}: {class} @ {msg}"),
162        }
163    });
164}
165
166#[cfg(test)]
167#[cfg_attr(all(test, coverage_nightly), coverage(off))]
168mod tests {
169    use std::ffi::CString;
170    use std::sync::{Arc, Mutex};
171
172    use crate::AVCodecID;
173    use crate::ffi::{av_log, av_log_get_level, avcodec_find_decoder};
174    use crate::log::{LogLevel, log_callback_set, log_callback_unset, set_log_level};
175
176    #[test]
177    fn test_log_level_as_str_using_from_i32() {
178        let test_cases = [
179            (LogLevel::Quiet, "quiet"),
180            (LogLevel::Panic, "panic"),
181            (LogLevel::Fatal, "fatal"),
182            (LogLevel::Error, "error"),
183            (LogLevel::Warning, "warning"),
184            (LogLevel::Info, "info"),
185            (LogLevel::Verbose, "verbose"),
186            (LogLevel::Debug, "debug"),
187            (LogLevel::Trace, "trace"),
188            (LogLevel(100), "unknown(100)"),
189            (LogLevel(-1), "unknown(-1)"),
190        ];
191
192        for &(input, expected) in &test_cases {
193            let log_level = input;
194            assert_eq!(
195                log_level.to_string(),
196                expected,
197                "Expected '{expected}' for input {input}, but got '{log_level}'"
198            );
199        }
200    }
201
202    #[test]
203    fn test_set_log_level() {
204        let log_levels = [
205            LogLevel::Quiet,
206            LogLevel::Panic,
207            LogLevel::Fatal,
208            LogLevel::Error,
209            LogLevel::Warning,
210            LogLevel::Info,
211            LogLevel::Verbose,
212            LogLevel::Debug,
213            LogLevel::Trace,
214        ];
215
216        for &level in &log_levels {
217            set_log_level(level);
218            // Safety: `av_log_get_level` is safe to call.
219            let current_level = unsafe { av_log_get_level() };
220
221            assert_eq!(
222                current_level, level.0,
223                "Expected log level to be {}, but got {}",
224                level.0, current_level
225            );
226        }
227    }
228
229    #[test]
230    fn test_log_callback_set() {
231        let captured_logs = Arc::new(Mutex::new(Vec::new()));
232        let callback_logs = Arc::clone(&captured_logs);
233        log_callback_set(move |level, class, message| {
234            let mut logs = callback_logs.lock().unwrap();
235            logs.push((level, class, message));
236        });
237
238        let log_message = CString::new("Test warning log message").expect("Failed to create CString");
239        // Safety: `av_log` is safe to call.
240        unsafe {
241            av_log(std::ptr::null_mut(), LogLevel::Warning.0, log_message.as_ptr());
242        }
243
244        let logs = captured_logs.lock().unwrap();
245        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
246
247        let (level, class, message) = &logs[0];
248        assert_eq!(*level, LogLevel::Warning, "Expected log level to be Warning");
249        assert!(class.is_none(), "Expected class to be None for this test");
250        assert_eq!(message, "Test warning log message", "Expected log message to match");
251        log_callback_unset();
252    }
253
254    #[test]
255    fn test_log_callback_with_class() {
256        // Safety: `avcodec_find_decoder` is safe to call.
257        let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
258        assert!(!codec.is_null(), "Failed to find H264 codec");
259
260        // Safety: `(*codec).priv_class` is safe to access.
261        let av_class_ptr = unsafe { (*codec).priv_class };
262        assert!(!av_class_ptr.is_null(), "AVClass for codec is null");
263
264        let captured_logs = Arc::new(Mutex::new(Vec::new()));
265
266        let callback_logs = Arc::clone(&captured_logs);
267        log_callback_set(move |level, class, message| {
268            let mut logs = callback_logs.lock().unwrap();
269            logs.push((level, class, message));
270        });
271
272        // Safety: `av_log` is safe to call.
273        unsafe {
274            av_log(
275                &av_class_ptr as *const _ as *mut _,
276                LogLevel::Info.0,
277                CString::new("Test log message with real AVClass").unwrap().as_ptr(),
278            );
279        }
280
281        let logs = captured_logs.lock().unwrap();
282        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
283
284        let (level, class, message) = &logs[0];
285        assert_eq!(*level, LogLevel::Info, "Expected log level to be Info");
286        assert!(class.is_some(), "Expected class name to be captured");
287        assert_eq!(message, "Test log message with real AVClass", "Expected log message to match");
288        log_callback_unset();
289    }
290
291    #[test]
292    fn test_log_callback_unset() {
293        let captured_logs = Arc::new(Mutex::new(Vec::new()));
294        let callback_logs = Arc::clone(&captured_logs);
295        log_callback_set(move |level, class, message| {
296            let mut logs = callback_logs.lock().unwrap();
297            logs.push((level, class, message));
298        });
299
300        // Safety: `av_log` is safe to call.
301        unsafe {
302            av_log(
303                std::ptr::null_mut(),
304                LogLevel::Info.0,
305                CString::new("Test log message before unset").unwrap().as_ptr(),
306            );
307        }
308
309        {
310            let logs = captured_logs.lock().unwrap();
311            assert_eq!(
312                logs.len(),
313                1,
314                "Expected one log message to be captured before unsetting the callback"
315            );
316            let (_, _, message) = &logs[0];
317            assert_eq!(message, "Test log message before unset", "Expected the log message to match");
318        }
319
320        log_callback_unset();
321
322        // Safety: `av_log` is safe to call.
323        unsafe {
324            av_log(
325                std::ptr::null_mut(),
326                LogLevel::Info.0,
327                CString::new("Test log message after unset").unwrap().as_ptr(),
328            );
329        }
330
331        let logs = captured_logs.lock().unwrap();
332        assert_eq!(
333            logs.len(),
334            1,
335            "Expected no additional log messages to be captured after unsetting the callback"
336        );
337    }
338
339    #[cfg(feature = "tracing")]
340    #[test]
341    #[tracing_test::traced_test]
342    fn test_log_callback_tracing() {
343        use tracing::Level;
344        use tracing::subscriber::set_default;
345        use tracing_subscriber::FmtSubscriber;
346
347        use crate::log::log_callback_tracing;
348
349        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
350        // Intentional improper error handling to cause an error that we handle later in the test.
351        let _ = set_default(subscriber);
352        log_callback_tracing();
353
354        let levels_and_expected_tracing = [
355            (LogLevel::Trace, "trace"),
356            (LogLevel::Verbose, "trace"),
357            (LogLevel::Debug, "debug"),
358            (LogLevel::Info, "info"),
359            (LogLevel::Warning, "warning"),
360            (LogLevel::Quiet, "error"),
361            (LogLevel::Error, "error"),
362            (LogLevel::Panic, "error"),
363            (LogLevel::Fatal, "error"),
364        ];
365
366        for (level, expected_tracing_level) in &levels_and_expected_tracing {
367            let message = format!("Test {expected_tracing_level} log message");
368            // Safety: `av_log` is safe to call.
369            unsafe {
370                av_log(
371                    std::ptr::null_mut(),
372                    level.0,
373                    CString::new(message.clone()).expect("Failed to create CString").as_ptr(),
374                );
375            }
376        }
377
378        for (_level, expected_tracing_level) in &levels_and_expected_tracing {
379            let expected_message = format!("{expected_tracing_level}: ffmpeg @ Test {expected_tracing_level} log message");
380
381            assert!(
382                logs_contain(&expected_message),
383                "Expected log message for '{expected_message}'"
384            );
385        }
386        log_callback_unset();
387    }
388
389    #[cfg(feature = "tracing")]
390    #[test]
391    #[tracing_test::traced_test]
392    fn test_log_callback_tracing_deprecated_message() {
393        use tracing::Level;
394        use tracing::subscriber::set_default;
395        use tracing_subscriber::FmtSubscriber;
396
397        use crate::log::log_callback_tracing;
398
399        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
400        // Intentional improper error handling to cause an error that we handle later in the test.
401        let _ = set_default(subscriber);
402        log_callback_tracing();
403
404        let deprecated_message = "deprecated pixel format used, make sure you did set range correctly";
405        // Safety: `av_log` is safe to call.
406        unsafe {
407            av_log(
408                std::ptr::null_mut(),
409                LogLevel::Trace.0,
410                CString::new(deprecated_message).expect("Failed to create CString").as_ptr(),
411            );
412        }
413
414        assert!(
415            logs_contain(&format!("debug: ffmpeg @ {deprecated_message}")),
416            "Expected log message for '{deprecated_message}'"
417        );
418        log_callback_unset();
419    }
420}