1use std::ffi::{CStr, c_char, c_double, c_int, c_uint, c_void};
13use std::path::{Path, PathBuf};
14use std::sync::OnceLock;
15
16pub 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#[cfg(target_os = "macos")]
37pub const AVERROR_EAGAIN: c_int = -35;
38#[cfg(not(target_os = "macos"))]
39pub const AVERROR_EAGAIN: c_int = -11;
40pub const AVERROR_EOF: c_int = -0x2046_4F45;
42pub 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 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#[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#[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#[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#[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#[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#[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
233pub struct Api {
236 pub version: String,
237 pub dir: PathBuf,
238 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 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 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 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 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 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 _libs: Vec<libloading::Library>,
314}
315
316#[derive(Clone, Debug, PartialEq)]
318pub enum LoadError {
319 Missing(Vec<PathBuf>),
321 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
338fn 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
349pub 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
374pub 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#[cfg(not(windows))]
393fn open_library(path: &Path) -> Result<libloading::Library, libloading::Error> {
394 unsafe { libloading::Library::new(path) }
397}
398
399#[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 let path = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
408 unsafe { Library::load_with_flags(path, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) }.map(Into::into)
410}
411
412fn 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 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 fn sym<T: Copy>(lib: &libloading::Library, name: &str) -> Result<T, LoadError> {
428 let cname = format!("{name}\0");
429 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 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 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 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 unsafe { (api.av_log_set_level)(AV_LOG_QUIET) };
514 Ok(api)
515}
516
517pub fn has_decoder(api: &Api, name: &str) -> bool {
520 let Ok(name) = std::ffi::CString::new(name) else { return false };
521 !unsafe { (api.avcodec_find_decoder_by_name)(name.as_ptr()) }.is_null()
523}
524
525pub fn license_ok(license: &str) -> bool {
528 let l = license.to_ascii_lowercase();
529 l.starts_with("lgpl") && !l.contains("nonfree")
530}
531
532pub fn error_text(api: &Api, code: c_int) -> String {
534 let mut buf = [0 as c_char; 128];
535 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 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}