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 unsafe {
74 av_log_set_callback(Some(log_cb));
75 }
76}
77
78pub fn log_callback_unset() {
80 LOG_CALLBACK.store(None);
81
82 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#[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 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 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 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 let codec = unsafe { avcodec_find_decoder(AVCodecID::H264.into()) };
243 assert!(!codec.is_null(), "Failed to find H264 codec");
244
245 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 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 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 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 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 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}