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}