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")]
141pub fn log_callback_tracing() {
142    log_callback_set(|mut level, class, msg| {
143        let class = class.as_deref().unwrap_or("ffmpeg");
144
145        // We purposely ignore this message because it's a false positive
146        if msg == "deprecated pixel format used, make sure you did set range correctly" {
147            level = LogLevel::Debug;
148        }
149
150        match level {
151            LogLevel::Trace => tracing::trace!("{level}: {class} @ {msg}"),
152            LogLevel::Verbose => tracing::trace!("{level}: {class} @ {msg}"),
153            LogLevel::Debug => tracing::debug!("{level}: {class} @ {msg}"),
154            LogLevel::Info => tracing::info!("{level}: {class} @ {msg}"),
155            LogLevel::Warning => tracing::warn!("{level}: {class} @ {msg}"),
156            LogLevel::Quiet => tracing::error!("{level}: {class} @ {msg}"),
157            LogLevel::Error => tracing::error!("{level}: {class} @ {msg}"),
158            LogLevel::Panic => tracing::error!("{level}: {class} @ {msg}"),
159            LogLevel::Fatal => tracing::error!("{level}: {class} @ {msg}"),
160            LogLevel(_) => tracing::debug!("{level}: {class} @ {msg}"),
161        }
162    });
163}
164
165#[cfg(test)]
166#[cfg_attr(all(test, coverage_nightly), coverage(off))]
167mod tests {
168    use std::ffi::CString;
169    use std::sync::{Arc, Mutex};
170
171    use crate::AVCodecID;
172    use crate::ffi::{av_log, av_log_get_level, avcodec_find_decoder};
173    use crate::log::{LogLevel, log_callback_set, log_callback_unset, set_log_level};
174
175    #[test]
176    fn test_log_level_as_str_using_from_i32() {
177        let test_cases = [
178            (LogLevel::Quiet, "quiet"),
179            (LogLevel::Panic, "panic"),
180            (LogLevel::Fatal, "fatal"),
181            (LogLevel::Error, "error"),
182            (LogLevel::Warning, "warning"),
183            (LogLevel::Info, "info"),
184            (LogLevel::Verbose, "verbose"),
185            (LogLevel::Debug, "debug"),
186            (LogLevel::Trace, "trace"),
187            (LogLevel(100), "unknown(100)"),
188            (LogLevel(-1), "unknown(-1)"),
189        ];
190
191        for &(input, expected) in &test_cases {
192            let log_level = input;
193            assert_eq!(
194                log_level.to_string(),
195                expected,
196                "Expected '{expected}' for input {input}, but got '{log_level}'"
197            );
198        }
199    }
200
201    #[test]
202    fn test_set_log_level() {
203        let log_levels = [
204            LogLevel::Quiet,
205            LogLevel::Panic,
206            LogLevel::Fatal,
207            LogLevel::Error,
208            LogLevel::Warning,
209            LogLevel::Info,
210            LogLevel::Verbose,
211            LogLevel::Debug,
212            LogLevel::Trace,
213        ];
214
215        for &level in &log_levels {
216            set_log_level(level);
217            // Safety: `av_log_get_level` is safe to call.
218            let current_level = unsafe { av_log_get_level() };
219
220            assert_eq!(
221                current_level, level.0,
222                "Expected log level to be {}, but got {}",
223                level.0, current_level
224            );
225        }
226    }
227
228    #[test]
229    fn test_log_callback_set() {
230        let captured_logs = Arc::new(Mutex::new(Vec::new()));
231        let callback_logs = Arc::clone(&captured_logs);
232        log_callback_set(move |level, class, message| {
233            let mut logs = callback_logs.lock().unwrap();
234            logs.push((level, class, message));
235        });
236
237        let log_message = CString::new("Test warning log message").expect("Failed to create CString");
238        // Safety: `av_log` is safe to call.
239        unsafe {
240            av_log(std::ptr::null_mut(), LogLevel::Warning.0, log_message.as_ptr());
241        }
242
243        let logs = captured_logs.lock().unwrap();
244        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
245
246        let (level, class, message) = &logs[0];
247        assert_eq!(*level, LogLevel::Warning, "Expected log level to be Warning");
248        assert!(class.is_none(), "Expected class to be None for this test");
249        assert_eq!(message, "Test warning log message", "Expected log message to match");
250        log_callback_unset();
251    }
252
253    #[test]
254    fn test_log_callback_with_class() {
255        // Safety: `avcodec_find_decoder` is safe to call.
256        let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
257        assert!(!codec.is_null(), "Failed to find H264 codec");
258
259        // Safety: `(*codec).priv_class` is safe to access.
260        let av_class_ptr = unsafe { (*codec).priv_class };
261        assert!(!av_class_ptr.is_null(), "AVClass for codec is null");
262
263        let captured_logs = Arc::new(Mutex::new(Vec::new()));
264
265        let callback_logs = Arc::clone(&captured_logs);
266        log_callback_set(move |level, class, message| {
267            let mut logs = callback_logs.lock().unwrap();
268            logs.push((level, class, message));
269        });
270
271        // Safety: `av_log` is safe to call.
272        unsafe {
273            av_log(
274                &av_class_ptr as *const _ as *mut _,
275                LogLevel::Info.0,
276                CString::new("Test log message with real AVClass").unwrap().as_ptr(),
277            );
278        }
279
280        let logs = captured_logs.lock().unwrap();
281        assert_eq!(logs.len(), 1, "Expected one log message to be captured");
282
283        let (level, class, message) = &logs[0];
284        assert_eq!(*level, LogLevel::Info, "Expected log level to be Info");
285        assert!(class.is_some(), "Expected class name to be captured");
286        assert_eq!(message, "Test log message with real AVClass", "Expected log message to match");
287        log_callback_unset();
288    }
289
290    #[test]
291    fn test_log_callback_unset() {
292        let captured_logs = Arc::new(Mutex::new(Vec::new()));
293        let callback_logs = Arc::clone(&captured_logs);
294        log_callback_set(move |level, class, message| {
295            let mut logs = callback_logs.lock().unwrap();
296            logs.push((level, class, message));
297        });
298
299        // Safety: `av_log` is safe to call.
300        unsafe {
301            av_log(
302                std::ptr::null_mut(),
303                LogLevel::Info.0,
304                CString::new("Test log message before unset").unwrap().as_ptr(),
305            );
306        }
307
308        {
309            let logs = captured_logs.lock().unwrap();
310            assert_eq!(
311                logs.len(),
312                1,
313                "Expected one log message to be captured before unsetting the callback"
314            );
315            let (_, _, message) = &logs[0];
316            assert_eq!(message, "Test log message before unset", "Expected the log message to match");
317        }
318
319        log_callback_unset();
320
321        // Safety: `av_log` is safe to call.
322        unsafe {
323            av_log(
324                std::ptr::null_mut(),
325                LogLevel::Info.0,
326                CString::new("Test log message after unset").unwrap().as_ptr(),
327            );
328        }
329
330        let logs = captured_logs.lock().unwrap();
331        assert_eq!(
332            logs.len(),
333            1,
334            "Expected no additional log messages to be captured after unsetting the callback"
335        );
336    }
337
338    #[cfg(feature = "tracing")]
339    #[test]
340    #[tracing_test::traced_test]
341    fn test_log_callback_tracing() {
342        use tracing::Level;
343        use tracing::subscriber::set_default;
344        use tracing_subscriber::FmtSubscriber;
345
346        use crate::log::log_callback_tracing;
347
348        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
349        // Intentional improper error handling to cause an error that we handle later in the test.
350        let _ = set_default(subscriber);
351        log_callback_tracing();
352
353        let levels_and_expected_tracing = [
354            (LogLevel::Trace, "trace"),
355            (LogLevel::Verbose, "trace"),
356            (LogLevel::Debug, "debug"),
357            (LogLevel::Info, "info"),
358            (LogLevel::Warning, "warning"),
359            (LogLevel::Quiet, "error"),
360            (LogLevel::Error, "error"),
361            (LogLevel::Panic, "error"),
362            (LogLevel::Fatal, "error"),
363        ];
364
365        for (level, expected_tracing_level) in &levels_and_expected_tracing {
366            let message = format!("Test {expected_tracing_level} log message");
367            // Safety: `av_log` is safe to call.
368            unsafe {
369                av_log(
370                    std::ptr::null_mut(),
371                    level.0,
372                    CString::new(message.clone()).expect("Failed to create CString").as_ptr(),
373                );
374            }
375        }
376
377        for (_level, expected_tracing_level) in &levels_and_expected_tracing {
378            let expected_message = format!("{expected_tracing_level}: ffmpeg @ Test {expected_tracing_level} log message");
379
380            assert!(
381                logs_contain(&expected_message),
382                "Expected log message for '{expected_message}'"
383            );
384        }
385        log_callback_unset();
386    }
387
388    #[cfg(feature = "tracing")]
389    #[test]
390    #[tracing_test::traced_test]
391    fn test_log_callback_tracing_deprecated_message() {
392        use tracing::Level;
393        use tracing::subscriber::set_default;
394        use tracing_subscriber::FmtSubscriber;
395
396        use crate::log::log_callback_tracing;
397
398        let subscriber = FmtSubscriber::builder().with_max_level(Level::TRACE).finish();
399        // Intentional improper error handling to cause an error that we handle later in the test.
400        let _ = set_default(subscriber);
401        log_callback_tracing();
402
403        let deprecated_message = "deprecated pixel format used, make sure you did set range correctly";
404        // Safety: `av_log` is safe to call.
405        unsafe {
406            av_log(
407                std::ptr::null_mut(),
408                LogLevel::Trace.0,
409                CString::new(deprecated_message).expect("Failed to create CString").as_ptr(),
410            );
411        }
412
413        assert!(
414            logs_contain(&format!("debug: ffmpeg @ {deprecated_message}")),
415            "Expected log message for '{deprecated_message}'"
416        );
417        log_callback_unset();
418    }
419}