simple_ffmpeg/lib.rs
1#![warn(missing_docs)]
2
3//! # Simple zero-dependency single-file crate for generating videos with ffmpeg in Rust
4//! This crate is meant to be extremely light-weight. If you need a feature this crate doesn't provide,
5//! go use something else.
6//!
7//! In fact, this crate can even be used without `cargo`. Just download `lib.rs` and add it to your source tree
8//! as a module.
9//!
10//! ## Basic Usage
11//! ```rust
12//! use simple_ffmpeg as ffmpeg;
13//!
14//! let mut ffmpeg = ffmpeg::start("out.mp4", WIDTH, HEIGHT, FPS)?;
15//!
16//! let mut pixels = [0u32; WIDTH * HEIGHT]
17//! for _ in 0..(DURATION * FPS) {
18//! // <draw frame into pixels array>
19//!
20//! ffmpeg.send_frame(&pixels)?;
21//! }
22//!
23//! ffmpeg.finalize()?;
24//! ```
25
26use std::error;
27use std::result;
28use std::fmt;
29use std::process::{Command, Child, Stdio, ExitStatus};
30use std::io::Write;
31use std::ffi::OsStr;
32
33/// Representation of a single pixel
34///
35/// This library assumes you store colors as RGBA32. Note that you have to keep byte order in mind when creating
36/// colors. So green with alpha 0 would be 0x0000FF00 on a little-endian machine and 0x00FF0000 on a big-endian machine.
37/// To solve this, a function called [`get_color`] is provided
38pub type Color = u32;
39
40/// Turn separate R, G, B, and A values into a single RGBA [`Color`]
41///
42/// This works regardless of endianness
43pub fn get_color(r: u8, g: u8, b: u8, a: u8) -> Color {
44 Color::from_be_bytes([r, g, b, a])
45}
46
47/// Main error type
48///
49/// This error is returned from every function in this crate that can fail (which is most of them)
50#[derive(Debug)]
51pub enum Error {
52 /// IO Error
53 IOError(std::io::Error),
54 /// FFMpeg exited with non-zero code
55 FFMpegExitedAbnormally(ExitStatus),
56}
57
58impl fmt::Display for Error {
59 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60 match self {
61 Error::FFMpegExitedAbnormally(code) => if let Some(code) = code.code() {
62 write!(f, "ffmpeg exited abnormally with code {code}")
63 } else {
64 write!(f, "ffmpeg exited abnormally")
65 },
66 Error::IOError(e) => write!(f, "io error: {e}"),
67 }
68 }
69}
70
71impl error::Error for Error {}
72
73impl From<std::io::Error> for Error {
74 fn from(e: std::io::Error) -> Error { Error::IOError(e) }
75}
76
77/// Main Result type
78///
79/// This Result is returned from every function in this crate that can fail (which is most of them)
80pub type Result<T> = result::Result<T, Error>;
81
82/// Main interface into FFMPEG
83///
84/// This struct holds a child ffmpeg process that you can send frames into. Remember to call [`FFMpeg::finalize`] when you're done.
85/// Dropping this struct basically calls `finalize` anyway but it ignores errors, so it's better to call `finalize` explicitly
86pub struct FFMpeg {
87 child: Child,
88 width: usize,
89 height: usize,
90 fps: u32,
91}
92
93/// Start the FFMPEG rendering
94///
95/// Alias for [`FFMpeg::start`]
96pub fn start(out_file: impl AsRef<OsStr>, width: usize, height: usize, fps: u32) -> Result<FFMpeg> {
97 FFMpeg::start(out_file, width, height, fps)
98}
99
100impl FFMpeg {
101 /// Start the FFMPEG rendering
102 ///
103 /// Starts the FFMPEG rendering.
104 pub fn start(out_file: impl AsRef<OsStr>, width: usize, height: usize, fps: u32) -> Result<FFMpeg> {
105 let child = Command::new("ffmpeg")
106 .args(["-loglevel", "verbose", "-y"])
107 // Input file options
108 .args(["-f", "rawvideo"])
109 .args(["-pix_fmt", "rgba"])
110 .args(["-s", &format!("{width}x{height}")])
111 .args(["-r", &format!("{fps}")])
112 .args(["-i", "-"])
113 // Output file options
114 .arg(out_file)
115 .stdin(Stdio::piped())
116 .spawn()?;
117
118 Ok(FFMpeg { child, width, height, fps })
119 }
120
121 /// Get the render width
122 pub fn width(&self) -> usize { self.width }
123
124 /// Get the render height
125 pub fn height(&self) -> usize { self.height }
126
127 /// Get the render FPS
128 pub fn fps(&self) -> u32 { self.fps }
129
130 /// Get the render resolution
131 pub fn resolution(&self) -> (usize, usize) { (self.width, self.height) }
132
133 /// Send a frame to the FFMPEG process
134 ///
135 /// Send a frame to the FFMPEG process. `pixels.len()` must be equal to `ffmpeg.width() * ffmpeg.height()`
136 pub fn send_frame(&mut self, pixels: &[Color]) -> Result<()> {
137 assert_eq!(pixels.len(), self.width * self.height);
138
139 let stdin = self.child.stdin.as_mut().expect("we set stdin to piped");
140
141 let pixels_u8: &[u8] = unsafe {
142 let ptr = pixels.as_ptr();
143 let len = pixels.len();
144
145 use std::mem::size_of;
146 std::slice::from_raw_parts(ptr as *const u8, len * (size_of::<Color>() / size_of::<u8>()))
147 };
148 stdin.write_all(pixels_u8)?;
149
150 Ok(())
151 }
152
153 /// Finalize the FFMPEG rendering
154 ///
155 /// If this method isn't called directly or indirectly (such as if `std::mem::forget` is called on `FFMpeg`),
156 /// the final video may not be complete
157 pub fn finalize(mut self) -> Result<()> {
158 let retcode = self.child.wait()?;
159 if !retcode.success() {
160 return Err(Error::FFMpegExitedAbnormally(retcode));
161 }
162 Ok(())
163 }
164}
165
166impl std::ops::Drop for FFMpeg {
167 fn drop(&mut self) {
168 _ = self.child.wait();
169 }
170}