From 9ad9ccf7780e6f22a0b9a7d8d74a7ebadde24fcc Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 19 Feb 2022 13:46:22 +0100 Subject: [PATCH] Remove static binding to ffmpeg and use command instead Signed-off-by: trivernis --- Cargo.toml | 20 ++---- src/error.rs | 16 +---- src/formats/mod.rs | 3 - src/formats/video_format.rs | 132 ++++++------------------------------ src/lib.rs | 1 + src/utils/ffmpeg_cli.rs | 58 ++++++++++++++++ src/utils/mod.rs | 1 + tests/video_reading.rs | 20 ++++-- 8 files changed, 103 insertions(+), 148 deletions(-) create mode 100644 src/utils/ffmpeg_cli.rs create mode 100644 src/utils/mod.rs diff --git a/Cargo.toml b/Cargo.toml index bb29a99..7ea012a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,22 +4,16 @@ readme = "README.md" license = "Apache-2.0" authors = ["trivernis "] description = "An image thumbnail creation library" -version = "0.3.0" +version = "0.4.0" edition = "2018" repository = "https://github.com/Trivernis/thumbnailer" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -webp = "^0.2.1" -mime = "^0.3.16" -rayon = "^1.5.1" -ffmpeg-next = {version = "^4.4.0", optional=true} -tempfile = "^3.3.0" - -[dependencies.image] -version = "^0.24.0" - -[features] -default = ["ffmpeg"] -ffmpeg = ["ffmpeg-next"] \ No newline at end of file +webp = "0.2.1" +mime = "0.3.16" +rayon = "1.5.1" +tempfile = "3.3.0" +image= "0.24.0" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 1c2ec1a..9e0fc11 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,8 +17,7 @@ pub enum ThumbError { NullVideo, - #[cfg(feature = "ffmpeg")] - FFMPEG(ffmpeg_next::Error), + FFMPEG(String), } impl Display for ThumbError { @@ -29,8 +28,6 @@ impl Display for ThumbError { ThumbError::Decode => write!(f, "failed to decode image"), ThumbError::Unsupported(mime) => write!(f, "Unsupported media type {}", mime), ThumbError::NullVideo => write!(f, "no video data found in file"), - - #[cfg(feature = "ffmpeg")] ThumbError::FFMPEG(e) => write!(f, "ffmpeg error: {}", e), } } @@ -41,10 +38,6 @@ impl std::error::Error for ThumbError { match self { ThumbError::IO(e) => e.source(), ThumbError::Image(i) => i.source(), - - #[cfg(feature = "ffmpeg")] - ThumbError::FFMPEG(e) => e.source(), - _ => None, } } @@ -61,10 +54,3 @@ impl From for ThumbError { Self::Image(e) } } - -#[cfg(feature = "ffmpeg")] -impl From for ThumbError { - fn from(e: ffmpeg_next::Error) -> Self { - Self::FFMPEG(e) - } -} diff --git a/src/formats/mod.rs b/src/formats/mod.rs index 7f91f29..9010e5d 100644 --- a/src/formats/mod.rs +++ b/src/formats/mod.rs @@ -4,18 +4,15 @@ use image::DynamicImage; use mime::Mime; use std::io::{BufRead, Seek}; -#[cfg(feature = "ffmpeg")] use crate::formats::video_format::get_video_frame; pub mod image_format; -#[cfg(feature = "ffmpeg")] pub mod video_format; /// Reads the buffer content into an image that can be used for thumbnail generation pub fn get_base_image(reader: R, mime: Mime) -> ThumbResult { match mime.type_() { mime::IMAGE => read_image(reader, mime), - #[cfg(feature = "ffmpeg")] mime::VIDEO => get_video_frame(reader, mime), _ => Err(ThumbError::Unsupported(mime)), } diff --git a/src/formats/video_format.rs b/src/formats/video_format.rs index 8737170..6ad0dac 100644 --- a/src/formats/video_format.rs +++ b/src/formats/video_format.rs @@ -1,17 +1,19 @@ use crate::error::{ThumbError, ThumbResult}; -use ffmpeg_next::codec::decoder::Video as VideoDecoder; -use ffmpeg_next::filter; -use ffmpeg_next::filter::Graph; -use ffmpeg_next::frame::Video; -use ffmpeg_next::media::Type as MediaType; -use ffmpeg_next::threading::Config; -use image::{DynamicImage, RgbaImage}; +use crate::utils::ffmpeg_cli::{get_png_frame, is_ffmpeg_installed}; +use image::io::Reader as ImageReader; +use image::{DynamicImage, ImageFormat}; use mime::Mime; use std::fs; -use std::io::{BufRead, Seek}; +use std::io::{BufRead, Cursor, Seek}; use std::path::PathBuf; pub fn get_video_frame(mut reader: R, mime: Mime) -> ThumbResult { + lazy_static::lazy_static! { static ref FFMPEG_INSTALLED: bool = is_ffmpeg_installed(); } + + if !*FFMPEG_INSTALLED { + return Err(ThumbError::Unsupported(mime)); + } + let tempdir = tempfile::tempdir()?; tempdir.path(); let path = PathBuf::from(tempdir.path()) @@ -22,112 +24,16 @@ pub fn get_video_frame(mut reader: R, mime: Mime) -> ThumbRes reader.read_to_end(&mut buf)?; fs::write(&path, buf)?; - let img = extract_frame_from_video(&path)?; + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + + let png_bytes = get_png_frame( + path.to_str() + .expect("path to tmpdir contains invalid characters"), + 16, + )?; // take the 16th frame tempdir.close()?; + let img = ImageReader::with_format(Cursor::new(png_bytes), ImageFormat::Png).decode()?; Ok(img) } - -fn extract_frame_from_video(path: &PathBuf) -> ThumbResult { - let mut input = ffmpeg_next::format::input(path)?; - let stream = input - .streams() - .best(MediaType::Video) - .ok_or_else(|| ThumbError::NullVideo)?; - - let mut decoder = stream.codec().decoder().video()?; - decoder.set_threading(Config::count(1)); - - let mut filter = get_format_filter(&mut decoder)?; - - let stream_index = stream.index(); - - let packets = input - .packets() - .filter(|(s, _)| s.index() == stream_index) - .map(|(_, p)| p); - - let mut frame = Video::empty(); - let mut output_frame = Video::empty(); - let mut count = 0; - - for packet in packets { - decoder.send_packet(&packet)?; - while let Err(ffmpeg_next::Error::DecoderNotFound) = decoder.receive_frame(&mut frame) {} - - if decode_single_frame(&mut filter, &mut frame, &mut output_frame).is_ok() { - count += 1; - } - if count > 2 { - // take the second frame because the first one often is just blank - break; - } - } - decoder.send_eof()?; - - convert_frame_to_image(&decoder, output_frame) -} - -fn get_format_filter(decoder: &mut VideoDecoder) -> ThumbResult { - let args = format!( - "width={w}:height={h}:video_size={w}x{h}:pix_fmt={fmt}:time_base={base}", - w = decoder.width(), - h = decoder.height(), - fmt = decoder - .format() - .descriptor() - .ok_or_else(|| ThumbError::NullVideo)? - .name(), - base = decoder.time_base(), - ); - let mut filter = Graph::new(); - filter.add( - &filter::find("buffer").ok_or_else(|| ThumbError::NullVideo)?, - "in", - &args, - )?; - filter.add( - &filter::find("buffersink").ok_or_else(|| ThumbError::NullVideo)?, - "out", - "", - )?; - filter - .output("in", 0)? - .input("out", 0)? - .parse("format=rgba")?; - filter.validate()?; - - Ok(filter) -} - -fn decode_single_frame( - filter: &mut Graph, - frame: &mut Video, - mut output_frame: &mut Video, -) -> ThumbResult<()> { - filter - .get("in") - .ok_or_else(|| ThumbError::NullVideo)? - .source() - .add(&frame)?; - let mut ctx = filter.get("out").ok_or_else(|| ThumbError::NullVideo)?; - let mut out = ctx.sink(); - out.frame(&mut output_frame)?; - - Ok(()) -} - -fn convert_frame_to_image( - decoder: &VideoDecoder, - output_frame: Video, -) -> ThumbResult { - let image = RgbaImage::from_raw( - decoder.width(), - decoder.height(), - output_frame.data(0).to_vec(), - ) - .ok_or_else(|| ThumbError::NullVideo)?; - let image = DynamicImage::ImageRgba8(image); - - Ok(image) -} diff --git a/src/lib.rs b/src/lib.rs index f39796d..d72b1f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ pub use size::ThumbnailSize; pub mod error; mod formats; mod size; +pub(crate) mod utils; #[derive(Clone, Debug)] pub struct Thumbnail { diff --git a/src/utils/ffmpeg_cli.rs b/src/utils/ffmpeg_cli.rs new file mode 100644 index 0000000..1d82361 --- /dev/null +++ b/src/utils/ffmpeg_cli.rs @@ -0,0 +1,58 @@ +use crate::error::ThumbError; +use crate::ThumbResult; +use std::ffi::OsStr; +use std::io::ErrorKind; +use std::process::{Command, Stdio}; + +const FFMPEG: &str = "ffmpeg"; + +/// Runs ffmpeg to retrieve a png video frame +pub fn get_png_frame(video_file: &str, index: usize) -> ThumbResult> { + ffmpeg([ + "-loglevel", + "panic", + "-i", + video_file, + "-vf", + format!("select=eq(n\\,{})", index).as_str(), + "-vframes", + "1", + "-c:v", + "png", + "-movflags", + "empty_moov", + "-f", + "image2pipe", + "pipe:1", + ]) +} + +/// Runs ffmpeg with the given args +fn ffmpeg, S: AsRef>(args: I) -> ThumbResult> { + let child = Command::new(FFMPEG) + .args(args) + .stdout(Stdio::piped()) + .spawn()?; + + let output = child.wait_with_output()?; + if output.status.success() && output.stdout.len() > 0 { + Ok(output.stdout) + } else { + Err(ThumbError::FFMPEG( + String::from_utf8_lossy(&output.stderr[..]).to_string(), + )) + } +} + +pub fn is_ffmpeg_installed() -> bool { + match Command::new("ffmpeg").args(["-loglevel", "quiet"]).spawn() { + Ok(_) => true, + Err(e) => { + if let ErrorKind::NotFound = e.kind() { + false + } else { + true + } + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..a33953f --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod ffmpeg_cli; diff --git a/tests/video_reading.rs b/tests/video_reading.rs index a0d88c0..f5f648e 100644 --- a/tests/video_reading.rs +++ b/tests/video_reading.rs @@ -1,6 +1,9 @@ +extern crate core; + use mime::Mime; use std::io::Cursor; use std::str::FromStr; +use thumbnailer::error::ThumbError; use thumbnailer::{create_thumbnails, ThumbnailSize}; const VIDEO_BYTES: &'static [u8] = include_bytes!("assets/test.mp4"); @@ -17,9 +20,18 @@ fn it_creates_thumbnails_for_mp4() { ThumbnailSize::Large, ], ); - #[cfg(feature = "ffmpeg")] - result.unwrap(); - #[cfg(not(feature = "ffmpeg"))] - assert!(result.is_err()) + match result { + Ok(_) => { + assert!(true) + } + Err(e) => match e { + ThumbError::Unsupported(_) => { + assert!(true, "ffmpeg is not installed"); + } + e => { + panic!("failed to create thumbnails {}", e); + } + }, + } }