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 pub enum LogLevel(i32) {
13 Quiet = AV_LOG_QUIET,
15 Panic = AV_LOG_PANIC as i32,
17 Fatal = AV_LOG_FATAL as i32,
19 Error = AV_LOG_ERROR as i32,
21 Warning = AV_LOG_WARNING as i32,
23 Info = AV_LOG_INFO as i32,
25 Verbose = AV_LOG_VERBOSE as i32,
27 Debug = AV_LOG_DEBUG as i32,
29 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
51pub fn set_log_level(level: LogLevel) {
53 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#[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
68pub fn log_callback_set_boxed(callback: Function) {
70 LOG_CALLBACK.store(Some(Arc::new(callback)));
71
72 #[allow(clippy::missing_transmute_annotations)]
75 let log_cb_transmuted = unsafe { std::mem::transmute(log_cb as *const ()) };
76 unsafe {
78 av_log_set_callback(Some(log_cb_transmuted));
79 }
80}
81
82pub fn log_callback_unset() {
84 LOG_CALLBACK.store(None);
85
86 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 NonNull::new(unsafe { *class.as_ptr() })
112 })
113 .and_then(|class| {
114 let class = unsafe { class.as_ref() };
116 let im = class.item_name?;
117 let c_str = unsafe { im(ptr) };
119 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 unsafe {
129 vsnprintf(buf.as_mut_ptr() as *mut _, buf.len() as _, fmt, va);
130 }
131
132 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#[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 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 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 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 let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
258 assert!(!codec.is_null(), "Failed to find H264 codec");
259
260 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 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 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 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 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 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 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 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}