Skip to main content

falcon_ffmpeg/
ffi.rs

1//! The hand-written FFmpeg surface falcon uses, and the loader that opens
2//! the shared libraries at run time.
3//!
4//! Only the functions in [`Api`] are called. Structs are declared as far as
5//! the last field falcon reads, laid out as FFmpeg 7.1 declares them
6//! (libavutil 59, libavcodec 61, libavformat 61, libswscale 8,
7//! libswresample 5, libavfilter 10). The loader refuses libraries of any other major
8//! version, since a struct prefix is only right for the version it was
9//! copied from, and refuses any build whose licence is not the LGPL. The
10//! `layout` test checks every offset against the installed headers.
11
12use std::ffi::{CStr, c_char, c_double, c_int, c_uint, c_void};
13use std::path::{Path, PathBuf};
14use std::sync::OnceLock;
15
16/// Major versions of the libraries the structs below describe.
17pub const AVUTIL_MAJOR: u32 = 59;
18pub const AVCODEC_MAJOR: u32 = 61;
19pub const AVFORMAT_MAJOR: u32 = 61;
20pub const SWSCALE_MAJOR: u32 = 8;
21pub const SWRESAMPLE_MAJOR: u32 = 5;
22pub const AVFILTER_MAJOR: u32 = 10;
23
24pub const AVMEDIA_TYPE_VIDEO: c_int = 0;
25pub const AVMEDIA_TYPE_AUDIO: c_int = 1;
26pub const AV_PIX_FMT_RGBA: c_int = 26;
27pub const AV_SAMPLE_FMT_FLT: c_int = 3;
28pub const AV_PKT_DATA_DISPLAYMATRIX: c_int = 5;
29pub const AV_NOPTS_VALUE: i64 = i64::MIN;
30pub const AV_TIME_BASE: i64 = 1_000_000;
31pub const AVSEEK_FLAG_BACKWARD: c_int = 1;
32pub const SWS_BILINEAR: c_int = 2;
33pub const SWS_AREA: c_int = 0x20;
34pub const AV_LOG_QUIET: c_int = -8;
35/// `AVERROR(EAGAIN)`: EAGAIN is 35 on macOS and 11 on Linux and in Windows's C runtimes.
36#[cfg(target_os = "macos")]
37pub const AVERROR_EAGAIN: c_int = -35;
38#[cfg(not(target_os = "macos"))]
39pub const AVERROR_EAGAIN: c_int = -11;
40/// `AVERROR_EOF`, `-MKTAG('E','O','F',' ')`.
41pub const AVERROR_EOF: c_int = -0x2046_4F45;
42/// `AVERROR_INVALIDDATA`, `-MKTAG('I','N','D','A')`.
43pub const AVERROR_INVALIDDATA: c_int = -0x4144_4E49;
44
45#[repr(C)]
46#[derive(Clone, Copy, Debug, Default, PartialEq)]
47pub struct AVRational {
48    pub num: c_int,
49    pub den: c_int,
50}
51
52impl AVRational {
53    /// The value as a float, or `None` for 0/x, x/0 and negatives.
54    pub fn value(self) -> Option<f64> {
55        (self.num > 0 && self.den > 0).then(|| f64::from(self.num) / f64::from(self.den))
56    }
57}
58
59#[repr(C)]
60pub struct AVChannelLayout {
61    pub order: c_int,
62    pub nb_channels: c_int,
63    pub u: u64,
64    pub opaque: *mut c_void,
65}
66
67impl Default for AVChannelLayout {
68    fn default() -> Self {
69        AVChannelLayout { order: 0, nb_channels: 0, u: 0, opaque: std::ptr::null_mut() }
70    }
71}
72
73/// The start of `AVFormatContext`, up to `duration`.
74#[repr(C)]
75pub struct AVFormatContext {
76    pub av_class: *const c_void,
77    pub iformat: *const c_void,
78    pub oformat: *const c_void,
79    pub priv_data: *mut c_void,
80    pub pb: *mut c_void,
81    pub ctx_flags: c_int,
82    pub nb_streams: c_uint,
83    pub streams: *mut *mut AVStream,
84    pub nb_stream_groups: c_uint,
85    pub stream_groups: *mut c_void,
86    pub nb_chapters: c_uint,
87    pub chapters: *mut c_void,
88    pub url: *mut c_char,
89    pub start_time: i64,
90    pub duration: i64,
91}
92
93/// The start of `AVStream`, up to `avg_frame_rate`.
94#[repr(C)]
95pub struct AVStream {
96    pub av_class: *const c_void,
97    pub index: c_int,
98    pub id: c_int,
99    pub codecpar: *mut AVCodecParameters,
100    pub priv_data: *mut c_void,
101    pub time_base: AVRational,
102    pub start_time: i64,
103    pub duration: i64,
104    pub nb_frames: i64,
105    pub disposition: c_int,
106    pub discard: c_int,
107    pub sample_aspect_ratio: AVRational,
108    pub metadata: *mut c_void,
109    pub avg_frame_rate: AVRational,
110}
111
112/// The start of `AVCodecParameters`, up to `framerate`.
113#[repr(C)]
114pub struct AVCodecParameters {
115    pub codec_type: c_int,
116    pub codec_id: c_int,
117    pub codec_tag: u32,
118    pub extradata: *mut u8,
119    pub extradata_size: c_int,
120    pub coded_side_data: *mut AVPacketSideData,
121    pub nb_coded_side_data: c_int,
122    pub format: c_int,
123    pub bit_rate: i64,
124    pub bits_per_coded_sample: c_int,
125    pub bits_per_raw_sample: c_int,
126    pub profile: c_int,
127    pub level: c_int,
128    pub width: c_int,
129    pub height: c_int,
130    pub sample_aspect_ratio: AVRational,
131    pub framerate: AVRational,
132}
133
134#[repr(C)]
135pub struct AVPacketSideData {
136    pub data: *mut u8,
137    pub size: usize,
138    pub kind: c_int,
139}
140
141/// The start of `AVPacket`, up to `stream_index`.
142#[repr(C)]
143pub struct AVPacket {
144    pub buf: *mut c_void,
145    pub pts: i64,
146    pub dts: i64,
147    pub data: *mut u8,
148    pub size: c_int,
149    pub stream_index: c_int,
150}
151
152/// `AVFrame` up to `ch_layout` (FFmpeg 7.1 still has the deprecated
153/// fields `key_frame`, `interlaced_frame`, `top_field_first`,
154/// `palette_has_changed`, `pkt_pos` and `pkt_size`).
155#[repr(C)]
156pub struct AVFrame {
157    pub data: [*mut u8; 8],
158    pub linesize: [c_int; 8],
159    pub extended_data: *mut *mut u8,
160    pub width: c_int,
161    pub height: c_int,
162    pub nb_samples: c_int,
163    pub format: c_int,
164    pub key_frame: c_int,
165    pub pict_type: c_int,
166    pub sample_aspect_ratio: AVRational,
167    pub pts: i64,
168    pub pkt_dts: i64,
169    pub time_base: AVRational,
170    pub quality: c_int,
171    pub opaque: *mut c_void,
172    pub repeat_pict: c_int,
173    pub interlaced_frame: c_int,
174    pub top_field_first: c_int,
175    pub palette_has_changed: c_int,
176    pub sample_rate: c_int,
177    pub buf: [*mut c_void; 8],
178    pub extended_buf: *mut *mut c_void,
179    pub nb_extended_buf: c_int,
180    pub side_data: *mut *mut c_void,
181    pub nb_side_data: c_int,
182    pub flags: c_int,
183    pub color_range: c_int,
184    pub color_primaries: c_int,
185    pub color_trc: c_int,
186    pub colorspace: c_int,
187    pub chroma_location: c_int,
188    pub best_effort_timestamp: i64,
189    pub pkt_pos: i64,
190    pub metadata: *mut c_void,
191    pub decode_error_flags: c_int,
192    pub pkt_size: c_int,
193    pub hw_frames_ctx: *mut c_void,
194    pub opaque_ref: *mut c_void,
195    pub crop_top: usize,
196    pub crop_bottom: usize,
197    pub crop_left: usize,
198    pub crop_right: usize,
199    pub private_ref: *mut c_void,
200    pub ch_layout: AVChannelLayout,
201}
202
203/// Opaque types, only ever behind pointers.
204#[repr(C)]
205pub struct AVCodec {
206    _private: [u8; 0],
207}
208#[repr(C)]
209pub struct AVCodecContext {
210    _private: [u8; 0],
211}
212#[repr(C)]
213pub struct SwsContext {
214    _private: [u8; 0],
215}
216#[repr(C)]
217pub struct SwrContext {
218    _private: [u8; 0],
219}
220#[repr(C)]
221pub struct AVFilter {
222    _private: [u8; 0],
223}
224#[repr(C)]
225pub struct AVFilterContext {
226    _private: [u8; 0],
227}
228#[repr(C)]
229pub struct AVFilterGraph {
230    _private: [u8; 0],
231}
232
233/// Every FFmpeg function falcon calls, resolved from the loaded libraries.
234/// The libraries stay loaded for the life of the process.
235pub struct Api {
236    pub version: String,
237    pub dir: PathBuf,
238    // libavutil
239    pub av_frame_alloc: unsafe extern "C" fn() -> *mut AVFrame,
240    pub av_frame_free: unsafe extern "C" fn(*mut *mut AVFrame),
241    pub av_frame_unref: unsafe extern "C" fn(*mut AVFrame),
242    pub av_frame_ref: unsafe extern "C" fn(*mut AVFrame, *const AVFrame) -> c_int,
243    pub av_frame_get_buffer: unsafe extern "C" fn(*mut AVFrame, c_int) -> c_int,
244    pub av_log_set_level: unsafe extern "C" fn(c_int),
245    pub av_channel_layout_default: unsafe extern "C" fn(*mut AVChannelLayout, c_int),
246    pub av_channel_layout_uninit: unsafe extern "C" fn(*mut AVChannelLayout),
247    pub av_opt_set: unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char, c_int) -> c_int,
248    pub av_display_rotation_get: unsafe extern "C" fn(*const i32) -> c_double,
249    pub av_strerror: unsafe extern "C" fn(c_int, *mut c_char, usize) -> c_int,
250    // libavcodec
251    pub avcodec_alloc_context3: unsafe extern "C" fn(*const AVCodec) -> *mut AVCodecContext,
252    pub avcodec_free_context: unsafe extern "C" fn(*mut *mut AVCodecContext),
253    pub avcodec_parameters_to_context: unsafe extern "C" fn(*mut AVCodecContext, *const AVCodecParameters) -> c_int,
254    pub avcodec_open2: unsafe extern "C" fn(*mut AVCodecContext, *const AVCodec, *mut *mut c_void) -> c_int,
255    pub avcodec_send_packet: unsafe extern "C" fn(*mut AVCodecContext, *const AVPacket) -> c_int,
256    pub avcodec_receive_frame: unsafe extern "C" fn(*mut AVCodecContext, *mut AVFrame) -> c_int,
257    pub avcodec_flush_buffers: unsafe extern "C" fn(*mut AVCodecContext),
258    pub avcodec_find_decoder_by_name: unsafe extern "C" fn(*const c_char) -> *const AVCodec,
259    pub av_packet_alloc: unsafe extern "C" fn() -> *mut AVPacket,
260    pub av_packet_free: unsafe extern "C" fn(*mut *mut AVPacket),
261    pub av_packet_unref: unsafe extern "C" fn(*mut AVPacket),
262    pub av_packet_side_data_get: unsafe extern "C" fn(*const AVPacketSideData, c_int, c_int) -> *const AVPacketSideData,
263    // libavformat
264    pub avformat_open_input: unsafe extern "C" fn(*mut *mut AVFormatContext, *const c_char, *const c_void, *mut *mut c_void) -> c_int,
265    pub avformat_find_stream_info: unsafe extern "C" fn(*mut AVFormatContext, *mut *mut c_void) -> c_int,
266    pub av_find_best_stream: unsafe extern "C" fn(*mut AVFormatContext, c_int, c_int, c_int, *mut *const AVCodec, c_int) -> c_int,
267    pub av_read_frame: unsafe extern "C" fn(*mut AVFormatContext, *mut AVPacket) -> c_int,
268    pub av_seek_frame: unsafe extern "C" fn(*mut AVFormatContext, c_int, i64, c_int) -> c_int,
269    pub avformat_close_input: unsafe extern "C" fn(*mut *mut AVFormatContext),
270    // libswscale
271    pub sws_get_cached_context: unsafe extern "C" fn(
272        *mut SwsContext,
273        c_int,
274        c_int,
275        c_int,
276        c_int,
277        c_int,
278        c_int,
279        c_int,
280        *mut c_void,
281        *mut c_void,
282        *const c_double,
283    ) -> *mut SwsContext,
284    pub sws_scale: unsafe extern "C" fn(*mut SwsContext, *const *const u8, *const c_int, c_int, c_int, *const *mut u8, *const c_int) -> c_int,
285    pub sws_free_context: unsafe extern "C" fn(*mut SwsContext),
286    // libswresample
287    pub swr_alloc_set_opts2: unsafe extern "C" fn(
288        *mut *mut SwrContext,
289        *const AVChannelLayout,
290        c_int,
291        c_int,
292        *const AVChannelLayout,
293        c_int,
294        c_int,
295        c_int,
296        *mut c_void,
297    ) -> c_int,
298    pub swr_init: unsafe extern "C" fn(*mut SwrContext) -> c_int,
299    pub swr_convert: unsafe extern "C" fn(*mut SwrContext, *const *mut u8, c_int, *const *const u8, c_int) -> c_int,
300    pub swr_get_out_samples: unsafe extern "C" fn(*mut SwrContext, c_int) -> c_int,
301    pub swr_free: unsafe extern "C" fn(*mut *mut SwrContext),
302    // libavfilter
303    pub avfilter_graph_alloc: unsafe extern "C" fn() -> *mut AVFilterGraph,
304    pub avfilter_graph_free: unsafe extern "C" fn(*mut *mut AVFilterGraph),
305    pub avfilter_get_by_name: unsafe extern "C" fn(*const c_char) -> *const AVFilter,
306    pub avfilter_graph_create_filter:
307        unsafe extern "C" fn(*mut *mut AVFilterContext, *const AVFilter, *const c_char, *const c_char, *mut c_void, *mut AVFilterGraph) -> c_int,
308    pub avfilter_link: unsafe extern "C" fn(*mut AVFilterContext, c_uint, *mut AVFilterContext, c_uint) -> c_int,
309    pub avfilter_graph_config: unsafe extern "C" fn(*mut AVFilterGraph, *mut c_void) -> c_int,
310    pub av_buffersrc_add_frame: unsafe extern "C" fn(*mut AVFilterContext, *mut AVFrame) -> c_int,
311    pub av_buffersink_get_frame: unsafe extern "C" fn(*mut AVFilterContext, *mut AVFrame) -> c_int,
312    /// Keeps the libraries loaded while the pointers above are in use.
313    _libs: Vec<libloading::Library>,
314}
315
316/// Why FFmpeg could not be used.
317#[derive(Clone, Debug, PartialEq)]
318pub enum LoadError {
319    /// No FFmpeg libraries were found in any of the places searched.
320    Missing(Vec<PathBuf>),
321    /// Libraries were found but can't be used (wrong version or licence,
322    /// a missing symbol, a library that would not load).
323    Unusable(String),
324}
325
326impl std::fmt::Display for LoadError {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        match self {
329            LoadError::Missing(dirs) => {
330                let dirs: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
331                write!(f, "FFmpeg libraries not found (looked in {})", dirs.join(", "))
332            }
333            LoadError::Unusable(why) => write!(f, "FFmpeg can't be used: {why}"),
334        }
335    }
336}
337
338/// The file name of library `name` with major version `major`.
339fn file_name(name: &str, major: u32) -> String {
340    if cfg!(target_os = "macos") {
341        format!("lib{name}.{major}.dylib")
342    } else if cfg!(windows) {
343        format!("{name}-{major}.dll")
344    } else {
345        format!("lib{name}.so.{major}")
346    }
347}
348
349/// Where the libraries are looked for, in order: `$FALCON_FFMPEG_DIR`,
350/// the app bundle's `Contents/Frameworks` next to the executable (Linux
351/// packages: `lib/falcon` beside `bin`; Windows: the executable's own folder, where the installer
352/// puts them, then `lib` in it, where the portable zip does), and the workspace's `target/ffmpeg/lib` that `scripts/build-ffmpeg.sh` fills.
353pub fn search_dirs() -> Vec<PathBuf> {
354    let mut dirs = Vec::new();
355    if let Some(d) = std::env::var_os("FALCON_FFMPEG_DIR") {
356        dirs.push(PathBuf::from(d));
357    }
358    if let Some(exe_dir) = std::env::current_exe().ok().and_then(|e| e.parent().map(Path::to_path_buf)) {
359        if cfg!(target_os = "macos") {
360            dirs.push(exe_dir.join("../Frameworks"));
361        } else if cfg!(windows) {
362            dirs.push(exe_dir.clone());
363            dirs.push(exe_dir.join("lib"));
364        } else {
365            dirs.push(exe_dir.join("../lib/falcon"));
366        }
367    }
368    dirs.push(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/ffmpeg/lib"));
369    dirs
370}
371
372static API: OnceLock<Result<Api, LoadError>> = OnceLock::new();
373
374/// The loaded FFmpeg, opened on first use.
375pub fn api() -> Result<&'static Api, LoadError> {
376    API.get_or_init(|| {
377        let dirs = search_dirs();
378        let Some(dir) = dirs.iter().find(|d| d.join(file_name("avformat", AVFORMAT_MAJOR)).is_file()) else {
379            return Err(LoadError::Missing(dirs));
380        };
381        load(dir)
382    })
383    .as_ref()
384    .map_err(Clone::clone)
385}
386
387fn unusable(msg: impl Into<String>) -> LoadError {
388    LoadError::Unusable(msg.into())
389}
390
391/// Opens the library at `path`, its links to the others resolved beside it.
392#[cfg(not(windows))]
393fn open_library(path: &Path) -> Result<libloading::Library, libloading::Error> {
394    // SAFETY: loading runs the library's initialisers; FFmpeg's do
395    // nothing beyond setting up its own tables.
396    unsafe { libloading::Library::new(path) }
397}
398
399/// Opens the DLL at `path`. Its own imports (avcodec's libdav1d, say) are
400/// looked for only in its folder and in System32: Windows would otherwise
401/// also try the executable's folder, the current directory and `PATH`, where
402/// a same-named DLL could be picked up.
403#[cfg(windows)]
404fn open_library(path: &Path) -> Result<libloading::Library, libloading::Error> {
405    use libloading::os::windows::{LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32, Library};
406    // The flag that searches the DLL's folder needs an absolute path.
407    let path = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
408    // SAFETY: as off Windows.
409    unsafe { Library::load_with_flags(path, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) }.map(Into::into)
410}
411
412/// Opens the six libraries in `dir` and resolves every function.
413fn load(dir: &Path) -> Result<Api, LoadError> {
414    let open = |name: &str, major: u32| -> Result<libloading::Library, LoadError> {
415        let path = dir.join(file_name(name, major));
416        open_library(&path).map_err(|e| unusable(format!("{}: {e}", path.display())))
417    };
418    // Dependencies first, so each library finds the ones it links.
419    let avutil = open("avutil", AVUTIL_MAJOR)?;
420    let swresample = open("swresample", SWRESAMPLE_MAJOR)?;
421    let avfilter = open("avfilter", AVFILTER_MAJOR)?;
422    let avcodec = open("avcodec", AVCODEC_MAJOR)?;
423    let avformat = open("avformat", AVFORMAT_MAJOR)?;
424    let swscale = open("swscale", SWSCALE_MAJOR)?;
425
426    /// Resolves `name` in `lib` as the function pointer type expected.
427    fn sym<T: Copy>(lib: &libloading::Library, name: &str) -> Result<T, LoadError> {
428        let cname = format!("{name}\0");
429        // SAFETY: each call site names the type FFmpeg 7.1 declares for the
430        // symbol, and the major-version checks below reject other ABIs.
431        unsafe { lib.get::<T>(cname.as_bytes()).map(|s| *s) }.map_err(|e| unusable(format!("{name}: {e}")))
432    }
433
434    type Version = unsafe extern "C" fn() -> c_uint;
435    type License = unsafe extern "C" fn() -> *const c_char;
436    let checks = [
437        (&avutil, "avutil", AVUTIL_MAJOR),
438        (&avcodec, "avcodec", AVCODEC_MAJOR),
439        (&avformat, "avformat", AVFORMAT_MAJOR),
440        (&swscale, "swscale", SWSCALE_MAJOR),
441        (&swresample, "swresample", SWRESAMPLE_MAJOR),
442        (&avfilter, "avfilter", AVFILTER_MAJOR),
443    ];
444    for (lib, name, major) in checks {
445        let version: Version = sym(lib, &format!("{name}_version"))?;
446        let license: License = sym(lib, &format!("{name}_license"))?;
447        // SAFETY: both take nothing and return a constant.
448        let (v, l) = unsafe { (version(), license()) };
449        if v >> 16 != major {
450            return Err(unusable(format!("lib{name} is version {}, falcon needs {major}", v >> 16)));
451        }
452        // SAFETY: FFmpeg returns a static NUL-terminated string.
453        let l = if l.is_null() { String::new() } else { unsafe { CStr::from_ptr(l) }.to_string_lossy().into_owned() };
454        if !license_ok(&l) {
455            return Err(unusable(format!("lib{name} is licensed \"{l}\"; falcon only loads an LGPL build")));
456        }
457    }
458    let info: License = sym(&avutil, "av_version_info")?;
459    // SAFETY: as for the licence string.
460    let version = unsafe { CStr::from_ptr(info()) }.to_string_lossy().into_owned();
461
462    let api = Api {
463        version,
464        dir: dir.to_path_buf(),
465        av_frame_alloc: sym(&avutil, "av_frame_alloc")?,
466        av_frame_free: sym(&avutil, "av_frame_free")?,
467        av_frame_unref: sym(&avutil, "av_frame_unref")?,
468        av_frame_ref: sym(&avutil, "av_frame_ref")?,
469        av_frame_get_buffer: sym(&avutil, "av_frame_get_buffer")?,
470        av_log_set_level: sym(&avutil, "av_log_set_level")?,
471        av_channel_layout_default: sym(&avutil, "av_channel_layout_default")?,
472        av_channel_layout_uninit: sym(&avutil, "av_channel_layout_uninit")?,
473        av_opt_set: sym(&avutil, "av_opt_set")?,
474        av_display_rotation_get: sym(&avutil, "av_display_rotation_get")?,
475        av_strerror: sym(&avutil, "av_strerror")?,
476        avcodec_alloc_context3: sym(&avcodec, "avcodec_alloc_context3")?,
477        avcodec_free_context: sym(&avcodec, "avcodec_free_context")?,
478        avcodec_parameters_to_context: sym(&avcodec, "avcodec_parameters_to_context")?,
479        avcodec_open2: sym(&avcodec, "avcodec_open2")?,
480        avcodec_send_packet: sym(&avcodec, "avcodec_send_packet")?,
481        avcodec_receive_frame: sym(&avcodec, "avcodec_receive_frame")?,
482        avcodec_flush_buffers: sym(&avcodec, "avcodec_flush_buffers")?,
483        avcodec_find_decoder_by_name: sym(&avcodec, "avcodec_find_decoder_by_name")?,
484        av_packet_alloc: sym(&avcodec, "av_packet_alloc")?,
485        av_packet_free: sym(&avcodec, "av_packet_free")?,
486        av_packet_unref: sym(&avcodec, "av_packet_unref")?,
487        av_packet_side_data_get: sym(&avcodec, "av_packet_side_data_get")?,
488        avformat_open_input: sym(&avformat, "avformat_open_input")?,
489        avformat_find_stream_info: sym(&avformat, "avformat_find_stream_info")?,
490        av_find_best_stream: sym(&avformat, "av_find_best_stream")?,
491        av_read_frame: sym(&avformat, "av_read_frame")?,
492        av_seek_frame: sym(&avformat, "av_seek_frame")?,
493        avformat_close_input: sym(&avformat, "avformat_close_input")?,
494        sws_get_cached_context: sym(&swscale, "sws_getCachedContext")?,
495        sws_scale: sym(&swscale, "sws_scale")?,
496        sws_free_context: sym(&swscale, "sws_freeContext")?,
497        swr_alloc_set_opts2: sym(&swresample, "swr_alloc_set_opts2")?,
498        swr_init: sym(&swresample, "swr_init")?,
499        swr_convert: sym(&swresample, "swr_convert")?,
500        swr_get_out_samples: sym(&swresample, "swr_get_out_samples")?,
501        swr_free: sym(&swresample, "swr_free")?,
502        avfilter_graph_alloc: sym(&avfilter, "avfilter_graph_alloc")?,
503        avfilter_graph_free: sym(&avfilter, "avfilter_graph_free")?,
504        avfilter_get_by_name: sym(&avfilter, "avfilter_get_by_name")?,
505        avfilter_graph_create_filter: sym(&avfilter, "avfilter_graph_create_filter")?,
506        avfilter_link: sym(&avfilter, "avfilter_link")?,
507        avfilter_graph_config: sym(&avfilter, "avfilter_graph_config")?,
508        av_buffersrc_add_frame: sym(&avfilter, "av_buffersrc_add_frame")?,
509        av_buffersink_get_frame: sym(&avfilter, "av_buffersink_get_frame")?,
510        _libs: vec![swscale, avformat, avcodec, avfilter, swresample, avutil],
511    };
512    // SAFETY: plain value; FFmpeg's log goes to stderr otherwise.
513    unsafe { (api.av_log_set_level)(AV_LOG_QUIET) };
514    Ok(api)
515}
516
517/// Whether the loaded FFmpeg has the decoder called `name` (FFmpeg's
518/// short name, such as `libdav1d` for AV1).
519pub fn has_decoder(api: &Api, name: &str) -> bool {
520    let Ok(name) = std::ffi::CString::new(name) else { return false };
521    // SAFETY: a NUL-terminated name; FFmpeg returns a static codec or null.
522    !unsafe { (api.avcodec_find_decoder_by_name)(name.as_ptr()) }.is_null()
523}
524
525/// Whether a library's licence string is one falcon may load: the LGPL,
526/// never the GPL (`--enable-gpl`) or a nonfree build.
527pub fn license_ok(license: &str) -> bool {
528    let l = license.to_ascii_lowercase();
529    l.starts_with("lgpl") && !l.contains("nonfree")
530}
531
532/// FFmpeg's message for an error code.
533pub fn error_text(api: &Api, code: c_int) -> String {
534    let mut buf = [0 as c_char; 128];
535    // SAFETY: the buffer is writable for its whole length and FFmpeg
536    // NUL-terminates what it writes.
537    unsafe {
538        (api.av_strerror)(code, buf.as_mut_ptr(), buf.len());
539        CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned()
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn only_lgpl_builds_are_accepted() {
549        assert!(license_ok("LGPL version 2.1 or later"));
550        assert!(license_ok("LGPL version 3 or later"));
551        assert!(!license_ok("GPL version 2 or later"));
552        assert!(!license_ok("GPL version 3 or later"));
553        assert!(!license_ok("nonfree and unredistributable"));
554        assert!(!license_ok(""));
555    }
556
557    #[cfg(windows)]
558    #[test]
559    fn windows_looks_beside_the_exe_then_in_its_lib_folder() {
560        let exe_dir = std::env::current_exe().unwrap().parent().unwrap().to_path_buf();
561        let dirs = search_dirs();
562        let beside = dirs.iter().position(|d| *d == exe_dir).unwrap();
563        assert_eq!(dirs[beside + 1], exe_dir.join("lib"));
564    }
565
566    #[test]
567    fn library_names_carry_the_major_version() {
568        let n = file_name("avformat", 61);
569        assert!(n == "libavformat.61.dylib" || n == "libavformat.so.61" || n == "avformat-61.dll");
570    }
571
572    #[test]
573    fn error_codes_match_ffmpeg() {
574        // MKTAG('E','O','F',' ') = 'E' | 'O' << 8 | 'F' << 16 | ' ' << 24.
575        let tag = i32::from(b'E') | i32::from(b'O') << 8 | i32::from(b'F') << 16 | i32::from(b' ') << 24;
576        assert_eq!(AVERROR_EOF, -tag);
577    }
578
579    #[test]
580    fn rationals() {
581        assert_eq!(AVRational { num: 30000, den: 1001 }.value().map(|v| (v * 1000.0).round()), Some(29970.0));
582        assert_eq!(AVRational { num: 0, den: 1 }.value(), None);
583        assert_eq!(AVRational { num: 1, den: 0 }.value(), None);
584    }
585}