diff --git a/Cargo.toml b/Cargo.toml index 26a86d4..fac54e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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. diff --git a/src/avcodec/parser.rs b/src/avcodec/parser.rs index 02e8545..52be93a 100644 --- a/src/avcodec/parser.rs +++ b/src/avcodec/parser.rs @@ -11,7 +11,7 @@ wrap!(AVCodecParserContext: ffi::AVCodecParserContext); impl AVCodecParserContext { /// Allocate a [`AVCodecParserContext`] with given [`AVCodecID`]. - pub fn find(codec_id: AVCodecID) -> Option { + pub fn init(codec_id: AVCodecID) -> Option { // On Windows enum is i32, On *nix enum is u32. // ref: https://github.com/rust-lang/rust-bindgen/issues/1361 #[cfg(not(windows))] @@ -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, @@ -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, diff --git a/src/avutil/opt.rs b/src/avutil/opt.rs index 15a3a27..07b565e 100644 --- a/src/avutil/opt.rs +++ b/src/avutil/opt.rs @@ -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 /// diff --git a/src/avutil/samplefmt.rs b/src/avutil/samplefmt.rs index e2991fc..abccd45 100644 --- a/src/avutil/samplefmt.rs +++ b/src/avutil/samplefmt.rs @@ -105,7 +105,7 @@ pub fn get_bytes_per_sample(sample_fmt: AVSampleFormat) -> Option { /// 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 } } @@ -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 diff --git a/tests/decode_audio.rs b/tests/decode_audio.rs index 0b15f18..60800eb 100644 --- a/tests/decode_audio.rs +++ b/tests/decode_audio.rs @@ -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 = [ @@ -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; @@ -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(); @@ -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")?; } @@ -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(()) } diff --git a/tests/decode_video.rs b/tests/decode_video.rs index a1439e2..26f378e 100644 --- a/tests/decode_video.rs +++ b/tests/decode_video.rs @@ -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<()> { @@ -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(()) } @@ -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(); }