Skip to content

Commit

Permalink
Revise decode_audio, decode_vidio example
Browse files Browse the repository at this point in the history
* Rename `AVCodecParserContext::find` to `AVCodecParserContext::init`

* Rename `is_planar`to `sample_fmt_is_planar`
  • Loading branch information
ldm0 committed Jan 7, 2024
1 parent d728f53 commit 21fc8ea
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 61 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ anyhow = "1.0.57"
cstr = "0.2.11"
once_cell = "1.12.0"
tempdir = "0.3.7"
camino = "1.1.6"

[features]
# linking system ffmpeg as fallback.
Expand Down
6 changes: 4 additions & 2 deletions src/avcodec/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ wrap!(AVCodecParserContext: ffi::AVCodecParserContext);

impl AVCodecParserContext {
/// Allocate a [`AVCodecParserContext`] with given [`AVCodecID`].
pub fn find(codec_id: AVCodecID) -> Option<Self> {
pub fn init(codec_id: AVCodecID) -> Option<Self> {
// On Windows enum is i32, On *nix enum is u32.
// ref: https://github.com/rust-lang/rust-bindgen/issues/1361
#[cfg(not(windows))]
Expand All @@ -26,6 +26,8 @@ impl AVCodecParserContext {
/// Return `Err(_)` On failure, `bool` field of returned tuple means if
/// packet is ready, `usize` field of returned tuple means the offset of the
/// data being parsed.
///
/// Note: if `data.len()` exceeds [`i32::MAX`], this function returns [`RsmpegError::TryFromIntError`].
pub fn parse_packet(
&mut self,
codec_context: &mut AVCodecContext,
Expand All @@ -41,7 +43,7 @@ impl AVCodecParserContext {
&mut packet_data,
&mut packet_size,
data.as_ptr(),
data.len() as i32,
data.len().try_into()?,
packet.pts,
packet.dts,
packet.pos,
Expand Down
2 changes: 1 addition & 1 deletion src/avutil/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub unsafe fn opt_set_q(
Ok(())
}

/// Note: if `val.len()` extends [`i32::MAX`], this function returns [`RsmpegError::TryFromIntError`].
/// Note: if `val.len()` exceeds [`i32::MAX`], this function returns [`RsmpegError::TryFromIntError`].
///
/// # Safety
///
Expand Down
4 changes: 2 additions & 2 deletions src/avutil/samplefmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub fn get_bytes_per_sample(sample_fmt: AVSampleFormat) -> Option<usize> {
/// Check if the sample format is planar.
///
/// Returns 1 if the sample format is planar, 0 if it is interleaved
pub fn is_planar(sample_fmt: AVSampleFormat) -> bool {
pub fn sample_fmt_is_planar(sample_fmt: AVSampleFormat) -> bool {
unsafe { ffi::av_sample_fmt_is_planar(sample_fmt) == 1 }
}

Expand Down Expand Up @@ -185,7 +185,7 @@ impl AVSamples {
AVSamples::get_buffer_size(nb_channels, nb_samples, sample_fmt, align)?;
let linear = vec![0u8; buffer_size as usize];

let nb_planes = if is_planar(sample_fmt) {
let nb_planes = if sample_fmt_is_planar(sample_fmt) {
nb_channels
} else {
1
Expand Down
78 changes: 47 additions & 31 deletions tests/decode_audio.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use rsmpeg::{
avcodec::{AVCodecContext, AVCodecParserContext, AVPacket},
avformat::AVFormatContextInput,
avutil::{
get_bytes_per_sample, get_packed_sample_fmt, get_sample_fmt_name, is_planar, AVFrame,
AVSampleFormat,
get_bytes_per_sample, get_packed_sample_fmt, get_sample_fmt_name, sample_fmt_is_planar,
AVFrame, AVSampleFormat,
},
error::RsmpegError,
ffi,
};
use std::ffi::CString;
use rusty_ffmpeg::ffi::AV_INPUT_BUFFER_PADDING_SIZE;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use std::slice::from_raw_parts;
use std::{ffi::CString, io::Read};

fn get_format_from_sample_fmt(sample_fmt: AVSampleFormat) -> Option<&'static str> {
let sample_fmt_entries = [
Expand Down Expand Up @@ -50,7 +51,8 @@ fn decode(
.send_packet(packet)
.context("Send packet failed.")?;
let channels = decode_context
.channels
.ch_layout
.nb_channels
.try_into()
.context("channels overflow")?;
let sample_fmt = decode_context.sample_fmt;
Expand All @@ -67,6 +69,8 @@ fn decode(
}

fn decode_audio(audio_path: &str, out_file_path: &str) -> Result<()> {
const AUDIO_INBUF_SIZE: usize = 20480;

let (decoder, mut decode_context) = {
// safety, &str ensures no internal null bytes.
let audio_path = CString::new(audio_path).unwrap();
Expand All @@ -86,47 +90,59 @@ fn decode_audio(audio_path: &str, out_file_path: &str) -> Result<()> {
.codecpar(),
)
.context("Apply codecpar failed.")?;
decode_context
.open(None)
.context("Open codec context failed.")?;
decode_context.open(None).context("Could not open codec")?;
input_format_context.dump(stream_index, &audio_path)?;
(decoder, decode_context)
};

let audio_data = fs::read(audio_path).context("Read audio file failed.")?;
fs::create_dir_all(Path::new(out_file_path).parent().unwrap())
.context("Create out file dir failed.")?;
let out_file = File::create(out_file_path).context("Open out file failed.")?;
let mut inbuf = [0u8; AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE as usize];

let mut parsed_offset = 0;
let mut parser_context =
AVCodecParserContext::find(decoder.id).context("Find parser context failed.")?;
let mut audio_file =
File::open(audio_path).with_context(|| anyhow!("Could not open {}", audio_path))?;
fs::create_dir_all(Path::new(out_file_path).parent().unwrap()).unwrap();
let out_file = File::create(out_file_path).context("Open out file failed.")?;

let mut parser_context = AVCodecParserContext::init(decoder.id).context("Parser not found")?;
let mut packet = AVPacket::new();
while parsed_offset < audio_data.len() {
let (get_packet, offset) = parser_context
.parse_packet(
&mut decode_context,
&mut packet,
&audio_data[parsed_offset..],
)
.context("Parse packet failed.")?;
if get_packet {
decode(&mut decode_context, Some(&packet), &out_file).context("Decode failed.")?;

loop {
let len = audio_file
.read(&mut inbuf[..AUDIO_INBUF_SIZE])
.context("Read input file failed.")?;
if len == 0 {
break;
}
let mut parsed_offset = 0;
while parsed_offset < len {
let (get_packet, offset) = parser_context
.parse_packet(&mut decode_context, &mut packet, &inbuf[parsed_offset..len])
.context("Error while parsing")?;
parsed_offset += offset;
if get_packet {
decode(&mut decode_context, Some(&packet), &out_file)?;
}
}
parsed_offset += offset;
}

decode(&mut decode_context, None, &out_file).context("Flush decode context failed.")?;
// Flush parser
let (get_packet, _) = parser_context
.parse_packet(&mut decode_context, &mut packet, &[])
.context("Error while parsing")?;
if get_packet {
decode(&mut decode_context, Some(&packet), &out_file)?;
}

// Flush decoder
decode(&mut decode_context, None, &out_file)?;

let mut sample_fmt = decode_context.sample_fmt;

if is_planar(sample_fmt) {
let name = get_sample_fmt_name(sample_fmt).context("Unknown sample fmt")?;
if sample_fmt_is_planar(sample_fmt) {
let name = get_sample_fmt_name(sample_fmt);
println!(
"Warning: the sample format the decoder produced is planar \
({}). This example will output the first channel only.",
name.to_str().unwrap()
name.map(|x| x.to_str().unwrap()).unwrap_or("?")
);
sample_fmt = get_packed_sample_fmt(sample_fmt).context("Cannot get packed sample fmt")?;
}
Expand All @@ -136,7 +152,7 @@ fn decode_audio(audio_path: &str, out_file_path: &str) -> Result<()> {
println!("Play the output audio file with the command:");
println!(
"ffplay -f {} -ac {} -ar {} {}",
fmt, decode_context.channels, decode_context.sample_rate, out_file_path
fmt, decode_context.ch_layout.nb_channels, decode_context.sample_rate, out_file_path
);
Ok(())
}
Expand Down
86 changes: 61 additions & 25 deletions tests/decode_video.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use anyhow::Result;
use anyhow::{anyhow, Context, Result};
use camino::Utf8Path as Path;
use rsmpeg::{
avcodec::{AVCodec, AVCodecContext, AVCodecParserContext, AVPacket},
avutil::AVFrame,
error::RsmpegError,
ffi,
};
use std::{fs, io::prelude::*, slice};
use rusty_ffmpeg::ffi::AV_INPUT_BUFFER_PADDING_SIZE;
use std::{
fs::{self, File},
io::prelude::*,
slice,
};

/// Save a `AVFrame` as pgm file.
fn pgm_save(frame: &AVFrame, filename: &str) -> Result<()> {
Expand All @@ -28,6 +34,9 @@ fn pgm_save(frame: &AVFrame, filename: &str) -> Result<()> {
for i in 0..height {
pgm_file.write_all(&buffer[i * linesize..i * linesize + width])?;
}

pgm_file.flush()?;

Ok(())
}

Expand All @@ -37,55 +46,82 @@ fn decode(
decode_context: &mut AVCodecContext,
packet: Option<&AVPacket>,
out_dir: &str,
out_filename: &str,
) -> Result<()> {
decode_context.send_packet(packet)?;
loop {
let frame = match decode_context.receive_frame() {
Ok(frame) => frame,
Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => break,
Err(e) => return Err(e.into()),
Err(e) => Err(e).context("Error during decoding")?,
};
println!("saving frame {}", decode_context.frame_num);
pgm_save(
&frame,
&format!("{}/{}.pgm", out_dir, decode_context.frame_number),
&format!(
"{}/{}-{}.pgm",
out_dir, out_filename, decode_context.frame_num
),
)?;
}
Ok(())
}

/// This function extracts frames from a MPEG1 video, then save them to `out_dir` as pgm.
fn decode_video(video_path: &str, out_dir: &str) {
let decoder = AVCodec::find_decoder(ffi::AVCodecID_AV_CODEC_ID_MPEG1VIDEO).unwrap();
fn decode_video(video_path: &str, out_dir: &str) -> Result<()> {
const INBUF_SIZE: usize = 4096;
let video_path = Path::new(video_path);
let out_filename = video_path.file_stem().unwrap();
fs::create_dir_all(out_dir).unwrap();

// set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams)
let mut inbuf = vec![0u8; INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE as usize];

let decoder =
AVCodec::find_decoder(ffi::AVCodecID_AV_CODEC_ID_MPEG1VIDEO).context("Codec not found")?;
let mut decode_context = AVCodecContext::new(&decoder);
decode_context.open(None).unwrap();
decode_context.open(None).context("Could not open codec")?;

let video_data = fs::read(video_path).unwrap();
// Create output dir
fs::create_dir_all(out_dir).unwrap();
let mut video_file =
File::open(video_path).with_context(|| anyhow!("Could not open {}", video_path))?;

let mut parsed_offset = 0;
let mut parser_context = AVCodecParserContext::find(decoder.id).unwrap();
let mut parser_context = AVCodecParserContext::init(decoder.id).context("Parser not found")?;
let mut packet = AVPacket::new();

while parsed_offset < video_data.len() {
let (get_packet, offset) = parser_context
.parse_packet(
&mut decode_context,
&mut packet,
&video_data[parsed_offset..],
)
.unwrap();
if get_packet {
decode(&mut decode_context, Some(&packet), out_dir).unwrap();
loop {
let len = video_file
.read(&mut inbuf[..INBUF_SIZE])
.context("Read input file failed.")?;
if len == 0 {
break;
}
let mut parsed_offset = 0;
while parsed_offset < len {
let (get_packet, offset) = parser_context
.parse_packet(&mut decode_context, &mut packet, &inbuf[parsed_offset..len])
.context("Error while parsing")?;
parsed_offset += offset;
if get_packet {
decode(&mut decode_context, Some(&packet), out_dir, out_filename)?;
}
}
parsed_offset += offset;
}

// Flush parser
let (get_packet, _) = parser_context
.parse_packet(&mut decode_context, &mut packet, &[])
.context("Error while parsing")?;
if get_packet {
decode(&mut decode_context, Some(&packet), out_dir, out_filename)?;
}

// Flush decoder
decode(&mut decode_context, None, out_dir).unwrap();
decode(&mut decode_context, None, out_dir, out_filename)?;

Ok(())
}

#[test]
fn decode_video_test() {
decode_video("tests/assets/vids/centaur.mpg", "tests/output/decode_video");
decode_video("tests/assets/vids/centaur.mpg", "tests/output/decode_video").unwrap();
}

0 comments on commit 21fc8ea

Please sign in to comment.