moonfire_ffmpeg/
lib.rs

1// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use libc::{c_char, c_int};
5use log::info;
6use parking_lot::Once;
7use std::convert::TryFrom;
8use std::ffi::CStr;
9use std::fmt::{self, Write};
10
11static START: Once = Once::new();
12
13pub mod avcodec;
14pub mod avformat;
15pub mod avutil;
16#[cfg(feature = "swscale")]
17pub mod swscale;
18
19pub use avutil::Error;
20
21type RustLogCallback = extern "C" fn(
22    avc_item_name: *const c_char,
23    avc: *const libc::c_void,
24    level: libc::c_int,
25    fmt: *const c_char,
26    vl: *mut libc::c_void,
27);
28
29//#[link(name = "wrapper")]
30extern "C" {
31    static moonfire_ffmpeg_version: *const libc::c_char;
32
33    fn moonfire_ffmpeg_init(cb: RustLogCallback);
34
35    fn moonfire_ffmpeg_vsnprintf(
36        buf: *mut u8,
37        size: usize,
38        fmt: *const c_char,
39        vl: *mut libc::c_void,
40    ) -> c_int;
41}
42
43pub struct Ffmpeg {}
44
45#[derive(Copy, Clone)]
46struct Version(libc::c_int);
47
48impl Version {
49    fn major(self) -> libc::c_int {
50        (self.0 >> 16) & 0xFF
51    }
52    fn minor(self) -> libc::c_int {
53        (self.0 >> 8) & 0xFF
54    }
55}
56
57impl fmt::Display for Version {
58    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59        write!(
60            f,
61            "{}.{}.{}",
62            (self.0 >> 16) & 0xFF,
63            (self.0 >> 8) & 0xFF,
64            self.0 & 0xFF
65        )
66    }
67}
68
69struct Library {
70    name: &'static str,
71    compiled: Version,
72    running: Version,
73    configuration: &'static CStr,
74}
75
76impl Library {
77    fn new(
78        name: &'static str,
79        compiled: libc::c_int,
80        running: libc::c_int,
81        configuration: &'static CStr,
82    ) -> Self {
83        Library {
84            name,
85            compiled: Version(compiled),
86            running: Version(running),
87            configuration,
88        }
89    }
90
91    fn is_compatible(&self) -> bool {
92        self.running.major() == self.compiled.major()
93            && self.running.minor() >= self.compiled.minor()
94    }
95}
96
97impl fmt::Display for Library {
98    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99        // Write in the same order as ffmpeg's PRINT_LIB_INFO to reduce confusion:
100        // compiled, then running, then configuration.
101        write!(
102            f,
103            "{}: compiled={} running={} configuration={:?}",
104            self.name, self.compiled, self.running, self.configuration
105        )
106    }
107}
108
109// Thread-local buffer for av_log.
110//
111// ffmpeg's av_log calls aren't actually 1-1 with log messages. When it calls
112// it without a trailing newline, it's building up a log message for later.
113// ffmpeg doesn't use a thread-local buffer, so if two threads' messages overlap,
114// it will produce weird results. But we might as well do this properly.
115//
116// There's one other behavior difference: ffmpeg uses the info from the first
117// call of the message, where we use the last one. This avoids having to do
118// an extra allocation. It should be the same result.
119thread_local! {
120    static LOG_BUF: std::cell::RefCell<Vec<u8>> = std::cell::RefCell::new(Vec::with_capacity(1024));
121}
122
123/// Appends the given `fmt` and `vl` to `buf` using `vsnprintf`.
124unsafe fn append_vprintf(buf: &mut Vec<u8>, fmt: *const libc::c_char, vl: *mut libc::c_void) {
125    let len = buf.len();
126    let left = buf.capacity() - len;
127    let ret = moonfire_ffmpeg_vsnprintf(buf.as_mut_ptr().add(len), left, fmt, vl);
128    let ret = match usize::try_from(ret) {
129        Ok(r) => r,
130        Err(_) => {
131            buf.extend(b"(vsnprintf failed)");
132            return;
133        }
134    };
135    if ret >= left {
136        // Buffer is too small to put in the contents (with the trailing NUL,
137        // which vsnprintf insists on). Now we know the correct size.
138        buf.reserve(ret + 1);
139        let ret2 = moonfire_ffmpeg_vsnprintf(buf.as_mut_ptr().add(len), ret + 1, fmt, vl);
140        assert_eq!(
141            usize::try_from(ret2).expect("2nd vsnprintf should succeed"),
142            ret
143        );
144    }
145    buf.set_len(len + ret);
146}
147
148/// Log callback which sends `av_log_default_callback`-like payloads into the
149/// log crate, turning ffmpeg's `avc_item_name` into a module path and ffmpeg's
150/// levels into log crate levels.
151extern "C" fn log_callback(
152    avc_item_name: *const c_char,
153    avc: *const libc::c_void,
154    level: libc::c_int,
155    fmt: *const c_char,
156    vl: *mut libc::c_void,
157) {
158    let log_level = avutil::convert_level(level);
159
160    // Fast path so trace calls don't allocate when trace isn't enabled anywhere.
161    if log::max_level()
162        .to_level()
163        .map(|l| l < log_level)
164        .unwrap_or(true)
165    {
166        return;
167    }
168    let avc_item_name = if avc_item_name.is_null() {
169        "null"
170    } else {
171        unsafe { CStr::from_ptr(avc_item_name) }
172            .to_str()
173            .unwrap_or("bad_utf8")
174    };
175    let target = format!("moonfire_ffmpeg::{}", avc_item_name);
176    let metadata = log::Metadata::builder()
177        .level(avutil::convert_level(level))
178        .target(&target)
179        .build();
180    let logger = log::logger();
181    if !logger.enabled(&metadata) {
182        return;
183    }
184
185    LOG_BUF.with(move |b| {
186        unsafe { log_callback_inner(&mut *b.borrow_mut(), logger, metadata, avc, fmt, vl) };
187    });
188}
189
190// Portion of log_callback_int that needs the thread-local data.
191unsafe fn log_callback_inner(
192    buf: &mut Vec<u8>,
193    logger: &dyn log::Log,
194    metadata: log::Metadata,
195    avc: *const libc::c_void,
196    fmt: *const c_char,
197    vl: *mut libc::c_void,
198) {
199    append_vprintf(buf, fmt, vl);
200
201    if !buf
202        .last()
203        .map(|&b| b == b'\r' || b == b'\n')
204        .unwrap_or(false)
205    {
206        return; // save for next time.
207    }
208
209    // ffmpeg log lines apparently sometimes have these low-ASCII control characters.
210    // av_log_default_callback "sanitizes" them; match its behavior.
211    for c in buf.iter_mut() {
212        if *c < 0x08 || (*c > 0x0D && *c < 0x20) {
213            *c = b'?';
214        }
215    }
216
217    let s = String::from_utf8_lossy(&buf[0..buf.len() - 1]);
218    let target = metadata.target();
219    logger.log(
220        &log::RecordBuilder::new()
221            .args(format_args!("{:?}: {}", avc, &s))
222            .metadata(metadata)
223            .module_path(Some(target))
224            .build(),
225    );
226    buf.clear();
227}
228
229impl Ffmpeg {
230    pub fn new() -> Ffmpeg {
231        START.call_once(|| unsafe {
232            // Initialize the lock and log callbacks before printing the libraries, because
233            // avutil_version() sometimes calls av_log().
234            moonfire_ffmpeg_init(log_callback);
235
236            let libs = &[
237                Library::new(
238                    "avutil",
239                    avutil::moonfire_ffmpeg_compiled_libavutil_version,
240                    avutil::avutil_version(),
241                    CStr::from_ptr(avutil::avutil_configuration()),
242                ),
243                Library::new(
244                    "avcodec",
245                    avcodec::moonfire_ffmpeg_compiled_libavcodec_version,
246                    avcodec::avcodec_version(),
247                    CStr::from_ptr(avcodec::avcodec_configuration()),
248                ),
249                Library::new(
250                    "avformat",
251                    avformat::moonfire_ffmpeg_compiled_libavformat_version,
252                    avformat::avformat_version(),
253                    CStr::from_ptr(avformat::avformat_configuration()),
254                ),
255                #[cfg(feature = "swscale")]
256                Library::new(
257                    "swscale",
258                    swscale::moonfire_ffmpeg_compiled_libswscale_version,
259                    swscale::swscale_version(),
260                    CStr::from_ptr(swscale::swscale_configuration()),
261                ),
262            ];
263            let mut msg = format!(
264                "\ncompiled={:?} running={:?}",
265                CStr::from_ptr(moonfire_ffmpeg_version),
266                CStr::from_ptr(avutil::av_version_info())
267            );
268            let mut compatible = true;
269            for l in libs {
270                write!(&mut msg, "\n{}", l).unwrap();
271                if !l.is_compatible() {
272                    compatible = false;
273                    msg.push_str(" <- not ABI-compatible!");
274                }
275            }
276            if !compatible {
277                panic!("Incompatible ffmpeg versions:{}", msg);
278            }
279            avformat::av_register_all();
280            if avformat::avformat_network_init() < 0 {
281                panic!("avformat_network_init failed");
282            }
283            info!("Initialized ffmpeg. Versions:{}", msg);
284        });
285        Ffmpeg {}
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use crate::avutil;
292    use cstr::*;
293    use parking_lot::Mutex;
294
295    /// Just tests that this doesn't crash with an ABI compatibility error.
296    #[test]
297    fn test_init() {
298        super::Ffmpeg::new();
299    }
300
301    #[test]
302    fn test_is_compatible() {
303        // compiled major/minor/patch, running major/minor/patch, expected compatible
304        use ::libc::c_int;
305        struct Test(c_int, c_int, c_int, c_int, c_int, c_int, bool);
306
307        let tests = &[
308            Test(55, 1, 2, 55, 1, 2, true),  // same version, compatible
309            Test(55, 1, 2, 55, 2, 1, true),  // newer minor version, compatible
310            Test(55, 1, 3, 55, 1, 2, true),  // older patch version, compatible (but weird)
311            Test(55, 2, 2, 55, 1, 2, false), // older minor version, incompatible
312            Test(55, 1, 2, 56, 1, 2, false), // newer major version, incompatible
313            Test(56, 1, 2, 55, 1, 2, false), // older major version, incompatible
314        ];
315
316        for t in tests {
317            let l = super::Library::new(
318                "avutil",
319                (t.0 << 16) | (t.1 << 8) | t.2,
320                (t.3 << 16) | (t.4 << 8) | t.5,
321                cstr!(""),
322            );
323            assert!(l.is_compatible() == t.6, "{} expected={}", l, t.6);
324        }
325    }
326
327    struct DummyLogger(Mutex<Vec<String>>);
328
329    impl log::Log for DummyLogger {
330        fn enabled(&self, _metadata: &log::Metadata) -> bool {
331            true
332        }
333        fn log(&self, record: &log::Record) {
334            let mut l = self.0.lock();
335            l.push(format!(
336                "{}: {}: {}",
337                record.level(),
338                record.target(),
339                record.args()
340            ));
341        }
342        fn flush(&self) {}
343    }
344
345    #[test]
346    fn test_logging() {
347        super::Ffmpeg::new();
348        let logger = Box::leak(Box::new(DummyLogger(Mutex::new(Vec::new()))));
349        log::set_logger(logger).unwrap();
350        log::set_max_level(log::LevelFilter::Trace);
351        unsafe {
352            avutil::av_log(
353                std::ptr::null(),
354                avutil::AV_LOG_INFO,
355                cstr!("foo %d\n").as_ptr(),
356                42 as i32,
357            );
358            avutil::av_log(
359                std::ptr::null(),
360                avutil::AV_LOG_INFO,
361                cstr!("partial ").as_ptr(),
362                1,
363            );
364            avutil::av_log(
365                std::ptr::null(),
366                avutil::AV_LOG_INFO,
367                cstr!("log\n").as_ptr(),
368                1,
369            );
370            avutil::av_log(
371                std::ptr::null(),
372                avutil::AV_LOG_INFO,
373                cstr!("bar\n").as_ptr(),
374            );
375        };
376        let l = logger.0.lock();
377        println!("{:?}", &l[..]);
378        assert_eq!(l.len(), 3);
379        assert_eq!(&l[0][..], "INFO: moonfire_ffmpeg::null: 0x0: foo 42");
380        assert_eq!(&l[1][..], "INFO: moonfire_ffmpeg::null: 0x0: partial log");
381        assert_eq!(&l[2][..], "INFO: moonfire_ffmpeg::null: 0x0: bar");
382    }
383}