Skip to content

Commit

Permalink
v2.0 docs & cleanup (#59)
Browse files Browse the repository at this point in the history
* ci: add cargo check without default features on mac m1
* refactor: prevent unused imports on all feature combinations
* refactor: organize imports so cargo test passes without default features
* docs: include and annotate optional features in cargo doc output
* refactor: clean up scripts
* docs: add missing rustdoc to most modules and structs
* docs: simplify hello world
* feat: re-export anyhow::Result
  • Loading branch information
nathanbabcock authored Nov 5, 2024
1 parent 31f93bd commit ba61aca
Show file tree
Hide file tree
Showing 24 changed files with 206 additions and 177 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/mac-m1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ jobs:
run: cargo run --example download_ffmpeg -- ../deps
- name: Build
run: cargo build --verbose
- name: Check without default features
run: cargo check --no-default-features
- name: Run tests
run: cargo test --all-targets --all-features --verbose -- --skip lib.rs
16 changes: 10 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ repository = "https://github.com/nathanbabcock/ffmpeg-sidecar"
readme = "README.md"
license = "MIT"

[lib]
crate-type = ["lib"]

[dependencies]
anyhow = "1.0.79"
ureq = { version = "2.10.1", optional = true }

[features]
default = ["download_ffmpeg"]
download_ffmpeg = ["dep:ureq", "dep:tar", "dep:xz2", "dep:zip"]
named_pipes = ["dep:winapi", "dep:nix"]

[lib]
crate-type = ["lib"]

[target.'cfg(target_os = "linux")'.dependencies]
tar = { version = "0.4.42", optional = true }
xz2 = { version = "0.1.7", optional = true }
Expand All @@ -38,6 +42,6 @@ nix = { version = "0.29.0", optional = true, features = [
"fs"
] }

[dependencies]
anyhow = "1.0.79"
ureq = { version = "2.10.1", optional = true }
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
71 changes: 26 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,49 +86,30 @@ of your client application.
Read raw video frames.

```rust
use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
use ffmpeg_sidecar::command::FfmpegCommand;

fn main() -> anyhow::Result<()> {
FfmpegCommand::new() // <- Builder API like `std::process::Command`
.testsrc() // <- Discoverable aliases for FFmpeg args
.rawvideo() // <- Convenient argument presets
.spawn()? // <- Uses an ordinary `std::process::Child`
.iter()? // <- Iterator over all log messages and video output
.for_each(|event: FfmpegEvent| {
match event {
FfmpegEvent::OutputFrame(frame) => {
println!("frame: {}x{}", frame.width, frame.height);
let _pixels: Vec<u8> = frame.data; // <- raw RGB pixels! 🎨
}
FfmpegEvent::Progress(progress) => {
eprintln!("Current speed: {}x", progress.speed); // <- parsed progress updates
}
FfmpegEvent::Log(_level, msg) => {
eprintln!("[ffmpeg] {}", msg); // <- granular log message from stderr
}
FfmpegEvent::ParsedInputStream(stream) => {
if let Some(video_data) = stream.video_data() {
println!(
"Found video stream with index {} in input {} that has fps {}, width {}px, height {}px.",
stream.stream_index,
stream.parent_index,
video_data.fps,
video_data.width,
video_data.height
);
}
}
_ => {}
}
});
// Run an FFmpeg command that generates a test video
let iter = FfmpegCommand::new() // <- Builder API like `std::process::Command`
.testsrc() // <- Discoverable aliases for FFmpeg args
.rawvideo() // <- Convenient argument presets
.spawn()? // <- Ordinary `std::process::Child`
.iter()?; // <- Blocking iterator over logs and output

// Use a regular "for" loop to read decoded video data
for frame in iter.filter_frames() {
println!("frame: {}x{}", frame.width, frame.height);
let _pixels: Vec<u8> = frame.data; // <- raw RGB pixels! 🎨
}

Ok(())
}
```

Source: [`/examples/hello_world.rs`](/examples/hello_world.rs)

```console
cargo run --example hello-world
cargo run --example hello_world
```

### H265 Transcoding
Expand All @@ -151,20 +132,20 @@ Source: [`/examples/ffplay_preview.rs`](/examples/ffplay_preview.rs)
cargo run --example ffplay_preview
```

### Others
### Named pipes

For a myriad of other examples, check any of the unit tests in
[/src/test.rs](/src/test.rs) in this repo.
Pipe multiple outputs from FFmpeg into a Rust program.

## Todo
Source: [`/examples/named_pipes.rs`](/examples/named_pipes.rs)

```console
cargo run --example named_pipes --features named_pipes
```

### Others

- [X] Add `/examples`
- [X] Take input from stdin, and pipe between iterators
- [X] Pipe directly to `ffplay` for debugging
- [X] Idiomatic error type instead of `Result<_, String>`
- [X] Handle indeterminate output formats like H264/H265
- Currently these formats are mutually exclusive with using `iter()` since
they require consuming `stdout` directly
For a myriad of other use cases, check any of the [examples](/examples/), as
well as the unit tests in [/src/test.rs](/src/test.rs).

## See also

Expand Down
32 changes: 20 additions & 12 deletions examples/download_ffmpeg.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use ffmpeg_sidecar::{
command::ffmpeg_is_installed,
download::{check_latest_version, download_ffmpeg_package, ffmpeg_download_url, unpack_ffmpeg},
paths::sidecar_dir,
version::ffmpeg_version_with_path,
};
use std::{
env::current_exe,
path::{Component, PathBuf},
};

#[cfg(feature = "download_ffmpeg")]
fn main() -> anyhow::Result<()> {
use ffmpeg_sidecar::{
command::ffmpeg_is_installed,
download::{check_latest_version, download_ffmpeg_package, ffmpeg_download_url, unpack_ffmpeg},
paths::sidecar_dir,
version::ffmpeg_version_with_path,
};
use std::env::current_exe;

if ffmpeg_is_installed() {
println!("FFmpeg is already installed! 🎉");
println!("For demo purposes, we'll re-download and unpack it anyway.");
Expand Down Expand Up @@ -56,7 +54,10 @@ fn main() -> anyhow::Result<()> {
Ok(())
}

fn resolve_relative_path(path_buf: PathBuf) -> PathBuf {
#[cfg(feature = "download_ffmpeg")]
fn resolve_relative_path(path_buf: std::path::PathBuf) -> std::path::PathBuf {
use std::path::{Component, PathBuf};

let mut components: Vec<PathBuf> = vec![];
for component in path_buf.as_path().components() {
match component {
Expand All @@ -72,3 +73,10 @@ fn resolve_relative_path(path_buf: PathBuf) -> PathBuf {
}
PathBuf::from_iter(components)
}

#[cfg(not(feature = "download_ffmpeg"))]
fn main() {
eprintln!(r#"This example requires the "download_ffmpeg" feature to be enabled."#);
println!("The feature is included by default unless manually disabled.");
println!("Please run `cargo run --example download_ffmpeg`.");
}
12 changes: 10 additions & 2 deletions examples/ffprobe.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use ffmpeg_sidecar::{download::auto_download, ffprobe::ffprobe_version};

#[cfg(feature = "download_ffmpeg")]
fn main() {
use ffmpeg_sidecar::{download::auto_download, ffprobe::ffprobe_version};

// Download ffprobe from a configured source.
// Note that not all distributions include ffprobe in their bundle.
auto_download().unwrap();
Expand All @@ -9,3 +10,10 @@ fn main() {
let version = ffprobe_version().unwrap();
println!("ffprobe version: {}", version);
}

#[cfg(not(feature = "download_ffmpeg"))]
fn main() {
eprintln!(r#"This example requires the "download_ffmpeg" feature to be enabled."#);
println!("The feature is included by default unless manually disabled.");
println!("Please run `cargo run --example download_ffmpeg`.");
}
45 changes: 13 additions & 32 deletions examples/hello_world.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
use ffmpeg_sidecar::command::FfmpegCommand;

/// Iterates over the frames of a `testsrc`.
///
/// ```console
/// cargo run --example hello_world
/// ```
fn main() -> anyhow::Result<()> {
FfmpegCommand::new() // <- Builder API like `std::process::Command`
.testsrc() // <- Discoverable aliases for FFmpeg args
// Run an FFmpeg command that generates a test video
let iter = FfmpegCommand::new() // <- Builder API like `std::process::Command`
.testsrc() // <- Discoverable aliases for FFmpeg args
.rawvideo() // <- Convenient argument presets
.spawn()? // <- Uses an ordinary `std::process::Child`
.iter()? // <- Iterator over all log messages and video output
.for_each(|event: FfmpegEvent| {
match event {
FfmpegEvent::OutputFrame(frame) => {
println!("frame: {}x{}", frame.width, frame.height);
let _pixels: Vec<u8> = frame.data; // <- raw RGB pixels! 🎨
}
FfmpegEvent::Progress(progress) => {
eprintln!("Current speed: {}x", progress.speed); // <- parsed progress updates
}
FfmpegEvent::Log(_level, msg) => {
eprintln!("[ffmpeg] {}", msg); // <- granular log message from stderr
}
FfmpegEvent::ParsedInputStream(stream) => {
if let Some(video_data) = stream.video_data() {
println!(
"Found video stream with index {} in input {} that has fps {}, width {}px, height {}px.",
stream.stream_index,
stream.parent_index,
video_data.fps,
video_data.width,
video_data.height
);
}
}
_ => {}
}
});
.spawn()? // <- Ordinary `std::process::Child`
.iter()?; // <- Blocking iterator over logs and output

// Use a regular "for" loop to read decoded video data
for frame in iter.filter_frames() {
println!("frame: {}x{}", frame.width, frame.height);
let _pixels: Vec<u8> = frame.data; // <- raw RGB pixels! 🎨
}

Ok(())
}
8 changes: 8 additions & 0 deletions scripts/cargo-doc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

# Source: <https://users.rust-lang.org/t/how-to-document-optional-features-in-api-docs/64577>

# First, install nightly toolchain if needed:
# rustup install nightly

RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --open
5 changes: 3 additions & 2 deletions scripts/cargo-test.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/bash

# Backtrace is on by default, and does not respect Cargo.toml
# [env] section, even with force = true.
export RUST_BACKTRACE=0
cargo test
RUST_BACKTRACE=0 cargo test
40 changes: 23 additions & 17 deletions scripts/ffmpeg.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
exit 1 # snippets only, not meant to be run together
#!/bin/bash

# testsrc
ffmpeg \
-v level+info \
-f lavfi \
-i testsrc \
-y output/test.mp4
# Each function body is intended to be copy-pasted directly into a terminal.

testsrc() {
ffmpeg \
-v level+info \
-f lavfi \
-i testsrc \
-y output/test.mp4
}

toDevNull() {
ffmpeg \
-v level+info \
-f lavfi \
-i testsrc \
-f rawvideo \
-pix_fmt rgb24 \
pipe > /dev/null
}

# to /dev/null
ffmpeg \
-v level+info \
-f lavfi \
-i testsrc \
-f rawvideo \
-pix_fmt rgb24 \
pipe > /dev/null
# to stdout: 'pipe', 'pipe:', 'pipe:1', '-'
# to stderr: 'pipe:2'

# pix_fmts
ffmpeg -hide_banner -pix_fmts
pix_fmts() {
ffmpeg -hide_banner -pix_fmts
}
8 changes: 3 additions & 5 deletions src/child.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Wrapper around `std::process::Child` containing a spawned FFmpeg command.
use std::{
io::{self, Write},
process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
Expand Down Expand Up @@ -68,11 +70,7 @@ impl FfmpegChild {
/// s Show QP histogram
/// ```
pub fn send_stdin_command(&mut self, command: &[u8]) -> anyhow::Result<()> {
let mut stdin = self
.inner
.stdin
.take()
.context("Missing child stdin")?;
let mut stdin = self.inner.stdin.take().context("Missing child stdin")?;
stdin.write_all(command)?;
self.inner.stdin.replace(stdin);
Ok(())
Expand Down
2 changes: 2 additions & 0 deletions src/comma_iter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! An internal utility used to parse comma-separated values in FFmpeg logs.
use std::str::Chars;

/// An iterator over comma-separated values, **ignoring commas inside parentheses**.
Expand Down
2 changes: 2 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Builder interface for FFmpeg commands.
use crate::{child::FfmpegChild, paths::ffmpeg_path};
use std::{
ffi::OsStr,
Expand Down
Loading

0 comments on commit ba61aca

Please sign in to comment.