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")]
141pub fn log_callback_tracing() {
142 log_callback_set(|mut level, class, msg| {
143 let class = class.as_deref().unwrap_or("ffmpeg");
144
145 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 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 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 let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
257 assert!(!codec.is_null(), "Failed to find H264 codec");
258
259 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 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 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 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 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 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 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 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}