Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add audio and subtitle stream parsing #50

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,39 @@ Read raw video frames.
```rust
use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};

fn main() -> anyhow::Result<()> {
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! 🎨
.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
);
}
}
_ => {}
}
FfmpegEvent::Progress(progress) => {
eprintln!("Current speed: {}x", progress.speed); // <- parsed progress updates
}
FfmpegEvent::Log(_level, msg) => {
eprintln!("[ffmpeg] {}", msg); // <- granular log message from stderr
}
_ => {}
}
});
});
Ok(())
}
```
Expand Down
12 changes: 12 additions & 0 deletions examples/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ fn main() -> anyhow::Result<()> {
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
);
}
}
_ => {}
}
});
Expand Down
78 changes: 68 additions & 10 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ pub enum FfmpegEvent {
ParsedStreamMapping(String),
ParsedInput(FfmpegInput),
ParsedOutput(FfmpegOutput),
ParsedInputStream(AVStream),
ParsedOutputStream(AVStream),
ParsedInputStream(Stream),
ParsedOutputStream(Stream),
ParsedDuration(FfmpegDuration),
Log(LogLevel, String),
LogEOF,
Expand Down Expand Up @@ -59,12 +59,74 @@ impl FfmpegOutput {
}
}

/// Represents metadata about a stream.
#[derive(Debug, Clone, PartialEq)]
pub struct AVStream {
/// Typically `video` or `audio`, but might be something else like `data` or `subtitle`.
pub stream_type: String,
/// Corresponds to stream `-f` parameter, e.g. `rawvideo`, `h264`, or `mpegts`
pub struct Stream {
/// Corresponds to stream `-f` parameter, e.g. `rawvideo`, `h264`, `opus` or `srt`.
pub format: String,
// The language of the stream as a three letter code such as `eng`, `ger` or `jpn`.
pub language: String,
/// The index of the input or output that this stream belongs to.
pub parent_index: u32,
/// The index of the stream inside the input.
pub stream_index: u32,
/// The stderr line that this stream was parsed from.
pub raw_log_message: String,
// Data that is specific to a certain stream type.
pub type_specific_data: StreamTypeSpecificData,
}

impl Stream {
pub fn is_audio(&self) -> bool {
matches!(self.type_specific_data, StreamTypeSpecificData::Audio(_))
}
pub fn is_subtitle(&self) -> bool {
matches!(self.type_specific_data, StreamTypeSpecificData::Subtitle())
}
pub fn is_video(&self) -> bool {
matches!(self.type_specific_data, StreamTypeSpecificData::Video(_))
}
pub fn is_other(&self) -> bool {
matches!(self.type_specific_data, StreamTypeSpecificData::Other())
}

pub fn audio_data(&self) -> Option<&AudioStream> {
match &self.type_specific_data {
StreamTypeSpecificData::Audio(audio_stream) => Some(audio_stream),
_ => None,
}
}
pub fn video_data(&self) -> Option<&VideoStream> {
match &self.type_specific_data {
StreamTypeSpecificData::Video(video_stream) => Some(video_stream),
_ => None,
}
}
}

/// Represents metadata that is specific to a stream, e.g. fields that are only found in audio
/// streams or that are only found in video streams, etc. Storing this in an enum allows function to
/// accept the generic `Stream` type regardless of its actual type (audio, video, ...).
#[derive(Debug, Clone, PartialEq)]
pub enum StreamTypeSpecificData {
Audio(AudioStream),
Video(VideoStream),
Subtitle(),
Other(),
}

/// Represents metadata that is specific to audio streams.
#[derive(Debug, Clone, PartialEq)]
pub struct AudioStream {
/// The sample rate of the audio stream, e.g. 48000 (Hz)
pub sample_rate: u32,
/// The number of channels of the audio stream, e.g. `stereo`, `5.1` or `7.1`
pub channels: String,
}

/// Represents metadata that is specific to video streams.
#[derive(Debug, Clone, PartialEq)]
pub struct VideoStream {
/// Corresponds to stream `-pix_fmt` parameter, e.g. `rgb24`
pub pix_fmt: String,
/// Width in pixels
Expand All @@ -73,10 +135,6 @@ pub struct AVStream {
pub height: u32,
/// Framerate in frames per second
pub fps: f32,
/// The index of the input or output that this stream belongs to
pub parent_index: usize,
/// The stderr line that this stream was parsed from
pub raw_log_message: String,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
47 changes: 27 additions & 20 deletions src/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use anyhow::Context;

use crate::{
child::FfmpegChild,
event::{AVStream, FfmpegEvent, FfmpegOutput, FfmpegProgress, LogLevel, OutputVideoFrame},
event::{FfmpegEvent, FfmpegOutput, FfmpegProgress, LogLevel, OutputVideoFrame, Stream},
log_parser::FfmpegLogParser,
metadata::FfmpegMetadata,
pix_fmt::get_bytes_per_frame,
Expand Down Expand Up @@ -181,35 +181,40 @@ impl Iterator for FfmpegIterator {
pub fn spawn_stdout_thread(
stdout: ChildStdout,
tx: SyncSender<FfmpegEvent>,
output_streams: Vec<AVStream>,
output_streams: Vec<Stream>,
outputs: Vec<FfmpegOutput>,
) -> JoinHandle<()> {
std::thread::spawn(move || {
// Filter streams which are sent to stdout
let stdout_output_streams = output_streams.iter().filter(|stream| {
outputs
.get(stream.parent_index)
.map(|o| o.is_stdout())
.unwrap_or(false)
});
let stdout_output_video_streams = output_streams
.iter()
.filter(|stream| stream.is_video())
.filter(|stream| {
outputs
.get(stream.parent_index as usize)
.map(|o| o.is_stdout())
.unwrap_or(false)
});

// Error on mixing rawvideo and non-rawvideo streams
// TODO: Maybe just revert to chunk mode if this happens?
let any_rawvideo = stdout_output_streams
let any_rawvideo = stdout_output_video_streams
.clone()
.any(|s| s.format == "rawvideo");
let any_non_rawvideo = stdout_output_streams
let any_non_rawvideo = stdout_output_video_streams
.clone()
.any(|s| s.format != "rawvideo");
if any_rawvideo && any_non_rawvideo {
panic!("Cannot mix rawvideo and non-rawvideo streams");
}

// Prepare buffers
let mut buffers = stdout_output_streams
.map(|stream| {
let bytes_per_frame = get_bytes_per_frame(stream);
let buf_size = match stream.format.as_str() {
let mut buffers = stdout_output_video_streams
.map(|video_stream| {
// Since we filtered for video_streams above, we can unwrap unconditionally.
let video_data = video_stream.video_data().unwrap();
let bytes_per_frame = get_bytes_per_frame(&video_data);
let buf_size = match video_stream.format.as_str() {
"rawvideo" => bytes_per_frame.expect("Should use a known pix_fmt") as usize,

// Arbitrary default buffer size for receiving indeterminate chunks
Expand Down Expand Up @@ -239,21 +244,23 @@ pub fn spawn_stdout_thread(
let mut frame_num = 0;
loop {
let i = buffer_index.next().unwrap();
let stream = &output_streams[i];
let video_stream = &output_streams[i];
// Since we filtered for video_streams above, we can unwrap unconditionally.
let video_data = video_stream.video_data().unwrap();
let buffer = &mut buffers[i];
let output_frame_num = frame_num / num_buffers;
let timestamp = output_frame_num as f32 / stream.fps;
let timestamp = output_frame_num as f32 / video_data.fps;
frame_num += 1;

// Handle two scenarios:
match stream.format.as_str() {
match video_stream.format.as_str() {
// 1. `rawvideo` with exactly known pixel layout
"rawvideo" => match reader.read_exact(buffer.as_mut_slice()) {
Ok(_) => tx
.send(FfmpegEvent::OutputFrame(OutputVideoFrame {
width: stream.width,
height: stream.height,
pix_fmt: stream.pix_fmt.clone(),
width: video_data.width,
height: video_data.height,
pix_fmt: video_data.pix_fmt.clone(),
output_index: i as u32,
data: buffer.clone(),
frame_num: output_frame_num as u32,
Expand Down
110 changes: 61 additions & 49 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,61 @@
//! Wrap a standalone FFmpeg binary in an intuitive Iterator interface.
//!
//! ## Example
//!
//! ```rust
//! use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
//!
//! 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
//! }
//! _ => {}
//! }
//! });
//! Ok(())
//! }
//! ```
//!

#[cfg(test)]
mod test;

pub mod child;
pub mod comma_iter;
pub mod command;
pub mod download;
pub mod event;
pub mod ffprobe;
pub mod iter;
pub mod log_parser;
pub mod metadata;
pub mod paths;
pub mod pix_fmt;
pub mod read_until_any;
pub mod version;
//! Wrap a standalone FFmpeg binary in an intuitive Iterator interface.
//!
//! ## Example
//!
//! ```rust
//! use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
//!
//!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
//! );
//! }
//! }
//! _ => {}
//! }
//! });
//! Ok(())
//!}
//! ```
//!

#[cfg(test)]
mod test;

pub mod child;
pub mod comma_iter;
pub mod command;
pub mod download;
pub mod event;
pub mod ffprobe;
pub mod iter;
pub mod log_parser;
pub mod metadata;
pub mod paths;
pub mod pix_fmt;
pub mod read_until_any;
pub mod version;
Loading
Loading